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).
79 lines
3.0 KiB
JavaScript
79 lines
3.0 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import { createPasswordRecoveryService } from '../../src/modules/auth/services/password-recovery-service.js';
|
|
|
|
test('does not issue a token for an unknown email while recording a generic request', async () => {
|
|
let resetIssued = false;
|
|
const auditEvents = [];
|
|
const service = createPasswordRecoveryService({
|
|
passwordResetRepository: { async issue() { resetIssued = true; } },
|
|
securityAuditRepository: { async record(event) { auditEvents.push(event); } },
|
|
userRepository: { async findAuthenticationUserByEmail() { return null; } },
|
|
});
|
|
|
|
await service.requestReset({ email: 'unknown@example.test' });
|
|
|
|
assert.equal(resetIssued, false);
|
|
assert.deepEqual(auditEvents, [
|
|
{ event: 'PASSWORD_RESET_REQUESTED', ipAddress: null, userAgent: null },
|
|
]);
|
|
});
|
|
|
|
test('delivers a reset token without persisting the raw token', async () => {
|
|
let issuedReset;
|
|
let deliveredReset;
|
|
const service = createPasswordRecoveryService({
|
|
now: () => new Date('2026-09-07T00:00:00.000Z'),
|
|
passwordResetNotifier: { async send(reset) { deliveredReset = reset; } },
|
|
passwordResetRepository: { async issue(reset) { issuedReset = reset; } },
|
|
userRepository: {
|
|
async findAuthenticationUserByEmail() {
|
|
return { id: 'user-id', email: 'admin@example.test' };
|
|
},
|
|
},
|
|
});
|
|
|
|
await service.requestReset({ email: 'admin@example.test' });
|
|
|
|
assert.equal(issuedReset.userId, 'user-id');
|
|
assert.notEqual(issuedReset.tokenHash, deliveredReset.token);
|
|
assert.equal(deliveredReset.email, 'admin@example.test');
|
|
assert.equal(deliveredReset.expiresAt.toISOString(), '2026-09-07T01:00:00.000Z');
|
|
});
|
|
|
|
test('consumes a reset token once and audits a successful password reset', async () => {
|
|
let consumedReset;
|
|
const auditEvents = [];
|
|
const service = createPasswordRecoveryService({
|
|
now: () => new Date('2026-09-07T00:00:00.000Z'),
|
|
passwordHasher: async (password) => `hashed:${password}`,
|
|
passwordResetRepository: {
|
|
async consume(reset) {
|
|
consumedReset = reset;
|
|
return 'user-id';
|
|
},
|
|
},
|
|
securityAuditRepository: { async record(event) { auditEvents.push(event); } },
|
|
});
|
|
|
|
await service.resetPassword({ token: 'a-valid-reset-token', password: 'new-password-value' });
|
|
|
|
assert.equal(consumedReset.passwordHash, 'hashed:new-password-value');
|
|
assert.notEqual(consumedReset.tokenHash, 'a-valid-reset-token');
|
|
assert.deepEqual(auditEvents, [
|
|
{ userId: 'user-id', event: 'PASSWORD_RESET', ipAddress: null, userAgent: null },
|
|
]);
|
|
});
|
|
|
|
test('rejects expired or previously used reset tokens', async () => {
|
|
const service = createPasswordRecoveryService({
|
|
passwordHasher: async () => 'new-password-hash',
|
|
passwordResetRepository: { async consume() { return null; } },
|
|
});
|
|
|
|
await assert.rejects(
|
|
() => service.resetPassword({ token: 'invalid-reset-token', password: 'new-password-value' }),
|
|
(error) => error.code === 'PASSWORD_RESET_TOKEN_INVALID',
|
|
);
|
|
}); |