import { describe, it, expect } from 'vitest' import { processIndexedDownload } from '../../skills/data-portal/template/functions/api/download' import type { PortalEnv } from '../../skills/data-portal/template/functions/api/_lib/types' const PATH = 'quotes/CR2969/quote.pdf' function makeEnv( opts: { accountId?: string secret?: string origin?: string paths?: string[] dirPaths?: string[] folders?: string onQuery?: (query: string) => void } = {}, ) { const { accountId = 'acc-a', paths = [PATH], dirPaths = [], onQuery = () => {} } = opts const folders = 'folders' in opts ? opts.folders : '' return { DB: { prepare(query: string) { onQuery(query) let bound: unknown[] = [] const stmt = { bind(...v: unknown[]) { bound = v return stmt }, async run() { return { meta: { changes: 0 } } }, async all() { return { results: [] as T[] } }, async first() { if (/FROM sessions/i.test(query)) { return (bound[0] === 'sess-a' ? { ownerId: 'alice', accountId, folders } : null) as T | null } if (/FROM directory/i.test(query)) { const rel = bound[1] as string if (dirPaths.includes(rel)) return { relPath: rel, isDir: 1 } as T return (paths.includes(rel) ? { relPath: rel, isDir: 0 } : null) as T | null } return null as T | null }, } return stmt }, }, PORTAL_FETCH_SECRET: 'secret' in opts ? opts.secret : 'a-secret', PORTAL_INSTALL_ORIGIN: 'origin' in opts ? opts.origin : 'https://install.example', } as unknown as PortalEnv } const okHead = async () => ({ ok: true, status: 200 }) const deadHead = async () => { throw new Error('ECONNREFUSED') } const errHead = async () => ({ ok: false, status: 502 }) const missingHead = async () => ({ ok: false, status: 404 }) describe('processIndexedDownload', () => { it('returns a signed url for an indexed file when the device answers', async () => { const r = await processIndexedDownload('sess-a', PATH, makeEnv(), () => {}, 1000, okHead as never) expect(r.status).toBe(200) const url = new URL(r.url as string) expect(url.origin).toBe('https://install.example') expect(url.pathname).toBe('/api/portal/fetch') expect(url.searchParams.get('path')).toBe(PATH) expect(url.searchParams.get('accountId')).toBe('acc-a') expect(url.searchParams.get('sig')).toMatch(/^[0-9a-f]{64}$/) expect(Number(url.searchParams.get('exp'))).toBeGreaterThan(1000) }) it('reports device-offline distinguishably when the probe throws', async () => { const r = await processIndexedDownload('sess-a', PATH, makeEnv(), () => {}, 1000, deadHead as never) expect(r.status).toBe(503) expect(r.payload.error).toBe('device-offline') expect(r.url).toBeUndefined() }) it('reports device-offline when the probe answers non-ok', async () => { const r = await processIndexedDownload('sess-a', PATH, makeEnv(), () => {}, 1000, errHead as never) expect(r.status).toBe(503) expect(r.payload.error).toBe('device-offline') }) // A live device that answers 404 means the indexed row is stale: the file was // renamed or removed while the index was frozen. That is "refresh the list", // not "the device is down", so it must not collapse into device-offline. it('reports file-missing when a reachable device 404s the path', async () => { const lines: string[] = [] const r = await processIndexedDownload('sess-a', PATH, makeEnv(), (l) => lines.push(l), 1000, missingHead as never) expect(r.status).toBe(404) expect(r.payload.error).toBe('file-missing') expect(r.url).toBeUndefined() expect(lines.some((l) => l.includes('result=file-missing'))).toBe(true) }) it('404s a path that is not in the index, without contacting the device', async () => { let touched = false const spy = async () => { touched = true return { ok: true, status: 200 } } const r = await processIndexedDownload( 'sess-a', 'documents/private.pdf', makeEnv(), () => {}, 1000, spy as never, ) expect(r.status).toBe(404) expect(touched).toBe(false) }) // Folders are in the index now, so a client can name one here. A folder has // no bytes and must never mint a signed link. result=is-directory separates // it from result=not-granted and result=absent, the only readings before. it('refuses a directory rather than signing a link for it', async () => { let touched = false const spy = async () => { touched = true return { ok: true, status: 200 } } const lines: string[] = [] const r = await processIndexedDownload( 'sess-a', 'quotes/CR2969', makeEnv({ paths: [PATH], dirPaths: ['quotes/CR2969'] }), (l) => lines.push(l), 1000, spy as never, ) expect(r.status).toBe(404) expect(r.url).toBeUndefined() expect(touched).toBe(false) expect(lines.some((l) => l.includes('result=is-directory'))).toBe(true) }) it('reads isDir in the directory select', async () => { const queries: string[] = [] const env = makeEnv({ onQuery: (q) => queries.push(q) }) await processIndexedDownload('sess-a', PATH, env, () => {}, 1000, okHead as never) expect(queries.some((q) => /FROM directory/i.test(q) && /isDir/.test(q))).toBe(true) }) it('401s without a session', async () => { const r = await processIndexedDownload('nope', PATH, makeEnv(), () => {}, 1000, okHead as never) expect(r.status).toBe(401) }) it('404s when the person has no account bound', async () => { const r = await processIndexedDownload( 'sess-a', PATH, makeEnv({ accountId: '' }), () => {}, 1000, okHead as never, ) expect(r.status).toBe(404) }) it('never mints a link when the secret or origin is unconfigured', async () => { for (const env of [makeEnv({ secret: undefined }), makeEnv({ origin: undefined })]) { const r = await processIndexedDownload('sess-a', PATH, env, () => {}, 1000, okHead as never) expect(r.status).toBe(503) expect(r.url).toBeUndefined() } }) it('never puts the secret in the url or the log', async () => { const lines: string[] = [] const r = await processIndexedDownload('sess-a', PATH, makeEnv(), (l) => lines.push(l), 1000, okHead as never) expect(r.url).not.toContain('a-secret') expect(lines.join('\n')).not.toContain('a-secret') }) it('logs the outcome with the path', async () => { const lines: string[] = [] await processIndexedDownload('sess-a', PATH, makeEnv(), (l) => lines.push(l), 1000, okHead as never) expect(lines.some((l) => l.includes('op=fetch-through') && l.includes('result=ok'))).toBe(true) }) it('refuses a path outside the grant with 404 and no device probe', async () => { let touched = false const spy = async () => { touched = true return { ok: true, status: 200 } } const lines: string[] = [] const env = makeEnv({ folders: 'quotes' }) const r = await processIndexedDownload( 'sess-a', 'documents/x.pdf', env, (l) => lines.push(l), 1000, spy as never, ) expect(r.status).toBe(404) expect(touched).toBe(false) expect(lines.some((l) => l.includes('result=not-granted'))).toBe(true) }) it('allows an in-grant path', async () => { const env = makeEnv({ folders: 'quotes' }) const r = await processIndexedDownload('sess-a', PATH, env, () => {}, 1000, okHead as never) expect(r.status).toBe(200) }) it('an empty grant allows any indexed path', async () => { const env = makeEnv({ folders: '' }) const r = await processIndexedDownload('sess-a', PATH, env, () => {}, 1000, okHead as never) expect(r.status).toBe(200) }) })