Files
Ekklesia/core/tests/host-configuration.test.ts
T
ekklesiaandClaude Opus 5 2b4f0afb72
CI / verify (pull_request) Successful in 42s
CI / infrastructure-smoke (pull_request) Successful in 14s
CI / migration-smoke (pull_request) Successful in 1m24s
fix: give Doctor the host configuration the runtime receives
`pnpm doctor` and `startApi` each read their own subset of the same
environment variables, and the subsets had drifted. Doctor never read
`S3_*` or module secrets at all, so a correctly configured installation
was told its object storage was "not configured" and every module
requiring a bucket or a secret was reported blocked -- while the runtime,
from the identical environment, bound those buckets and ran the modules.

Both executables now map the environment through one
`loadHostConfiguration(env)` in Core, which takes the environment as an
argument and never reads `process.env` itself. Validation stays with the
existing per-domain validators, so each caller keeps its own failure
behaviour: startup refuses a rejected value, Doctor reports it as a
sanitized code.

Doctor's storage option becomes raw input like its mail option, so a
half-configured provider is reported as a blocking configuration failure
instead of throwing before a report exists. The outbound-email check --
computed, counted toward the result, but absent from every rendered
section -- is now printed, and the rendered set is asserted against a
real report so a future check cannot go missing the same way.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-24 19:15:49 -05:00

193 lines
8.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { createDatabaseTransportPolicy } from "../src/database/index.js";
import { loadHostConfiguration } from "../src/host/index.js";
import { createIdentityConfig } from "../src/identity/index.js";
import { createMailProviderConfig } from "../src/mail/index.js";
import { createStorageProviderConfig } from "../src/storage/index.js";
/**
* A complete, valid production deployment. Every optional service is present, because the bug this
* suite exists for was a *configured* service being reported absent — an empty environment would
* never have shown it.
*/
const productionEnvironment = Object.freeze({
DATABASE_URL: "postgres://[email protected]:5432/ekklesia",
EKKLESIA_CLIENT_ADDRESS_MODE: "trusted-proxy",
EKKLESIA_CLIENT_ADDRESS_TRUSTED_PROXIES: "10.40.0.1, 10.40.0.2 ,",
EKKLESIA_IDENTITY_BASE_URL: "https://ekklesia.example.org",
EKKLESIA_IDENTITY_SECRET: "b7QwZ2rTn4Xs9LmVpKdF6yHc3JgEuNaW",
EKKLESIA_IDENTITY_TRUSTED_ORIGINS: "https://ekklesia.example.org, https://display.example.org",
EKKLESIA_MODULE_SECRET__ANNOUNCEMENTS__STORAGE_CREDENTIAL: "module-secret-material",
EKKLESIA_SMTP_FROM_ADDRESS: "[email protected]",
EKKLESIA_SMTP_FROM_NAME: "Ekklesia",
EKKLESIA_SMTP_HOST: "smtp.example.org",
EKKLESIA_SMTP_PASSWORD: "sup3r-secret-relay-password",
EKKLESIA_SMTP_PORT: "587",
EKKLESIA_SMTP_SECURITY: "starttls",
EKKLESIA_SMTP_USERNAME: "[email protected]",
NODE_ENV: "production",
S3_ACCESS_KEY: "AKIAEXAMPLEACCESSID",
S3_BUCKET_PREFIX: "ekklesia",
S3_DELIVERY_ENDPOINT: "https://files.example.org",
S3_ENDPOINT: "https://storage.example.org",
S3_FORCE_PATH_STYLE: "true",
S3_REGION: "us-east-1",
S3_SECRET_KEY: "sk-example-secret-material",
});
describe("host configuration loading", () => {
it("never reads the process environment, only the object it is given", () => {
// Proved by construction: an empty environment produces an unconfigured deployment even though
// this test process's own `process.env` may carry DATABASE_URL, S3_*, or EKKLESIA_SMTP_*.
const host = loadHostConfiguration({});
expect(host.databaseUrl).toBeUndefined();
expect(host.storage.endpoint).toBeUndefined();
expect(host.mail.host).toBeUndefined();
expect(host.identity.secret).toBeUndefined();
});
it("carries every object-storage field the storage passes established", () => {
const host = loadHostConfiguration(productionEnvironment);
expect(host.storage).toEqual({
accessKey: "AKIAEXAMPLEACCESSID",
bucketPrefix: "ekklesia",
deliveryEndpoint: "https://files.example.org",
endpoint: "https://storage.example.org",
forcePathStyle: "true",
region: "us-east-1",
secretKey: "sk-example-secret-material",
});
});
it("carries every SMTP field, leaving credentials optional as a pair", () => {
const host = loadHostConfiguration(productionEnvironment);
expect(host.mail).toEqual({
fromAddress: "[email protected]",
fromName: "Ekklesia",
host: "smtp.example.org",
password: "sup3r-secret-relay-password",
port: "587",
security: "starttls",
username: "[email protected]",
});
const relayByAddress = loadHostConfiguration({
EKKLESIA_SMTP_FROM_ADDRESS: "[email protected]",
EKKLESIA_SMTP_HOST: "smtp-relay.example.org",
});
expect(relayByAddress.mail.username).toBeUndefined();
expect(relayByAddress.mail.password).toBeUndefined();
// A source-IP-authorized relay has no credentials at all, and that must still validate.
expect(createMailProviderConfig(relayByAddress.mail)).toBeDefined();
});
it("resolves module secrets only under names derived from a module's own declared key", () => {
const host = loadHostConfiguration(productionEnvironment);
expect(host.moduleSecrets.get("announcements", "storageCredential")).toBe(
"module-secret-material",
);
expect(host.moduleSecrets.get("announcements", "unknownKey")).toBeUndefined();
// There is no accessor that takes a raw variable name, so a module can never reach these.
expect(host.moduleSecrets.get("announcements", "DATABASE_URL")).toBeUndefined();
});
it("treats only the exact string development as development", () => {
expect(loadHostConfiguration({ NODE_ENV: "development" }).environment).toBe("development");
for (const value of ["Development", "dev", "test", "", "production", undefined]) {
expect(loadHostConfiguration({ NODE_ENV: value }).environment).toBe("production");
}
});
it("omits client-address settings entirely unless a mode is set", () => {
expect(loadHostConfiguration({}).clientAddress).toBeUndefined();
// Half-configuring proxy mode by setting only the header must not silently enable it.
expect(
loadHostConfiguration({ EKKLESIA_CLIENT_ADDRESS_HEADER: "x-forwarded-for" }).clientAddress,
).toBeUndefined();
expect(loadHostConfiguration(productionEnvironment).clientAddress).toEqual({
header: undefined,
mode: "trusted-proxy",
trustedProxies: ["10.40.0.1", "10.40.0.2"],
});
});
it("omits database transport keys that are unset rather than passing undefined values", () => {
expect(loadHostConfiguration({}).databaseTransport).toEqual({});
expect(
loadHostConfiguration({
EKKLESIA_DATABASE_CA: "-----BEGIN CERTIFICATE-----",
EKKLESIA_DATABASE_TLS_MODE: "verify-full",
}).databaseTransport,
).toEqual({
certificateAuthority: "-----BEGIN CERTIFICATE-----",
mode: "verify-full",
});
});
it("splits comma-separated lists identically wherever they appear", () => {
const host = loadHostConfiguration({
EKKLESIA_CLIENT_ADDRESS_MODE: "trusted-proxy",
EKKLESIA_CLIENT_ADDRESS_TRUSTED_PROXIES: " 10.0.0.1 ,, 10.0.0.2 ",
EKKLESIA_IDENTITY_TRUSTED_ORIGINS: " https://a.example.org ,, https://b.example.org ",
});
expect(host.identity.trustedOrigins).toEqual([
"https://a.example.org",
"https://b.example.org",
]);
expect(host.clientAddress?.trustedProxies).toEqual(["10.0.0.1", "10.0.0.2"]);
// A variable that is set but empty yields an empty list, not `undefined` — the two mean
// different things to the identity validator (`TRUSTED_ORIGINS_EMPTY` vs
// `TRUSTED_ORIGINS_MISSING`), and both executables must see the same one.
expect(
loadHostConfiguration({ EKKLESIA_IDENTITY_TRUSTED_ORIGINS: "" }).identity.trustedOrigins,
).toEqual([]);
expect(loadHostConfiguration({}).identity.trustedOrigins).toBeUndefined();
});
it("passes a malformed value through unchanged rather than repairing or discarding it", () => {
// The loader deliberately does not validate: `startApi` wants a malformed value to abort
// startup and Doctor wants it reported, and neither can happen if the load already threw or
// silently dropped it.
const host = loadHostConfiguration({
EKKLESIA_PUBLIC_SIGNUP_ENABLED: "",
S3_ENDPOINT: "not-a-url",
});
expect(host.identity.publicSignupEnabled).toBe("");
expect(host.storage.endpoint).toBe("not-a-url");
expect(() => createStorageProviderConfig(host.storage)).toThrowError(/absolute http/u);
expect(() => createIdentityConfig(host.identity)).toThrowError();
});
it("is accepted unchanged by every Core validator that consumes it", () => {
const host = loadHostConfiguration(productionEnvironment);
expect(createIdentityConfig(host.identity, { environment: host.environment })).toMatchObject({
baseUrl: "https://ekklesia.example.org",
publicSignupEnabled: false,
});
expect(
createDatabaseTransportPolicy(host.databaseTransport, { environment: host.environment }),
).toBeDefined();
expect(createStorageProviderConfig(host.storage, { environment: host.environment })).toEqual({
accessKey: "AKIAEXAMPLEACCESSID",
bucketPrefix: "ekklesia",
deliveryEndpoint: "https://files.example.org",
endpoint: "https://storage.example.org",
forcePathStyle: true,
region: "us-east-1",
secretKey: "sk-example-secret-material",
});
expect(createMailProviderConfig(host.mail, { environment: host.environment })).toMatchObject({
host: "smtp.example.org",
port: 587,
security: "starttls",
});
});
});