import { describe, expect, it } from 'vitest' import { processDownload } from '../../skills/data-portal/template/functions/api/download' import type { PortalEnv } from '../../skills/data-portal/template/functions/api/_lib/types' const ACC = '098a18a3-1741-447f-9778-65470645d57d' function harness(row: { devicePath: string } | null, opts: { configured?: boolean } = {}) { const configured = opts.configured ?? true const lines: string[] = [] const env = { BUCKET: { async get(key: string) { // Only an in-transit object exists. A moved row's bytes are gone from // R2 entirely, which is what makes reading them here the wrong move. return row && !row.devicePath ? { body: null, size: 4, key } : null }, }, DB: { prepare(sql: string) { return { bind() { return { async first() { if (sql.includes('FROM sessions')) { return { ownerId: 'alice', accountId: ACC, folders: '' } } if (sql.includes('devicePath')) return row return null }, } }, } }, }, PORTAL_FETCH_SECRET: configured ? 'test-secret-value' : undefined, PORTAL_INSTALL_ORIGIN: configured ? 'https://install.example' : undefined, } as unknown as PortalEnv return { env, lines, log: (l: string) => lines.push(l) } } describe('downloading an upload that has moved to the device', () => { it('streams R2 bytes while the row is still in transit', async () => { const h = harness({ devicePath: '' }) const res = await processDownload('s', 'alice/a.pdf', h.env, h.log, 1000) expect(res.status).toBe(200) expect(res.body).not.toBeNull() expect(res.url).toBeUndefined() }) it('mints an own-upload link once the row has moved', async () => { // The object is gone from R2. Without this branch the client's own upload // would 404 the moment the device collected it. const h = harness({ devicePath: 'uploads/alice/a.pdf' }) const res = await processDownload('s', 'alice/a.pdf', h.env, h.log, 1000) expect(res.status).toBe(200) expect(res.url).toContain('/api/portal/fetch') expect(new URL(res.url!).searchParams.get('owner')).toBe('alice') expect(new URL(res.url!).searchParams.get('path')).toBe('uploads/alice/a.pdf') expect(h.lines.some((l) => l.includes('op=download') && l.includes('result=moved'))).toBe(true) }) it('refuses another person key before anything is minted', async () => { const h = harness({ devicePath: 'uploads/bob/a.pdf' }) const res = await processDownload('s', 'bob/a.pdf', h.env, h.log, 1000) expect(res.status).toBe(403) expect(res.url).toBeUndefined() }) it('reports device-offline rather than a server error when unconfigured', async () => { // Same reasoning as the indexed path: from the client's side the file // genuinely is unreachable, and the operator's signal is the log line. const h = harness({ devicePath: 'uploads/alice/a.pdf' }, { configured: false }) const res = await processDownload('s', 'alice/a.pdf', h.env, h.log, 1000) expect(res.status).toBe(503) expect(h.lines.some((l) => l.includes('result=unconfigured'))).toBe(true) }) it('reports absent for a key with no manifest row at all', async () => { const h = harness(null) const res = await processDownload('s', 'alice/gone.pdf', h.env, h.log, 1000) expect(res.status).toBe(404) }) })