Files
cartsnitch/auth/src/__tests__/health.test.ts
T
Barcode Betty b2c4692400
CI / deploy-uat (pull_request) Has been skipped
CI / test (pull_request) Successful in 12s
CI / lint (pull_request) Successful in 13s
CI / build-and-push-receiptwitness (pull_request) Has been skipped
CI / build-and-push-api (pull_request) Has been skipped
CI / build-and-push-auth (pull_request) Has been skipped
CI / audit (pull_request) Successful in 40s
CI / e2e (pull_request) Successful in 1m11s
CI / build-and-push (pull_request) Has been skipped
CI / deploy-dev (pull_request) Has been skipped
CI / lighthouse (pull_request) Failing after 1m15s
fix(auth): log /health 503 error and surface message in body (CAR-1276)
The /health handler's catch block was empty, so when the DB probe
failed we had no log line to diagnose from. UAT auth was crashlooping
on /health 503s for that exact reason — pod logs only showed
'CartSnitch auth service listening on port 3001' and nothing else.

Add console.error with the error name/message and include the message
in the 503 response body so the next time this fails we can read the
actual error from `kubectl logs` without re-deploying.

This is the dev-side observability half of CAR-1276. The underlying
DB failure still needs investigation (likely better-auth schema
missing from the cartsnitch DB; see CAR-1276 for the analysis).

Tests updated to assert the new error field is present and a string.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-05 07:05:46 +00:00

136 lines
4.7 KiB
TypeScript

import { describe, it } from 'node:test';
import { equal } from 'node:assert';
import http from 'node:http';
describe('Auth health endpoint', () => {
const startHealthServer = (poolMock) => {
return new Promise((resolve) => {
const server = http.createServer(async (req, res) => {
if (req.url === '/health' && req.method === 'GET') {
try {
const client = await poolMock.connect();
try {
await Promise.race([
client.query('SELECT 1'),
new Promise((_, reject) => setTimeout(() => reject(new Error('DB timeout')), 2000)),
]);
} finally {
client.release();
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', db: 'reachable' }));
} catch (err) {
// Mirror src/index.ts: log the error and include the message in the
// response body so /health 503s are diagnosable from pod logs.
console.error(
'[auth /health] DB probe failed:',
err instanceof Error ? `${err.name}: ${err.message}` : err,
);
const detail = err instanceof Error ? err.message : 'unknown error';
res.writeHead(503, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({ status: 'error', db: 'unreachable', error: detail }),
);
}
return;
}
res.writeHead(404);
res.end();
});
server.listen(0, '0.0.0.0', () => {
const addr = server.address();
const port = typeof addr === 'object' && addr ? addr.port : 0;
resolve({ port, close: () => server.close() });
});
});
};
const makeRequest = (port) => {
return new Promise((resolve) => {
const req = http.get(`http://localhost:${port}/health`, (res) => {
let body = '';
res.on('data', (chunk) => { body += chunk; });
res.on('end', () => {
resolve({ status: res.statusCode, body });
});
});
req.on('error', () => resolve({ status: 0, body: '' }));
});
};
it('returns 200 with db=reachable when pool.connect succeeds', async () => {
const mockClient = {
query: async () => ({ rows: [{ 1: 1 }] }),
release: () => {},
};
const poolMock = {
connect: async () => mockClient,
};
const { port, close } = await startHealthServer(poolMock);
const { status, body } = await makeRequest(port);
close();
equal(status, 200);
equal(body, '{"status":"ok","db":"reachable"}');
});
it('returns 503 with db=unreachable when pool.connect throws', async () => {
const poolMock = {
connect: async () => { throw new Error('connection refused'); },
};
const { port, close } = await startHealthServer(poolMock);
const { status, body } = await makeRequest(port);
close();
equal(status, 503);
const parsed = JSON.parse(body);
equal(parsed.status, 'error');
equal(parsed.db, 'unreachable');
equal(parsed.error, 'connection refused');
});
it('returns 503 with db=unreachable when query times out', async () => {
const mockClient = {
query: async () => {
await new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000));
},
release: () => {},
};
const poolMock = {
connect: async () => mockClient,
};
const { port, close } = await startHealthServer(poolMock);
const { status, body } = await makeRequest(port);
close();
equal(status, 503);
const parsed = JSON.parse(body);
equal(parsed.status, 'error');
equal(parsed.db, 'unreachable');
// The query promise rejects with a synthetic 'timeout' error; the
// Promise.race wrapper also rejects with 'DB timeout'. The body should
// surface whichever error was thrown — accept either to stay robust.
equal(typeof parsed.error, 'string');
equal(parsed.error.length > 0, true);
});
it('returns a terminal response for unknown paths (no hang)', async () => {
const poolMock = { connect: async () => ({ query: async () => {}, release: () => {} }) };
const { port, close } = await startHealthServer(poolMock);
const result = await new Promise<{ status: number }>((resolve) => {
const req = http.get(`http://localhost:${port}/`, (res) => {
res.resume();
res.on('end', () => resolve({ status: res.statusCode ?? 0 }));
});
req.on('error', () => resolve({ status: 0 }));
setTimeout(() => resolve({ status: -1 }), 1000);
});
close();
equal(result.status !== -1, true, 'Unknown path must return a terminal response within 1s');
});
});