Files
Portfolio/tests/integration/verify-artifacts.test.ts

363 lines
16 KiB
TypeScript

import { createHash } from 'node:crypto';
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { afterEach, describe, expect, it } from 'vitest';
import { loadProductionInputs } from '@/content/load-production-inputs';
import { createPageMetadata, createStaticHostArtifacts } from '@/content/metadata';
import { render } from '@/entry-server';
import { assemblePrerenderedHtml, type ViteManifest } from '../../scripts/prerender.mjs';
import { verifyArtifactDirectory } from '../../scripts/verify-artifacts.mjs';
import type { ProductionInputs, SiteConfig } from '@/content/types';
const repositoryRoot = process.cwd();
const locations = {
contentJson: pathToFileURL(resolve(repositoryRoot, 'content/public/portfolio-content.json')),
assetManifestJson: pathToFileURL(resolve(repositoryRoot, 'content/public/asset-manifest.json')),
preparedAssetRoot: pathToFileURL(`${resolve(repositoryRoot, 'inputs/prepared-assets')}/`),
siteConfigJson: pathToFileURL(resolve(repositoryRoot, 'config/site.preview.json')),
};
const manifest: ViteManifest = {
'index.html': {
file: 'assets/index-test.js',
src: 'index.html',
isEntry: true,
css: ['assets/index-test.css'],
},
};
const template = `<!doctype html><html lang="en"><head><meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Portfolio</title>
<!--portfolio-head-->
<script type="module" crossorigin src="/assets/index-test.js"></script>
<link rel="stylesheet" href="/assets/index-test.css"></head><body><div id="app"></div>
<!--portfolio-body--></body></html>`;
const clientJavaScript = 'export {};\n';
const clientStylesheet = 'body { margin: 0; }\n';
const sha256 = (value: string): string => createHash('sha256').update(value).digest('hex');
const wrapAppContents = (html: string, opening: string, closing: string): string => {
const appOpening = '<div id="app">';
const appContentStart = html.indexOf(appOpening) + appOpening.length;
const snapshotStart = html.indexOf('<script id="portfolio-inputs"');
const appContentEnd = html.lastIndexOf('</div>', snapshotStart);
return `${html.slice(0, appContentStart)}${opening}${html.slice(appContentStart, appContentEnd)}${closing}${html.slice(appContentEnd)}`;
};
const temporaryDirectories: string[] = [];
afterEach(async () => {
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })));
});
const writeValidArtifacts = async (site?: SiteConfig) => {
const loaded = await loadProductionInputs(locations);
const inputs: Readonly<ProductionInputs> = site === undefined ? loaded : { ...loaded, site };
const directory = await mkdtemp(join(tmpdir(), 'portfolio-artifacts-'));
temporaryDirectories.push(directory);
await mkdir(join(directory, 'assets'));
const html = assemblePrerenderedHtml(template, manifest, await render('/', inputs), inputs);
const hostArtifacts = createStaticHostArtifacts(createPageMetadata(inputs.content, inputs.manifest, inputs.site), inputs.site);
const publicAssets = inputs.manifest.assets
.map((asset) => ({
path: `assets/${asset.fileName}`,
bytesPath: resolve(repositoryRoot, 'inputs/prepared-assets', asset.fileName),
}))
.sort((left, right) => left.path.localeCompare(right.path));
const publicAssetBytes = await Promise.all(publicAssets.map(async (asset) => ({
...asset,
bytes: await readFile(asset.bytesPath),
})));
const contract = {
schema: 'portfolio-artifact-contract/v1',
buildId: inputs.site.buildId,
entryModule: 'assets/index-test.js',
executableModules: ['assets/index-test.js'],
modulePreloads: ['assets/index-test.js'],
styles: ['assets/index-test.css'],
viteAssets: [],
publicAssets: publicAssets.map((asset) => asset.path),
files: [
{ path: 'assets/index-test.css', sha256: sha256(clientStylesheet) },
{ path: 'assets/index-test.js', sha256: sha256(clientJavaScript) },
...publicAssetBytes.map((asset) => ({
path: asset.path,
sha256: createHash('sha256').update(asset.bytes).digest('hex'),
})),
],
} as const;
await Promise.all([
writeFile(join(directory, 'index.html'), html),
writeFile(join(directory, 'robots.txt'), hostArtifacts.robots),
writeFile(join(directory, '_headers.example'), hostArtifacts.headers),
writeFile(join(directory, '_redirects.example'), hostArtifacts.redirects),
writeFile(join(directory, 'assets/index-test.js'), clientJavaScript),
writeFile(join(directory, 'assets/index-test.css'), clientStylesheet),
...publicAssetBytes.map((asset) => writeFile(join(directory, asset.path), asset.bytes)),
...(hostArtifacts.sitemap === undefined
? []
: [writeFile(join(directory, 'sitemap.xml'), hostArtifacts.sitemap)]),
]);
return { directory, html, inputs, contract };
};
describe('artifact verification', () => {
it('accepts the complete private preview artifact set', async () => {
const { directory, inputs, contract } = await writeValidArtifacts();
await expect(verifyArtifactDirectory(directory, inputs, contract)).resolves.toBeUndefined();
});
it('accepts the pure production artifact path with canonical sitemap and allow policy', async () => {
const productionSite: SiteConfig = {
environment: 'production',
canonicalUrl: 'https://portfolio.example.test/',
buildId: 'production-test',
};
const { directory, inputs, contract } = await writeValidArtifacts(productionSite);
await expect(verifyArtifactDirectory(directory, inputs, contract)).resolves.toBeUndefined();
});
it.each([
{
name: 'an unexpected preview sitemap',
mutate: async (directory: string) => writeFile(join(directory, 'sitemap.xml'), '<urlset />'),
error: /preview.*sitemap/i,
},
{
name: 'a source map',
mutate: async (directory: string) => writeFile(join(directory, 'assets/index-test.js.map'), '{}'),
error: /source map|unexpected artifact/i,
},
{
name: 'an unknown artifact',
mutate: async (directory: string) => writeFile(join(directory, 'private-notes.txt'), 'notes'),
error: /unexpected artifact/i,
},
{
name: 'a prohibited private surface',
mutate: async (directory: string) => writeFile(join(directory, 'assets/index-test.js'), 'const evidenceId = "fixture";'),
error: /prohibited surface|digest does not match/i,
},
])('rejects $name', async ({ mutate, error }) => {
const { directory, inputs, contract } = await writeValidArtifacts();
await mutate(directory);
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(error);
});
it.each([
{
name: 'duplicate canonical metadata',
mutate: (html: string) => html.replace('</head>', '<link rel="canonical" href="https://portfolio.localhost/" /></head>'),
error: /exactly one canonical/i,
},
{
name: 'duplicate H1 identity',
mutate: (html: string) => html.replace('</main>', '<h1>Luca Pîslaru</h1></main>'),
error: /exactly one (?:visible )?h1/i,
},
{
name: 'malformed JSON-LD',
mutate: (html: string) => html.replace(/<script type="application\/ld\+json">.*?<\/script>/u, '<script type="application/ld+json">{</script>'),
error: /json-ld/i,
},
{
name: 'missing infrastructure evidence',
mutate: (html: string) => html.replace('id="infrastructure"', 'id="removed-infrastructure"'),
error: /section.*infrastructure|visible sections/i,
},
{
name: 'executable snapshot assignment',
mutate: (html: string) => html.replace('<script id="portfolio-inputs" type="application/json">', '<script>window.__PORTFOLIO_INPUTS__='),
error: /inert input snapshot|prohibited surface/i,
},
])('rejects raw HTML with $name', async ({ mutate, error }) => {
const { directory, html, inputs, contract } = await writeValidArtifacts();
await writeFile(join(directory, 'index.html'), mutate(html));
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(error);
});
it('rejects a missing crawl artifact', async () => {
const { directory, inputs, contract } = await writeValidArtifacts();
await rm(join(directory, 'robots.txt'));
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(/robots/i);
});
it('checks the CSP hash against the actual serialized JSON-LD bytes', async () => {
const { directory, inputs, contract } = await writeValidArtifacts();
const headersPath = join(directory, '_headers.example');
const headers = await readFile(headersPath, 'utf8');
await writeFile(headersPath, headers.replace(/sha256-[A-Za-z0-9+/=]+/u, 'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='));
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(/csp.*json-ld|json-ld.*csp|exact.*headers/i);
});
it.each([
{
name: 'a final-HTML-authorized substitute module',
mutate: async (directory: string, html: string) => {
await rm(join(directory, 'assets/index-test.js'));
await writeFile(join(directory, 'assets/substitute.js'), clientJavaScript);
return html.replaceAll('assets/index-test.js', 'assets/substitute.js');
},
error: /trusted.*module|artifact contract|unexpected artifact/i,
},
{
name: 'an extra inline executable script',
mutate: async (_directory: string, html: string) => html.replace('</body>', '<script>void 0</script></body>'),
error: /unexpected executable script/i,
},
{
name: 'an extra external executable script',
mutate: async (_directory: string, html: string) => html.replace('</body>', '<script src="https://example.test/evil.js"></script></body>'),
error: /unexpected executable script|external executable/i,
},
{
name: 'a final-HTML-authorized extra stylesheet',
mutate: async (directory: string, html: string) => {
await writeFile(join(directory, 'assets/extra.css'), 'body {}');
return html.replace('</head>', '<link rel="stylesheet" href="/assets/extra.css" /></head>');
},
error: /trusted.*style|artifact contract|unexpected artifact/i,
},
{
name: 'a missing trusted module preload',
mutate: async (_directory: string, html: string) => html.replace(/<link rel="modulepreload"[^>]+>\n?/u, ''),
error: /trusted.*preload|module preload/i,
},
])('rejects $name instead of trusting final HTML', async ({ mutate, error }) => {
const { directory, html, inputs, contract } = await writeValidArtifacts();
await writeFile(join(directory, 'index.html'), await mutate(directory, html));
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(error);
});
it.each([
{
name: 'an appended CSP directive',
mutate: (headers: string) => `${headers} script-src https://example.test\n`,
},
{
name: 'an external script-src relaxation',
mutate: (headers: string) => headers.replace("script-src 'self'", "script-src 'self' https://example.test"),
},
{
name: 'a duplicate partial CSP policy',
mutate: (headers: string) => `${headers} Content-Security-Policy: default-src 'self'\n`,
},
{
name: 'commented header impostors',
mutate: (headers: string) => headers.split('\n').map((line) => `# ${line}`).join('\n'),
},
])('rejects $name by exact header bytes', async ({ mutate }) => {
const { directory, inputs, contract } = await writeValidArtifacts();
const path = join(directory, '_headers.example');
await writeFile(path, mutate(await readFile(path, 'utf8')));
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(/exact.*headers|headers.*exact/i);
});
it('rejects modified redirect example bytes', async () => {
const { directory, inputs, contract } = await writeValidArtifacts();
const path = join(directory, '_redirects.example');
await writeFile(path, `${await readFile(path, 'utf8')}# appended\n`);
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(/exact.*redirect|redirect.*exact/i);
});
it('rejects modified production sitemap bytes', async () => {
const productionSite: SiteConfig = {
environment: 'production',
canonicalUrl: 'https://portfolio.example.test/',
buildId: 'production-test',
};
const { directory, inputs, contract } = await writeValidArtifacts(productionSite);
const path = join(directory, 'sitemap.xml');
await writeFile(path, `${await readFile(path, 'utf8')}<!-- appended -->\n`);
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(/exact.*sitemap|sitemap.*exact/i);
});
it.each([
{
name: 'a hidden application root',
mutate: (html: string) => html.replace('<div id="app">', '<div id="app" hidden>'),
},
{
name: 'identity rendered only inside a template',
mutate: (html: string) => html.replace(
/(<h1[^>]*>Luca Pîslaru<\/h1><p class="hero-journey__role">System Administrator &amp; Full-Stack Developer<\/p>)/u,
'<template>$1</template>',
),
},
{
name: 'an aria-hidden native navigation',
mutate: (html: string) => html.replace('<nav class="site-navigation"', '<nav aria-hidden="true" class="site-navigation"'),
},
{
name: 'an inert canonical section',
mutate: (html: string) => html.replace('<section id="infrastructure"', '<section id="infrastructure" inert'),
},
{
name: 'infrastructure evidence present only in hydration JSON',
mutate: (html: string) => {
const snapshotAt = html.indexOf('<script id="portfolio-inputs"');
return `${html.slice(0, snapshotAt).replaceAll('Secure self-hosted platform and lifecycle', 'Removed evidence')}${html.slice(snapshotAt)}`;
},
},
{
name: 'all semantic SSR inside a closed dialog',
mutate: (html: string) => wrapAppContents(html, '<dialog>', '</dialog>'),
},
{
name: 'required evidence outside the summary of closed details',
mutate: (html: string, inputs: Readonly<ProductionInputs>) => {
const evidence = inputs.content.infrastructure.bands[1]!.summary;
return html.replace(evidence, `<details><summary>Infrastructure evidence</summary><span>${evidence}</span></details>`);
},
},
{
name: 'mixed-case true aria-hidden navigation',
mutate: (html: string) => html.replace(
'<nav class="site-navigation"',
'<nav aria-hidden="TRUE" class="site-navigation"',
),
},
])('rejects $name as non-visible semantic evidence', async ({ mutate }) => {
const { directory, html, inputs, contract } = await writeValidArtifacts();
await writeFile(join(directory, 'index.html'), mutate(html, inputs));
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(/visible semantic|visible h1|visible navigation|visible section|connected/i);
});
it.each([
{
name: 'semantic SSR inside an open dialog',
mutate: (html: string, _inputs: Readonly<ProductionInputs>) =>
wrapAppContents(html, '<dialog open>', '</dialog>'),
},
{
name: 'required evidence inside open details',
mutate: (html: string, inputs: Readonly<ProductionInputs>) => {
const evidence = inputs.content.infrastructure.bands[1]!.summary;
return html.replace(evidence, `<details open><summary>Infrastructure evidence</summary><span>${evidence}</span></details>`);
},
},
{
name: 'required evidence inside the active summary of closed details',
mutate: (html: string, inputs: Readonly<ProductionInputs>) => {
const evidence = inputs.content.infrastructure.bands[1]!.summary;
return html.replace(evidence, `<details><summary>${evidence}</summary><span>Collapsed evidence</span></details>`);
},
},
])('accepts $name as visible semantic evidence', async ({ mutate }) => {
const { directory, html, inputs, contract } = await writeValidArtifacts();
await writeFile(join(directory, 'index.html'), mutate(html, inputs));
await expect(verifyArtifactDirectory(directory, inputs, contract)).resolves.toBeUndefined();
});
});