141 lines
6.0 KiB
TypeScript
141 lines
6.0 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
|
|
import { EmailActionToken } from '../ts/reception/classes.emailactiontoken.js';
|
|
import { LoginSession } from '../ts/reception/classes.loginsession.js';
|
|
import { RegistrationSession } from '../ts/reception/classes.registrationsession.js';
|
|
import { User } from '../ts/reception/classes.user.js';
|
|
import * as plugins from '../ts/plugins.js';
|
|
|
|
const createTestLoginSession = () => {
|
|
const loginSession = new LoginSession();
|
|
loginSession.id = 'test-session';
|
|
loginSession.data.userId = 'test-user';
|
|
(loginSession as LoginSession & { save: () => Promise<void> }).save = async () => undefined;
|
|
return loginSession;
|
|
};
|
|
|
|
const createTestEmailActionToken = () => {
|
|
const emailActionToken = new EmailActionToken();
|
|
emailActionToken.id = 'email-action-token';
|
|
emailActionToken.data.email = 'user@example.com';
|
|
emailActionToken.data.action = 'emailLogin';
|
|
emailActionToken.data.validUntil = Date.now() + 60_000;
|
|
|
|
let deleted = false;
|
|
(emailActionToken as EmailActionToken & { delete: () => Promise<void> }).delete = async () => {
|
|
deleted = true;
|
|
};
|
|
|
|
return {
|
|
emailActionToken,
|
|
wasDeleted: () => deleted,
|
|
};
|
|
};
|
|
|
|
const createTestRegistrationSession = () => {
|
|
const registrationSession = new RegistrationSession();
|
|
registrationSession.id = 'registration-session';
|
|
registrationSession.data.emailAddress = 'user@example.com';
|
|
registrationSession.data.validUntil = Date.now() + 60_000;
|
|
|
|
let deleted = false;
|
|
(registrationSession as RegistrationSession & { save: () => Promise<void> }).save = async () => undefined;
|
|
(registrationSession as RegistrationSession & { delete: () => Promise<void> }).delete = async () => {
|
|
deleted = true;
|
|
};
|
|
|
|
return {
|
|
registrationSession,
|
|
wasDeleted: () => deleted,
|
|
};
|
|
};
|
|
|
|
tap.test('hashes passwords with argon2 and verifies them', async () => {
|
|
const passwordHash = await User.hashPassword('correct horse battery staple');
|
|
|
|
expect(passwordHash.startsWith('$argon2')).toBeTrue();
|
|
expect(await User.verifyPassword('correct horse battery staple', passwordHash)).toBeTrue();
|
|
expect(await User.verifyPassword('wrong password', passwordHash)).toBeFalse();
|
|
expect(User.shouldUpgradePasswordHash(passwordHash)).toBeFalse();
|
|
});
|
|
|
|
tap.test('accepts legacy sha256 hashes and marks them for upgrade', async () => {
|
|
const legacyHash = await plugins.smarthash.sha256FromString('legacy-password');
|
|
|
|
expect(User.isLegacyPasswordHash(legacyHash)).toBeTrue();
|
|
expect(await User.verifyPassword('legacy-password', legacyHash)).toBeTrue();
|
|
expect(await User.verifyPassword('different-password', legacyHash)).toBeFalse();
|
|
expect(User.shouldUpgradePasswordHash(legacyHash)).toBeTrue();
|
|
});
|
|
|
|
tap.test('rotates refresh tokens and detects reuse', async () => {
|
|
const loginSession = createTestLoginSession();
|
|
|
|
const firstRefreshToken = await loginSession.getRefreshToken();
|
|
const secondRefreshToken = await loginSession.getRefreshToken();
|
|
|
|
expect(firstRefreshToken.startsWith('refresh_')).toBeTrue();
|
|
expect(secondRefreshToken.startsWith('refresh_')).toBeTrue();
|
|
expect(firstRefreshToken).not.toEqual(secondRefreshToken);
|
|
expect(loginSession.data.refreshToken).toBeNullOrUndefined();
|
|
expect(loginSession.data.refreshTokenHash).toBeTruthy();
|
|
expect(await loginSession.validateRefreshToken(secondRefreshToken)).toEqual('current');
|
|
expect(await loginSession.validateRefreshToken(firstRefreshToken)).toEqual('reused');
|
|
|
|
await loginSession.invalidate();
|
|
expect(await loginSession.validateRefreshToken(secondRefreshToken)).toEqual('invalidated');
|
|
});
|
|
|
|
tap.test('persists transfer tokens as one-time hashes', async () => {
|
|
const loginSession = createTestLoginSession();
|
|
const transferToken = await loginSession.getTransferToken();
|
|
|
|
expect(transferToken.startsWith('transfer_')).toBeTrue();
|
|
expect(loginSession.data.transferTokenHash).toBeTruthy();
|
|
expect(await loginSession.validateTransferToken(transferToken)).toBeTrue();
|
|
expect(await loginSession.validateTransferToken(transferToken)).toBeFalse();
|
|
});
|
|
|
|
tap.test('consumes email action tokens exactly once', async () => {
|
|
const { emailActionToken, wasDeleted } = createTestEmailActionToken();
|
|
const plainToken = EmailActionToken.createOpaqueToken('emailLogin');
|
|
emailActionToken.data.tokenHash = EmailActionToken.hashToken(plainToken);
|
|
|
|
expect(await emailActionToken.consume(plainToken)).toBeTrue();
|
|
expect(wasDeleted()).toBeTrue();
|
|
});
|
|
|
|
tap.test('invalidates expired email action tokens', async () => {
|
|
const { emailActionToken, wasDeleted } = createTestEmailActionToken();
|
|
emailActionToken.data.tokenHash = EmailActionToken.hashToken('expired-token');
|
|
emailActionToken.data.validUntil = Date.now() - 1;
|
|
|
|
expect(await emailActionToken.consume('expired-token')).toBeFalse();
|
|
expect(wasDeleted()).toBeTrue();
|
|
});
|
|
|
|
tap.test('persists registration token validation and sms verification state', async () => {
|
|
const { registrationSession } = createTestRegistrationSession();
|
|
const emailToken = 'registration-token';
|
|
registrationSession.data.hashedEmailToken = RegistrationSession.hashToken(emailToken);
|
|
|
|
expect(await registrationSession.validateEmailToken(emailToken)).toBeTrue();
|
|
expect(registrationSession.data.status).toEqual('emailValidated');
|
|
expect(registrationSession.data.collectedData.userData.email).toEqual('user@example.com');
|
|
|
|
registrationSession.data.smsCodeHash = RegistrationSession.hashToken('123456');
|
|
expect(await registrationSession.validateSmsCode('123456')).toBeTrue();
|
|
expect(registrationSession.data.status).toEqual('mobileVerified');
|
|
});
|
|
|
|
tap.test('removes expired registration sessions on token validation', async () => {
|
|
const { registrationSession, wasDeleted } = createTestRegistrationSession();
|
|
registrationSession.data.hashedEmailToken = RegistrationSession.hashToken('expired-registration');
|
|
registrationSession.data.validUntil = Date.now() - 1;
|
|
|
|
expect(await registrationSession.validateEmailToken('expired-registration')).toBeFalse();
|
|
expect(wasDeleted()).toBeTrue();
|
|
});
|
|
|
|
export default tap.start();
|