From ea0eeadc432b3c4723c25f71ff0a360cc150baea Mon Sep 17 00:00:00 2001 From: Sarthak Doshi Date: Sun, 10 May 2026 23:55:28 +0530 Subject: [PATCH 1/2] feat: add onRequest logging hook to capture method and URL - Add app.addHook('onRequest') in app.ts that logs request.method and request.url via pino (app.log.info) for every incoming request - Add apps/backend/src/__tests__/app.test.ts with two vitest tests: one asserting the log entry is created for GET /health, one asserting existing health check behavior is unchanged (200 + status ok) Closes #8 --- apps/backend/src/__tests__/app.test.ts | 25 +++++++++++++++++++++++++ apps/backend/src/app.ts | 6 ++++++ 2 files changed, 31 insertions(+) create mode 100644 apps/backend/src/__tests__/app.test.ts diff --git a/apps/backend/src/__tests__/app.test.ts b/apps/backend/src/__tests__/app.test.ts new file mode 100644 index 0000000..8f2f795 --- /dev/null +++ b/apps/backend/src/__tests__/app.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect, vi } from 'vitest'; +import { buildApp } from '../app.js'; + +describe('request logging middleware', () => { + it('logs method and url for each incoming request', async () => { + const app = await buildApp(); + const logSpy = vi.spyOn(app.log, 'info'); + + await app.inject({ method: 'GET', url: '/health' }); + + 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('returns 200 for health check (existing behavior unchanged)', async () => { + const 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..f0bf1c7 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,11 @@ export async function buildApp() { } }); + // ─── Request Logger ─── + app.addHook('onRequest', async (request) => { + app.log.info({ method: request.method, url: request.url }, 'incoming request'); + }); + // ─── Routes ─── await app.register(authRoutes, { prefix: '/auth' }); await app.register(profileRoutes, { prefix: '/api/profiles' }); From 110d0f7c7b2331a96c1ddea79e02d30c8b552fa7 Mon Sep 17 00:00:00 2001 From: Sarthak Doshi Date: Mon, 11 May 2026 00:15:57 +0530 Subject: [PATCH 2/2] fix: strip query string from logged URL, add afterEach app.close() in tests --- apps/backend/src/__tests__/app.test.ts | 29 +++++++++++++++++++++----- apps/backend/src/app.ts | 3 ++- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/apps/backend/src/__tests__/app.test.ts b/apps/backend/src/__tests__/app.test.ts index 8f2f795..3b4b640 100644 --- a/apps/backend/src/__tests__/app.test.ts +++ b/apps/backend/src/__tests__/app.test.ts @@ -1,12 +1,18 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi, afterEach } from 'vitest'; import { buildApp } from '../app.js'; describe('request logging middleware', () => { - it('logs method and url for each incoming request', async () => { - const app = await buildApp(); + 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' }); + await app.inject({ method: 'GET', url: '/health?foo=bar' }); const calls = logSpy.mock.calls.map((c) => c[0]); const loggedRequest = calls.find( @@ -15,8 +21,21 @@ describe('request logging middleware', () => { 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 () => { - const app = await buildApp(); + app = await buildApp(); const res = await app.inject({ method: 'GET', url: '/health' }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body); diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index f0bf1c7..f60df94 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -69,7 +69,8 @@ export async function buildApp() { // ─── Request Logger ─── app.addHook('onRequest', async (request) => { - app.log.info({ method: request.method, url: request.url }, 'incoming request'); + const path = request.url.split('?')[0]; + app.log.info({ method: request.method, url: path }, 'incoming request'); }); // ─── Routes ───