From bba88c5729159efccceb6f43a51dac38dcd19c49 Mon Sep 17 00:00:00 2001 From: Sakariyah Abdulhazeem Date: Thu, 1 Oct 2026 12:00:39 +0100 Subject: [PATCH] security: keep slow plans fixture test-only --- docs/api-plans-timeout.md | 7 ++++++- src/routes/plans.test.ts | 10 +++++++++- src/routes/plans.ts | 16 ++++++++++------ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/docs/api-plans-timeout.md b/docs/api-plans-timeout.md index 1ccf5bea..ecb4469e 100644 --- a/docs/api-plans-timeout.md +++ b/docs/api-plans-timeout.md @@ -74,7 +74,12 @@ When the timeout fires, the response uses the canonical error envelope: |--------|--------------|-------------------------------------------------------| | GET | `/api/plans` | List all available subscription plans. | | GET | `/api/plans/:id` | Get a single plan by ID. | -| GET | `/api/plans/slow` | Simulates a slow handler (3s delay) for testing timeout behaviour. | +| GET | `/api/plans/slow` | Test-only fixture; never registered by the production router. | + +The slow fixture is opt-in (`enableSlowRoute: true`) and should only be mounted +by isolated timeout tests. It is intentionally unavailable in production, where +`GET /api/plans/slow` returns 404. Timeout behavior can also be tested with a +stubbed repository or a standalone route using `createTimeoutMiddleware`. ## Disabling the Timeout diff --git a/src/routes/plans.test.ts b/src/routes/plans.test.ts index 59854643..20e665c2 100644 --- a/src/routes/plans.test.ts +++ b/src/routes/plans.test.ts @@ -88,10 +88,18 @@ describe('/api/plans', () => { expect(res.body.error.code).toBe('NOT_FOUND'); }); + it('does not expose the slow fixture unless explicitly enabled', async () => { + const repo = new InMemoryPlansRepository(seedPlans); + const app = buildApp(repo); + + const res = await request(app).get('/api/plans/slow'); + expect(res.status).toBe(404); + }); + it('should return 504 when slow endpoint exceeds timeout', async () => { const repo = new InMemoryPlansRepository(seedPlans); const app = express(); - app.use('/api/plans', createPlansRouter(10, { plansRepository: repo })); + app.use('/api/plans', createPlansRouter(10, { plansRepository: repo, enableSlowRoute: true })); app.use(errorHandler); const res = await request(app).get('/api/plans/slow'); diff --git a/src/routes/plans.ts b/src/routes/plans.ts index 1ee52d84..3eb74c73 100644 --- a/src/routes/plans.ts +++ b/src/routes/plans.ts @@ -11,6 +11,8 @@ import { export interface PlansRouterDeps { plansRepository?: PlansRepository; + /** Enables the intentionally slow fixture only in isolated tests. */ + enableSlowRoute?: boolean; } /** @@ -80,12 +82,14 @@ export function createPlansRouter( res.json(successEnvelope(plans, requestId)); })); - router.get('/slow', asyncHandler(async (req: Request, res: Response) => { - await sleepWithAbort(3000, req.signal ?? req.abortSignal); - const requestId = getRequestId(req) ?? 'unknown'; - const plans = await plansRepository.list(); - res.json(successEnvelope(plans, requestId)); - })); + if (deps.enableSlowRoute) { + router.get('/slow', asyncHandler(async (req: Request, res: Response) => { + await sleepWithAbort(3000, req.signal ?? req.abortSignal); + const requestId = getRequestId(req) ?? 'unknown'; + const plans = await plansRepository.list(); + res.json(successEnvelope(plans, requestId)); + })); + } router.get('/:id', asyncHandler(async (req: Request, res: Response) => { const requestId = getRequestId(req) ?? 'unknown';