import { readFileSync } from 'node:fs'; import { createHash } from 'node:crypto'; import { expect, test } from '@playwright/test'; const nginxHeaders = readFileSync(new URL('../../../deploy/nginx/security-headers.conf', import.meta.url), 'utf8'); const desktopConfig = JSON.parse(readFileSync(new URL('../../desktop/src-tauri/tauri.conf.json', import.meta.url), 'utf8')); const policies: Record = { web: nginxHeaders.match(/add_header Content-Security-Policy "([^"]+)" always;/)![1], desktop: desktopConfig.app.security.csp, }; for (const [platform, policy] of Object.entries(policies)) { test(`upload hash worker runs under ${platform} CSP`, async ({ page }) => { // Apply the deployed policy to both the document and worker responses. await page.route('**/*', async (route) => { if (new URL(route.request().url()).pathname === '/csp-upload-test') { await route.fulfill({ contentType: 'text/html', body: 'Upload CSP', headers: { 'Content-Security-Policy': policy } }); return; } if (new URL(route.request().url()).pathname === '/csp-eval-test.js') { await route.fulfill({ contentType: 'application/javascript', body: ` try { new Function('return 1')(); document.documentElement.dataset.evalBlocked = 'false'; } catch { document.documentElement.dataset.evalBlocked = 'true'; } WebAssembly.compile(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0])).then( () => { document.documentElement.dataset.wasmBlocked = 'false'; }, () => { document.documentElement.dataset.wasmBlocked = 'true'; }, ); ` }); return; } const response = await route.fetch(); await route.fulfill({ response, headers: { ...response.headers(), 'Content-Security-Policy': policy } }); }); await page.goto('/csp-upload-test'); const result = await page.evaluate(async () => { const worker = new Worker('/src/features/uploads/hash.worker.ts', { type: 'module' }); try { const results = []; // Reuse one worker for empty, known-vector and multi-chunk binary files. const binary = Uint8Array.from({ length: (9 << 20) + 17 }, (_, i) => i % 251); for (const [id, bytes] of [['empty', new Uint8Array()], ['abc', new TextEncoder().encode('abc')], ['binary', binary]] as const) { results.push(await new Promise((resolve, reject) => { const progress: number[] = []; worker.onerror = (event) => reject(new Error(event.message)); worker.onmessage = ({ data }) => { if (data.type === 'progress') { progress.push(data.done); return; } resolve({ ...data, progress }); }; worker.postMessage({ id, file: new File([bytes], `${id}.flac`) }); })); } return results; } finally { worker.terminate(); } }); const binary = Uint8Array.from({ length: (9 << 20) + 17 }, (_, i) => i % 251); expect(result).toEqual([ { id: 'empty', type: 'done', sha256: createHash('sha256').update('').digest('hex'), progress: [] }, { id: 'abc', type: 'done', sha256: createHash('sha256').update('abc').digest('hex'), progress: [] }, { id: 'binary', type: 'done', sha256: createHash('sha256').update(binary).digest('hex'), progress: expect.arrayContaining([expect.any(Number)]) }, ]); // A page script observes CSP; Playwright's evaluate bypasses the eval restriction. await page.addScriptTag({ url: '/csp-eval-test.js' }); await expect(page.locator('html')).toHaveAttribute('data-eval-blocked', 'true'); await expect(page.locator('html')).toHaveAttribute('data-wasm-blocked', 'true'); }); }