Files
menu/scripts/check-site.ts
T
tanshu af083415a7 Make the admin's buttons work in a browser
Every mutating form now carries the CSRF token as a hidden field, and the
upload route accepts it as a multipart field as well as a header. Driving the
real UI in Firefox found what the header-only tests could not: Create Draft,
Save, Publish, Delete and the up/down buttons all posted without a token and
were rejected. Adds npm run check-site, which verifies a running Site from the
outside and exits non-zero so it can gate a cutover.
2026-09-22 21:32:52 +05:30

126 lines
4.4 KiB
TypeScript

/**
* Checks a running Site from the outside, the way a customer and a phone would
* meet it. Read-only: it never signs in and never writes.
*
* npm run check-site -- https://menu-new.hopsngrains.com
*
* Exits non-zero if anything fails, so it can gate a cutover.
*/
const base = (process.argv[2] ?? 'http://127.0.0.1:3000').replace(/\/$/, '');
interface Result {
name: string;
ok: boolean;
detail: string;
}
const results: Result[] = [];
function record(name: string, ok: boolean, detail: string): void {
results.push({ name, ok, detail });
}
async function request(path: string): Promise<Response> {
return fetch(`${base}${path}`, { redirect: 'manual' });
}
async function checkPage(path: string, expected: string[]): Promise<string | null> {
let response: Response;
try {
response = await request(path);
} catch (error) {
record(`GET ${path}`, false, `unreachable: ${(error as Error).message}`);
return null;
}
const body = await response.text();
const missing = expected.filter((needle) => !body.includes(needle));
record(
`GET ${path}`,
response.status === 200 && missing.length === 0,
`status ${response.status}${missing.length === 0 ? '' : `, missing ${missing.join(', ')}`}`,
);
return body;
}
// --- The site answers at all -------------------------------------------------
const landing = await checkPage('/', ['<html', 'class="landing"']);
if (landing !== null && !/class="row"|The menus are being updated/.test(landing)) {
record('landing has menus or an empty state', false, 'neither Section rows nor the empty state');
}
await checkPage('/healthz', ['ok']);
await checkPage('/robots.txt', ['Disallow: /admin']);
await checkPage('/admin/login', ['type="password"']);
await checkPage('/assets/styles.css', ['--copper']);
await checkPage('/assets/app.js', ['data-zoom']);
await checkPage('/assets/fonts/archivo-latin.woff2', []);
const missingPage = await request('/definitely-not-here');
record('GET /definitely-not-here', missingPage.status === 404, `status ${missingPage.status}`);
// --- Each Section a customer can reach, and its images -----------------------
const sections = landing === null ? [] : [...landing.matchAll(/class="row" href="([^"]+)"/g)].map((m) => m[1]!);
if (sections.length === 0) {
record('at least one Section has a Current Menu Set', false, 'the landing page lists none');
}
for (const href of sections) {
const body = await checkPage(href, ['class="page"']);
if (body === null) continue;
const pages = [...body.matchAll(/data-page="(\d+)"/g)].map((match) => match[1]!);
record(`${href} shows Pages`, pages.length > 0, `${pages.length} page(s)`);
const media = body.match(/\/media\/[0-9a-f-]{36}\.[0-9a-f]+\.webp/);
if (media === null) {
record(`${href} links a Viewing image`, false, 'no media URL in the page');
continue;
}
const image = await request(media[0]);
const cache = image.headers.get('cache-control') ?? '';
record(
`GET ${media[0]}`,
image.status === 200 &&
(image.headers.get('content-type') ?? '').startsWith('image/webp') &&
cache.includes('immutable'),
`status ${image.status}, type ${image.headers.get('content-type')}, cache ${cache}`,
);
const jpeg = await request(media[0].replace(/\.webp$/, '.jpg'));
record(
`GET ${media[0].replace(/\.webp$/, '.jpg')}`,
jpeg.status === 200 && (jpeg.headers.get('content-type') ?? '').startsWith('image/jpeg'),
`status ${jpeg.status}, type ${jpeg.headers.get('content-type')}`,
);
const wrongExtension = await request(media[0].replace(/\.webp$/, '.jpeg'));
record(
'Originals are not reachable',
wrongExtension.status === 404,
`a non-variant extension gives ${wrongExtension.status}`,
);
const traversal = await request(`${media[0].replace('/media/', '/media/..%2Foriginals%2F')}`);
record(
'media paths cannot escape the set directory',
traversal.status === 404,
`status ${traversal.status}`,
);
}
// --- Report -----------------------------------------------------------------
const width = Math.max(...results.map((result) => result.name.length));
for (const result of results) {
console.log(`${result.ok ? 'ok ' : 'FAIL'} ${result.name.padEnd(width)} ${result.detail}`);
}
const failures = results.filter((result) => !result.ok);
console.log(`\n${results.length - failures.length}/${results.length} checks passed against ${base}`);
if (failures.length > 0) process.exitCode = 1;