test: lock in app health endpoint criteria (E00-S02-T03)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Docker Compose baseline test — locks in the [E00-S02-T01/T02] `docker
|
||||
* compose up -d` DB + app baseline and the PostgreSQL health gate for the
|
||||
* workspace.
|
||||
* Docker Compose baseline test — locks in the [E00-S02-T01/T02/T03] `docker
|
||||
* compose up -d` DB + app baseline, the PostgreSQL health gate and the app
|
||||
* health endpoint for the workspace.
|
||||
*
|
||||
* Acceptance criteria covered (each test fails without the committed state):
|
||||
* - "docker compose up -d starts the database" → the committed
|
||||
@@ -16,10 +16,11 @@
|
||||
* `compose.yaml` declares an `app` service built from the committed
|
||||
* `apps/server/Dockerfile` (multi-stage: Node 24.19.0 bookworm-slim +
|
||||
* frozen pnpm install → `tsc` build of `@personal-blog/server` →
|
||||
* `node apps/server/dist/index.js`),
|
||||
* with a published default port. The real-stack probe asserts the `app`
|
||||
* container is created and starts cleanly (exit 0 when the placeholder
|
||||
* process exits).
|
||||
* `node apps/server/dist/index.js`), with a published default port. Since
|
||||
* T03 the real-stack probe asserts the `app` container stays **running**
|
||||
* (the server now serves the health endpoint instead of exiting) and the
|
||||
* health endpoint answers HTTP 200 with a healthy body inside the
|
||||
* container.
|
||||
* - "PostgreSQL health check gates application start" → the committed
|
||||
* `compose.yaml` declares a `healthcheck` on the `db` service that probes
|
||||
* readiness with `pg_isready` against the same credentials the database
|
||||
@@ -315,7 +316,7 @@ test('compose.yaml exists, parses, and declares exactly the db and app services'
|
||||
assert.deepEqual(
|
||||
Object.keys(compose.services ?? {}).sort(),
|
||||
['app', 'db'],
|
||||
'compose.yaml must declare exactly the "db" and "app" services at T01',
|
||||
'compose.yaml must declare exactly the "db" and "app" services at T01/T02/T03',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -434,16 +435,44 @@ test('docker compose up -d starts the database and application containers', { sk
|
||||
const app = containers.find((c) => field(c, 'Service', 'service') === 'app');
|
||||
assert.ok(app, 'docker compose up -d must create the "app" container');
|
||||
const appState = String(field(app, 'State', 'state') ?? '');
|
||||
const appExit = Number(field(app, 'ExitCode', 'exit_code', 'exitCode'));
|
||||
if (/exited/i.test(appState)) {
|
||||
assert.equal(
|
||||
appExit,
|
||||
0,
|
||||
`the "app" container exited non-zero (exit ${appExit}) — the server entrypoint must start cleanly`,
|
||||
);
|
||||
} else {
|
||||
assert.match(appState, /running|up/i, `the "app" container must start after "docker compose up -d" (state: "${appState}")`);
|
||||
assert.match(
|
||||
appState,
|
||||
/running|up/i,
|
||||
`the "app" container must stay running after "docker compose up -d" (state: "${appState}") — since T03 the server serves the health endpoint and must not exit`,
|
||||
);
|
||||
|
||||
// T03: the app serves the health endpoint — HTTP smoke test against the
|
||||
// endpoint inside the app container (no host-port dependency), polling
|
||||
// until it answers or times out.
|
||||
let healthOutput = '';
|
||||
let healthOk = false;
|
||||
for (let attempt = 0; attempt < 30 && !healthOk; attempt += 1) {
|
||||
const probe = run('docker', ['compose', 'exec', '-T', 'app', 'node', '-e', `
|
||||
fetch('http://127.0.0.1:3000/health')
|
||||
.then(async (res) => { console.log(res.status, await res.text()); process.exit(res.ok ? 0 : 1); })
|
||||
.catch(() => process.exit(2));
|
||||
`], { cwd: REPO_ROOT, timeout: 15_000 });
|
||||
const output = String(probe.stdout ?? '') + String(probe.stderr ?? '');
|
||||
if (probe.status === 0) {
|
||||
healthOk = true;
|
||||
healthOutput = output;
|
||||
} else if (probe.status === 1) {
|
||||
healthOutput = output; // answered but not 2xx — fail fast
|
||||
break;
|
||||
} else {
|
||||
run(process.execPath, ['-e', 'setTimeout(() => {}, 1000)']); // app still starting — retry
|
||||
}
|
||||
}
|
||||
assert.ok(
|
||||
healthOk,
|
||||
`GET /health must answer 2xx inside the app container once the stack is up (last probe: "${healthOutput.trim()}")`,
|
||||
);
|
||||
assert.match(healthOutput, /200/, `GET /health must return HTTP 200 (got: "${healthOutput.trim()}")`);
|
||||
assert.match(
|
||||
healthOutput,
|
||||
/"status":"ok"/,
|
||||
`GET /health must report a healthy application (got: "${healthOutput.trim()}")`,
|
||||
);
|
||||
} finally {
|
||||
run('docker', ['compose', 'down'], { cwd: REPO_ROOT });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user