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).
91 lines
2.8 KiB
JavaScript
91 lines
2.8 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import { buildApp } from '../../src/app/build-app.js';
|
|
|
|
const userId = '464e9b49-8617-4d00-af3c-78b3294e1d70';
|
|
const methodId = '6833868a-2a1b-4d33-90d1-fc9f2b7be869';
|
|
const headers = {
|
|
cookie: 'pokemon_inventory_session=session-token',
|
|
'x-csrf-token': 'csrf-token',
|
|
};
|
|
|
|
function createTestApp(mfaService) {
|
|
return buildApp({
|
|
authenticationService: {
|
|
async authorizeUnsafeRequest(sessionToken, csrfToken) {
|
|
assert.equal(sessionToken, 'session-token');
|
|
assert.equal(csrfToken, 'csrf-token');
|
|
return { userId };
|
|
},
|
|
},
|
|
loginRateLimiter: { async consume() { return { allowed: true }; } },
|
|
mfaService,
|
|
});
|
|
}
|
|
|
|
test('enrolls MFA only for the authenticated user and returns its provisioning URI', async (testContext) => {
|
|
const app = createTestApp({
|
|
async enroll(input) {
|
|
assert.deepEqual(input, { userId, name: 'Authenticator' });
|
|
return { id: methodId, provisioningUri: 'otpauth://totp/example' };
|
|
},
|
|
});
|
|
testContext.after(() => app.close());
|
|
|
|
const response = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/v1/auth/mfa/enroll',
|
|
headers,
|
|
payload: { name: 'Authenticator' },
|
|
});
|
|
|
|
assert.equal(response.statusCode, 201);
|
|
assert.equal(response.json().data.provisioningUri, 'otpauth://totp/example');
|
|
});
|
|
|
|
test('verifies MFA with CSRF protection and does not expose secret material', async (testContext) => {
|
|
const app = createTestApp({
|
|
async verifyEnrollment(input) {
|
|
assert.deepEqual(input, { userId, methodId, code: '123456' });
|
|
return { id: methodId, name: 'Authenticator', type: 'TOTP', secretCiphertext: 'hidden' };
|
|
},
|
|
});
|
|
testContext.after(() => app.close());
|
|
|
|
const response = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/v1/auth/mfa/verify',
|
|
headers,
|
|
payload: { methodId, code: '123456' },
|
|
});
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.json().data.secretCiphertext, undefined);
|
|
assert.equal(response.json().data.type, 'TOTP');
|
|
});
|
|
|
|
test('rejects MFA enrollment without CSRF validation', async (testContext) => {
|
|
const app = buildApp({
|
|
authenticationService: {
|
|
async authorizeUnsafeRequest() {
|
|
const error = new Error('CSRF validation failed.');
|
|
error.code = 'FORBIDDEN';
|
|
error.statusCode = 403;
|
|
throw error;
|
|
},
|
|
},
|
|
loginRateLimiter: { async consume() { return { allowed: true }; } },
|
|
mfaService: { async enroll() {} },
|
|
});
|
|
testContext.after(() => app.close());
|
|
|
|
const response = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/v1/auth/mfa/enroll',
|
|
payload: { name: 'Authenticator' },
|
|
});
|
|
|
|
assert.equal(response.statusCode, 403);
|
|
assert.equal(response.json().error.code, 'FORBIDDEN');
|
|
}); |