import { describe, it, expect } from "vitest"; /** * CORS Configuration Tests * * These tests verify that CORS is configured correctly for cookie-based authentication. * * CRITICAL REQUIREMENTS: * - credentials: true (allows cookies to be sent) * - origin: must be specific origins, NOT wildcard (security requirement with credentials) * - Access-Control-Allow-Credentials: true header * - Access-Control-Allow-Origin: specific origin (not *) * - No-origin requests blocked in production (SEC-API-26) */ /** * Replicates the CORS origin validation logic from main.ts * so we can test it in isolation. */ function buildOriginValidator(nodeEnv: string | undefined): { allowedOrigins: string[]; isDevelopment: boolean; validate: ( origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void ) => void; } { const isDevelopment = nodeEnv !== "production"; const allowedOrigins = [ process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000", "https://app.mosaicstack.dev", "https://api.mosaicstack.dev", ]; if (isDevelopment) { allowedOrigins.push("http://localhost:3001"); } const validate = ( origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void ): void => { if (!origin) { if (isDevelopment) { callback(null, true); } else { callback(new Error("CORS: Origin header is required")); } return; } if (allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error(`Origin ${origin} not allowed by CORS`)); } }; return { allowedOrigins, isDevelopment, validate }; } describe("CORS Configuration", () => { describe("Configuration requirements", () => { it("should document required CORS settings for cookie-based auth", () => { const requiredSettings = { origin: ["http://localhost:3000", "https://app.mosaicstack.dev"], credentials: true, allowedHeaders: ["Content-Type", "Authorization", "Cookie"], exposedHeaders: ["Set-Cookie"], methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], }; expect(requiredSettings.credentials).toBe(true); expect(requiredSettings.origin).not.toContain("*"); expect(requiredSettings.allowedHeaders).toContain("Cookie"); }); it("should NOT use wildcard origin with credentials (security violation)", () => { const validConfig1 = { origin: "*", credentials: false }; const validConfig2 = { origin: "http://localhost:3000", credentials: true }; const invalidConfig = { origin: "*", credentials: true }; expect(validConfig1.origin === "*" && !validConfig1.credentials).toBe(true); expect(validConfig2.origin !== "*" && validConfig2.credentials).toBe(true); const isInvalidCombination = invalidConfig.origin === "*" && invalidConfig.credentials; expect(isInvalidCombination).toBe(true); }); }); describe("Origin validation", () => { it("should define allowed origins list", () => { const { allowedOrigins } = buildOriginValidator("development"); expect(allowedOrigins).toContain("http://localhost:3000"); expect(allowedOrigins).toContain("https://app.mosaicstack.dev"); expect(allowedOrigins).toContain("https://api.mosaicstack.dev"); }); it("should match exact origins, not partial matches", () => { const origin = "http://localhost:3000"; const maliciousOrigin = "http://localhost:3000.evil.com"; expect(origin).toBe("http://localhost:3000"); expect(maliciousOrigin).not.toBe(origin); }); it("should support dynamic origin from environment variable", () => { const defaultOrigin = "http://localhost:3000"; const envOrigin = process.env.NEXT_PUBLIC_APP_URL ?? defaultOrigin; expect(envOrigin).toBeDefined(); expect(typeof envOrigin).toBe("string"); }); }); describe("Development mode CORS behavior", () => { it("should allow requests with no origin in development", () => { const { validate } = buildOriginValidator("development"); return new Promise((resolve) => { validate(undefined, (err, allow) => { expect(err).toBeNull(); expect(allow).toBe(true); resolve(); }); }); }); it("should include localhost:3001 in development origins", () => { const { allowedOrigins } = buildOriginValidator("development"); expect(allowedOrigins).toContain("http://localhost:3001"); }); it("should allow valid origins in development", () => { const { validate } = buildOriginValidator("development"); return new Promise((resolve) => { validate("http://localhost:3000", (err, allow) => { expect(err).toBeNull(); expect(allow).toBe(true); resolve(); }); }); }); it("should reject invalid origins in development", () => { const { validate } = buildOriginValidator("development"); return new Promise((resolve) => { validate("http://evil.com", (err) => { expect(err).toBeInstanceOf(Error); expect(err?.message).toContain("not allowed by CORS"); resolve(); }); }); }); }); describe("Production mode CORS behavior (SEC-API-26)", () => { it("should reject requests with no origin in production", () => { const { validate } = buildOriginValidator("production"); return new Promise((resolve) => { validate(undefined, (err) => { expect(err).toBeInstanceOf(Error); expect(err?.message).toBe("CORS: Origin header is required"); resolve(); }); }); }); it("should NOT include localhost:3001 in production origins", () => { const { allowedOrigins } = buildOriginValidator("production"); expect(allowedOrigins).not.toContain("http://localhost:3001"); }); it("should allow valid production origins", () => { const { validate } = buildOriginValidator("production"); return new Promise((resolve) => { validate("https://app.mosaicstack.dev", (err, allow) => { expect(err).toBeNull(); expect(allow).toBe(true); resolve(); }); }); }); it("should reject invalid origins in production", () => { const { validate } = buildOriginValidator("production"); return new Promise((resolve) => { validate("http://evil.com", (err) => { expect(err).toBeInstanceOf(Error); expect(err?.message).toContain("not allowed by CORS"); resolve(); }); }); }); it("should reject malicious origins that try partial matching", () => { const { validate } = buildOriginValidator("production"); return new Promise((resolve) => { validate("https://app.mosaicstack.dev.evil.com", (err) => { expect(err).toBeInstanceOf(Error); expect(err?.message).toContain("not allowed by CORS"); resolve(); }); }); }); }); describe("ValidationPipe strict mode (SEC-API-25)", () => { it("should document that forbidNonWhitelisted must be true", () => { // This verifies the configuration intent: // forbidNonWhitelisted: true rejects requests with unknown properties // preventing mass-assignment vulnerabilities const validationPipeConfig = { transform: true, whitelist: true, forbidNonWhitelisted: true, transformOptions: { enableImplicitConversion: false, }, }; expect(validationPipeConfig.forbidNonWhitelisted).toBe(true); expect(validationPipeConfig.whitelist).toBe(true); expect(validationPipeConfig.transformOptions.enableImplicitConversion).toBe(false); }); }); });