joplock/tests/mfaService.test.js
igor b735224377
Some checks are pending
Build and push Joplock image / build-and-push (push) Waiting to run
Refactor route rendering and harden editor session handling
2026-04-27 00:24:23 +12:00

190 lines
6 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const { base32Decode, base32Encode, createMfaService, generateSeed, hotp, normalizeSeed, otpauthUri, qrCodeDataUrl, verifyWithSeed } = require('../app/auth/mfaService');
// --- normalizeSeed ---
test('normalizeSeed uppercases and trims', () => {
assert.equal(normalizeSeed(' abc def '), 'ABCDEF');
});
test('normalizeSeed returns empty for null/undefined', () => {
assert.equal(normalizeSeed(null), '');
assert.equal(normalizeSeed(undefined), '');
});
// --- base32 round-trip ---
test('base32Encode then base32Decode round-trips', () => {
const buf = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
const encoded = base32Encode(buf);
const decoded = base32Decode(encoded);
assert.deepEqual(decoded, buf);
});
test('base32Decode rejects invalid characters', () => {
assert.throws(() => base32Decode('!!!'), /Invalid TOTP seed/);
});
test('base32Decode strips trailing padding', () => {
const buf = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
const encoded = base32Encode(buf) + '====';
const decoded = base32Decode(encoded);
assert.deepEqual(decoded, buf);
});
// --- generateSeed ---
test('generateSeed returns 32-char base32 string', () => {
const seed = generateSeed();
assert.ok(/^[A-Z2-7]+$/.test(seed), 'should be valid base32');
assert.equal(seed.length, 32); // 20 bytes * 8 bits / 5 = 32 chars
});
test('generateSeed produces unique values', () => {
const a = generateSeed();
const b = generateSeed();
assert.notEqual(a, b);
});
// --- hotp ---
test('hotp returns 6-digit string', () => {
const secret = base32Decode('JBSWY3DPEHPK3PXP');
const code = hotp(secret, 1);
assert.match(code, /^\d{6}$/);
});
test('hotp is deterministic for same inputs', () => {
const secret = base32Decode('JBSWY3DPEHPK3PXP');
assert.equal(hotp(secret, 42), hotp(secret, 42));
});
test('hotp produces different codes for different counters', () => {
const secret = base32Decode('JBSWY3DPEHPK3PXP');
assert.notEqual(hotp(secret, 1), hotp(secret, 2));
});
// --- verifyWithSeed ---
test('verifyWithSeed accepts correct code for current window', () => {
const seed = 'JBSWY3DPEHPK3PXP';
const now = Date.now();
const counter = Math.floor(now / 30000);
const secret = base32Decode(seed);
const code = hotp(secret, counter);
assert.ok(verifyWithSeed(seed, code, now));
});
test('verifyWithSeed accepts code from adjacent window', () => {
const seed = 'JBSWY3DPEHPK3PXP';
const now = Date.now();
const counter = Math.floor(now / 30000);
const secret = base32Decode(seed);
const code = hotp(secret, counter - 1);
assert.ok(verifyWithSeed(seed, code, now));
});
test('verifyWithSeed rejects wrong code', () => {
assert.ok(!verifyWithSeed('JBSWY3DPEHPK3PXP', '000000'));
});
test('verifyWithSeed rejects non-6-digit input', () => {
assert.ok(!verifyWithSeed('JBSWY3DPEHPK3PXP', 'abcdef'));
assert.ok(!verifyWithSeed('JBSWY3DPEHPK3PXP', '12345'));
assert.ok(!verifyWithSeed('JBSWY3DPEHPK3PXP', ''));
assert.ok(!verifyWithSeed('JBSWY3DPEHPK3PXP', null));
});
test('verifyWithSeed returns false for empty seed', () => {
assert.ok(!verifyWithSeed('', '123456'));
assert.ok(!verifyWithSeed(null, '123456'));
});
test('verifyWithSeed returns false for invalid seed', () => {
assert.ok(!verifyWithSeed('!!!INVALID!!!', '123456'));
});
// --- otpauthUri ---
test('otpauthUri generates valid URI', () => {
const uri = otpauthUri('JBSWY3DPEHPK3PXP', 'user@example.com');
assert.ok(uri.startsWith('otpauth://totp/'));
assert.ok(uri.includes('secret=JBSWY3DPEHPK3PXP'));
assert.ok(uri.includes('issuer=Joplock'));
assert.ok(uri.includes('user%40example.com'));
});
test('otpauthUri uses custom issuer', () => {
const uri = otpauthUri('JBSWY3DPEHPK3PXP', 'user@example.com', 'MyApp');
assert.ok(uri.includes('issuer=MyApp'));
});
test('otpauthUri returns empty for null seed', () => {
assert.equal(otpauthUri(null, 'user@example.com'), '');
});
// --- qrCodeDataUrl ---
test('qrCodeDataUrl returns SVG data URL', () => {
const url = qrCodeDataUrl('otpauth://totp/test?secret=JBSWY3DPEHPK3PXP');
assert.ok(url.startsWith('data:image/svg+xml;charset=utf-8,'));
});
test('qrCodeDataUrl returns empty for empty input', () => {
assert.equal(qrCodeDataUrl(''), '');
assert.equal(qrCodeDataUrl(null), '');
});
test('qrCodeDataUrl caches results', () => {
const text = 'otpauth://totp/cache-test?secret=ABC';
const a = qrCodeDataUrl(text);
const b = qrCodeDataUrl(text);
assert.equal(a, b);
});
// --- createMfaService ---
test('createMfaService disabled when no seed', () => {
const mfa = createMfaService({ seed: '' });
assert.equal(mfa.enabled(), false);
assert.equal(mfa.maskedSeed(), '');
assert.equal(mfa.otpauthUri('user@test.com'), '');
assert.equal(mfa.qrDataUrl('user@test.com'), '');
assert.ok(mfa.verify('anything'), 'disabled MFA should accept any code');
});
test('createMfaService enabled with valid seed', () => {
const seed = 'JBSWY3DPEHPK3PXP';
const mfa = createMfaService({ seed });
assert.equal(mfa.enabled(), true);
assert.equal(mfa.maskedSeed(), seed);
assert.ok(mfa.otpauthUri('user@test.com').includes(seed));
assert.ok(mfa.qrDataUrl('user@test.com').startsWith('data:'));
});
test('createMfaService verify accepts correct code', () => {
const seed = 'JBSWY3DPEHPK3PXP';
const mfa = createMfaService({ seed });
const now = Date.now();
const counter = Math.floor(now / 30000);
const secret = base32Decode(seed);
const code = hotp(secret, counter);
assert.ok(mfa.verify(code, now));
});
test('createMfaService verify rejects wrong code', () => {
const mfa = createMfaService({ seed: 'JBSWY3DPEHPK3PXP' });
assert.ok(!mfa.verify('000000'));
});
test('createMfaService issuer defaults to Joplock', () => {
const mfa = createMfaService({ seed: 'JBSWY3DPEHPK3PXP' });
assert.equal(mfa.issuer(), 'Joplock');
});
test('createMfaService uses custom issuer', () => {
const mfa = createMfaService({ seed: 'JBSWY3DPEHPK3PXP', issuer: 'Custom' });
assert.equal(mfa.issuer(), 'Custom');
assert.ok(mfa.otpauthUri('user@test.com').includes('issuer=Custom'));
});