import { describe, expect, it } from "vitest"; import { ApiConfigError, createApiRuntimeConfig } from "../src/config.js"; const validSecret = "correct-horse-battery-staple-0123456789"; function validInput() { return { databaseUrl: "postgresql://ekklesia:ekklesia@127.0.0.1:5432/ekklesia", host: "127.0.0.1", identity: { baseUrl: "https://identity.example.test", secret: validSecret, trustedOrigins: ["https://identity.example.test"], }, port: 3000, }; } describe("API runtime configuration", () => { it("accepts valid API and identity configuration", () => { const config = createApiRuntimeConfig(validInput(), { environment: "production" }); expect(config).toEqual({ databaseUrl: validInput().databaseUrl, host: "127.0.0.1", identity: { baseUrl: "https://identity.example.test", secret: validSecret, trustedOrigins: ["https://identity.example.test"], }, port: 3000, }); expect(Object.isFrozen(config)).toBe(true); }); it("defaults host to loopback and port to 3000 when omitted", () => { const { host: _host, port: _port, ...rest } = validInput(); const config = createApiRuntimeConfig(rest, { environment: "production" }); expect(config.host).toBe("127.0.0.1"); expect(config.port).toBe(3000); }); it("permits an explicit non-loopback bind address", () => { const config = createApiRuntimeConfig( { ...validInput(), host: "0.0.0.0" }, { environment: "production" }, ); expect(config.host).toBe("0.0.0.0"); }); it("rejects a missing database URL", () => { let captured: unknown; try { createApiRuntimeConfig({ ...validInput(), databaseUrl: undefined }); } catch (error) { captured = error; } expect(captured).toBeInstanceOf(ApiConfigError); expect(captured).toMatchObject({ code: "DATABASE_URL_MISSING" }); }); it("rejects an invalid database URL", () => { for (const databaseUrl of ["", "not-a-url", "mysql://example.test/db", "http://example.test"]) { let captured: unknown; try { createApiRuntimeConfig({ ...validInput(), databaseUrl }); } catch (error) { captured = error; } expect(captured).toBeInstanceOf(ApiConfigError); } }); it("rejects an empty listen host", () => { let captured: unknown; try { createApiRuntimeConfig({ ...validInput(), host: "" }); } catch (error) { captured = error; } expect(captured).toMatchObject({ code: "HOST_INVALID" }); }); it("rejects an invalid listen port", () => { for (const port of [-1, 1.5, 70_000, Number.NaN, "not-a-port"]) { let captured: unknown; try { createApiRuntimeConfig({ ...validInput(), port }); } catch (error) { captured = error; } expect(captured).toMatchObject({ code: "PORT_INVALID" }); } }); it("accepts port 0 as an explicit request for an OS-assigned ephemeral port", () => { const config = createApiRuntimeConfig({ ...validInput(), port: 0 }); expect(config.port).toBe(0); }); it("delegates identity validation to Core's createIdentityConfig, rejecting a short secret", () => { let captured: unknown; try { createApiRuntimeConfig({ ...validInput(), identity: { ...validInput().identity, secret: "too-short" }, }); } catch (error) { captured = error; } expect(captured).toMatchObject({ code: "SECRET_TOO_SHORT" }); }); it("explicitly allows local development origins only when environment is development", () => { const config = createApiRuntimeConfig( { databaseUrl: validInput().databaseUrl, host: "127.0.0.1", identity: { baseUrl: "http://localhost:3000", secret: validSecret, trustedOrigins: ["http://localhost:3000"], }, port: 3000, }, { environment: "development" }, ); expect(config.identity.baseUrl).toBe("http://localhost:3000"); }); it("never leaks the database URL, secret, or trusted origins through a thrown error", () => { const hostileUrl = "postgresql://admin:s3cr3t@db.internal.example:5432/ekklesia"; let captured: unknown; try { createApiRuntimeConfig({ ...validInput(), databaseUrl: hostileUrl, port: -1 }); } catch (error) { captured = error; } expect(captured).toBeInstanceOf(ApiConfigError); const publicText = `${String(captured)}\n${ captured instanceof Error ? (captured.stack ?? "") : "" }\n${JSON.stringify(captured)}`; expect(publicText).not.toContain(hostileUrl); expect(publicText).not.toContain("s3cr3t"); expect(publicText).not.toContain("db.internal.example"); }); });