Files
pokemon-inventory/apps/api/tests/application/authentication-service.test.js
T
fragginwagon c728016dec feat: establish auth listing and image vertical slices
Includes MFA-capable authentication, session and CSRF security, password recovery, listing lifecycle and ownership rules, inventory image metadata, local ImageStorage, migrations, tests, and implementation status documentation. Verification: npm test (62 passing) and integration suite (14 passing).
2026-09-08 17:32:03 -04:00

201 lines
6.6 KiB
JavaScript

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 });
});