geocrop-platform./apps/nextgen/server/tests/require-role.test.js

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);
});
});