|
| 1 | +/** |
| 2 | + * Route-level tests for the health endpoints. |
| 3 | + * |
| 4 | + * The regression these lock down (issue #35): /health used to have no route at |
| 5 | + * all, so it fell through to the catch-all redirect `/:shortCode` and was |
| 6 | + * answered as a short-code lookup. The last test registers the real redirect |
| 7 | + * plugin alongside health to prove the static path wins. |
| 8 | + */ |
| 9 | +import{describe,it,expect,vi,beforeEach,afterEach}from'vitest'; |
| 10 | +importFastify,{typeFastifyInstance}from'fastify'; |
| 11 | +import{healthRoutes}from'./health.js'; |
| 12 | +import{redirectRoutes}from'./redirect.js'; |
| 13 | + |
| 14 | +constquery=vi.fn(); |
| 15 | +vi.mock('../lib/database.js',()=>({ |
| 16 | +db: { |
| 17 | +query: (...args: unknown[])=>query(...args), |
| 18 | +}, |
| 19 | +})); |
| 20 | + |
| 21 | +letapp: FastifyInstance; |
| 22 | + |
| 23 | +beforeEach(async()=>{ |
| 24 | +query.mockReset(); |
| 25 | +query.mockResolvedValue({rows: [{'?column?': 1}],rowCount: 1}); |
| 26 | +app=Fastify(); |
| 27 | +awaitapp.register(healthRoutes); |
| 28 | +awaitapp.ready(); |
| 29 | +}); |
| 30 | + |
| 31 | +afterEach(async()=>{ |
| 32 | +awaitapp.close(); |
| 33 | +}); |
| 34 | + |
| 35 | +describe('GET /health',()=>{ |
| 36 | +it('reports the process is up',async()=>{ |
| 37 | +constres=awaitapp.inject({method: 'GET',url: '/health'}); |
| 38 | +expect(res.statusCode).toBe(200); |
| 39 | +expect(res.json()).toMatchObject({status: 'ok'}); |
| 40 | +}); |
| 41 | + |
| 42 | +it('never touches the database, so a database outage cannot fail liveness',async()=>{ |
| 43 | +query.mockRejectedValue(newError('connection refused')); |
| 44 | +constres=awaitapp.inject({method: 'GET',url: '/health'}); |
| 45 | +expect(res.statusCode).toBe(200); |
| 46 | +expect(query).not.toHaveBeenCalled(); |
| 47 | +}); |
| 48 | +}); |
| 49 | + |
| 50 | +describe('GET /health/ready',()=>{ |
| 51 | +it('is ready when the database answers',async()=>{ |
| 52 | +constres=awaitapp.inject({method: 'GET',url: '/health/ready'}); |
| 53 | +expect(res.statusCode).toBe(200); |
| 54 | +expect(res.json()).toEqual({status: 'ok',checks: {database: 'ok'}}); |
| 55 | +}); |
| 56 | + |
| 57 | +it('is 503 when the database is unreachable',async()=>{ |
| 58 | +query.mockRejectedValue(newError('connection refused')); |
| 59 | +constres=awaitapp.inject({method: 'GET',url: '/health/ready'}); |
| 60 | +expect(res.statusCode).toBe(503); |
| 61 | +expect(res.json()).toEqual({status: 'error',checks: {database: 'error'}}); |
| 62 | +}); |
| 63 | + |
| 64 | +it('reports a failing Redis as degraded, not unready',async()=>{ |
| 65 | +constwithRedis=Fastify(); |
| 66 | +withRedis.decorate('redis',{ping: async()=>{thrownewError('down');}}asnever); |
| 67 | +awaitwithRedis.register(healthRoutes); |
| 68 | +awaitwithRedis.ready(); |
| 69 | + |
| 70 | +constres=awaitwithRedis.inject({method: 'GET',url: '/health/ready'}); |
| 71 | +expect(res.statusCode).toBe(200); |
| 72 | +expect(res.json()).toEqual({status: 'ok',checks: {database: 'ok',redis: 'error'}}); |
| 73 | +awaitwithRedis.close(); |
| 74 | +}); |
| 75 | +}); |
| 76 | + |
| 77 | +describe('regression: /health is not swallowed by the redirect route',()=>{ |
| 78 | +it('answers health, not a short-code lookup, when both plugins are registered',async()=>{ |
| 79 | +constcombined=Fastify(); |
| 80 | +// Redirect first — the static route must win on specificity, not order. |
| 81 | +awaitcombined.register(redirectRoutes); |
| 82 | +awaitcombined.register(healthRoutes); |
| 83 | +awaitcombined.ready(); |
| 84 | + |
| 85 | +constres=awaitcombined.inject({method: 'GET',url: '/health'}); |
| 86 | +expect(res.statusCode).toBe(200); |
| 87 | +expect(res.json()).toMatchObject({status: 'ok'}); |
| 88 | +// The redirect handler would have run a links lookup; health must not. |
| 89 | +constlinkLookups=query.mock.calls.filter(([sql])=>/FROMlinks/i.test(String(sql))); |
| 90 | +expect(linkLookups).toHaveLength(0); |
| 91 | + |
| 92 | +awaitcombined.close(); |
| 93 | +}); |
| 94 | +}); |
0 commit comments