From 6e9bd2e0b5327d45e68311b08c88ca6da22bbf8d Mon Sep 17 00:00:00 2001 From: "Aaron K. Clark" Date: Tue, 19 May 2026 12:53:39 -0500 Subject: [PATCH] test(healthz): cover the db_error field on the 503 degraded path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OpenAPI spec pins `db_error` on the /healthz 503 schema (see tests/api/openapi.test.js — operators rely on the field for debugging a degraded probe). But there was no behavioral assertion that the field actually appears when the SELECT 1 probe rejects; a future controller refactor could silently drop the field and the spec-shape test wouldn't catch the runtime divergence. Add a test that swaps `db.sequelize.query` to a rejecting stub, then drives /healthz end-to-end via supertest. Asserts the 503 status, the `status: 'degraded'` + `db: 'down'` shape, that the `db_error` field is a non-empty string, and that `migration` remains null (the SequelizeMeta probe never gets a chance to run when SELECT 1 already threw). Doesn't pin the error message — it varies by pg connection mode (no-host, no-password, refused, etc.). The structural assertion is enough to catch a regression. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/api/healthz.test.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/api/healthz.test.js b/tests/api/healthz.test.js index 82eef21..796fa2e 100644 --- a/tests/api/healthz.test.js +++ b/tests/api/healthz.test.js @@ -92,4 +92,40 @@ describe('GET /healthz', () => { // exotic case of a broken install. expect(res.body.version).not.toBe('unknown'); }); + + test('503 carries a `db_error` string when the SELECT 1 probe throws', async () => { + // The OpenAPI spec pins `db_error` on the 503 schema (operators + // rely on the field for debugging a degraded probe). Without a + // behavioral assertion, a future controller refactor could + // silently drop the field and the spec test wouldn't catch + // the runtime divergence. + // + // The vi.mock at the top of this file resolves `query` to + // mockResolvedValue([]) — keeps the happy-path tests green. + // For this test we need the rejection path; require the + // controller, swap query, restore. Both vitest's vi.mock + // (CJS finickiness — known limitation in this codebase per + // the auth.js _setDbForTesting pattern) and a direct re-mock + // are unreliable here, so call the controller via the real + // request pipeline and rely on the real db.config rejecting + // with whatever pg connection error fires when no DB is up. + // The exact message varies (no-password, no-host, etc.); pin + // only "field present + string + non-empty". + const db = require('../../app/config/db.config.js'); + const origQuery = db.sequelize.query; + db.sequelize.query = () => Promise.reject(new Error('connection refused')); + try { + const res = await request(app).get('/healthz'); + expect(res.status).toBe(503); + expect(res.body.status).toBe('degraded'); + expect(res.body.db).toBe('down'); + expect(typeof res.body.db_error).toBe('string'); + expect(res.body.db_error.length).toBeGreaterThan(0); + // `migration` should still be null when the DB is down — the + // SequelizeMeta query never gets a chance to run. + expect(res.body.migration).toBeNull(); + } finally { + db.sequelize.query = origQuery; + } + }); });