// Unit tests for requireRole middleware. // // These exercise the factory + shorthand helpers without standing up an // Express app. We keep this Vitest-friendly (no jest globals) — the // project uses Vitest, not Jest. const { ROLE, requireRole, requireAdmin, requireStaff, requireOwnerOrAdmin, ADMIN_ROLES, ADMIN_AND_PRINCIPAL, STAFF_ROLES, } = require('../src/middleware/requireRole'); function fakeRes() { const r = { statusCode: null, body: null }; r.status = (code) => { r.statusCode = code; return r; }; r.json = (body) => { r.body = body; return r; }; return r; } function captureNext() { const calls = []; const fn = (...args) => { calls.push(args); }; return [fn, calls]; } describe('requireRole middleware factory', () => { it('allows a caller whose role is in the allowed set', () => { const mw = requireRole(ROLE.TEACHER, ROLE.SCHOOL_ADMIN); const [next, calls] = captureNext(); mw({ user: { role: ROLE.TEACHER } }, fakeRes(), next); expect(calls.length).toBe(1); }); it('rejects with 403 when the caller role is not allowed', () => { const mw = requireRole(ROLE.SCHOOL_ADMIN); const res = fakeRes(); const [next, calls] = captureNext(); mw({ user: { role: ROLE.STUDENT } }, res, next); expect(res.statusCode).toBe(403); expect(res.body.error).toMatch(/forbidden/i); expect(calls.length).toBe(0); }); it('rejects with 401 when no user is attached (defence in depth)', () => { const mw = requireRole(ROLE.TEACHER); const res = fakeRes(); const [next, calls] = captureNext(); mw({}, res, next); expect(res.statusCode).toBe(401); expect(calls.length).toBe(0); }); it('treats missing role as unauthenticated', () => { const mw = requireRole(ROLE.TEACHER); const res = fakeRes(); const [next, calls] = captureNext(); mw({ user: {} }, res, next); expect(res.statusCode).toBe(401); expect(calls.length).toBe(0); }); }); describe('requireAdmin shorthand', () => { it('allows school_admin, systems_admin, and principal', () => { for (const role of ADMIN_AND_PRINCIPAL) { const [next, calls] = captureNext(); requireAdmin({ user: { role } }, fakeRes(), next); expect(calls.length).toBe(1); } }); it('rejects teachers, students, and parents', () => { for (const role of [ROLE.TEACHER, ROLE.STUDENT, ROLE.PARENT, ROLE.BURSAR]) { const res = fakeRes(); const [next, calls] = captureNext(); requireAdmin({ user: { role } }, res, next); expect(res.statusCode).toBe(403); expect(calls.length).toBe(0); } }); }); describe('requireStaff shorthand', () => { it('admits every staff role (admin, hr, bursar, librarian, etc.)', () => { for (const role of STAFF_ROLES) { const [next, calls] = captureNext(); requireStaff({ user: { role } }, fakeRes(), next); expect(calls.length).toBe(1); } }); it('admits teachers and librarian + clubs_head (operational roles)', () => { for (const role of [ROLE.TEACHER, ROLE.LIBRARIAN, ROLE.CLUBS_HEAD]) { const [next, calls] = captureNext(); requireStaff({ user: { role } }, fakeRes(), next); expect(calls.length).toBe(1); } }); it('rejects students and parents', () => { for (const role of [ROLE.STUDENT, ROLE.PARENT]) { const res = fakeRes(); const [next, calls] = captureNext(); requireStaff({ user: { role } }, res, next); expect(res.statusCode).toBe(403); expect(calls.length).toBe(0); } }); }); describe('requireOwnerOrAdmin', () => { it('allows admins unconditionally', () => { for (const role of ADMIN_ROLES) { const [next, calls] = captureNext(); requireOwnerOrAdmin({ user: { role, id: 1 }, params: {} }, fakeRes(), next); expect(calls.length).toBe(1); } }); it('allows the user whose id matches the URL param', () => { const [next, calls] = captureNext(); requireOwnerOrAdmin( { user: { role: ROLE.STUDENT, id: 42 }, params: { id: '42' } }, fakeRes(), next ); expect(calls.length).toBe(1); }); it('rejects a non-owner who is not admin and not parent', () => { const res = fakeRes(); const [next, calls] = captureNext(); requireOwnerOrAdmin( { user: { role: ROLE.STUDENT, id: 1 }, params: { id: '2' } }, res, next ); expect(res.statusCode).toBe(403); expect(calls.length).toBe(0); }); it('allows parents through (controller does its own scope check)', () => { const [next, calls] = captureNext(); requireOwnerOrAdmin( { user: { role: ROLE.PARENT, id: 7 }, params: { studentId: '999' } }, fakeRes(), next ); expect(calls.length).toBe(1); }); }); describe('ROLE constant', () => { it('exposes the role strings in a stable shape', () => { expect(ROLE.SCHOOL_ADMIN).toBe('school_admin'); expect(ROLE.SYSTEMS_ADMIN).toBe('systems_admin'); }); it('is frozen so callers cannot mutate the registry', () => { expect(Object.isFrozen(ROLE)).toBe(true); }); });