import { drainAudioPurgeQueue, handleAudioPurgeRequest, type AudioPurgeQueuePort } from './handler.ts' import { isPurgeableStorageKey, planPurge, type PurgeQueueEntry } from './policy.ts' import { parseClaimedRows } from './queue-adapter.ts' function assert(condition: boolean, message: string): asserts condition { if (!condition) throw new Error(message) } function assertEquals(actual: unknown, expected: unknown, message: string): void { const a = JSON.stringify(actual) const e = JSON.stringify(expected) if (a !== e) throw new Error(`${message}: expected ${e}, got ${a}`) } const USER = '24000000-0000-4000-8000-000000000001' const OTHER = '24000000-0000-4000-8000-000000000002' function entry(id: number, storageKey: string, userId = USER, leaseToken = 'lease-1'): PurgeQueueEntry { return { id, userId, storageKey, leaseToken } } class FakeQueue implements AudioPurgeQueuePort { completed: Array<{ ids: number[]; leaseToken: string }> = [] constructor(private batches: PurgeQueueEntry[][]) {} claim(): Promise { return Promise.resolve(this.batches.shift() ?? []) } complete(ids: number[], leaseToken: string): Promise { this.completed.push({ ids, leaseToken }) return Promise.resolve(ids.length) } } class FakeStorage { removed: string[][] = [] constructor(private failing = false) {} remove(paths: string[]): Promise { if (this.failing) return Promise.reject(new Error('storage_delete_failed:audio')) this.removed.push(paths) return Promise.resolve() } } Deno.test('redteam r2-24: keys inside the owner prefix are purgeable', () => { assert(isPurgeableStorageKey(USER, `${USER}/history/abc.wav`), 'desktop history key') assert(isPurgeableStorageKey(USER, `${USER}/${'a'.repeat(64)}/memo.m4a`), 'mobile content-addressed key') }) Deno.test('redteam r2-24: keys that could reach another user or escape the prefix are rejected', () => { assert(!isPurgeableStorageKey(USER, `${OTHER}/history/abc.wav`), 'other user prefix') assert(!isPurgeableStorageKey(USER, USER), 'bare prefix (whole folder)') assert(!isPurgeableStorageKey(USER, `${USER}/../${OTHER}/x.wav`), 'dot-dot segment') assert(!isPurgeableStorageKey(USER, `${USER}/./x.wav`), 'dot segment') assert(!isPurgeableStorageKey(USER, `${USER}//x.wav`), 'empty segment') assert(!isPurgeableStorageKey(USER, `${USER}/x.wav/`), 'trailing slash') assert(!isPurgeableStorageKey(USER, `${USER}\\..\\x.wav`), 'backslash') assert(!isPurgeableStorageKey(USER, `${USER}/x\u0000.wav`), 'control character') assert(!isPurgeableStorageKey('not-a-uuid', 'not-a-uuid/x.wav'), 'non-uuid owner') assert(!isPurgeableStorageKey(USER, `${USER}/${'a'.repeat(1100)}`), 'overlong key') }) Deno.test('redteam r2-24: planPurge splits removable from rejected', () => { const plan = planPurge([entry(1, `${USER}/history/a.wav`), entry(2, `${OTHER}/history/b.wav`)]) assertEquals(plan.removable.map((e) => e.id), [1], 'removable ids') assertEquals(plan.rejected.map((e) => e.id), [2], 'rejected ids') }) Deno.test('redteam r2-24: drain removes the objects, then completes the batch under its lease', async () => { const queue = new FakeQueue([[ entry(1, `${USER}/history/a.wav`), entry(2, `${USER}/meetings/m.wav`), entry(3, `${OTHER}/history/stolen.wav`), ]]) const storage = new FakeStorage() const summary = await drainAudioPurgeQueue({ queue, storage }) assertEquals(storage.removed, [[`${USER}/history/a.wav`, `${USER}/meetings/m.wav`]], 'removed paths') assertEquals(queue.completed, [{ ids: [3, 1, 2], leaseToken: 'lease-1' }], 'completed under lease') assertEquals(summary, { batches: 1, claimed: 3, removed: 2, rejected: 1, failed: 0, completed: 3 }, 'summary') }) Deno.test('redteam r2-24: a storage failure never completes the unremoved entries', async () => { const queue = new FakeQueue([[entry(1, `${USER}/history/a.wav`), entry(2, `${OTHER}/x.wav`)]]) const summary = await drainAudioPurgeQueue({ queue, storage: new FakeStorage(true) }) assertEquals(queue.completed, [{ ids: [2], leaseToken: 'lease-1' }], 'only the rejected entry completes') assertEquals(summary.failed, 1, 'failed count') assertEquals(summary.removed, 0, 'nothing removed') }) Deno.test('redteam r2-24: drain keeps claiming full batches and stops on an empty claim', async () => { const full = Array.from({ length: 100 }, (_, i) => entry(i + 1, `${USER}/history/${i}.wav`)) const queue = new FakeQueue([full, [entry(101, `${USER}/history/last.wav`, USER, 'lease-2')]]) const storage = new FakeStorage() const summary = await drainAudioPurgeQueue({ queue, storage }) assertEquals(summary.batches, 2, 'two batches') assertEquals(summary.completed, 101, 'all completed') assertEquals(queue.completed.map((c) => c.leaseToken), ['lease-1', 'lease-2'], 'each batch under its own lease') assertEquals(storage.removed.map((b) => b.length), [100, 1], 'API-sized removal batches') }) Deno.test('redteam r2-24: parseClaimedRows accepts RPC rows and skips malformed ones', () => { const rows = parseClaimedRows([ { id: 7, user_id: USER, storage_key: `${USER}/a.wav`, lease_token: 'l' }, { id: '8', user_id: USER, storage_key: `${USER}/b.wav`, lease_token: 'l' }, { id: 0, user_id: USER, storage_key: `${USER}/c.wav`, lease_token: 'l' }, { id: 9, user_id: USER, storage_key: null, lease_token: 'l' }, 'junk', ]) assertEquals(rows.map((r) => r.id), [7, 8], 'parsed ids') assertEquals(parseClaimedRows(null), [], 'null data') }) Deno.test('redteam r2-24: only an authorized POST runs the purge', async () => { let ran = 0 const deps = (authorized: boolean) => ({ authorize: () => Promise.resolve(authorized), createDeps: () => { ran += 1 return { queue: new FakeQueue([]), storage: new FakeStorage() } }, }) const get = await handleAudioPurgeRequest(new Request('http://x/audio-purge'), deps(true)) assertEquals(get.status, 405, 'GET rejected') const denied = await handleAudioPurgeRequest(new Request('http://x/audio-purge', { method: 'POST' }), deps(false)) assertEquals(denied.status, 401, 'unauthorized rejected') assertEquals(ran, 0, 'no work before authorization') const ok = await handleAudioPurgeRequest(new Request('http://x/audio-purge', { method: 'POST' }), deps(true)) assertEquals(ok.status, 200, 'authorized POST succeeds') assertEquals(ran, 1, 'purge ran once') })