📄 src/downloader/test/file-worker.test.ts
D-OPEN SOVEREIGN
1import { describe, it, beforeEach, expect, vi } from 'vitest';
2import type { WorkerMessage } from '../worker-protocol';
3
4const fetchFromGateways = vi.fn();
5const putPdf = vi.fn();
6const putPreview = vi.fn();
7const putDownloadRecord = vi.fn();
8const runLruPruneAndEmit = vi.fn();
9
10vi.mock('../fetcher', () => ({
11    fetchFromGateways: (...args: unknown[]) => fetchFromGateways(...args),
12    DEFAULT_GATEWAYS: ['https://arweave.net'],
13}));
14vi.mock('../cache-store', () => ({
15    putPdf: (...args: unknown[]) => putPdf(...args),
16    putPreview: (...args: unknown[]) => putPreview(...args),
17}));
18vi.mock('../meta-db', () => ({
19    putDownloadRecord: (...args: unknown[]) => putDownloadRecord(...args),
20}));
21vi.mock('../lru', () => ({
22    runLruPruneAndEmit: (...args: unknown[]) => runLruPruneAndEmit(...args),
23}));
24
25let posted: WorkerMessage[];
26let selfOnMessage: (e: MessageEvent) => void;
27
28beforeEach(async () => {
29    vi.clearAllMocks();
30    posted = [];
31    // @ts-expect-error minimal Worker-global stub the module writes onmessage/postMessage onto
32    globalThis.self = {
33        postMessage: (msg: WorkerMessage) => posted.push(msg),
34        set onmessage(fn: (e: MessageEvent) => void) { selfOnMessage = fn; },
35        get onmessage() { return selfOnMessage; },
36    };
37    vi.resetModules();
38    await import('../worker/file-worker');
39    // Let the module settle after re-import so the freshly mocked self.onmessage is bound.
40});
41
42async function flush() {
43    await new Promise(r => setTimeout(r, 0));
44}
45
46describe('file-worker — handleGetFile', () => {
47    it('measures sizeBytes from the decoded body, not a Content-Length header (regression: was always 0)', async () => {
48        const body = new Uint8Array(5000); // larger payload — content-length would previously be stripped/0
49        fetchFromGateways.mockResolvedValue(new Response(body));
50        runLruPruneAndEmit.mockResolvedValue(undefined);
51
52        selfOnMessage({ data: { type: 'GetFile', txId: 'tx1', mime: 'pdf' } } as unknown as MessageEvent);
53        await flush();
54
55        expect(putDownloadRecord).toHaveBeenCalledWith(expect.objectContaining({ txId: 'tx1', sizeBytes: 5000 }));
56        const [, storedResponse] = putPdf.mock.calls[0];
57        expect(storedResponse.headers.get('Content-Length')).toBe('5000');
58    });
59
60    it('stores pdf mime in the pdf cache with a application/pdf Content-Type', async () => {
61        fetchFromGateways.mockResolvedValue(new Response(new Uint8Array(10)));
62        selfOnMessage({ data: { type: 'GetFile', txId: 'tx2', mime: 'pdf' } } as unknown as MessageEvent);
63        await flush();
64
65        expect(putPdf).toHaveBeenCalledTimes(1);
66        expect(putPreview).not.toHaveBeenCalled();
67        const [txId, resp] = putPdf.mock.calls[0];
68        expect(txId).toBe('tx2');
69        expect(resp.headers.get('Content-Type')).toBe('application/pdf');
70    });
71
72    it('stores preview mime in the preview cache, preserving upstream Content-Type', async () => {
73        fetchFromGateways.mockResolvedValue(new Response(new Uint8Array(10), { headers: { 'content-type': 'image/png' } }));
74        selfOnMessage({ data: { type: 'GetFile', txId: 'tx3', mime: 'preview' } } as unknown as MessageEvent);
75        await flush();
76
77        expect(putPreview).toHaveBeenCalledTimes(1);
78        expect(putPdf).not.toHaveBeenCalled();
79        const [, resp] = putPreview.mock.calls[0];
80        expect(resp.headers.get('Content-Type')).toBe('image/png');
81    });
82
83    it('posts DownloadFinished then a FileRpcResponse keyed by txId:mime on success', async () => {
84        fetchFromGateways.mockResolvedValue(new Response(new Uint8Array(1)));
85        selfOnMessage({ data: { type: 'GetFile', txId: 'tx4', mime: 'pdf' } } as unknown as MessageEvent);
86        await flush();
87
88        expect(posted).toContainEqual({ type: 'DownloadFinished', txId: 'tx4', key: 'tx4', mime: 'pdf' });
89        expect(posted).toContainEqual({ type: 'FileRpcResponse', ok: true, key: 'tx4:pdf' });
90    });
91
92    it('triggers LRU pruning after every successful download', async () => {
93        fetchFromGateways.mockResolvedValue(new Response(new Uint8Array(1)));
94        selfOnMessage({ data: { type: 'GetFile', txId: 'tx5', mime: 'pdf' } } as unknown as MessageEvent);
95        await flush();
96
97        expect(runLruPruneAndEmit).toHaveBeenCalledTimes(1);
98    });
99
100    it('posts DownloadError and a failed FileRpcResponse when the gateway fetch rejects', async () => {
101        fetchFromGateways.mockRejectedValue(new Error('all gateways failed'));
102        selfOnMessage({ data: { type: 'GetFile', txId: 'tx6', mime: 'pdf' } } as unknown as MessageEvent);
103        await flush();
104
105        expect(posted).toContainEqual({ type: 'DownloadError', txId: 'tx6', error: 'all gateways failed', retryable: true });
106        expect(posted).toContainEqual({ type: 'FileRpcResponse', ok: false, key: 'tx6:pdf', error: 'all gateways failed' });
107        expect(putPdf).not.toHaveBeenCalled();
108    });
109
110    it('does not prune when the download failed', async () => {
111        fetchFromGateways.mockRejectedValue(new Error('boom'));
112        selfOnMessage({ data: { type: 'GetFile', txId: 'tx7', mime: 'pdf' } } as unknown as MessageEvent);
113        await flush();
114
115        expect(runLruPruneAndEmit).not.toHaveBeenCalled();
116    });
117
118    it('CancelDownload is accepted without throwing (Phase A stub)', () => {
119        expect(() => selfOnMessage({ data: { type: 'CancelDownload', txId: 'tx8' } } as unknown as MessageEvent)).not.toThrow();
120    });
121});
122