Users can turn on 2FA from Profile & Security and receive a 6-digit login code by email or WhatsApp, with a "trust this device for 30 days" option. Admins can reset it for a locked-out user, and the self-service kiosk handles the code step at setup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
2.3 KiB
JavaScript
75 lines
2.3 KiB
JavaScript
process.env.JWT_SECRET = 'test-secret';
|
|
|
|
// The pure helpers under test never touch the database.
|
|
jest.mock('../src/config/db', () => ({}));
|
|
|
|
const {
|
|
generateCode,
|
|
hashCode,
|
|
hashDeviceToken,
|
|
maskEmail,
|
|
maskPhone,
|
|
hasRealEmail,
|
|
} = require('../src/utils/twoFactor');
|
|
|
|
describe('generateCode', () => {
|
|
test('always returns exactly six digits', () => {
|
|
for (let i = 0; i < 500; i++) {
|
|
expect(generateCode()).toMatch(/^\d{6}$/);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('hashCode / hashDeviceToken', () => {
|
|
test('is deterministic and never returns the input', () => {
|
|
expect(hashCode('123456')).toBe(hashCode('123456'));
|
|
expect(hashCode('123456')).not.toContain('123456');
|
|
expect(hashCode('123456')).toMatch(/^[0-9a-f]{64}$/);
|
|
});
|
|
|
|
test('different codes hash differently', () => {
|
|
expect(hashCode('123456')).not.toBe(hashCode('123457'));
|
|
});
|
|
|
|
test('code and device-token hashes are namespaced apart', () => {
|
|
expect(hashCode('123456')).not.toBe(hashDeviceToken('123456'));
|
|
});
|
|
|
|
test('depends on JWT_SECRET', () => {
|
|
const before = hashCode('123456');
|
|
process.env.JWT_SECRET = 'another-secret';
|
|
expect(hashCode('123456')).not.toBe(before);
|
|
process.env.JWT_SECRET = 'test-secret';
|
|
});
|
|
});
|
|
|
|
describe('masking', () => {
|
|
test('maskEmail keeps the first letter and the domain', () => {
|
|
expect(maskEmail('jane@example.com')).toBe('j•••@example.com');
|
|
});
|
|
|
|
test('maskEmail copes with junk input', () => {
|
|
expect(maskEmail('')).toBe('•••');
|
|
expect(maskEmail(null)).toBe('•••');
|
|
});
|
|
|
|
test('maskPhone keeps only the last four digits', () => {
|
|
expect(maskPhone('27821234567')).toBe('••• ••• 4567');
|
|
expect(maskPhone('+27 82 123 4567')).toBe('••• ••• 4567');
|
|
});
|
|
});
|
|
|
|
describe('hasRealEmail', () => {
|
|
test('accepts a normal address', () => {
|
|
expect(hasRealEmail({ email: 'jane@example.com' })).toBe(true);
|
|
});
|
|
|
|
test('rejects guest and deleted placeholders and missing values', () => {
|
|
expect(hasRealEmail({ email: 'abc@guest.local' })).toBe(false);
|
|
expect(hasRealEmail({ email: 'deleted-1@deleted.invalid' })).toBe(false);
|
|
expect(hasRealEmail({ email: 'deleted-1@deleted.local' })).toBe(false);
|
|
expect(hasRealEmail({})).toBe(false);
|
|
expect(hasRealEmail(null)).toBe(false);
|
|
});
|
|
});
|