test: update suites locked to the old direct process.env wiring for the adapter (E00-S04-T04)
This commit is contained in:
@@ -157,7 +157,7 @@ function assertReadinessGate(src) {
|
||||
* after migrations finish.
|
||||
*/
|
||||
function assertReadyAfterRun(src) {
|
||||
const dbUrlIndex = src.indexOf('const databaseUrl = process.env.DATABASE_URL');
|
||||
const dbUrlIndex = src.indexOf('const databaseUrl = config.databaseUrl');
|
||||
assert.ok(dbUrlIndex !== -1, 'the server must read DATABASE_URL for the startup migration path');
|
||||
const elseStart = src.indexOf('} else {', dbUrlIndex);
|
||||
assert.ok(elseStart !== -1, 'the DATABASE_URL-configured startup path must exist (else branch)');
|
||||
@@ -191,7 +191,7 @@ function assertReadyAfterRun(src) {
|
||||
* non-container path and the E00-S02-T03 health endpoint working).
|
||||
*/
|
||||
function assertNoDatabaseUrlPath(src) {
|
||||
const startup = src.slice(src.indexOf('const databaseUrl = process.env.DATABASE_URL'));
|
||||
const startup = src.slice(src.indexOf('const databaseUrl = config.databaseUrl'));
|
||||
assert.match(
|
||||
startup,
|
||||
/if \(databaseUrl === undefined\) \{/,
|
||||
|
||||
@@ -159,8 +159,8 @@ function assertServerSource(src) {
|
||||
// package's scrubber before it reaches stdout/stderr.
|
||||
assert.match(
|
||||
src,
|
||||
/import \{ assertValidConfig \} from '@personal-blog\/config'/,
|
||||
'the server must import the startup validation entry point from the config package',
|
||||
/import \{ loadConfigFromEnv \} from '@personal-blog\/config'/,
|
||||
'the server must import the environment adapter (loadConfigFromEnv) from the config package',
|
||||
);
|
||||
assert.match(
|
||||
src,
|
||||
@@ -188,12 +188,13 @@ function assertServerSource(src) {
|
||||
'error lines must be written to stderr',
|
||||
);
|
||||
|
||||
// The wiring — the server keeps its validated config, creates the logger
|
||||
// with it and logs the resolved configuration redacted.
|
||||
// The wiring — the server keeps its validated config (loaded through the
|
||||
// environment adapter, E00-S04-T04), creates the logger with it and logs
|
||||
// the resolved configuration redacted.
|
||||
assert.match(
|
||||
src,
|
||||
/const config = assertValidConfig\(\{/,
|
||||
'the server must keep its validated configuration (const config = assertValidConfig(...))',
|
||||
/const config = loadConfigFromEnv\(\);/,
|
||||
'the server must keep its validated configuration (const config = loadConfigFromEnv(), E00-S04-T04)',
|
||||
);
|
||||
assert.match(
|
||||
src,
|
||||
@@ -215,13 +216,19 @@ function assertServerSource(src) {
|
||||
/console\.(log|error)\(/,
|
||||
'the server must not write log output with bare console.log/console.error (they would bypass the redaction)',
|
||||
);
|
||||
// The startup validation runs before the logger is created, so a missing
|
||||
// required setting still fails fast (E00-S04-T02) before any log output.
|
||||
const validationIndex = src.indexOf('assertValidConfig({');
|
||||
assert.doesNotMatch(
|
||||
src,
|
||||
/process\.env\.[A-Z_]+/,
|
||||
'the server must not read process.env directly (all settings flow through the config adapter, E00-S04-T04)',
|
||||
);
|
||||
// The startup configuration loads through the adapter (which validates it)
|
||||
// before the logger is created, so a missing required setting still fails
|
||||
// fast (E00-S04-T02) before any log output.
|
||||
const validationIndex = src.indexOf('loadConfigFromEnv(');
|
||||
const loggerIndex = src.indexOf('createLogger(config)');
|
||||
assert.ok(
|
||||
validationIndex !== -1 && loggerIndex !== -1 && validationIndex < loggerIndex,
|
||||
'the startup validation must run before the logger is created (a missing required setting is still a startup error)',
|
||||
'the startup configuration must load (and validate) before the logger is created (a missing required setting is still a startup error)',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +6,20 @@
|
||||
* - "missing required setting gives a field-specific startup error" → the
|
||||
* `packages/config` package exposes the startup validation entry point
|
||||
* (`assertValidConfig`, building on the E00-S04-T01 TypeBox/Ajv schema)
|
||||
* and the committed `apps/server/src/index.ts` calls it before the server
|
||||
* and the environment adapter (`loadConfigFromEnv`, E00-S04-T04 — the
|
||||
* config package's single owner of `process.env` reads) validates the
|
||||
* mapped environment through it; the committed `apps/server/src/index.ts`
|
||||
* loads its startup configuration through the adapter before the server
|
||||
* binds, so a deployment missing a required setting (the admin-session
|
||||
* secret `EPPP_SESSION_SECRET` — the schema's required field,
|
||||
* Security-and-Operations §32/§26) fails fast at startup instead of
|
||||
* booting with an invalid configuration. Locked in statically (mutation
|
||||
* probes prove non-vacuity: dropping the startup validation call,
|
||||
* moving it after the bind, or dropping the compose/Dockerfile support
|
||||
* all fail) and behaviorally by the deterministic probes (the issue's
|
||||
* test plan: "start with a missing required field and confirm the error
|
||||
* names it" — booting the committed server without `EPPP_SESSION_SECRET`
|
||||
* exits non-zero with the error naming the missing field).
|
||||
* probes prove non-vacuity: dropping the adapter call, moving it after
|
||||
* the bind, or dropping the compose/Dockerfile support all fail) and
|
||||
* behaviorally by the deterministic probes (the issue's test plan: "start
|
||||
* with a missing required field and confirm the error names it" — booting
|
||||
* the committed server without `EPPP_SESSION_SECRET` exits non-zero with
|
||||
* the error naming the missing field).
|
||||
* - "the error names the missing field" → a missing required setting throws
|
||||
* `MissingRequiredSettingError` whose message and `missingField` name the
|
||||
* missing field (e.g. `"missing required setting: sessionSecret"`); other
|
||||
@@ -149,33 +152,35 @@ function assertStartupErrorSource(src) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the committed server validates the required settings at startup:
|
||||
* it imports `assertValidConfig` from `@personal-blog/config` and calls it
|
||||
* with the parsed startup configuration (including the required
|
||||
* `EPPP_SESSION_SECRET`) BEFORE the server binds — so a missing required
|
||||
* setting is a startup error, never a silently-booted invalid configuration.
|
||||
* Asserts the committed server loads its startup configuration through the
|
||||
* environment adapter (E00-S04-T04) before it binds: it imports
|
||||
* `loadConfigFromEnv` from `@personal-blog/config` and calls it (the adapter
|
||||
* validates the mapped environment with `assertValidConfig`, including the
|
||||
* required `EPPP_SESSION_SECRET`) BEFORE `server.listen` — so a missing
|
||||
* required setting is a startup error, never a silently-booted invalid
|
||||
* configuration, and the server itself never reads `process.env` directly.
|
||||
*/
|
||||
function assertServerStartupValidation(src) {
|
||||
assert.match(
|
||||
src,
|
||||
/import \{ assertValidConfig \} from '@personal-blog\/config'/,
|
||||
'the server must import the startup validation entry point from the config package',
|
||||
/import \{ loadConfigFromEnv \} from '@personal-blog\/config'/,
|
||||
'the server must import the environment adapter (loadConfigFromEnv) from the config package',
|
||||
);
|
||||
assert.match(
|
||||
src,
|
||||
/assertValidConfig\(\{/,
|
||||
'the server must call assertValidConfig with its startup configuration',
|
||||
/const config = loadConfigFromEnv\(\);/,
|
||||
'the server must load its startup configuration through the environment adapter (const config = loadConfigFromEnv())',
|
||||
);
|
||||
assert.match(
|
||||
assert.doesNotMatch(
|
||||
src,
|
||||
/sessionSecret: process\.env\.EPPP_SESSION_SECRET/,
|
||||
'the server must feed the required admin-session secret (EPPP_SESSION_SECRET) into the startup validation',
|
||||
/process\.env\.[A-Z_]+/,
|
||||
'the server must not read process.env directly (all settings flow through the config adapter, E00-S04-T04)',
|
||||
);
|
||||
const callIndex = src.indexOf('assertValidConfig({');
|
||||
const callIndex = src.indexOf('loadConfigFromEnv(');
|
||||
const listenIndex = src.indexOf('server.listen(');
|
||||
assert.ok(
|
||||
callIndex !== -1 && listenIndex !== -1 && callIndex < listenIndex,
|
||||
'the startup validation must run before the server binds (server.listen) so a missing required setting is a startup error',
|
||||
'the startup configuration must load through the adapter before the server binds (server.listen) so a missing required setting is a startup error',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -274,25 +279,35 @@ test('the config-startup-error criterion is enforced in CI', () => {
|
||||
// Mutation probes — the static assertions are non-vacuous
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('dropping the startup validation call fails the server wiring assertion (mutation probe)', () => {
|
||||
test('dropping the adapter call fails the server wiring assertion (mutation probe)', () => {
|
||||
const src = read(SERVER_SRC);
|
||||
const withoutCall = src.replace('assertValidConfig({\n', 'assertValidConfigx({\n');
|
||||
assert.notEqual(withoutCall, src, 'the mutation must actually replace the assertValidConfig call');
|
||||
assert.throws(() => assertServerStartupValidation(withoutCall), /must call assertValidConfig/);
|
||||
const withoutCall = src.replace('const config = loadConfigFromEnv();', 'const configX = loadConfigFromEnv();');
|
||||
assert.notEqual(withoutCall, src, 'the mutation must actually break the loadConfigFromEnv wiring');
|
||||
assert.throws(() => assertServerStartupValidation(withoutCall), /must load its startup configuration/);
|
||||
});
|
||||
|
||||
test('moving the startup validation after the server binds fails the order assertion (mutation probe)', () => {
|
||||
test('moving the adapter call after the server binds fails the order assertion (mutation probe)', () => {
|
||||
const src = read(SERVER_SRC);
|
||||
const moved = src
|
||||
.replace(/assertValidConfig\(\{\n host: '0\.0\.0\.0',\n port: PORT,\n databaseUrl: process\.env\.DATABASE_URL,\n sessionSecret: process\.env\.EPPP_SESSION_SECRET,\n\}\);\n/, '')
|
||||
.replace('const config = loadConfigFromEnv();\n', '')
|
||||
.replace(
|
||||
'server.listen(PORT, () => {',
|
||||
'server.listen(PORT, () => {\n assertValidConfig({ sessionSecret: process.env.EPPP_SESSION_SECRET });',
|
||||
'server.listen(config.port, () => {',
|
||||
'server.listen(config.port, () => {\n const config = loadConfigFromEnv();',
|
||||
);
|
||||
assert.notEqual(moved, src, 'the mutation must actually move the validation call after the bind');
|
||||
assert.notEqual(moved, src, 'the mutation must actually move the adapter call after the bind');
|
||||
assert.throws(() => assertServerStartupValidation(moved), /before the server binds/);
|
||||
});
|
||||
|
||||
test('the server reading process.env directly fails the no-direct-read assertion (mutation probe)', () => {
|
||||
const src = read(SERVER_SRC);
|
||||
const directRead = src.replace(
|
||||
'const config = loadConfigFromEnv();',
|
||||
'const config = loadConfigFromEnv();\nconst PORT = process.env.PORT;',
|
||||
);
|
||||
assert.notEqual(directRead, src, 'the mutation must actually add a direct process.env read');
|
||||
assert.throws(() => assertServerStartupValidation(directRead), /must not read process\.env/);
|
||||
});
|
||||
|
||||
test('an error message that does not name the missing field fails the naming assertion (mutation probe)', () => {
|
||||
const src = read(STARTUP_SRC);
|
||||
const noField = src.replace(/missing required setting: \$\{missingField\}/g, 'missing required setting');
|
||||
|
||||
@@ -73,8 +73,8 @@ function assertHealthEndpointSource(src) {
|
||||
);
|
||||
assert.match(
|
||||
src,
|
||||
/3000/,
|
||||
'the server must default to the application port 3000 (Dockerfile EXPOSE / compose :3000)',
|
||||
/server\.listen\(config\.port/,
|
||||
'the server must bind the port from the validated configuration (config.port, E00-S04-T04)',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -189,6 +189,19 @@ test('the endpoint reports a healthy application (committed health payload is {"
|
||||
);
|
||||
});
|
||||
|
||||
test('the application port default (3000) lives in the config adapter (E00-S04-T04)', () => {
|
||||
// The server binds `config.port` (asserted above); the port default (3000,
|
||||
// matching the Dockerfile EXPOSE / compose :3000) and the PORT override
|
||||
// live in the config package's environment adapter — the single owner of
|
||||
// process.env reads.
|
||||
const envSrc = read('packages/config/src/env.ts');
|
||||
assert.match(
|
||||
envSrc,
|
||||
/3000/,
|
||||
'the config adapter must default the application port to 3000 (Dockerfile EXPOSE / compose :3000)',
|
||||
);
|
||||
});
|
||||
|
||||
test('an HTTP smoke test against the booted server succeeds for GET /health (200 + healthy body)', async (t) => {
|
||||
if (!tsExecMode()) {
|
||||
t.skip(
|
||||
|
||||
Reference in New Issue
Block a user