Files
binect-chrome/tests/metadata-only.test.ts
tegwick 90a4b7a936 Add release smoke path for pre-store verification (WP-0002)
Introduce npm run smoke with automated build, test, lint, dist, and
metadata-only compliance checks. Document manual Chrome steps in
RELEASE_SMOKE.md and fix unused imports blocking lint.
2026-06-24 15:27:54 +02:00

103 lines
3.2 KiB
TypeScript

/**
* 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/*'])
);
});
});