/** * Config startup error test — locks in the [E00-S04-T02] guarantee that a * missing required setting gives a field-specific startup error. * * Acceptance criteria covered (each test fails without the committed state): * - "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 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 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 * schema violations throw `ConfigStartupError` whose message names the * violating field too. Locked in statically and by the deterministic * probes (boundary + booted server). * - the compose stack and the server image stay runnable with the required * secret: `compose.yaml` provides `EPPP_SESSION_SECRET` for the `app` * service (dev-only default, ≥ 32 chars — override via .env / shell) and * the `apps/server/Dockerfile` ships the compiled `packages/config` next * to the other workspace deps, so the app container boots (the * deterministic probe boots with a valid secret and `GET /health` answers * 200 — a valid startup still works). * * Run: `node --test tests/config-startup-error.test.mjs` * (node:test — built into Node >= 18; no dependencies, lockfile untouched. * The deterministic probes boot the committed server, which imports * `@personal-blog/config` and `@personal-blog/database-postgres` — build those * packages first, exactly as the CI job does.) */ import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, existsSync, writeFileSync, rmSync } from 'node:fs'; import { spawn, spawnSync } from 'node:child_process'; import { once } from 'node:events'; import { createServer as createNetServer } from 'node:net'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8'); /** The committed config package and server entrypoint under test. */ const CONFIG_DIR = 'packages/config'; const STARTUP_SRC = `${CONFIG_DIR}/src/startup.ts`; const INDEX_SRC = `${CONFIG_DIR}/src/index.ts`; const SERVER_SRC = 'apps/server/src/index.ts'; const COMPOSE_PATH = 'compose.yaml'; const DOCKERFILE_PATH = 'apps/server/Dockerfile'; /** The root test glob (root `scripts.test`, E00-S01-T12) that runs every suite. */ const ROOT_TEST_GLOB = 'tests/**/*.test.mjs'; /** The CI job that gates the startup-error criterion on every PR. */ const CI_JOB = 'config-startup-error'; const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // --------------------------------------------------------------------------- // Static assertions on the committed sources // --------------------------------------------------------------------------- /** * Asserts the config package exposes the field-specific startup error: the * startup module compiles the TypeBox `configSchema` with Ajv and exports * `assertValidConfig` plus the field-specific errors — `ConfigStartupError` * for any schema violation, and `MissingRequiredSettingError` (whose message * and `missingField` name the missing field, e.g. * `"missing required setting: sessionSecret"`) for a missing required * setting. Fails fast on a deviation; the mutation probes below prove the * assertions are non-vacuous. */ function assertStartupErrorSource(src) { // Built on the E00-S04-T01 schema boundary: the module compiles the // TypeBox configSchema with Ajv (same golden-tuple validator as // validate.ts) and derives the missing-field names from the Ajv `required` // keyword errors. assert.match( src, /import \{ Ajv, type ErrorObject \} from 'ajv'/, 'the startup module must import Ajv (the golden-tuple validator) and the ErrorObject type', ); assert.match( src, /import \{ configSchema, type Config \} from '\.\/schema\.js'/, 'the startup module must build on the committed configSchema (import from ./schema.js)', ); assert.match( src, /new Ajv\(\{ allErrors: true \}\)/, 'the startup module must create the Ajv instance with allErrors (report every violation)', ); assert.match( src, /\.compile\(configSchema\)/, 'the startup module must compile the TypeBox configSchema with Ajv', ); // The field-specific errors. assert.match( src, /export class ConfigStartupError extends Error/, 'the startup module must export ConfigStartupError (any invalid configuration is a field-specific startup error)', ); assert.match( src, /export class MissingRequiredSettingError extends ConfigStartupError/, 'the startup module must export MissingRequiredSettingError (a missing required setting is the field-specific startup error)', ); assert.match( src, /missing required setting: \$\{missingField\}/, 'the MissingRequiredSettingError message must name the missing field ("missing required setting: ")', ); assert.match( src, /readonly missingField: string/, 'MissingRequiredSettingError must carry the missing field name (missingField)', ); // The startup entry point: validates and throws the field-specific error // for a missing required setting (Ajv `required` keyword -> missingProperty). assert.match( src, /export function assertValidConfig\(value: unknown\): Config/, 'the startup module must export assertValidConfig (the startup validation entry point)', ); assert.match( src, /error\.keyword === 'required'/, 'assertValidConfig must detect missing required settings from the Ajv "required" keyword errors', ); assert.match( src, /throw new MissingRequiredSettingError\(missingFields\.join\(', '\)\)/, 'assertValidConfig must throw MissingRequiredSettingError naming the missing field(s)', ); } /** * 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 \{ loadConfigFromEnv \} from '@personal-blog\/config'/, 'the server must import the environment adapter (loadConfigFromEnv) from the config package', ); assert.match( src, /const config = loadConfigFromEnv\(\);/, 'the server must load its startup configuration through the environment adapter (const config = loadConfigFromEnv())', ); 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)', ); const callIndex = src.indexOf('loadConfigFromEnv('); const listenIndex = src.indexOf('server.listen('); assert.ok( callIndex !== -1 && listenIndex !== -1 && callIndex < listenIndex, 'the startup configuration must load through the adapter before the server binds (server.listen) so a missing required setting is a startup error', ); } /** * Asserts the compose app service provides the required admin-session secret * (`EPPP_SESSION_SECRET`) with a dev-only default of at least 32 characters * (the schema's required field, Security-and-Operations §32/§26), so * `docker compose up -d` keeps working from a clean clone while the app's * startup validation has the secret it requires. */ function assertComposeSecret(composeText) { // The app service block runs from the top-level " app:" key to the // top-level "volumes:" map (the db service has its own nested "volumes:" // key earlier, so slice to the root-level one). const appBlock = composeText.slice(composeText.indexOf(' app:'), composeText.indexOf('\nvolumes:')); assert.match( appBlock, /EPPP_SESSION_SECRET: \$\{EPPP_SESSION_SECRET:-[^}]{32,}\}/, 'the app service must provide EPPP_SESSION_SECRET with a >= 32 char dev-only default (${EPPP_SESSION_SECRET:-...}), so the app boots with the required secret from a clean clone', ); } /** * Asserts the server image ships the config package: the build stage copies * `packages/config` source (the server's build compiles it) and the runtime * stage copies the compiled `packages/config/dist` + manifest next to the * other workspace deps the server imports. */ function assertDockerfileConfig(dockerfile) { assert.match( dockerfile, /COPY packages\/config packages\/config/, 'the build stage must copy the config package source (the server build compiles its workspace dependency)', ); assert.match( dockerfile, /COPY --from=build \/app\/packages\/config\/dist \.\/packages\/config\/dist/, 'the runtime stage must ship the compiled config package (packages/config/dist) so the server import resolves in the image', ); } // --------------------------------------------------------------------------- // Criterion tests // --------------------------------------------------------------------------- test('the config package exposes the field-specific startup error (MissingRequiredSettingError + assertValidConfig)', () => { assert.ok(existsSync(path.join(REPO_ROOT, STARTUP_SRC)), `committed ${STARTUP_SRC} must exist`); assertStartupErrorSource(read(STARTUP_SRC)); }); test('the package boundary re-exports the startup validation entry point and its errors', () => { const src = read(INDEX_SRC); assert.match( src, /export \{ assertValidConfig, ConfigStartupError, MissingRequiredSettingError \} from '\.\/startup\.js'/, 'the boundary must re-export assertValidConfig and the field-specific startup errors', ); }); test('the server validates the required settings at startup, before it binds', () => { assert.ok(existsSync(path.join(REPO_ROOT, SERVER_SRC)), `committed ${SERVER_SRC} must exist`); assertServerStartupValidation(read(SERVER_SRC)); }); test('the compose app service provides the required admin-session secret (EPPP_SESSION_SECRET)', () => { assertComposeSecret(read(COMPOSE_PATH)); }); test('the server image ships the config package (build source + runtime dist)', () => { assertDockerfileConfig(read(DOCKERFILE_PATH)); }); test('the config-startup-error criterion is enforced in CI', () => { // Picked up by the root test command (root `scripts.test` glob). const scripts = JSON.parse(read('package.json')).scripts ?? {}; assert.equal( scripts.test, `node --test "${ROOT_TEST_GLOB}"`, `root scripts.test must run the "${ROOT_TEST_GLOB}" glob so this suite runs with the rest`, ); // And a dedicated CI job gates it on every PR. const workflow = read('.gitea/workflows/ci.yml'); assert.ok( workflow.includes(`node --test tests/config-startup-error.test.mjs`), `CI must run the config-startup-error suite (job "${CI_JOB}") on every PR`, ); assert.ok( workflow.includes( 'pnpm --filter @personal-blog/config build && pnpm --filter @personal-blog/database-postgres build', ), 'the CI job must build the config and database-postgres packages (the probes boot the committed server which imports them)', ); }); // --------------------------------------------------------------------------- // Mutation probes — the static assertions are non-vacuous // --------------------------------------------------------------------------- test('dropping the adapter call fails the server wiring assertion (mutation probe)', () => { const src = read(SERVER_SRC); 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 adapter call after the server binds fails the order assertion (mutation probe)', () => { const src = read(SERVER_SRC); const moved = src .replace('const config = loadConfigFromEnv();\n', '') .replace( 'server.listen(config.port, config.host, () => {', 'server.listen(config.port, config.host, () => {\n const config = loadConfigFromEnv();', ); 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'); assert.notEqual(noField, src, 'the mutation must actually drop the field name from the message'); assert.throws(() => assertStartupErrorSource(noField), /must name the missing field/); }); test('dropping MissingRequiredSettingError fails the field-specific error assertion (mutation probe)', () => { const src = read(STARTUP_SRC); const noError = src.replace('export class MissingRequiredSettingError extends ConfigStartupError', 'export class MissingRequiredSettingErrorX extends ConfigStartupError'); assert.notEqual(noError, src, 'the mutation must actually rename the error class'); assert.throws(() => assertStartupErrorSource(noError), /must export MissingRequiredSettingError/); }); test('dropping the missing-field detection fails the required-setting assertion (mutation probe)', () => { const src = read(STARTUP_SRC); const noDetection = src.replace("error.keyword === 'required'", "error.keyword === 'minLength'"); assert.notEqual(noDetection, src, 'the mutation must actually change the missing-field detection'); assert.throws(() => assertStartupErrorSource(noDetection), /"required" keyword/); }); test('removing EPPP_SESSION_SECRET from the compose app service fails the compose assertion (mutation probe)', () => { const composeText = read(COMPOSE_PATH); const withoutSecret = composeText.replace( / EPPP_SESSION_SECRET: \$\{EPPP_SESSION_SECRET:-[^}]*\}\n/, '', ); assert.notEqual(withoutSecret, composeText, 'the mutation must actually remove the secret env entry'); assert.throws(() => assertComposeSecret(withoutSecret), /EPPP_SESSION_SECRET/); }); test('dropping the config package from the image fails the Dockerfile assertion (mutation probe)', () => { const dockerfile = read(DOCKERFILE_PATH); const withoutDist = dockerfile.replace( 'COPY --from=build /app/packages/config/dist ./packages/config/dist\n', '', ); assert.notEqual(withoutDist, dockerfile, 'the mutation must actually drop the runtime dist copy'); assert.throws(() => assertDockerfileConfig(withoutDist), /runtime stage must ship the compiled config package/); }); // --------------------------------------------------------------------------- // Deterministic behavioral probe — the issue's test plan: "start with a // missing required field and confirm the error names it" // --------------------------------------------------------------------------- /** * How the current Node executes TypeScript sources: `default` (>= 23.6, type * stripping on by default), `strip-types-flag` (>= 22.6 via * `--experimental-strip-types`) or `null` (cannot run .ts at all). The * workspace pins engines.node to 24.x, where type stripping is stable. */ function tsExecMode() { const [major, minor] = process.versions.node.split('.').map(Number); if (major > 23 || (major === 23 && minor >= 6)) return 'default'; if (major === 22 && minor >= 6) return 'strip-types-flag'; return null; } /** True when this Node can execute the committed `.ts` server source (>= 22.6, type stripping). */ const TS_STRIPPING = tsExecMode() !== null; /** The compiled config package boundary the probes import (built by the CI job first). */ const CONFIG_DIST = existsSync(path.join(REPO_ROOT, CONFIG_DIR, 'dist', 'index.js')); /** The compiled database-postgres package the booted server also imports. */ const DATABASE_POSTGRES_DIST = existsSync( path.join(REPO_ROOT, 'packages/database-postgres', 'dist', 'index.js'), ); /** Why the boot probes may be skipped on a clean clone without a build step. */ const BUILD_HINT = 'build the config and database-postgres packages first (pnpm --filter @personal-blog/config build && pnpm --filter @personal-blog/database-postgres build)'; /** * The boundary probe source: exercises the compiled `@personal-blog/config` * startup boundary (`assertValidConfig` + the field-specific errors) exactly * as the server consumes it — a valid configuration passes (including the * no-database local path), a missing required setting throws * `MissingRequiredSettingError` naming the field, and other violations throw * `ConfigStartupError` naming the violating field. Written to a temp file * inside `packages/config/` so `ajv`/`@sinclair/typebox` resolve through the * package's own dependency links, then removed. */ const PROBE_SOURCE = ` import { assertValidConfig, ConfigStartupError, MissingRequiredSettingError } from './dist/index.js'; const capture = (fn) => { try { return { threw: false, value: fn() }; } catch (error) { return { threw: true, name: error && typeof error === 'object' ? error.name : String(error), message: error instanceof Error ? error.message : String(error), missingField: error && typeof error === 'object' ? error.missingField : undefined, isMissingRequired: error instanceof MissingRequiredSettingError, isConfigStartup: error instanceof ConfigStartupError, }; } }; const result = { // A valid configuration passes and is returned — including the local // non-container path with no databaseUrl (host/port/databaseUrl optional). validReturnsConfig: capture(() => assertValidConfig({ sessionSecret: 's'.repeat(32) })).threw === false, fullValid: capture(() => assertValidConfig({ host: '0.0.0.0', port: 3000, databaseUrl: 'postgres://eppp:eppp@db:5432/eppp', sessionSecret: 's'.repeat(32) })).threw === false, // The issue's test plan: start with a missing required field — the error // must be MissingRequiredSettingError and name the missing field. missingSecret: capture(() => assertValidConfig({ host: '0.0.0.0', port: 3000 })), // Other violations are field-specific too (the message names the field). shortSecret: capture(() => assertValidConfig({ sessionSecret: 'short' })), unknownProperty: capture(() => assertValidConfig({ sessionSecret: 's'.repeat(32), extra: true })), }; console.log('CONFIG_STARTUP_PROBE_RESULT ' + JSON.stringify(result)); `; test('the compiled startup boundary throws a field-specific error naming the missing required setting (deterministic probe)', { skip: !CONFIG_DIST ? BUILD_HINT : false }, () => { const probeFile = path.join(REPO_ROOT, CONFIG_DIR, `.config-startup-probe-${process.pid}.mjs`); try { writeFileSync(probeFile, PROBE_SOURCE); const run = spawnSync(process.execPath, [path.basename(probeFile)], { cwd: path.join(REPO_ROOT, CONFIG_DIR), encoding: 'utf8', timeout: 60_000, }); assert.equal( run.status, 0, `the probe must exit 0 (status ${run.status}):\n${(run.stderr || run.stdout || '').trim()}`, ); const match = run.stdout.match(/CONFIG_STARTUP_PROBE_RESULT (\{.*\})/); assert.ok(match, `the probe must print CONFIG_STARTUP_PROBE_RESULT:\n${run.stdout.trim()}`); const result = JSON.parse(match[1]); // A valid configuration passes (including the no-database local path). assert.equal(result.validReturnsConfig, true, 'a valid config (only the required secret) must pass assertValidConfig'); assert.equal(result.fullValid, true, 'a full valid config must pass assertValidConfig'); // Missing required setting: field-specific startup error naming the field. assert.equal(result.missingSecret.threw, true, 'a config missing the required secret must throw'); assert.equal(result.missingSecret.isMissingRequired, true, 'a missing required setting must throw MissingRequiredSettingError'); assert.equal(result.missingSecret.isConfigStartup, true, 'MissingRequiredSettingError must be a ConfigStartupError'); assert.equal( result.missingSecret.missingField, 'sessionSecret', `MissingRequiredSettingError must carry the missing field name (got: ${JSON.stringify(result.missingSecret)})`, ); assert.match( result.missingSecret.message, /missing required setting: sessionSecret/, `the startup error must name the missing field (got: ${JSON.stringify(result.missingSecret)})`, ); // Other violations are field-specific too. assert.equal(result.shortSecret.threw, true, 'a too-short secret must throw'); assert.equal(result.shortSecret.isMissingRequired, false, 'a too-short secret is not a missing required setting'); assert.equal(result.shortSecret.isConfigStartup, true, 'a too-short secret must throw ConfigStartupError'); assert.match( result.shortSecret.message, /sessionSecret/, `the startup error must name the violating field (got: ${JSON.stringify(result.shortSecret)})`, ); assert.equal(result.unknownProperty.threw, true, 'an unknown property must throw'); assert.match( result.unknownProperty.message, /extra/, `the startup error must name the unexpected field (got: ${JSON.stringify(result.unknownProperty)})`, ); } finally { rmSync(probeFile, { force: true }); } }); // --------------------------------------------------------------------------- // Server-boot probes — the issue's test plan executed against the real // committed startup: "start with a missing required field and confirm the // error names it" // --------------------------------------------------------------------------- /** Reserves an ephemeral TCP port, then releases it for the child to bind. */ function reservePort() { return new Promise((resolve, reject) => { const probe = createNetServer(); probe.once('error', reject); probe.listen(0, '127.0.0.1', () => { const address = probe.address(); const port = typeof address === 'object' && address !== null ? address.port : 0; probe.close(() => resolve(port)); }); }); } /** * Boots the committed server source on `port` with the given env overrides. * An inherited `DATABASE_URL` (the no-database path must be deterministic) * and `EPPP_SESSION_SECRET` (the missing-secret case must be deterministic) * are stripped unless explicitly provided. Returns `{ child, stdout, stderr }` * with closures for diagnostics. */ function bootServer(port, envOverrides = {}) { const args = tsExecMode() === 'strip-types-flag' ? ['--experimental-strip-types', SERVER_SRC] : [SERVER_SRC]; const env = { ...process.env, PORT: String(port) }; delete env.DATABASE_URL; delete env.EPPP_SESSION_SECRET; Object.assign(env, envOverrides); const child = spawn(process.execPath, args, { cwd: REPO_ROOT, env, stdio: ['ignore', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; child.stdout.on('data', (chunk) => { stdout += String(chunk); }); child.stderr.on('data', (chunk) => { stderr += String(chunk); }); return { child, stdout: () => stdout, stderr: () => stderr }; } /** Waits for the child to exit (it fails fast on a startup error). */ function waitForExit(child, deadlineMs = 10_000) { return new Promise((resolve, reject) => { if (child.exitCode !== null || child.signalCode !== null) { resolve({ code: child.exitCode, signal: child.signalCode }); return; } const timer = setTimeout( () => reject(new Error('the server did not exit within the deadline (expected a startup error)')), deadlineMs, ); child.once('exit', (code, signal) => { clearTimeout(timer); resolve({ code, signal }); }); }); } /** * Polls `GET /health` until the server answers with ANY status, the child * exits, or the deadline passes. Returns the fetch Response. */ async function waitForAnswer(port, child, stderr, deadlineMs = 10_000) { const deadline = Date.now() + deadlineMs; let lastError = ''; while (Date.now() < deadline) { if (child.exitCode !== null) { throw new Error( `the server exited before answering GET /health (code ${child.exitCode}): ${stderr().trim()}`, ); } try { return await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(1_000), }); } catch (err) { lastError = err instanceof Error ? err.message : String(err); await delay(100); } } throw new Error( `GET /health did not answer within ${deadlineMs}ms (last error: ${lastError}; server stderr: ${stderr().trim()})`, ); } /** Kills a booted child (SIGTERM, then SIGKILL if needed) and waits for exit. */ async function stopChild(child) { if (child.exitCode !== null || child.signalCode !== null) return; child.kill('SIGTERM'); await Promise.race([once(child, 'exit'), delay(2_000)]); if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL'); } test('starting without the required setting exits non-zero naming the missing field (server boot probe)', { skip: !TS_STRIPPING || !CONFIG_DIST || !DATABASE_POSTGRES_DIST ? BUILD_HINT : false }, async () => { // The issue's test plan: "start with a missing required field and confirm // the error names it". The committed server validates its required settings // at startup, so booting it without EPPP_SESSION_SECRET must fail fast — // the process exits non-zero and the error names the missing field. const port = await reservePort(); const { child, stdout, stderr } = bootServer(port); // EPPP_SESSION_SECRET stripped const { code, signal } = await waitForExit(child); assert.notEqual( code, 0, `the server must exit non-zero when a required setting is missing (code ${code}, signal ${signal}); output: ${stdout().trim()} ${stderr().trim()}`, ); assert.match( stderr() + stdout(), /missing required setting: sessionSecret/, `the startup error must name the missing field (sessionSecret); got: ${stdout().trim()} ${stderr().trim()}`, ); }); test('starting with an invalid required secret exits non-zero naming the field (server boot probe)', { skip: !TS_STRIPPING || !CONFIG_DIST || !DATABASE_POSTGRES_DIST ? BUILD_HINT : false }, async () => { // A required setting that violates the schema (a too-short secret, < 32 // chars) is also a field-specific startup error naming the field. const port = await reservePort(); const { child, stdout, stderr } = bootServer(port, { EPPP_SESSION_SECRET: 'short' }); const { code, signal } = await waitForExit(child); assert.notEqual( code, 0, `the server must exit non-zero when the required secret is invalid (code ${code}, signal ${signal}); output: ${stdout().trim()} ${stderr().trim()}`, ); assert.match( stderr() + stdout(), /sessionSecret/, `the startup error must name the invalid field (sessionSecret); got: ${stdout().trim()} ${stderr().trim()}`, ); }); test('starting with the required secret present boots to GET /health 200 (server boot probe)', { skip: !TS_STRIPPING || !CONFIG_DIST || !DATABASE_POSTGRES_DIST ? BUILD_HINT : false }, async () => { // The startup validation must not reject a valid configuration: with the // required admin-session secret (and no DATABASE_URL — the local // non-container path) the server reports ready immediately and GET /health // answers 200 {"status":"ok"}. const port = await reservePort(); const { child, stdout, stderr } = bootServer(port, { EPPP_SESSION_SECRET: 's'.repeat(32) }); try { const response = await waitForAnswer(port, child, stderr); assert.equal( response.status, 200, `GET /health with a valid required secret must answer 200 (got ${response.status}); server output: ${stdout().trim()} ${stderr().trim()}`, ); assert.deepEqual( await response.json(), { status: 'ok' }, 'the app must report a healthy application ({"status":"ok"}) when the required settings are valid', ); } finally { await stopChild(child); } });