Files
Portfolio/tests/deployment/seo.spec.ts

83 lines
5.7 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 html = `<!doctype html><html><head>
<title>Luca Pîslaru — System Administrator &amp; Full-Stack Developer</title>
<meta name="description" content="Systems administration and full-stack development portfolio." />
<meta name="robots" content="index, follow" />
<link rel="canonical" href="${canonical}" />
<meta property="og:title" content="Luca Pîslaru" />
<meta property="og:description" content="Systems administration and full-stack development portfolio." />
<meta property="og:url" content="${canonical}" />
<script type="application/ld+json">{"@context":"https://schema.org","@type":"Person","name":"Luca Pîslaru","url":"${canonical}"}</script>
<script type="module" src="/assets/app.12345678.js"></script></head>
<body><main><h1>Luca Pîslaru</h1><p>System Administrator &amp; 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())))));
async function fixtureFetch() {
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 'none'; 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(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/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 });
});
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);
});
});