diff --git a/apps/facilitator/src/__tests__/config.test.ts b/apps/facilitator/src/__tests__/config.test.ts new file mode 100644 index 0000000..9a7abea --- /dev/null +++ b/apps/facilitator/src/__tests__/config.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { loadConfig } from "../config.js"; + +const BASE_ENV = { + DATABASE_URL: "postgres://user:pass@localhost:5432/db", + SOLANA_PRIVATE_KEY: "test-key", +}; + +describe("loadConfig boolean env vars", () => { + const original = { ...process.env }; + + afterEach(() => { + process.env = { ...original }; + }); + + function load(extra: Record) { + process.env = { ...BASE_ENV, ...extra }; + return loadConfig(); + } + + it.each([ + ["false", false], + ["0", false], + ["FALSE", false], + ["true", true], + ["1", true], + ["TRUE", true], + ])("parses SUPABASE_SMTP_CONFIGURED=%s as %s", (value, expected) => { + expect(load({ SUPABASE_SMTP_CONFIGURED: value }).SUPABASE_SMTP_CONFIGURED).toBe(expected); + expect(load({ OFAC_ENABLED: value }).OFAC_ENABLED).toBe(expected); + }); + + it("defaults both flags to false when unset", () => { + const config = load({}); + expect(config.SUPABASE_SMTP_CONFIGURED).toBe(false); + expect(config.OFAC_ENABLED).toBe(false); + }); + + it("rejects values that are not a recognized boolean", () => { + expect(() => load({ SUPABASE_SMTP_CONFIGURED: "yes" })).toThrow(/SUPABASE_SMTP_CONFIGURED/); + }); +}); diff --git a/apps/facilitator/src/config.ts b/apps/facilitator/src/config.ts index 0b48238..50a0715 100644 --- a/apps/facilitator/src/config.ts +++ b/apps/facilitator/src/config.ts @@ -1,5 +1,13 @@ import { z } from "zod"; +/** + * Boolean env var. Unlike z.coerce.boolean(), which treats any non-empty + * string (including "false") as true, this accepts only true/false/1/0. + */ +const envBoolean = z + .enum(["true", "false", "1", "0", "TRUE", "FALSE"]) + .transform((v) => v === "true" || v === "1" || v === "TRUE"); + const envSchema = z.object({ PORT: z.coerce.number().default(4402), NODE_ENV: z.enum(["development", "production", "test"]).default("development"), @@ -45,7 +53,7 @@ const envSchema = z.object({ SHUTDOWN_TIMEOUT_MS: z.coerce.number().default(10_000), /** Enable OFAC compliance screening on settlement routes */ - OFAC_ENABLED: z.coerce.boolean().default(false), + OFAC_ENABLED: envBoolean.default("false"), /** OFAC SDN list refresh interval in milliseconds (default: 24 hours) */ OFAC_REFRESH_INTERVAL_MS: z.coerce.number().default(86_400_000), @@ -60,7 +68,7 @@ const envSchema = z.object({ * When false, /signup returns 503 instead of falsely reporting that an OTP * email was sent (Supabase's built-in email service silently drops mail). */ - SUPABASE_SMTP_CONFIGURED: z.coerce.boolean().default(false), + SUPABASE_SMTP_CONFIGURED: envBoolean.default("false"), }).refine( (data) => data.SOLANA_PRIVATE_KEY || data.KORA_RPC_URL, { message: "At least one of SOLANA_PRIVATE_KEY or KORA_RPC_URL is required", path: ["SOLANA_PRIVATE_KEY"] },