import assert from 'node:assert/strict'; import test from 'node:test'; import type { TestContext } from 'node:test'; import bcrypt from 'bcrypt'; import type { Transaction } from 'sequelize'; import config from '../../src/config.ts'; import db from '../../src/db/models/index.ts'; import UsersDBApi from '../../src/db/api/users.ts'; import AuthService from '../../src/services/auth.ts'; import type { UserRecord } from '../../src/types/index.ts'; const suffix = `${Date.now()}-${process.pid}`; void test.after(async () => { await db.sequelize.close(); }); async function authenticateWithTimeout(timeoutMs = 1500): Promise { let timeoutId: NodeJS.Timeout | undefined; const timeout = new Promise((_, reject) => { timeoutId = setTimeout( () => reject(new Error(`Database unavailable after ${timeoutMs}ms`)), timeoutMs, ); }); try { await Promise.race([db.sequelize.authenticate(), timeout]); } finally { if (timeoutId !== undefined) { clearTimeout(timeoutId); } } } async function withTransaction( t: TestContext, callback: (transaction: Transaction) => Promise, ): Promise { try { await authenticateWithTimeout(); } catch (error) { const message = error instanceof Error ? error.message : 'unknown error'; t.skip(`Database unavailable: ${message}`); return; } const transaction = await db.sequelize.transaction(); try { await callback(transaction); } finally { await transaction.rollback(); } } async function createAuthUser( email: string, password: string, transaction: Transaction, ): Promise { const hashedPassword = await bcrypt.hash(password, config.bcrypt.saltRounds); return db.users.create( { email, password: hashedPassword, emailVerified: false, disabled: false, provider: config.providers.LOCAL, createdById: null, updatedById: null, }, { transaction }, ); } async function getUserPassword( id: string, transaction: Transaction, ): Promise { const user = await db.users.findByPk(id, { transaction }); if (!user?.password) { throw new Error('Expected user password to exist.'); } return user.password; } function requireEmail(user: UserRecord): string { if (!user.email) { throw new Error('Expected user email to exist.'); } return user.email; } void test('passwordReset updates password for a valid reset token', async (t) => { await withTransaction(t, async (transaction) => { const user = await createAuthUser( `reset-${suffix}@example.test`, 'old-password', transaction, ); const token = await UsersDBApi.generatePasswordResetToken(requireEmail(user), { transaction, }); await AuthService.passwordReset(token, 'new-password', { transaction }); const updatedPassword = await getUserPassword(user.id, transaction); assert.equal(await bcrypt.compare('new-password', updatedPassword), true); assert.equal(await bcrypt.compare('old-password', updatedPassword), false); }); }); void test('passwordReset rejects invalid reset tokens', async (t) => { await withTransaction(t, async (transaction) => { await assert.rejects( () => AuthService.passwordReset('invalid-token', 'new-password', { transaction, }), /Password reset link is invalid or has expired/, ); }); }); void test('passwordUpdate requires current password and rejects password reuse', async (t) => { await withTransaction(t, async (transaction) => { const user = await createAuthUser( `update-${suffix}@example.test`, 'current-password', transaction, ); const currentPassword = await getUserPassword(user.id, transaction); await assert.rejects( () => AuthService.passwordUpdate('wrong-password', 'next-password', { currentUser: { id: user.id, password: currentPassword, }, transaction, }), /credentials/, ); await assert.rejects( () => AuthService.passwordUpdate('current-password', 'current-password', { currentUser: { id: user.id, password: currentPassword, }, transaction, }), /same password/i, ); }); }); void test('verifyEmail marks users as verified for a valid email token', async (t) => { await withTransaction(t, async (transaction) => { const user = await createAuthUser( `verify-${suffix}@example.test`, 'password', transaction, ); const token = await UsersDBApi.generateEmailVerificationToken(requireEmail(user), { transaction, }); assert.equal( await AuthService.verifyEmail(token, { transaction }), true, ); const reloaded = await db.users.findByPk(user.id, { transaction }); assert.equal(reloaded?.emailVerified, true); }); });