release: verify production portfolio deployment
This commit is contained in:
82
tests/deployment/seo.spec.ts
Normal file
82
tests/deployment/seo.spec.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
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 & 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 & 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user