import { describe, expect, it } from 'vitest'; import { buildContentSecurityPolicy, createSecurityHeaders, serializeStaticHostHeaders, } from '@/security/security-policy'; import type { SiteConfig } from '@/content/types'; const jsonLdHash = 'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='; 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', }; describe('static-host security policy', () => { it('builds the exact restrictive CSP around the serialized JSON-LD hash', () => { expect(buildContentSecurityPolicy(jsonLdHash)).toBe( [ "default-src 'self'", `script-src 'self' '${jsonLdHash}'`, "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', ].join('; '), ); }); it('emits production security and revalidation headers without widening HSTS', () => { const headers = createSecurityHeaders(productionSite, jsonLdHash); expect(headers).toEqual({ csp: buildContentSecurityPolicy(jsonLdHash), contentTypeOptions: 'nosniff', referrerPolicy: 'no-referrer', permissionsPolicy: 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), accelerometer=(), gyroscope=(), magnetometer=()', frameOptions: 'DENY', cacheControl: 'public, max-age=0, must-revalidate', hsts: 'max-age=31536000', }); expect(headers.hsts).not.toContain('includeSubDomains'); expect(headers).not.toHaveProperty('xRobotsTag'); }); it('keeps preview responses private, noindex, and outside HSTS policy', () => { const headers = createSecurityHeaders(previewSite, jsonLdHash); expect(headers.cacheControl).toBe('private, no-store'); expect(headers.xRobotsTag).toBe('noindex, nofollow, noarchive'); expect(headers).not.toHaveProperty('hsts'); }); it.each([ "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' 'unsafe-inline", "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' *", 'sha256-not-base64!', "'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='", ])('rejects an invalid or policy-injecting JSON-LD hash: %s', (value) => { expect(() => buildContentSecurityPolicy(value)).toThrow(/JSON-LD CSP hash/i); }); it('serializes one exact response-header template with no wildcard or external source expressions', () => { const serialized = serializeStaticHostHeaders(productionSite, jsonLdHash); expect(serialized).toContain(`Content-Security-Policy: ${buildContentSecurityPolicy(jsonLdHash)}`); expect(serialized).toContain('Strict-Transport-Security: max-age=31536000'); expect(serialized).not.toMatch(/unsafe-inline|unsafe-eval|https?:\/\/|\*\.(?:js|css)/u); }); });