- The render is uploaded with the new review_render_upload action (backend kind review_render, the only image a review job accepts); mocks follow. - summarizeCanvas() never lets a newline or a quote into a node line (the server validates review summaries line by line); test. - test:browser:sitesmith-cleaned (Firefox + Chromium): a page published from a cleaned ordinary Sitesmith response requests nothing outside the allow-listed hosts (own assets, fonts, Font Awesome CDN, images.pexels.com, placehold.co) and has no inline handlers or javascript:/data: URL attributes; the same response uncleaned is the control (requests example.com). All requests are answered or aborted locally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
4.7 KiB
JavaScript
83 lines
4.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Real-browser check (Firefox AND Chromium, headless) for the Sitesmith apply-time cleaner (G8): a page
|
|
* published from a CLEANED model response loads nothing outside the allow-listed hosts and carries no
|
|
* inline event handlers or javascript: URLs. The same response UNCLEANED is loaded as a control, to
|
|
* show the check can see an off-list request. Nothing leaves the machine: every request is answered
|
|
* or aborted by page.route (off-site requests are only recorded).
|
|
* npm run test:browser:sitesmith-cleaned Env: CHROMIUM_PATH / FIREFOX_PATH
|
|
*/
|
|
import { chromium, firefox } from '@playwright/test';
|
|
import { build as esbuild } from 'esbuild';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const out = await esbuild({
|
|
entryPoints: [path.join(root, 'scripts/e2e-sitesmith-cleaned-entry.ts')],
|
|
bundle: true, write: false, format: 'esm', platform: 'node', jsx: 'automatic', logLevel: 'error',
|
|
define: { 'process.env.NODE_ENV': '"production"', __EDITOR_BUILD__: '"test"' },
|
|
banner: { js: "import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);" },
|
|
});
|
|
const tmp = path.join(os.tmpdir(), `wsb-ss-cleaned-${process.pid}.mjs`);
|
|
fs.writeFileSync(tmp, out.outputFiles[0].text);
|
|
const ent = await import(tmp);
|
|
fs.rmSync(tmp, { force: true });
|
|
|
|
const SITE = 'http://site.test';
|
|
// The published page's own head resources (fonts, Font Awesome) + the cleaner's image hosts.
|
|
const ALLOWED_HOSTS = new Set(['site.test', 'fonts.googleapis.com', 'fonts.gstatic.com', 'cdnjs.cloudflare.com', 'images.pexels.com', 'placehold.co']);
|
|
const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64');
|
|
|
|
let pass = 0; let fail = 0;
|
|
const ok = (c, m, d = '') => { if (c) { pass++; console.log('PASS ', m); } else { fail++; console.log('FAIL ', m, d); } };
|
|
|
|
async function load(browser, html) {
|
|
const ctx = await browser.newContext();
|
|
const page = await ctx.newPage();
|
|
const hosts = new Set();
|
|
await page.route('**/*', async (route) => {
|
|
const u = new URL(route.request().url());
|
|
hosts.add(u.hostname);
|
|
if (u.origin === SITE) {
|
|
if (u.pathname === '/' || u.pathname === '/index.html') return route.fulfill({ status: 200, contentType: 'text/html', body: html });
|
|
if (u.pathname.startsWith('/assets/')) return route.fulfill({ status: 200, contentType: 'image/png', body: PNG });
|
|
return route.fulfill({ status: 404, body: '' });
|
|
}
|
|
return route.abort(); // never contact a real host
|
|
});
|
|
await page.goto(SITE + '/', { waitUntil: 'load' });
|
|
await page.waitForTimeout(300);
|
|
const dom = await page.evaluate(() => {
|
|
const handlers = [];
|
|
const jsUrls = [];
|
|
for (const el of document.querySelectorAll('*')) {
|
|
for (const a of el.attributes) {
|
|
if (/^on/i.test(a.name)) handlers.push(`${el.tagName}.${a.name}`);
|
|
if (['href', 'src', 'action', 'formaction', 'poster', 'data'].includes(a.name) && /^\s*(javascript|vbscript|data):/i.test(a.value)) jsUrls.push(`${el.tagName}.${a.name}`);
|
|
}
|
|
}
|
|
return { handlers, jsUrls, text: document.body.innerText };
|
|
});
|
|
await ctx.close();
|
|
return { hosts: [...hosts].sort(), dom };
|
|
}
|
|
|
|
for (const [name, launcher, exe] of [['chromium', chromium, process.env.CHROMIUM_PATH], ['firefox', firefox, process.env.FIREFOX_PATH]]) {
|
|
const browser = await launcher.launch({ executablePath: exe || undefined, ...(name === 'chromium' ? { args: ['--no-sandbox'] } : {}) });
|
|
const control = await load(browser, ent.render(false));
|
|
ok(control.hosts.includes('example.com'), `[${name}] control (uncleaned): the check sees the off-list request to example.com`, JSON.stringify(control.hosts));
|
|
const cleaned = await load(browser, ent.render(true));
|
|
const offList = cleaned.hosts.filter((h) => !ALLOWED_HOSTS.has(h));
|
|
ok(offList.length === 0, `[${name}] cleaned: no request outside the allow-listed hosts`, JSON.stringify(cleaned.hosts));
|
|
ok(cleaned.hosts.includes('site.test'), `[${name}] cleaned: the site's own asset was requested`, JSON.stringify(cleaned.hosts));
|
|
ok(cleaned.dom.handlers.length === 0, `[${name}] cleaned: no inline event handlers`, JSON.stringify(cleaned.dom.handlers));
|
|
ok(cleaned.dom.jsUrls.length === 0, `[${name}] cleaned: no javascript:/data: URLs in URL attributes`, JSON.stringify(cleaned.dom.jsUrls));
|
|
ok(/Fresh bread every morning/.test(cleaned.dom.text) && /Book a table/.test(cleaned.dom.text), `[${name}] cleaned: the ordinary content is all there`);
|
|
await browser.close();
|
|
}
|
|
console.log(fail ? `\nFAILED (${fail})` : `\nALL PASS (${pass})`);
|
|
process.exit(fail ? 1 : 0);
|