import assert from 'node:assert/strict'; import test from 'node:test'; import { createSecretProtector } from '../../src/modules/auth/domain/secret-protection.js'; import { createMfaService } from '../../src/modules/auth/services/mfa-service.js'; function createService({ method, isCodeValid = true } = {}) { const createdMethods = []; const auditEvents = []; const repository = { async create(input) { const createdMethod = { id: 'method-id', isEnabled: false, ...input }; createdMethods.push(createdMethod); return createdMethod; }, async findActiveByIdAndUserId() { return method ?? createdMethods[0] ?? null; }, async enable(id) { return { id, name: 'Authenticator', type: 'TOTP', isEnabled: true }; }, async disable(id) { return { id, isEnabled: false }; }, }; const service = createMfaService({ mfaMethodRepository: repository, now: () => new Date('2026-09-07T00:00:00.000Z'), secretProtector: { encrypt(secret) { return `encrypted:${secret}`; }, decrypt(ciphertext) { return ciphertext.replace('encrypted:', ''); }, }, securityAuditRepository: { async record(event) { auditEvents.push(event); } }, totpEnrollment: () => ({ secret: 'TOTPSECRET', provisioningUri: 'otpauth://test' }), totpVerifier: () => isCodeValid, }); return { auditEvents, createdMethods, service }; } test('encrypts the TOTP secret and returns the provisioning URI only during enrollment', async () => { const { createdMethods, service } = createService(); const enrollment = await service.enroll({ userId: 'user-id', name: 'Authenticator' }); assert.deepEqual(enrollment, { id: 'method-id', provisioningUri: 'otpauth://test' }); assert.equal(createdMethods[0].secretCiphertext, 'encrypted:TOTPSECRET'); assert.equal(createdMethods[0].isEnabled, false); assert.equal('secret' in enrollment, false); }); test('encrypts MFA secrets with authenticated encryption', () => { const protector = createSecretProtector(Buffer.alloc(32, 7).toString('base64')); const ciphertext = protector.encrypt('TOTPSECRET'); assert.notEqual(ciphertext, 'TOTPSECRET'); assert.equal(protector.decrypt(ciphertext), 'TOTPSECRET'); }); test('enables an owned MFA method only after a valid code and records enrollment', async () => { const { auditEvents, service } = createService({ method: { id: 'method-id', userId: 'user-id', type: 'TOTP', name: 'Authenticator', secretCiphertext: 'encrypted:TOTPSECRET', isEnabled: false, }, }); const method = await service.verifyEnrollment({ userId: 'user-id', methodId: 'method-id', code: '123456', }); assert.deepEqual(method, { id: 'method-id', name: 'Authenticator', type: 'TOTP' }); assert.deepEqual(auditEvents, [{ userId: 'user-id', event: 'MFA_ENROLLED' }]); }); test('rejects invalid MFA codes without enabling or disabling a method', async () => { const { auditEvents, service } = createService({ isCodeValid: false, method: { id: 'method-id', userId: 'user-id', type: 'TOTP', name: 'Authenticator', secretCiphertext: 'encrypted:TOTPSECRET', isEnabled: true, }, }); await assert.rejects( () => service.disable({ userId: 'user-id', methodId: 'method-id', code: '000000' }), (error) => error.code === 'MFA_CODE_INVALID', ); assert.deepEqual(auditEvents, [{ userId: 'user-id', event: 'MFA_FAILURE' }]); }); test('requires a valid TOTP code when an enabled MFA method exists at login', async () => { let markedMethod; const repository = { async listEnabledByUserId(userId) { assert.equal(userId, 'user-id'); return [{ id: 'method-id', secretCiphertext: 'encrypted:TOTPSECRET' }]; }, async markUsed(id) { markedMethod = id; }, }; const service = createMfaService({ mfaMethodRepository: repository, secretProtector: { decrypt() { return 'TOTPSECRET'; } }, totpVerifier: ({ code }) => code === '123456', }); await assert.rejects( () => service.verifyLogin({ userId: 'user-id', code: undefined }), (error) => error.code === 'MFA_CODE_INVALID', ); await service.verifyLogin({ userId: 'user-id', code: '123456' }); assert.equal(markedMethod, 'method-id'); });