/** * Security Headers Suite — Task A (HSTS / HTTPS-only) * * Verifies next.config.ts: the Strict-Transport-Security header is configured * with the full "max-age=63072000; includeSubDomains; preload" directive, is * emitted only when the request actually arrived over HTTPS * (x-forwarded-proto: https), and the remaining security headers stay * unconditional on the catch-all rule. Pure config logic — no database or * running server required. */ import { describe, test, expect } from "./runner"; import configModule from "../../next.config"; // Under tsx (ESM) the CJS-style default export can arrive wrapped as // `{ default: nextConfig }`; unwrap it so `config.headers()` is callable in // either interop mode. const config = ((configModule as { default?: typeof configModule }).default ?? configModule) as typeof configModule; interface HeaderItem { key: string; value: string; } interface HeaderCondition { type: string; key: string; value?: string; } interface HeaderRule { source: string; has?: HeaderCondition[]; headers: HeaderItem[]; } async function catchAllRules(): Promise { // `headers` is optional on the NextConfig type; next.config.ts always defines it. const rules = (await config.headers!()) as HeaderRule[]; return rules.filter((rule) => rule.source === "/(.*)"); } function findHstsRule(rules: HeaderRule[]): HeaderRule | undefined { return rules.find((rule) => rule.headers.some( (header) => header.key.toLowerCase() === "strict-transport-security" ) ); } describe("Security headers — HSTS (HTTPS-only)", () => { test("the Strict-Transport-Security header is configured on the catch-all rule", async () => { const rules = await catchAllRules(); expect(findHstsRule(rules)).toBeDefined(); }); test("HSTS carries max-age=63072000, includeSubDomains and preload", async () => { const rule = findHstsRule(await catchAllRules()); expect(rule).toBeDefined(); const hsts = rule!.headers.find( (header) => header.key.toLowerCase() === "strict-transport-security" ); expect(hsts).toBeDefined(); expect(hsts!.value).toContain("max-age=63072000"); expect(hsts!.value).toContain("includeSubDomains"); expect(hsts!.value).toContain("preload"); }); test("HSTS is emitted only when the request arrived over HTTPS (x-forwarded-proto)", async () => { const rule = findHstsRule(await catchAllRules()); expect(rule).toBeDefined(); expect(rule!.has).toBeDefined(); const conditioned = rule!.has!.some( (condition) => condition.type === "header" && condition.key.toLowerCase() === "x-forwarded-proto" && condition.value === "https" ); expect(conditioned).toBe(true); }); test("the remaining security headers stay unconditional on the catch-all rule", async () => { const rules = await catchAllRules(); const unconditional = rules.filter((rule) => !rule.has); expect(unconditional.length).toBeGreaterThan(0); const allHeaders = unconditional.flatMap((rule) => rule.headers); const keys = new Set(allHeaders.map((header) => header.key.toLowerCase())); expect(keys.has("x-frame-options")).toBe(true); expect(keys.has("x-content-type-options")).toBe(true); expect(keys.has("referrer-policy")).toBe(true); expect(keys.has("permissions-policy")).toBe(true); expect(keys.has("content-security-policy")).toBe(true); const xfo = allHeaders.find( (header) => header.key.toLowerCase() === "x-frame-options" ); const xcto = allHeaders.find( (header) => header.key.toLowerCase() === "x-content-type-options" ); expect(xfo).toBeDefined(); expect(xcto).toBeDefined(); expect(xfo!.value).toBe("DENY"); expect(xcto!.value).toBe("nosniff"); expect( allHeaders.some( (header) => header.key.toLowerCase() === "referrer-policy" && header.value.includes("strict-origin-when-cross-origin") ) ).toBe(true); expect( allHeaders.some( (header) => header.key.toLowerCase() === "content-security-policy" && header.value.length > 0 ) ).toBe(true); }); });