diff --git a/db/migrations/005_performance_indexes.sql b/db/migrations/005_performance_indexes.sql index 76f5afd..b4376c7 100644 --- a/db/migrations/005_performance_indexes.sql +++ b/db/migrations/005_performance_indexes.sql @@ -4,4 +4,4 @@ CREATE INDEX IF NOT EXISTS idx_paycheck_bills_paycheck_id ON paycheck_bills(payc CREATE INDEX IF NOT EXISTS idx_actuals_paycheck_id ON actuals(paycheck_id); CREATE INDEX IF NOT EXISTS idx_one_time_expenses_paycheck_id ON one_time_expenses(paycheck_id); CREATE INDEX IF NOT EXISTS idx_financing_payments_plan_id ON financing_payments(plan_id); -CREATE INDEX IF NOT EXISTS idx_financing_plans_active ON financing_plans(active); +CREATE INDEX IF NOT EXISTS idx_financing_plans_active ON financing_plans(active) WHERE active = true; diff --git a/server/src/__tests__/timing.middleware.test.js b/server/src/__tests__/timing.middleware.test.js new file mode 100644 index 0000000..f834219 --- /dev/null +++ b/server/src/__tests__/timing.middleware.test.js @@ -0,0 +1,98 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +const timingMiddleware = require('../middleware/timing'); + +function makeResMock() { + const listeners = {}; + return { + statusCode: 200, + on(event, cb) { + listeners[event] = cb; + }, + emit(event) { + if (listeners[event]) listeners[event](); + }, + }; +} + +describe('timingMiddleware', () => { + let consoleSpy; + let warnSpy; + + beforeEach(() => { + consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + warnSpy.mockRestore(); + vi.useRealTimers(); + }); + + it('calls next()', () => { + const req = { method: 'GET', path: '/api/health' }; + const res = makeResMock(); + const next = vi.fn(); + + timingMiddleware(req, res, next); + expect(next).toHaveBeenCalledOnce(); + }); + + it('logs timing on response finish', () => { + const req = { method: 'GET', path: '/api/health' }; + const res = makeResMock(); + + timingMiddleware(req, res, vi.fn()); + res.emit('finish'); + + expect(consoleSpy).toHaveBeenCalledOnce(); + const msg = consoleSpy.mock.calls[0][0]; + expect(msg).toMatch(/\[timing\] GET \/api\/health 200 \d+ms/); + }); + + it('emits SLOW warning when duration exceeds 200ms threshold', () => { + vi.useFakeTimers(); + + const req = { method: 'POST', path: '/api/paychecks' }; + const res = makeResMock(); + + timingMiddleware(req, res, vi.fn()); + + // Advance time past the threshold + vi.advanceTimersByTime(250); + res.emit('finish'); + + expect(warnSpy).toHaveBeenCalledOnce(); + const msg = warnSpy.mock.calls[0][0]; + expect(msg).toMatch(/\[SLOW\] POST \/api\/paychecks/); + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('does not emit SLOW warning for fast requests', () => { + vi.useFakeTimers(); + + const req = { method: 'GET', path: '/api/financing' }; + const res = makeResMock(); + + timingMiddleware(req, res, vi.fn()); + + vi.advanceTimersByTime(50); + res.emit('finish'); + + expect(consoleSpy).toHaveBeenCalledOnce(); + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('includes status code in the log message', () => { + const req = { method: 'GET', path: '/api/bills' }; + const res = makeResMock(); + res.statusCode = 404; + + timingMiddleware(req, res, vi.fn()); + res.emit('finish'); + + const msg = consoleSpy.mock.calls[0][0]; + expect(msg).toContain('404'); + }); +}); diff --git a/server/src/app.js b/server/src/app.js index 2deda15..072d487 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -15,9 +15,8 @@ const app = express(); app.use(cors()); app.use(express.json()); -app.use(timingMiddleware); - // API routes +app.use('/api', timingMiddleware); app.use('/api', healthRouter); app.use('/api', configRouter); app.use('/api', billsRouter);