import { describe, expect, it } from 'vitest'; import type { FastifyInstance } from 'fastify'; import { Writable } from 'node:stream'; import { API_DEFAULT_HOST, API_DEFAULT_PORT, buildApp, createListenOptions, REDACT_PATHS, } from './app.js'; const registerProbe = async (app: FastifyInstance): Promise => { app.post( '/api/v1/test-only/schema-probe', { schema: { body: { type: 'object', additionalProperties: false, required: ['name'], properties: { name: { type: 'string', minLength: 1 } }, }, }, }, async (request) => ({ name: (request.body as { name: string }).name }), ); app.get('/api/v1/test-only/failure', async () => { throw new Error('internal secret detail'); }); }; describe('Fastify API skeleton', () => { it('is injectable without listening and exposes health/readiness with server request IDs', async () => { const app = buildApp({ readiness: () => ({ ready: true, checks: { bootstrap: 'ready' } }) }); const health = await app.inject({ method: 'GET', url: '/healthz', headers: { 'x-request-id': 'attacker-controlled' }, }); const ready = await app.inject({ method: 'GET', url: '/readyz' }); expect(health.statusCode).toBe(200); expect(health.json()).toEqual({ status: 'ok' }); expect(ready.statusCode).toBe(200); expect(ready.json()).toEqual({ ready: true, checks: { bootstrap: 'ready' } }); expect(health.headers['x-request-id']).toMatch(/^[0-9a-f-]{36}$/); expect(health.headers['x-request-id']).not.toBe('attacker-controlled'); await app.close(); }, 15_000); it('returns RFC Problem Details for schema failures and unexpected errors without leaking detail', async () => { let logs = ''; const logStream = new Writable({ write(chunk, _encoding, callback) { logs += String(chunk); callback(); }, }); const app = buildApp({ logger: { stream: logStream }, registerRoutes: registerProbe }); const invalid = await app.inject({ method: 'POST', url: '/api/v1/test-only/schema-probe', headers: { 'content-type': 'application/json' }, payload: {}, }); expect(invalid.statusCode).toBe(400); expect(invalid.headers['content-type']).toContain('application/problem+json'); expect(invalid.json()).toMatchObject({ type: 'about:blank', title: 'Bad Request', status: 400, code: 'VALIDATION_FAILED', requestId: invalid.headers['x-request-id'], }); expect(invalid.json().validation).toEqual( expect.arrayContaining([ expect.objectContaining({ field: expect.any(String), code: expect.any(String), message: expect.any(String), }), ]), ); const failure = await app.inject({ method: 'GET', url: '/api/v1/test-only/failure' }); expect(failure.statusCode).toBe(500); expect(failure.headers['content-type']).toContain('application/problem+json'); expect(JSON.stringify(failure.json())).not.toContain('internal secret detail'); expect(logs).toContain('request failed'); expect(logs).not.toContain('internal secret detail'); await app.close(); }); it.each([ { name: 'malformed JSON', request: { method: 'POST' as const, url: '/api/v1/test-only/schema-probe', headers: { 'content-type': 'application/json' }, payload: '{', }, status: 400, }, { name: 'unsupported media type', request: { method: 'POST' as const, url: '/api/v1/test-only/schema-probe', headers: { 'content-type': 'application/x-unsupported' }, payload: 'x', }, status: 415, }, { name: 'oversized body', request: { method: 'POST' as const, url: '/api/v1/test-only/schema-probe', headers: { 'content-type': 'application/json' }, payload: JSON.stringify({ name: 'x'.repeat(1_048_577) }), }, status: 413, }, ])('preserves Fastify client status for $name', async ({ request, status }) => { const app = buildApp({ registerRoutes: registerProbe }); const response = await app.inject(request); expect(response.statusCode).toBe(status); expect(response.headers['content-type']).toContain('application/problem+json'); expect(response.json()).toMatchObject({ status, requestId: response.headers['x-request-id'] }); await app.close(); }); it('rejects non-loopback Host and cross-origin browser requests before route execution', async () => { const app = buildApp({ registerRoutes: registerProbe }); const foreignHost = await app.inject({ method: 'GET', url: '/healthz', headers: { host: 'example.com' }, }); expect(foreignHost.statusCode).toBe(403); expect(foreignHost.headers['content-type']).toContain('application/problem+json'); const crossOrigin = await app.inject({ method: 'GET', url: '/healthz', headers: { host: '127.0.0.1:8790', origin: 'http://localhost:8790' }, }); expect(crossOrigin.statusCode).toBe(403); const sameOrigin = await app.inject({ method: 'GET', url: '/healthz', headers: { host: '127.0.0.1:8790', origin: 'http://127.0.0.1:8790' }, }); expect(sameOrigin.statusCode).toBe(200); for (const host of [ '127.0.0.1@example.com', '127.0.0.1/path', '[::ffff:127.0.0.1]:8790', '2130706433', '0177.0.0.1', '0x7f.0.0.1', '127.1', '127.0.1', '[0:0:0:0:0:0:0:1]', ]) { const response = await app.inject({ method: 'GET', url: '/healthz', headers: { host } }); expect(response.statusCode, host).toBe(403); } for (const host of ['localhost:8790', '[::1]:8790']) { const response = await app.inject({ method: 'GET', url: '/healthz', headers: { host, origin: `http://${host}` }, }); expect(response.statusCode, host).toBe(200); } for (const origin of [ 'https://127.0.0.1:8790', 'http://evil@127.0.0.1:8790', 'http://127.0.0.1:8790/path', 'http://2130706433:8790', ]) { const response = await app.inject({ method: 'GET', url: '/healthz', headers: { host: '127.0.0.1:8790', origin }, }); expect(response.statusCode, origin).toBe(403); } await app.close(); }); it('returns 503 with structured readiness checks when dependencies are not ready', async () => { const app = buildApp({ readiness: () => ({ ready: false, checks: { database: 'unavailable' } }), }); const response = await app.inject({ method: 'GET', url: '/readyz' }); expect(response.statusCode).toBe(503); expect(response.json()).toEqual({ ready: false, checks: { database: 'unavailable' } }); await app.close(); }); it('defaults to a separate loopback listener and rejects unsafe listen overrides', () => { expect(API_DEFAULT_HOST).toBe('127.0.0.1'); expect(API_DEFAULT_PORT).not.toBe(8788); expect(createListenOptions({})).toEqual({ host: API_DEFAULT_HOST, port: API_DEFAULT_PORT }); expect(createListenOptions({ API_HOST: '::1' })).toEqual({ host: '::1', port: API_DEFAULT_PORT, }); expect(() => createListenOptions({ API_HOST: '0.0.0.0' })).toThrow(/loopback/i); expect(() => createListenOptions({ API_PORT: '8788' })).toThrow(/reserved|legacy/i); expect(() => createListenOptions({ API_PORT: 'not-a-port' })).toThrow(/port/i); }); it('defines immutable logger redaction for credentials, confirmation headers and cookies', () => { expect(REDACT_PATHS).toEqual( expect.arrayContaining([ 'req.headers.authorization', 'req.headers.cookie', 'req.headers.x-confirmation-token', 'req.body.password', 'req.body.newPassword', 'res.headers.set-cookie', ]), ); expect(new Set(REDACT_PATHS).size).toBe(REDACT_PATHS.length); }); });