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 }).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 }).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 }).save = async () => undefined; (registrationSession as RegistrationSession & { delete: () => Promise }).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();