import assert from 'node:assert/strict'; import test from 'node:test'; import { hashPassword, verifyPassword } from '../../src/modules/auth/domain/password.js'; import { createAuthenticationService } from '../../src/modules/auth/services/authentication-service.js'; test('verifies a password hash without accepting a different password', async () => { const passwordHash = await hashPassword('correct-password'); assert.equal(await verifyPassword('correct-password', passwordHash), true); assert.equal(await verifyPassword('incorrect-password', passwordHash), false); }); test('creates only a hashed session token after valid credentials', async () => { let createdSession; const auditEvents = []; const service = createAuthenticationService({ now: () => new Date('2026-09-07T00:00:00.000Z'), passwordVerifier: async (password) => password === 'correct-password', sessionRepository: { async create(session) { createdSession = session; }, }, securityAuditRepository: { async record(event) { auditEvents.push(event); } }, userRepository: { async findAuthenticationUserByEmail() { return { id: 'user-id', sellerId: 'seller-id', role: 'ADMIN', passwordHash: 'stored-hash', }; }, }, }); const result = await service.login({ email: 'admin@example.test', password: 'correct-password', }); assert.notEqual(createdSession.sessionTokenHash, result.sessionToken); assert.notEqual(createdSession.csrfTokenHash, result.csrfToken); assert.equal(createdSession.userId, 'user-id'); assert.equal(result.principal.sellerId, 'seller-id'); assert.deepEqual(auditEvents, [ { userId: 'user-id', event: 'LOGIN_SUCCESS', ipAddress: null, userAgent: null }, { userId: 'user-id', event: 'SESSION_CREATED', ipAddress: null, userAgent: null }, ]); }); test('does not create a session for invalid credentials', async () => { let sessionCreated = false; const auditEvents = []; const service = createAuthenticationService({ passwordVerifier: async () => false, sessionRepository: { async create() { sessionCreated = true; }, }, securityAuditRepository: { async record(event) { auditEvents.push(event); } }, userRepository: { async findAuthenticationUserByEmail() { return { id: 'user-id', passwordHash: 'stored-hash' }; }, }, }); await assert.rejects( () => service.login({ email: 'admin@example.test', password: 'incorrect-password' }), (error) => error.code === 'UNAUTHORIZED', ); assert.equal(sessionCreated, false); assert.deepEqual(auditEvents, [{ event: 'LOGIN_FAILURE', ipAddress: null, userAgent: null }]); }); test('does not create a session until enabled MFA verifies the login code', async () => { let sessionCreated = false; let receivedMfaInput; const service = createAuthenticationService({ mfaService: { async verifyLogin(input) { receivedMfaInput = input; const error = new Error('MFA code is invalid.'); error.code = 'MFA_CODE_INVALID'; throw error; }, }, passwordVerifier: async () => true, sessionRepository: { async create() { sessionCreated = true; } }, userRepository: { async findAuthenticationUserByEmail() { return { id: 'user-id', sellerId: 'seller-id', role: 'ADMIN', passwordHash: 'stored-hash', }; }, }, }); await assert.rejects( () => service.login({ email: 'admin@example.test', password: 'correct-password' }), (error) => error.code === 'MFA_CODE_INVALID', ); assert.deepEqual(receivedMfaInput, { userId: 'user-id', code: undefined }); assert.equal(sessionCreated, false); }); test('rejects an unsafe request without the session-bound CSRF token', async () => { const service = createAuthenticationService({ now: () => new Date('2026-09-07T00:00:00.000Z'), sessionRepository: { async findActiveAuthenticationSession() { return { csrfTokenHash: 'different-token-hash' }; }, }, }); await assert.rejects( () => service.authorizeUnsafeRequest('session-token', 'csrf-token'), (error) => error.code === 'FORBIDDEN', ); }); test('lists and revokes only sessions owned by the authenticated user', async () => { let revokedSession; let revokedOthers; const auditEvents = []; const service = createAuthenticationService({ now: () => new Date('2026-09-07T00:00:00.000Z'), sessionRepository: { async findActiveAuthenticationSession() { return { id: 'current-session', userId: 'user-id', sellerId: 'seller-id', role: 'ADMIN' }; }, async listActiveByUserId(userId) { assert.equal(userId, 'user-id'); return [{ id: 'current-session' }, { id: 'other-session' }]; }, async revokeByIdAndUserId(id, userId) { revokedSession = { id, userId }; return true; }, async revokeOtherByUserId(userId, currentSessionId) { revokedOthers = { userId, currentSessionId }; }, }, securityAuditRepository: { async record(event) { auditEvents.push(event); } }, }); const sessions = await service.listActiveSessions('session-token'); const revoked = await service.revokeSession('session-token', 'other-session'); await service.revokeOtherSessions('session-token'); assert.equal(sessions.currentSessionId, 'current-session'); assert.deepEqual(revokedSession, { id: 'other-session', userId: 'user-id' }); assert.equal(revoked.currentSessionRevoked, false); assert.deepEqual(revokedOthers, { userId: 'user-id', currentSessionId: 'current-session' }); assert.deepEqual(auditEvents, [ { userId: 'user-id', event: 'SESSION_REVOKED', metadata: { targetSessionId: 'other-session' }, }, { userId: 'user-id', event: 'SESSION_REVOKED', metadata: { scope: 'OTHER_SESSIONS' }, }, ]); }); test('renews stale active session activity without updating recent activity', async () => { let touchedAt; const currentTime = new Date('2026-09-07T01:00:00.000Z'); const service = createAuthenticationService({ now: () => currentTime, sessionRepository: { async findActiveAuthenticationSession() { return { id: 'current-session', userId: 'user-id', sellerId: 'seller-id', role: 'ADMIN', lastActivityAt: new Date('2026-09-07T00:50:00.000Z'), }; }, async touchById(id, activityTime) { touchedAt = { id, activityTime }; }, }, }); await service.authenticate('session-token'); assert.deepEqual(touchedAt, { id: 'current-session', activityTime: currentTime }); });