|
1 | | -import { describe, it, expect } from 'vitest'; |
2 | | -import app from '../../src/index'; |
| 1 | +import { describe, it, expect, beforeEach, vi } from 'vitest'; |
| 2 | +import { Hono } from 'hono'; |
| 3 | +import { requestId } from 'hono/request-id'; |
| 4 | +import { pingDatabase } from '../../src/db/ping'; |
| 5 | +import { healthRoutes } from '../../src/routes/health'; |
| 6 | + |
| 7 | +vi.mock('../../src/db/ping', () => ({ |
| 8 | + pingDatabase: vi.fn(), |
| 9 | +})); |
| 10 | + |
| 11 | +interface HealthBody { |
| 12 | + status: 'healthy' | 'unhealthy'; |
| 13 | + version: string; |
| 14 | + uptime: number; |
| 15 | + timestamp: string; |
| 16 | + requestId: string; |
| 17 | + checks: { database: 'connected' | 'disconnected' }; |
| 18 | +} |
| 19 | + |
| 20 | +type TestEnv = { |
| 21 | + APP_VERSION: string; |
| 22 | + HEALTH_DB_TIMEOUT_MS: string; |
| 23 | + DB: D1Database | undefined; |
| 24 | +}; |
| 25 | + |
| 26 | +const env: TestEnv = { |
| 27 | + APP_VERSION: '0.0.1', |
| 28 | + HEALTH_DB_TIMEOUT_MS: '2000', |
| 29 | + DB: undefined, |
| 30 | +}; |
| 31 | + |
| 32 | +const makeApp = (): Hono<{ Bindings: TestEnv }> => { |
| 33 | + const app = new Hono<{ Bindings: TestEnv }>(); |
| 34 | + app.use('*', requestId()); |
| 35 | + app.route('/health', healthRoutes); |
| 36 | + return app; |
| 37 | +}; |
| 38 | + |
| 39 | +const mockedPing = vi.mocked(pingDatabase); |
3 | 40 |
|
4 | 41 | describe('Health endpoint', () => { |
5 | | - it('should return 200 with ok status', async () => { |
6 | | - const res = await app.request('/health'); |
| 42 | + beforeEach(() => { |
| 43 | + mockedPing.mockReset(); |
| 44 | + }); |
| 45 | + |
| 46 | + it('returns 200 + healthy when DB is connected', async () => { |
| 47 | + mockedPing.mockResolvedValue('connected'); |
| 48 | + const res = await makeApp().request('/health', {}, env); |
7 | 49 | expect(res.status).toBe(200); |
8 | | - const body = (await res.json()) as { status: string }; |
9 | | - expect(body.status).toBe('ok'); |
| 50 | + const body = (await res.json()) as HealthBody; |
| 51 | + expect(body.status).toBe('healthy'); |
| 52 | + expect(body.checks.database).toBe('connected'); |
10 | 53 | }); |
11 | 54 |
|
12 | | - it('should include a requestId in the response body', async () => { |
13 | | - const res = await app.request('/health'); |
14 | | - const body = (await res.json()) as { requestId: string }; |
15 | | - expect(body.requestId).toBeDefined(); |
16 | | - expect(typeof body.requestId).toBe('string'); |
17 | | - expect(body.requestId.length).toBeGreaterThan(0); |
| 55 | + it('returns version from APP_VERSION binding', async () => { |
| 56 | + mockedPing.mockResolvedValue('connected'); |
| 57 | + const res = await makeApp().request('/health', {}, env); |
| 58 | + const body = (await res.json()) as HealthBody; |
| 59 | + expect(body.version).toBe('0.0.1'); |
18 | 60 | }); |
19 | 61 |
|
20 | | - it('should include X-Request-Id response header', async () => { |
21 | | - const res = await app.request('/health'); |
| 62 | + it('returns numeric uptime >= 0', async () => { |
| 63 | + mockedPing.mockResolvedValue('connected'); |
| 64 | + const res = await makeApp().request('/health', {}, env); |
| 65 | + const body = (await res.json()) as HealthBody; |
| 66 | + expect(typeof body.uptime).toBe('number'); |
| 67 | + expect(body.uptime).toBeGreaterThanOrEqual(0); |
| 68 | + }); |
| 69 | + |
| 70 | + it('returns 503 + unhealthy when DB is disconnected', async () => { |
| 71 | + mockedPing.mockResolvedValue('disconnected'); |
| 72 | + const res = await makeApp().request('/health', {}, env); |
| 73 | + expect(res.status).toBe(503); |
| 74 | + const body = (await res.json()) as HealthBody; |
| 75 | + expect(body.status).toBe('unhealthy'); |
| 76 | + expect(body.checks.database).toBe('disconnected'); |
| 77 | + }); |
| 78 | + |
| 79 | + it('returns 503 when ping exceeds timeout', async () => { |
| 80 | + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }); |
| 81 | + mockedPing.mockImplementation(() => new Promise(() => {})); |
| 82 | + const reqPromise = makeApp().request('/health', {}, env); |
| 83 | + await vi.advanceTimersByTimeAsync(2001); |
| 84 | + const res = await reqPromise; |
| 85 | + expect(res.status).toBe(503); |
| 86 | + vi.useRealTimers(); |
| 87 | + }); |
| 88 | + |
| 89 | + it('returns 503 when ping throws (never 500)', async () => { |
| 90 | + mockedPing.mockRejectedValue(new Error('boom')); |
| 91 | + const res = await makeApp().request('/health', {}, env); |
| 92 | + expect(res.status).toBe(503); |
| 93 | + }); |
| 94 | + |
| 95 | + it('preserves requestId in body and X-Request-Id header', async () => { |
| 96 | + mockedPing.mockResolvedValue('connected'); |
| 97 | + const res = await makeApp().request('/health', {}, env); |
| 98 | + const body = (await res.json()) as HealthBody; |
| 99 | + expect(typeof body.requestId).toBe('string'); |
| 100 | + expect(body.requestId.length).toBeGreaterThan(0); |
22 | 101 | expect(res.headers.get('X-Request-Id')).not.toBeNull(); |
23 | 102 | }); |
24 | 103 | }); |
0 commit comments