diff --git a/__tests__/config_validation.test.mjs b/__tests__/config_validation.test.mjs new file mode 100644 index 0000000..51a3ede --- /dev/null +++ b/__tests__/config_validation.test.mjs @@ -0,0 +1,82 @@ +/** + * config_validation.test.mjs — Validate build configuration integrity. + * + * Prevents regressions like the phantom entry module issue that kfj001 + * hit: vite.config.ts referenced probeFn.js and scrapeFn.js but those + * files were never created. + */ +import { describe, it } from './runner.mjs'; +import { readFileSync, existsSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = resolve(__dirname, '..'); + +describe('Config: build integrity', () => { + it('vite.config.ts entry modules must exist as source files', () => { + const viteConfig = readFileSync(resolve(root, 'vite.config.ts'), 'utf8'); + + const matches = viteConfig.matchAll(/resolve\(__dirname,\s*'([^']+)'\)/g); + const inputEntries = [...matches] + .map(m => m[1]) + .filter(p => p.startsWith('src/')); + + const missing = inputEntries.filter(p => !existsSync(resolve(root, p))); + + if (missing.length > 0) { + throw new Error( + `vite.config.ts references ${missing.length} missing file(s): ${missing.join(', ')}` + ); + } + if (inputEntries.length === 0) { + throw new Error('No entry points found in vite.config.ts'); + } + }); + + it('content.js must NOT use inline script injection (textContent)', () => { + const contentJs = readFileSync(resolve(root, 'src/content/content.js'), 'utf8'); + + // Strip comments before checking for textContent usage + const codeOnly = contentJs + .replace(/\/\*[\s\S]*?\*\//g, '') // block comments + .replace(/\/\/.*$/gm, ''); // line comments + + if (codeOnly.includes('textContent')) { + throw new Error( + 'content.js uses inline script injection (textContent). ' + + 'Sites like Pornhub block inline scripts via CSP. Use script.src instead.' + ); + } + if (!codeOnly.includes('script.src') || !codeOnly.includes('chrome.runtime.getURL')) { + throw new Error( + 'content.js does not use CSP-safe script.src = chrome.runtime.getURL(...)' + ); + } + }); + + it('interceptors.js must be a standalone file', () => { + const path = resolve(root, 'src/inject/interceptors.js'); + if (!existsSync(path)) { + throw new Error('src/inject/interceptors.js does not exist'); + } + const content = readFileSync(path, 'utf8'); + if (!content.includes('(function ()') || !content.includes('})();')) { + throw new Error('interceptors.js should be a self-contained IIFE'); + } + }); + + it('manifest.json content_scripts must reference content.js', () => { + const manifest = JSON.parse( + readFileSync(resolve(root, 'manifest.json'), 'utf8') + ); + const cs = manifest.content_scripts; + if (!cs || !Array.isArray(cs)) { + throw new Error('manifest.json has no content_scripts'); + } + const hasContentScript = cs.some(s => s.js && s.js.includes('content.js')); + if (!hasContentScript) { + throw new Error('No content_scripts entry references content.js'); + } + }); +}); diff --git a/__tests__/run.mjs b/__tests__/run.mjs index 1d4b674..1fee7f2 100644 --- a/__tests__/run.mjs +++ b/__tests__/run.mjs @@ -5,6 +5,7 @@ import './schema.test.mjs'; import './viewer_units.test.mjs'; import './data_flow.test.mjs'; import './viewer_integration.test.mjs'; +import './config_validation.test.mjs'; import { run } from './runner.mjs'; const result = await run();