diff --git a/dashcaddy-api/__tests__/routes/deploys.routes.test.js b/dashcaddy-api/__tests__/routes/deploys.routes.test.js index 27afff5..4f7a74e 100644 --- a/dashcaddy-api/__tests__/routes/deploys.routes.test.js +++ b/dashcaddy-api/__tests__/routes/deploys.routes.test.js @@ -140,6 +140,45 @@ describe('routes/deploys — proxied endpoints', () => { expect(body.output).toContain('[FAIL]'); }); + test('GET /status maps bridge auth failure (401) to 502, not a probe result', async () => { + const f = jsonFetcher({ + 'GET /api/status?service=helloworld': { status: 401, body: { ok: false, error: 'invalid token' } }, + }); + const app = buildApp(f.fetchT); + const server = app.listen(0); + const port = server.address().port; + const r = await fetch(`http://127.0.0.1:${port}/api/v1/deploys/status?service=helloworld`); + const body = await r.json(); + server.close(); + expect(r.status).toBe(502); + expect(body.success).toBe(false); + expect(body.error).toMatch(/bridge/i); + }); + + test('GET /status maps unexpected bridge statuses to 502', async () => { + const f = jsonFetcher({ + 'GET /api/status?service=helloworld': { status: 404, body: { ok: false, error: 'not found' } }, + }); + const app = buildApp(f.fetchT); + const server = app.listen(0); + const port = server.address().port; + const r = await fetch(`http://127.0.0.1:${port}/api/v1/deploys/status?service=helloworld`); + server.close(); + expect(r.status).toBe(502); + }); + + test('GET /status maps bridge connection failure to 502', async () => { + const fetchT = jest.fn(async () => { throw new Error('ECONNREFUSED'); }); + const app = buildApp(fetchT); + const server = app.listen(0); + const port = server.address().port; + const r = await fetch(`http://127.0.0.1:${port}/api/v1/deploys/status?service=helloworld`); + const body = await r.json(); + server.close(); + expect(r.status).toBe(502); + expect(body.error).toMatch(/unreachable/); + }); + test('POST /deploy proxies dir and audits', async () => { const f = jsonFetcher({ 'POST /api/deploy': { status: 200, body: { ok: true, exit: 0, output: 'DEPLOYED helloworld in 30.0s' } }, diff --git a/dashcaddy-api/routes/deploys.js b/dashcaddy-api/routes/deploys.js index b35fbb1..7d004d1 100644 --- a/dashcaddy-api/routes/deploys.js +++ b/dashcaddy-api/routes/deploys.js @@ -117,9 +117,27 @@ module.exports = function ({ asyncHandler, log, auditLogger, fetchT }) { if (!SERVICE_RE.test(service)) { return errorResponse(res, 400, 'invalid service name'); } - const { status, body } = await bridge('GET', `/api/status?service=${encodeURIComponent(service)}`); - // shipdeck status exits non-zero when checks fail — surface that as ok:false - // with the probe output so the UI can render the failing checks. + let status, body; + try { + ({ status, body } = await bridge('GET', `/api/status?service=${encodeURIComponent(service)}`)); + } catch (e) { + log.error('deploys', 'status probe: bridge unreachable', { service, error: e.message }); + return errorResponse(res, 502, 'shipdeck bridge unreachable: ' + e.message); + } + if (status === 401 || status === 403) { + // bridge auth/protocol failure = infrastructure problem, NOT a probe result + log.error('deploys', 'status probe: bridge auth failed', { service, status }); + return errorResponse(res, 502, 'shipdeck bridge rejected the request (auth/config error)'); + } + if (status >= 500 && body && body.ok === false && body.output) { + // shipdeck status exits non-zero when checks fail — that's an EXPECTED + // probe result (failing checks), surface as ok:false with the output. + return ok(res, { ok: false, output: body.output || '' }); + } + if (status !== 200) { + // malformed upstream route or other unexpected bridge failure + return errorResponse(res, 502, body && body.error ? body.error : 'unexpected bridge response'); + } return ok(res, { ok: body.ok === true, output: body.output || '' }); }));