Files
Portfolio/tests/integration/nginx-config.test.ts

179 lines
6.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import type { SiteConfig } from '@/content/types';
import {
parseHashedArtifactManifest,
renderNginxConfig,
// @ts-expect-error The executable ESM build script intentionally has no publishable declaration file.
} from '../../scripts/generate-host-config.mjs';
const jsonLdHash = 'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
const releaseManifestDigest = 'd'.repeat(64);
const productionSite: SiteConfig = {
environment: 'production',
canonicalUrl: 'https://portfolio.example.test/',
buildId: 'release-1',
};
const previewSite: SiteConfig = {
environment: 'preview',
canonicalUrl: 'https://portfolio.localhost/',
buildId: 'preview-1',
};
const manifest = {
schema: 'portfolio-host-artifact-manifest/v1',
files: [
{ path: 'assets/app-a1b2c3d4.css', sha256: 'b'.repeat(64) },
{ path: 'assets/app-a1b2c3d4.js', sha256: 'a'.repeat(64) },
{ path: 'assets/demo-c3d4e5f6.webm', sha256: 'c'.repeat(64) },
],
};
describe('generated Nginx static-host contract', () => {
it('binds immutable caching to the exact hashed artifact manifest', () => {
const config = renderNginxConfig({
site: productionSite,
jsonLdCspHash: jsonLdHash,
artifactManifest: manifest,
documentRoot: '/srv/portfolio/current',
alternateHosts: ['www.portfolio.example.test'],
tlsCertificatePath: '/etc/portfolio/tls/fullchain.pem',
tlsKeyPath: '/etc/portfolio/tls/privkey.pem',
manifestDigest: releaseManifestDigest,
});
for (const file of manifest.files) expect(config).toContain(`~^/${file.path.replaceAll('.', '\\.')}$`);
expect(config).toContain('public, max-age=31536000, immutable');
expect(config).toContain('public, max-age=0, must-revalidate');
expect(config).toContain(
'Requires standard MIME mappings and static-file defaults in the parent Nginx http context.',
);
expect(config).not.toContain('include /etc/nginx/mime.types;');
expect(config).not.toContain('default_type application/octet-stream;');
expect(config).not.toContain('sendfile on;');
expect(config).toContain('max_ranges 1;');
expect(config).not.toContain('max_ranges 0');
});
it('redirects HTTP and an alternate HTTPS host directly to the canonical HTTPS host', () => {
const config = renderNginxConfig({
site: productionSite,
jsonLdCspHash: jsonLdHash,
artifactManifest: manifest,
documentRoot: '/srv/portfolio/current',
alternateHosts: ['www.portfolio.example.test'],
tlsCertificatePath: '/etc/portfolio/tls/fullchain.pem',
tlsKeyPath: '/etc/portfolio/tls/privkey.pem',
manifestDigest: releaseManifestDigest,
});
expect(config).toContain('listen 80;');
expect(config).toContain('server_name portfolio.example.test www.portfolio.example.test;');
expect(config.match(/return 308 https:\/\/portfolio\.example\.test\$request_uri;/gu)).toHaveLength(2);
expect(config).not.toMatch(/return 30[127] /u);
expect(config).not.toContain('http://portfolio.example.test');
});
it('applies exact browser headers and production indexability without public source maps', () => {
const config = renderNginxConfig({
site: productionSite,
jsonLdCspHash: jsonLdHash,
artifactManifest: manifest,
documentRoot: '/srv/portfolio/current',
alternateHosts: [],
tlsCertificatePath: '/etc/portfolio/tls/fullchain.pem',
tlsKeyPath: '/etc/portfolio/tls/privkey.pem',
manifestDigest: releaseManifestDigest,
});
expect(config).toContain("default-src 'self'; script-src 'self' 'sha256-");
expect(config).toContain("img-src 'self' data:");
expect(config).toContain("connect-src 'self'");
expect(config).toContain('add_header Strict-Transport-Security "max-age=31536000" always;');
expect(config).toContain(`add_header X-Portfolio-Manifest-Digest "${releaseManifestDigest}" always;`);
expect(config).toContain('gzip on;');
expect(config).toContain('gzip_min_length 1;');
expect(config).toContain('gzip_vary on;');
expect(config).toContain('gzip_types text/css application/javascript application/json image/svg+xml;');
expect(config).not.toContain('includeSubDomains');
expect(config).not.toContain('X-Robots-Tag');
expect(config).not.toMatch(/unsafe-inline|unsafe-eval|sourceMappingURL|\.map(?:\s|$)/u);
});
it('makes preview responses private and noindex without a sitemap or HSTS', () => {
const config = renderNginxConfig({
site: previewSite,
jsonLdCspHash: jsonLdHash,
artifactManifest: manifest,
documentRoot: '/srv/portfolio/preview',
alternateHosts: [],
});
expect(config).toContain('add_header X-Robots-Tag "noindex, nofollow, noarchive" always;');
expect(config).toContain('private, no-store');
expect(config).not.toContain('Strict-Transport-Security');
expect(config).not.toContain('sitemap.xml');
expect(config).not.toContain('X-Portfolio-Manifest-Digest');
});
it('rejects production configuration without the signed manifest digest', () => {
expect(() => renderNginxConfig({
site: productionSite,
jsonLdCspHash: jsonLdHash,
artifactManifest: manifest,
documentRoot: '/srv/portfolio/current',
alternateHosts: [],
tlsCertificatePath: '/etc/portfolio/tls/fullchain.pem',
tlsKeyPath: '/etc/portfolio/tls/privkey.pem',
})).toThrow(/manifest digest/i);
});
it.each([
{
name: 'a source map',
value: {
...manifest,
files: [{ path: 'assets/app-a1b2c3d4.js.map', sha256: 'a'.repeat(64) }],
},
},
{
name: 'an external or unsafe path',
value: {
...manifest,
files: [{ path: '../app-a1b2c3d4.js', sha256: 'a'.repeat(64) }],
},
},
{
name: 'a non-canonical digest',
value: {
...manifest,
files: [{ path: 'assets/app-a1b2c3d4.js', sha256: 'A'.repeat(64) }],
},
},
])('rejects $name in the hashed artifact manifest', ({ value }) => {
expect(() => parseHashedArtifactManifest(value)).toThrow(/artifact manifest/i);
});
it('rejects document-root values that cannot be emitted as one Nginx token', () => {
expect(() =>
renderNginxConfig({
site: previewSite,
jsonLdCspHash: jsonLdHash,
artifactManifest: manifest,
documentRoot: '/srv/portfolio/injected root',
alternateHosts: [],
}),
).toThrow(/document root/i);
});
it('ships a domain-neutral example instead of inventing Luca’s production hostname', () => {
const example = readFileSync(resolve(process.cwd(), 'nginx.portfolio.conf.example'), 'utf8');
expect(example).toContain('__CANONICAL_HOST__');
expect(example).toContain('__JSON_LD_CSP_HASH__');
expect(example).toContain('__MANIFEST_DIGEST__');
expect(example).not.toContain('portfolio.example.test');
expect(example).not.toMatch(/server_name\s+[a-z0-9.-]+\.[a-z]{2,}/iu);
});
});