/** * Release smoke — metadata-only compliance checks * * Verifies the extension never persists PDF content to chrome.storage. * Run as part of `npm test` and `npm run smoke`. */ import * as fs from 'fs'; import * as path from 'path'; const SRC = path.join(__dirname, '..', 'src'); const PDF_CONTENT_FIELD_PATTERN = /\b(pdfContent|pdfBytes|contentBase64|base64Content|pdfData)\b/; function readSourceFiles(dir: string): string[] { const files: string[] = []; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { files.push(...readSourceFiles(full)); } else if (entry.name.endsWith('.ts')) { files.push(full); } } return files; } describe('metadata-only storage compliance', () => { test('DocumentProxy queue does not define PDF content fields', () => { const queueSource = fs.readFileSync( path.join(SRC, 'utils', 'pdf-queue.ts'), 'utf-8' ); expect(queueSource).not.toMatch(PDF_CONTENT_FIELD_PATTERN); expect(queueSource).toContain('contentHash'); expect(queueSource).not.toContain('pdfContent'); }); test('DetectedPDF interface is metadata-only', () => { const detectorSource = fs.readFileSync( path.join(SRC, 'utils', 'pdf-detector.ts'), 'utf-8' ); const interfaceMatch = detectorSource.match( /export interface DetectedPDF \{([^}]+)\}/ ); expect(interfaceMatch).not.toBeNull(); const fields = interfaceMatch![1]; expect(fields).toMatch(/filename/); expect(fields).toMatch(/url/); expect(fields).toMatch(/size/); expect(fields).not.toMatch(PDF_CONTENT_FIELD_PATTERN); }); test('no source file persists PDF bytes via chrome.storage', () => { const offenders: string[] = []; for (const file of readSourceFiles(SRC)) { const content = fs.readFileSync(file, 'utf-8'); if (!content.includes('chrome.storage')) { continue; } const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; if ( line.includes('chrome.storage') && line.includes('.set') && PDF_CONTENT_FIELD_PATTERN.test(line) ) { offenders.push(`${path.relative(SRC, file)}:${i + 1}`); } } } expect(offenders).toEqual([]); }); test('upload path fetches bytes in-memory and delegates to Binect API', () => { const popupSource = fs.readFileSync( path.join(SRC, 'popup', 'popup.ts'), 'utf-8' ); expect(popupSource).toMatch(/fetchPDFBytes/); expect(popupSource).toMatch(/uploadPDF/); expect(popupSource).toMatch(/computeContentFingerprint/); }); test('manifest declares required MV3 permissions', () => { const manifest = JSON.parse( fs.readFileSync( path.join(__dirname, '..', 'public', 'manifest.json'), 'utf-8' ) ); expect(manifest.manifest_version).toBe(3); for (const perm of ['downloads', 'storage', 'alarms', 'activeTab']) { expect(manifest.permissions).toContain(perm); } expect(manifest.host_permissions).toEqual( expect.arrayContaining(['https://api.binect.de/*']) ); }); });