diff --git a/dashboards/tracker-web/src/__tests__/health.test.ts b/dashboards/tracker-web/src/__tests__/health.test.ts index d9971635..165d885d 100644 --- a/dashboards/tracker-web/src/__tests__/health.test.ts +++ b/dashboards/tracker-web/src/__tests__/health.test.ts @@ -2,7 +2,7 @@ * Tests for GET /api/health (tracker dashboard) */ -import { describe, it, expect, afterEach } from 'vitest'; +import { describe, it, expect, afterEach, vi } from 'vitest'; import { GET } from '@/app/api/health/route'; @@ -11,12 +11,17 @@ describe('GET /api/health', () => { afterEach(() => { process.env = { ...originalEnv }; + vi.unstubAllGlobals(); }); - it('returns ok when all required env vars are set', async () => { + it('returns ok when all required env vars are set and platform-service is healthy', async () => { process.env.PLATFORM_API_URL = 'http://localhost:4003'; process.env.JWT_SECRET = 'test-secret'; process.env.DEFAULT_PRODUCT_ID = 'test-product'; + vi.stubGlobal( + 'fetch', + vi.fn(async () => new Response(JSON.stringify({ status: 'ok' }), { status: 200 })) + ); const res = await GET(); expect(res.status).toBe(200); @@ -30,7 +35,33 @@ describe('GET /api/health', () => { status: 'pass', message: '3 required vars set', }, + { + name: 'platform-service', + status: 'pass', + message: 'healthy', + }, ]); + expect(fetch).toHaveBeenCalledWith('http://localhost:4003/health', expect.any(Object)); + }); + + it('returns degraded when platform-service health check fails', async () => { + process.env.PLATFORM_API_URL = 'http://localhost:4003'; + process.env.JWT_SECRET = 'test-secret'; + process.env.DEFAULT_PRODUCT_ID = 'test-product'; + vi.stubGlobal( + 'fetch', + vi.fn(async () => new Response(JSON.stringify({ status: 'degraded' }), { status: 503 })) + ); + + const res = await GET(); + expect(res.status).toBe(503); + const data = await res.json(); + expect(data.status).toBe('degraded'); + expect(data.checks).toContainEqual({ + name: 'platform-service', + status: 'fail', + message: 'HTTP 503', + }); }); it('returns degraded when env vars are missing', async () => { diff --git a/dashboards/tracker-web/src/app/api/health/route.ts b/dashboards/tracker-web/src/app/api/health/route.ts index 39f665d5..ea812e79 100644 --- a/dashboards/tracker-web/src/app/api/health/route.ts +++ b/dashboards/tracker-web/src/app/api/health/route.ts @@ -7,6 +7,7 @@ interface Check { } const REQUIRED_ENV = ['PLATFORM_API_URL', 'JWT_SECRET', 'DEFAULT_PRODUCT_ID']; +const PLATFORM_HEALTH_TIMEOUT_MS = 2_000; function checkEnvVars(): Check { const missing = REQUIRED_ENV.filter(key => !process.env[key]); @@ -16,10 +17,42 @@ function checkEnvVars(): Check { return { name: 'env', status: 'pass', message: `${REQUIRED_ENV.length} required vars set` }; } +async function checkPlatformService(): Promise { + const baseUrl = process.env.PLATFORM_API_URL; + if (!baseUrl) { + return { name: 'platform-service', status: 'fail', message: 'PLATFORM_API_URL missing' }; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), PLATFORM_HEALTH_TIMEOUT_MS); + try { + const res = await fetch(new URL('/health', baseUrl).toString(), { + cache: 'no-store', + signal: controller.signal, + }); + if (!res.ok) { + return { name: 'platform-service', status: 'fail', message: `HTTP ${res.status}` }; + } + return { name: 'platform-service', status: 'pass', message: 'healthy' }; + } catch (err) { + return { + name: 'platform-service', + status: 'fail', + message: err instanceof Error ? err.message : 'health check failed', + }; + } finally { + clearTimeout(timeout); + } +} + export async function GET() { const checks: Check[] = []; - checks.push(checkEnvVars()); + const envCheck = checkEnvVars(); + checks.push(envCheck); + if (envCheck.status === 'pass') { + checks.push(await checkPlatformService()); + } const overall = checks.every(c => c.status === 'pass') ? 'ok' : 'degraded';