diff --git a/routes/admin/courses.routes.js b/routes/admin/courses.routes.js index 425a543..2fa24d0 100644 --- a/routes/admin/courses.routes.js +++ b/routes/admin/courses.routes.js @@ -3,11 +3,7 @@ const ctrl = require("../../controllers/admin/courses.controller"); const progressCtrl = require("../../controllers/admin/course_reading_progress.controller"); const requirementsCtrl = require("../../controllers/admin/completion_requirements.controller"); -const { authenticate } = require("../../middleware/auth.middleware"); -const { requireAdmin } = require("../../middleware/rbac.middleware"); -const { adminLimiter } = require("../../middleware/rateLimiter.middleware"); - -router.use(authenticate, requireAdmin(), adminLimiter); +// Guards are applied by admin.routes.js (authenticate → requireAdmin → adminLimiter) // ══════════════════════════════════════════════════════════════════════════════ // COURSES diff --git a/tests/middleware/rateLimiter.test.js b/tests/middleware/rateLimiter.test.js new file mode 100644 index 0000000..88a7793 --- /dev/null +++ b/tests/middleware/rateLimiter.test.js @@ -0,0 +1,76 @@ +'use strict'; + +// courses.routes.js used to re-declare `router.use(authenticate, requireAdmin(), adminLimiter)` +// on top of the guard chain admin.routes.js already applies to every mounted admin sub-router. +// That double-application silently halved the effective adminLimiter budget (each request +// incremented the shared store twice) and surfaced as premature "Too many admin requests" +// errors. This suite locks in the invariant: the guard chain is applied exactly once, in +// admin.routes.js, and never re-declared by the sub-routers it mounts. + +process.env.NODE_ENV = 'test'; + +const path = require('path'); +const fs = require('fs'); + +const { authenticate } = require('../../middleware/auth.middleware'); +const { adminLimiter } = require('../../middleware/rateLimiter.middleware'); + +const adminRoutesDir = path.join(__dirname, '../../routes/admin'); + +// Every admin sub-router mounted by admin.routes.js, discovered dynamically so newly +// added route files are covered automatically. +const subRouterFiles = fs + .readdirSync(adminRoutesDir) + .filter((f) => f.endsWith('.routes.js') && f !== 'admin.routes.js'); + +function stackHandles(router) { + return router.stack.map((layer) => layer.handle); +} + +describe('admin sub-routers do not re-declare inherited guards', () => { + test.each(subRouterFiles)('%s does not re-apply authenticate or adminLimiter', (file) => { + const router = require(path.join(adminRoutesDir, file)); + const handles = stackHandles(router); + + expect(handles).not.toContain(authenticate); + expect(handles).not.toContain(adminLimiter); + }); + + // Explicit named cases for clarity: the one that broke, plus known-good references. + test('courses.routes.js (regression case) has no top-level guard layer', () => { + const router = require(path.join(adminRoutesDir, 'courses.routes.js')); + const handles = stackHandles(router); + + expect(handles).not.toContain(authenticate); + expect(handles).not.toContain(adminLimiter); + }); + + test('units.routes.js (already correct) has no top-level guard layer', () => { + const router = require(path.join(adminRoutesDir, 'units.routes.js')); + const handles = stackHandles(router); + + expect(handles).not.toContain(authenticate); + expect(handles).not.toContain(adminLimiter); + }); + + test('lessons.routes.js (already correct) has no top-level guard layer', () => { + const router = require(path.join(adminRoutesDir, 'lessons.routes.js')); + const handles = stackHandles(router); + + expect(handles).not.toContain(authenticate); + expect(handles).not.toContain(adminLimiter); + }); +}); + +describe('admin.routes.js applies the guard chain exactly once', () => { + test('authenticate and adminLimiter each appear exactly one time in the stack', () => { + const adminRouter = require(path.join(adminRoutesDir, 'admin.routes.js')); + const handles = stackHandles(adminRouter); + + const authCount = handles.filter((h) => h === authenticate).length; + const limitCount = handles.filter((h) => h === adminLimiter).length; + + expect(authCount).toBe(1); + expect(limitCount).toBe(1); + }); +});