import { ApiConfigError, type ApiConfig } from "@toolctrl/config"; import { afterEach, describe, expect, test } from "vitest"; import { getApiRuntimeConfig, resetApiRuntimeConfigForTests, setApiRuntimeConfigForTests } from "./config.js"; const originalEnv = { ...process.env }; const REQUIRED_PRODUCTION_ENV = { APP_NAME: "Production API", DATABASE_URL: "postgres://example.invalid/app", APP_BASE_DOMAIN: "example.com", WEB_APP_HOST: "app.example.com", ADMIN_APP_HOST: "admin.example.com", API_HOST: "api.example.com", SESSION_SECRET: "session-secret-value", CRYPTO_SECRET: "crypto-secret-value", API_ACCESS_TOKEN: "api-access-token-value", JWT_SECRET: "jwt-secret-value", ANALYTICS_TOKEN: "analytics-token-value", API_PORT: "8081", APP_PROTOCOL: "https", }; const SECRET_VALUES = [ "api-access-token-value", "jwt-secret-value", "secret-like-invalid-port-value", "session-secret-value", "crypto-secret-value", "analytics-token-value", ]; afterEach(() => { resetApiRuntimeConfigForTests(); process.env = { ...originalEnv }; }); describe("getApiRuntimeConfig", () => { test("loads required API runtime config from a complete production-like environment", () => { process.env = { ...originalEnv, ...REQUIRED_PRODUCTION_ENV, NODE_ENV: "production", }; expect(getApiRuntimeConfig()).toMatchObject({ appName: "Production API", nodeEnv: "production", listen: { port: 8081 }, routing: { appBaseDomain: "example.com", webAppHost: "app.example.com", adminAppHost: "admin.example.com", apiHost: "api.example.com", protocol: "https", }, apiAccessToken: "api-access-token-value", jwtSecret: "jwt-secret-value", analytics: { token: "analytics-token-value", }, }); }); test("throws redacted ApiConfigError when API_ACCESS_TOKEN is missing", () => { process.env = { ...originalEnv, ...REQUIRED_PRODUCTION_ENV, NODE_ENV: "production", API_ACCESS_TOKEN: undefined, }; expect(() => getApiRuntimeConfig()).toThrow(ApiConfigError); try { getApiRuntimeConfig(); } catch (error) { expect(error).toBeInstanceOf(ApiConfigError); const apiConfigError = error as ApiConfigError; expect(apiConfigError.message).toBe("Invalid API configuration (API_ACCESS_TOKEN:missing-required)."); expect(apiConfigError.details.diagnostics).toEqual([ expect.objectContaining({ code: "missing-required", name: "API_ACCESS_TOKEN", classification: "required-per-app", }), ]); const errorText = JSON.stringify({ message: apiConfigError.message, details: apiConfigError.details }); for (const secretValue of SECRET_VALUES) { expect(errorText).not.toContain(secretValue); } } }); test("throws redacted ApiConfigError when API_PORT is invalid", () => { process.env = { ...originalEnv, ...REQUIRED_PRODUCTION_ENV, NODE_ENV: "production", API_PORT: "secret-like-invalid-port-value", }; try { getApiRuntimeConfig(); } catch (error) { expect(error).toBeInstanceOf(ApiConfigError); const apiConfigError = error as ApiConfigError; expect(apiConfigError.message).toBe("Invalid API configuration (API_PORT:invalid-port)."); expect(apiConfigError.details.diagnostics).toEqual([ expect.objectContaining({ code: "invalid-port", name: "API_PORT", }), ]); const errorText = JSON.stringify({ message: apiConfigError.message, details: apiConfigError.details }); for (const secretValue of SECRET_VALUES) { expect(errorText).not.toContain(secretValue); } } }); test("uses setApiRuntimeConfigForTests override without reading required environment", () => { process.env = { NODE_ENV: "production" }; const overrideConfig: ApiConfig = { appName: "Override API", nodeEnv: "test", revision: "unknown", listen: { port: 9999 }, routing: { appBaseDomain: "override.localhost", webAppHost: "app.override.localhost", adminAppHost: "admin.override.localhost", apiHost: "api.override.localhost", protocol: "http", }, origins: {}, apiAccessToken: "override-token", jwtSecret: "override-jwt-secret", analytics: {}, mailjet: {}, stripe: {}, apiKeys: { allowLegacyPlaintextFallback: true, }, storage: {}, }; setApiRuntimeConfigForTests(overrideConfig); expect(getApiRuntimeConfig()).toBe(overrideConfig); resetApiRuntimeConfigForTests(); expect(() => getApiRuntimeConfig()).toThrow(ApiConfigError); }); });