271 lines
14 KiB
TypeScript
271 lines
14 KiB
TypeScript
import { createServer } from 'node:http';
|
|
import { brotliCompressSync, gzipSync } from 'node:zlib';
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
|
|
// @ts-expect-error The production smoke checker is an executable ESM module.
|
|
import { verifyProductionSmoke } from '../../scripts/smoke-production.mjs';
|
|
|
|
const digest = 'a'.repeat(64);
|
|
const canonical = 'https://portfolio.example/';
|
|
const alternate = 'http://www.portfolio.example/';
|
|
const escapedTitle = 'Luca Pîslaru — System Administrator & Full-Stack Developer';
|
|
const description = 'Portfolio of Luca Pîslaru, a Bucharest-based System Administrator and Full-Stack Developer working across Linux infrastructure and end-to-end software delivery.';
|
|
const socialImage = `${canonical}assets/project-social-preview.webp`;
|
|
const projectStillMaxBytes = 400 * 1024;
|
|
const socialImageFixture = Buffer.alloc(15_068);
|
|
const html = `<!doctype html><html><head>
|
|
<title>${escapedTitle}</title>
|
|
<meta name="description" content="${description}" />
|
|
<meta name="robots" content="index, follow" />
|
|
<link rel="canonical" href="${canonical}" />
|
|
<meta property="og:type" content="profile" />
|
|
<meta property="og:title" content="${escapedTitle}" />
|
|
<meta property="og:description" content="${description}" />
|
|
<meta property="og:url" content="${canonical}" />
|
|
<meta property="og:image" content="${socialImage}" />
|
|
<meta name="twitter:card" content="summary_large_image" />
|
|
<meta name="twitter:title" content="${escapedTitle}" />
|
|
<meta name="twitter:description" content="${description}" />
|
|
<meta name="twitter:image" content="${socialImage}" />
|
|
<script type="application/ld+json">{"@context":"https://schema.org","@type":"ProfilePage","name":"Luca Pîslaru","description":"${description}","url":"${canonical}","mainEntity":{"@type":"Person","name":"Luca Pîslaru","jobTitle":"System Administrator & Full-Stack Developer","description":"${description}","url":"${canonical}","email":"mailto:luca.pislaru@gmail.com","homeLocation":{"@type":"Place","address":{"@type":"PostalAddress","addressLocality":"Bucharest","addressCountry":"RO"}},"sameAs":["https://github.com/LucaPisl","https://gitlab.com/luca_pisl","https://linkedin.com/in/luca-pisl"]}}</script>
|
|
<script type="module" src="/assets/app.12345678.js"></script></head>
|
|
<body><main><h1>Luca Pîslaru</h1><p>System Administrator & Full-Stack Developer</p><section><h2>Selected builds</h2><p>Evidence-led project work.</p></section><section><h2>Contact</h2><p>Discuss systems and software work.</p><a href="mailto:hello@example.com">Contact</a></section></main></body></html>`;
|
|
|
|
const servers: ReturnType<typeof createServer>[] = [];
|
|
afterEach(async () => Promise.all(servers.splice(0).map((server) => new Promise<void>((resolve) => server.close(() => resolve())))));
|
|
|
|
interface FixtureOptions {
|
|
html?: string;
|
|
imageStatus?: number;
|
|
imageType?: string;
|
|
imageContentLength?: string | null;
|
|
imageChunks?: readonly Buffer[];
|
|
}
|
|
|
|
const mutateJsonLd = (source: string, mutate: (graph: Record<string, any>) => void): string =>
|
|
source.replace(
|
|
/(<script type="application\/ld\+json">)([\s\S]*?)(<\/script>)/u,
|
|
(_match, opening: string, json: string, closing: string) => {
|
|
const graph = JSON.parse(json) as Record<string, any>;
|
|
mutate(graph);
|
|
return `${opening}${JSON.stringify(graph)}${closing}`;
|
|
},
|
|
);
|
|
|
|
async function fixtureFetch(options: FixtureOptions = {}) {
|
|
const server = createServer((request, response) => {
|
|
const original = request.headers['x-original-url'];
|
|
const url = new URL(String(original));
|
|
if (url.origin === new URL(alternate).origin) {
|
|
response.writeHead(308, { location: canonical }); response.end(); return;
|
|
}
|
|
const base = {
|
|
'x-portfolio-manifest-digest': digest,
|
|
'content-security-policy': "default-src 'self'; script-src 'self' 'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='; style-src 'self'; img-src 'self' data:; media-src 'self'; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests",
|
|
'strict-transport-security': 'max-age=31536000',
|
|
'x-content-type-options': 'nosniff',
|
|
'referrer-policy': 'no-referrer',
|
|
'permissions-policy': 'camera=(), microphone=(), geolocation=()',
|
|
'x-frame-options': 'DENY',
|
|
};
|
|
if (url.pathname === '/') {
|
|
response.writeHead(200, { ...base, 'content-type': 'text/html; charset=utf-8', 'cache-control': 'public, max-age=0, must-revalidate', 'content-encoding': 'br' }); response.end(brotliCompressSync(options.html ?? html)); return;
|
|
}
|
|
if (url.pathname === '/robots.txt') {
|
|
response.writeHead(200, { ...base, 'content-type': 'text/plain', 'cache-control': 'public, max-age=0, must-revalidate' }); response.end(`User-agent: *\nAllow: /\nSitemap: ${canonical}sitemap.xml\n`); return;
|
|
}
|
|
if (url.pathname === '/sitemap.xml') {
|
|
response.writeHead(200, { ...base, 'content-type': 'application/xml', 'cache-control': 'public, max-age=0, must-revalidate' }); response.end(`<?xml version="1.0"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>${canonical}</loc></url></urlset>`); return;
|
|
}
|
|
if (url.pathname === '/assets/app.12345678.js') {
|
|
response.writeHead(200, { ...base, 'content-type': 'text/javascript', 'cache-control': 'public, max-age=31536000, immutable', 'content-encoding': 'gzip' }); response.end(gzipSync('console.log("portfolio")')); return;
|
|
}
|
|
if (url.pathname === '/assets/project-social-preview.webp') {
|
|
const status = options.imageStatus ?? 200;
|
|
const chunks = status === 200 ? options.imageChunks ?? [socialImageFixture] : [];
|
|
const contentLength = options.imageContentLength === undefined
|
|
? String(chunks.reduce((total, chunk) => total + chunk.byteLength, 0))
|
|
: options.imageContentLength;
|
|
const headers: Record<string, string> = {
|
|
...base,
|
|
'content-type': options.imageType ?? 'image/webp',
|
|
'cache-control': 'public, max-age=31536000, immutable',
|
|
};
|
|
if (contentLength !== null) headers['content-length'] = contentLength;
|
|
response.writeHead(status, headers);
|
|
for (const chunk of chunks) response.write(chunk);
|
|
response.end();
|
|
return;
|
|
}
|
|
if (url.pathname === '/assets/demo.12345678.webm') {
|
|
if (request.headers.range !== 'bytes=0-1023') { response.writeHead(416, base); response.end(); return; }
|
|
response.writeHead(206, { ...base, 'content-type': 'video/webm', 'cache-control': 'public, max-age=31536000, immutable', 'accept-ranges': 'bytes', 'content-range': 'bytes 0-1023/2048', 'content-length': '1024' }); response.end(Buffer.alloc(1024)); return;
|
|
}
|
|
response.writeHead(404, base); response.end();
|
|
});
|
|
servers.push(server);
|
|
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
const address = server.address();
|
|
if (address === null || typeof address === 'string') throw new Error('fixture server did not bind');
|
|
return async (input: string | URL | Request, init?: RequestInit) => {
|
|
const requested = input instanceof Request ? input.url : input.toString();
|
|
const headers = new Headers(input instanceof Request ? input.headers : init?.headers);
|
|
headers.set('x-original-url', requested);
|
|
return fetch(`http://127.0.0.1:${address.port}${new URL(requested).pathname}`, { ...init, redirect: 'manual', headers });
|
|
};
|
|
}
|
|
|
|
describe('production smoke and SEO contract', () => {
|
|
it('verifies canonical metadata, response policy, compression, MIME, ranges, and one-hop redirects', async () => {
|
|
const fetchImpl = await fixtureFetch();
|
|
await expect(verifyProductionSmoke({ canonicalUrl: canonical, alternateUrls: [alternate], expectedManifestDigest: digest, videoPath: '/assets/demo.12345678.webm', fetchImpl })).resolves.toMatchObject({ manifestDigest: digest, canonicalUrl: canonical, jsDisabledContentVerified: true, socialImageVerified: true });
|
|
});
|
|
|
|
it('rejects a deployed digest mismatch and redirect chains', async () => {
|
|
const fetchImpl = await fixtureFetch();
|
|
await expect(verifyProductionSmoke({ canonicalUrl: canonical, alternateUrls: [alternate], expectedManifestDigest: 'b'.repeat(64), videoPath: '/assets/demo.12345678.webm', fetchImpl })).rejects.toThrow(/manifest-digest|manifest digest/u);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: 'a missing Twitter image tag',
|
|
html: html.replace(/<meta name="twitter:image"[^>]+>\n/u, ''),
|
|
error: /twitter:image|metadata/iu,
|
|
},
|
|
{
|
|
name: 'a missing Open Graph image tag',
|
|
html: html.replace(/<meta property="og:image"[^>]+>\n/u, ''),
|
|
error: /og:image|metadata/iu,
|
|
},
|
|
{
|
|
name: 'a duplicate Open Graph title tag',
|
|
html: html.replace('</head>', `<meta property="og:title" content="${escapedTitle}" /></head>`),
|
|
error: /exactly one og:title/iu,
|
|
},
|
|
{
|
|
name: 'a mismatched Twitter title',
|
|
html: html.replace(`<meta name="twitter:title" content="${escapedTitle}" />`, '<meta name="twitter:title" content="Different role" />'),
|
|
error: /twitter:title|metadata/iu,
|
|
},
|
|
{
|
|
name: 'a mismatched Open Graph canonical URL',
|
|
html: html.replace(`<meta property="og:url" content="${canonical}" />`, '<meta property="og:url" content="https://other.example/" />'),
|
|
error: /open graph|canonical|metadata/iu,
|
|
},
|
|
{
|
|
name: 'a duplicate Twitter card tag',
|
|
html: html.replace('</head>', '<meta name="twitter:card" content="summary_large_image" /></head>'),
|
|
error: /exactly one twitter:card/iu,
|
|
},
|
|
{
|
|
name: 'a missing Person mainEntity',
|
|
html: mutateJsonLd(html, (graph) => { delete graph.mainEntity; }),
|
|
error: /mainEntity|profilePage|json-ld/iu,
|
|
},
|
|
{
|
|
name: 'a missing public homeLocation',
|
|
html: mutateJsonLd(html, (graph) => { delete graph.mainEntity.homeLocation; }),
|
|
error: /homeLocation|location|json-ld/iu,
|
|
},
|
|
{
|
|
name: 'missing public sameAs profiles',
|
|
html: mutateJsonLd(html, (graph) => { delete graph.mainEntity.sameAs; }),
|
|
error: /sameAs|profile|json-ld/iu,
|
|
},
|
|
{
|
|
name: 'a forbidden portrait field',
|
|
html: mutateJsonLd(html, (graph) => { graph.mainEntity.image = socialImage; }),
|
|
error: /image|portrait|json-ld|structured/iu,
|
|
},
|
|
{
|
|
name: 'private address detail',
|
|
html: mutateJsonLd(html, (graph) => { graph.mainEntity.homeLocation.address.streetAddress = 'Private street'; }),
|
|
error: /address|location|json-ld|structured/iu,
|
|
},
|
|
{
|
|
name: 'a forbidden postal code',
|
|
html: mutateJsonLd(html, (graph) => { graph.mainEntity.homeLocation.address.postalCode = '000000'; }),
|
|
error: /address|location|json-ld|structured/iu,
|
|
},
|
|
{
|
|
name: 'a forbidden telephone',
|
|
html: mutateJsonLd(html, (graph) => { graph.mainEntity.telephone = '+40000000000'; }),
|
|
error: /person|json-ld|structured/iu,
|
|
},
|
|
{
|
|
name: 'a fabricated interaction statistic',
|
|
html: mutateJsonLd(html, (graph) => { graph.mainEntity.interactionStatistic = { '@type': 'InteractionCounter', userInteractionCount: 1 }; }),
|
|
error: /person|json-ld|structured/iu,
|
|
},
|
|
{
|
|
name: 'a duplicate JSON-LD graph',
|
|
html: html.replace('</head>', `${html.match(/<script type="application\/ld\+json">[\s\S]*?<\/script>/u)![0]}</head>`),
|
|
error: /exactly one json-ld/iu,
|
|
},
|
|
{
|
|
name: 'malformed JSON-LD',
|
|
html: html.replace(/(<script type="application\/ld\+json">)[\s\S]*?(<\/script>)/u, '$1{$2'),
|
|
error: /json-ld.*valid|valid json/iu,
|
|
},
|
|
{
|
|
name: 'an off-host social image',
|
|
html: html.replaceAll(socialImage, 'https://cdn.example.test/social-preview.webp'),
|
|
error: /social image|canonical host|same host/iu,
|
|
},
|
|
])('rejects $name', async ({ html: mutatedHtml, error }) => {
|
|
const fetchImpl = await fixtureFetch({ html: mutatedHtml });
|
|
|
|
await expect(verifyProductionSmoke({
|
|
canonicalUrl: canonical,
|
|
expectedManifestDigest: digest,
|
|
fetchImpl,
|
|
})).rejects.toThrow(error);
|
|
});
|
|
|
|
it.each([
|
|
{ name: 'a missing social image response', options: { imageStatus: 404 }, error: /social image.*200|social image.*return/iu },
|
|
{ name: 'a non-image social response', options: { imageType: 'text/plain' }, error: /social image.*content-type|image mime/iu },
|
|
{ name: 'a MIME that disagrees with the approved WebP', options: { imageType: 'image/png' }, error: /social image.*content-type|image mime/iu },
|
|
])('rejects $name', async ({ options, error }) => {
|
|
const fetchImpl = await fixtureFetch(options);
|
|
|
|
await expect(verifyProductionSmoke({
|
|
canonicalUrl: canonical,
|
|
expectedManifestDigest: digest,
|
|
fetchImpl,
|
|
})).rejects.toThrow(error);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: 'a zero-byte body',
|
|
options: { imageChunks: [], imageContentLength: '0' },
|
|
error: /social image.*empty|zero-byte/iu,
|
|
},
|
|
{
|
|
name: 'a declared body above the project-still limit',
|
|
options: {
|
|
imageChunks: [Buffer.alloc(projectStillMaxBytes + 1)],
|
|
imageContentLength: String(projectStillMaxBytes + 1),
|
|
},
|
|
error: /social image.*(?:limit|large|400)/iu,
|
|
},
|
|
{
|
|
name: 'an undeclared chunked body above the project-still limit',
|
|
options: {
|
|
imageChunks: [Buffer.alloc(projectStillMaxBytes), Buffer.alloc(1)],
|
|
imageContentLength: null,
|
|
},
|
|
error: /social image.*(?:limit|large|400)/iu,
|
|
},
|
|
])('rejects $name without buffering the social image', async ({ options, error }) => {
|
|
const fetchImpl = await fixtureFetch(options);
|
|
|
|
await expect(verifyProductionSmoke({
|
|
canonicalUrl: canonical,
|
|
expectedManifestDigest: digest,
|
|
fetchImpl,
|
|
})).rejects.toThrow(error);
|
|
});
|
|
});
|