import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { contentSecurityPolicy, resolveApiOrigin } from "./csp.ts"; describe("resolveApiOrigin", () => { it("returns null for empty or same-origin API URL", () => { assert.equal(resolveApiOrigin("", "http://localhost:5174"), null); assert.equal(resolveApiOrigin("http://localhost:5174", "http://localhost:5174"), null); assert.equal(resolveApiOrigin("http://localhost:5174/", "http://localhost:5174"), null); }); it("returns cross-origin API origin", () => { assert.equal( resolveApiOrigin("http://localhost:8080", "http://localhost:5174"), "http://localhost:8080" ); assert.equal( resolveApiOrigin("https://api.example.com/v1", "https://app.example.com"), "https://api.example.com" ); }); it("returns null for invalid URLs", () => { assert.equal(resolveApiOrigin("not a url", "http://localhost:5174"), null); }); }); describe("contentSecurityPolicy", () => { it("production policy omits Google hosts when GTM id is unset", () => { const csp = contentSecurityPolicy({ dev: false }); assert.match(csp, /default-src 'self'/); assert.match(csp, /frame-ancestors 'none'/); assert.match(csp, /object-src 'none'/); assert.match(csp, /script-src 'self' 'unsafe-inline'/); assert.doesNotMatch(csp, /googletagmanager\.com/); assert.doesNotMatch(csp, /google-analytics\.com/); assert.doesNotMatch(csp, /analytics\.google\.com/); assert.doesNotMatch(csp, /'unsafe-eval'/); assert.doesNotMatch(csp, /\bws:/); assert.match(csp, /connect-src 'self' blob:/); assert.match(csp, /font-src 'self' data:/); assert.doesNotMatch(csp, /scalar\.com/); }); it("production policy allowlists Google hosts when GTM id is valid", () => { const csp = contentSecurityPolicy({ dev: false, gtmId: "GTM-ABC123" }); assert.match(csp, /script-src 'self' 'unsafe-inline' https:\/\/www\.googletagmanager\.com/); assert.match(csp, /https:\/\/www\.google-analytics\.com/); assert.match(csp, /frame-src 'self' https:\/\/www\.googletagmanager\.com/); assert.match(csp, /connect-src 'self' blob: https:\/\/www\.googletagmanager\.com/); }); it("invalid or empty gtmId does not allowlist Google hosts", () => { for (const gtmId of ["", " ", "G-XXXX", "gtm-bad!", null, undefined]) { const csp = contentSecurityPolicy({ dev: false, gtmId }); assert.doesNotMatch(csp, /googletagmanager\.com/); assert.doesNotMatch(csp, /google-analytics\.com/); } }); it("production connect-src includes cross-origin API without GTM hosts by default", () => { const csp = contentSecurityPolicy({ dev: false, apiOrigin: "http://localhost:8080" }); assert.match(csp, /connect-src 'self' blob: http:\/\/localhost:8080/); assert.doesNotMatch(csp, /googletagmanager\.com/); assert.doesNotMatch(csp, /scalar\.com/); }); it("production connect-src includes API and GTM hosts when gtmId is valid", () => { const csp = contentSecurityPolicy({ dev: false, apiOrigin: "http://localhost:8080", gtmId: "GTM-ABC123" }); assert.match( csp, /connect-src 'self' blob: https:\/\/www\.googletagmanager\.com .*http:\/\/localhost:8080/ ); assert.doesNotMatch(csp, /scalar\.com/); }); it("development policy allows Vite HMR eval and websockets", () => { const csp = contentSecurityPolicy({ dev: true }); assert.match(csp, /'unsafe-eval'/); assert.match(csp, /\bws:/); assert.match(csp, /\bwss:/); assert.match(csp, /http:\/\/localhost:\*/); assert.match(csp, /http:\/\/127\.0\.0\.1:\*/); assert.match(csp, /connect-src 'self' blob:/); assert.match(csp, /font-src 'self' data:/); assert.doesNotMatch(csp, /googletagmanager\.com/); assert.doesNotMatch(csp, /scalar\.com/); }); });