39948-vm/backend/tests/integration/auth-service.test.ts
2026-07-02 14:22:33 +02:00

185 lines
4.8 KiB
TypeScript

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<void> {
let timeoutId: NodeJS.Timeout | undefined;
const timeout = new Promise<never>((_, 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<void>,
): Promise<void> {
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<UserRecord> {
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<string> {
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);
});
});