176 lines
5.0 KiB
JavaScript
176 lines
5.0 KiB
JavaScript
// 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);
|
|
});
|
|
});
|