diff --git a/apps/backend/src/__tests__/app.test.ts b/apps/backend/src/__tests__/app.test.ts new file mode 100644 index 0000000..3b4b640 --- /dev/null +++ b/apps/backend/src/__tests__/app.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { buildApp } from '../app.js'; + +describe('request logging middleware', () => { + let app: Awaited>; + + afterEach(async () => { + await app?.close(); + }); + + it('logs method and path (without query string) for each request', async () => { + app = await buildApp(); + const logSpy = vi.spyOn(app.log, 'info'); + + await app.inject({ method: 'GET', url: '/health?foo=bar' }); + + const calls = logSpy.mock.calls.map((c) => c[0]); + const loggedRequest = calls.find( + (c: any) => typeof c === 'object' && c?.method === 'GET' && c?.url === '/health' + ); + expect(loggedRequest).toBeDefined(); + }); + + it('does not log query string parameters', async () => { + app = await buildApp(); + const logSpy = vi.spyOn(app.log, 'info'); + + await app.inject({ method: 'GET', url: '/health?secret=token123' }); + + const calls = logSpy.mock.calls.map((c) => c[0]); + const leaked = calls.find( + (c: any) => typeof c === 'object' && typeof c?.url === 'string' && c.url.includes('secret') + ); + expect(leaked).toBeUndefined(); + }); + + it('returns 200 for health check (existing behavior unchanged)', async () => { + app = await buildApp(); + const res = await app.inject({ method: 'GET', url: '/health' }); + expect(res.statusCode).toBe(200); + const body = JSON.parse(res.body); + expect(body.status).toBe('ok'); + }); +}); diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index dc023a2..f60df94 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -17,6 +17,7 @@ import { publicRoutes } from './routes/public.js'; import { followRoutes } from './routes/follow.js'; import { connectRoutes } from './routes/connect.js'; import { analyticsRoutes } from './routes/analytics.js'; +import { nfcRoutes } from './routes/nfc.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -66,6 +67,12 @@ export async function buildApp() { } }); + // ─── Request Logger ─── + app.addHook('onRequest', async (request) => { + const path = request.url.split('?')[0]; + app.log.info({ method: request.method, url: path }, 'incoming request'); + }); + // ─── Routes ─── await app.register(authRoutes, { prefix: '/auth' }); await app.register(profileRoutes, { prefix: '/api/profiles' });