Files
hope-events/backend/tests/requiredTwoFactor.test.js
T
joshuaandClaude Opus 5.5 c3012cb3f6 Require two-factor authentication for admins and supervisors
Admins and supervisors without 2FA are sent a code at their next login,
and verifying it turns 2FA on. They can change the channel but not turn
it off. Older sessions without 2FA are rejected with TWO_FACTOR_REQUIRED,
and promoting a user ends their sessions so they enrol at next login.
The requirement is skipped when no email or WhatsApp channel can deliver
a code, and TWO_FACTOR_REQUIRED_ROLES=false switches it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:00:12 +02:00

126 lines
4.6 KiB
JavaScript

process.env.JWT_SECRET = 'test-secret';
process.env.NODE_ENV = 'test';
// Two-factor is required for admins and supervisors — but only when some channel
// can actually deliver a code, so a site with no SMTP/WhatsApp never locks them out.
const mockChannels = { smtp: true, whatsapp: true };
const mockUsers = new Map();
jest.mock('../src/config/db', () => ({
user: { findUnique: async ({ where }) => mockUsers.get(where.id) || null },
}));
jest.mock('../src/utils/settingsCache', () => ({ getSettingSync: (_key, fallback) => fallback }));
jest.mock('../src/utils/email', () => ({ isSmtpConfigured: () => mockChannels.smtp }));
jest.mock('../src/utils/whatsapp', () => ({
isValidZAPhone: (p) => /^27\d{9}$/.test(String(p || '')),
isConfigured: async () => mockChannels.whatsapp,
}));
const jwt = require('jsonwebtoken');
const { requiresTwoFactor, enforcementMethod, mustEnrolTwoFactor } = require('../src/utils/twoFactor');
const { protect } = require('../src/middleware/authMiddleware');
const ADMIN = { id: 'a1', name: 'Ann', email: 'ann@example.com', phoneNumber: '27821234567', role: 'admin', isActive: true, tokenVersion: 0, twoFactorEnabled: false };
beforeEach(() => {
mockChannels.smtp = true;
mockChannels.whatsapp = true;
mockUsers.clear();
delete process.env.TWO_FACTOR_REQUIRED_ROLES;
});
describe('requiresTwoFactor', () => {
test('applies to admins and supervisors only', () => {
expect(requiresTwoFactor({ role: 'admin' })).toBe(true);
expect(requiresTwoFactor({ role: 'supervisor' })).toBe(true);
expect(requiresTwoFactor({ role: 'staff' })).toBe(false);
expect(requiresTwoFactor({ role: 'user' })).toBe(false);
});
test('TWO_FACTOR_REQUIRED_ROLES=false turns the requirement off', () => {
process.env.TWO_FACTOR_REQUIRED_ROLES = 'false';
expect(requiresTwoFactor({ role: 'admin' })).toBe(false);
});
});
describe('enforcementMethod', () => {
test('prefers email when SMTP is configured', async () => {
expect(await enforcementMethod(ADMIN)).toBe('email');
});
test('falls back to WhatsApp without SMTP', async () => {
mockChannels.smtp = false;
expect(await enforcementMethod(ADMIN)).toBe('whatsapp');
});
test('skips a guest placeholder email', async () => {
expect(await enforcementMethod({ ...ADMIN, email: 'x@guest.local' })).toBe('whatsapp');
});
test('is null when no channel can deliver a code', async () => {
mockChannels.smtp = false;
mockChannels.whatsapp = false;
expect(await enforcementMethod(ADMIN)).toBeNull();
});
});
describe('mustEnrolTwoFactor', () => {
test('true for an admin without 2FA', async () => {
expect(await mustEnrolTwoFactor(ADMIN)).toBe(true);
});
test('false once enrolled, for other roles, or with no channel', async () => {
expect(await mustEnrolTwoFactor({ ...ADMIN, twoFactorEnabled: true })).toBe(false);
expect(await mustEnrolTwoFactor({ ...ADMIN, role: 'staff' })).toBe(false);
mockChannels.smtp = false;
mockChannels.whatsapp = false;
expect(await mustEnrolTwoFactor(ADMIN)).toBe(false);
});
});
describe('protect', () => {
function run(user, claims = {}) {
mockUsers.set(user.id, user);
const token = jwt.sign({ id: user.id, role: user.role, tokenVersion: 0, ...claims }, process.env.JWT_SECRET);
const req = { headers: { authorization: `Bearer ${token}` } };
const res = { statusCode: 200, body: null };
res.status = (c) => { res.statusCode = c; return res; };
res.json = (b) => { res.body = b; return res; };
return new Promise((resolve) => {
protect(req, res, (err) => resolve({ req, res, err, passed: !err })).then(() => {
if (res.body) resolve({ req, res, passed: false });
});
});
}
test('rejects an un-enrolled admin session with TWO_FACTOR_REQUIRED', async () => {
const { res, passed } = await run(ADMIN);
expect(passed).toBe(false);
expect(res.statusCode).toBe(401);
expect(res.body.code).toBe('TWO_FACTOR_REQUIRED');
});
test('lets an enrolled admin through', async () => {
const { passed } = await run({ ...ADMIN, twoFactorEnabled: true });
expect(passed).toBe(true);
});
test('lets a regular user through without 2FA', async () => {
const { passed } = await run({ ...ADMIN, id: 'u1', role: 'user' });
expect(passed).toBe(true);
});
test('gives the setup wizard token a grace period', async () => {
const { passed } = await run(ADMIN, { setup: true });
expect(passed).toBe(true);
});
test('does not enforce when no channel can deliver a code', async () => {
mockChannels.smtp = false;
mockChannels.whatsapp = false;
const { passed } = await run(ADMIN);
expect(passed).toBe(true);
});
});