/** * 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 { return fetch(`${base}${path}`, { redirect: 'manual' }); } async function checkPage(path: string, expected: string[]): Promise { 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('/', [' 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;