📄 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