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).
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
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',
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user