154 lines
4.7 KiB
TypeScript
154 lines
4.7 KiB
TypeScript
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);
|
|
});
|
|
});
|