Prisma models per data-model.md: users (status enum, site-admin flag), user_identities (password provider now, OIDC later — subject is the stable user id), sessions (hashed ids), auth_tokens (hashed, single- use), plus rate_limits and mail_outbox for the upcoming M1 stories. UsersService creates accounts transactionally with Argon2id-hashed password identities (OWASP parameters, rehash detection) and maps uniqueness violations to field-level conflicts. Database-backed suites run when TEST_DATABASE_URL is set — locally against the dev db, in CI via a new postgres service container; shared auth schemas (username, password policy incl. common-password blocklist) ship with tests. Closes #10 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
56 lines
2.0 KiB
TypeScript
56 lines
2.0 KiB
TypeScript
import { ConflictException } from '@nestjs/common';
|
|
import { afterAll, describe, expect, it } from 'vitest';
|
|
|
|
import { createTestPrisma, hasTestDb, uniqueSuffix } from '../testing/test-db';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { UsersService } from './users.service';
|
|
|
|
describe.skipIf(!hasTestDb)('UsersService (database)', () => {
|
|
const prisma = hasTestDb ? (createTestPrisma() as unknown as PrismaService) : null!;
|
|
const users = hasTestDb ? new UsersService(prisma) : null!;
|
|
const suffix = uniqueSuffix();
|
|
|
|
const input = {
|
|
username: `uma-${suffix}`,
|
|
email: `uma-${suffix}@example.org`,
|
|
displayName: 'Uma Test',
|
|
password: 'korrekt pferd batterie',
|
|
locale: 'de',
|
|
};
|
|
|
|
afterAll(async () => {
|
|
if (!hasTestDb) return;
|
|
await prisma.user.deleteMany({ where: { username: { contains: suffix } } });
|
|
await prisma.$disconnect();
|
|
});
|
|
|
|
it('creates a user with a password identity and verifies the password', async () => {
|
|
const user = await users.createUser(input);
|
|
expect(user.status).toBe('PENDING_VERIFICATION');
|
|
expect(await users.checkPassword(user.id, input.password)).toBe(true);
|
|
expect(await users.checkPassword(user.id, 'wrong')).toBe(false);
|
|
});
|
|
|
|
it('rejects duplicate usernames with a field-level conflict', async () => {
|
|
await expect(
|
|
users.createUser({ ...input, email: `other-${suffix}@example.org` }),
|
|
).rejects.toBeInstanceOf(ConflictException);
|
|
});
|
|
|
|
it('rejects duplicate e-mail addresses case-insensitively', async () => {
|
|
await expect(
|
|
users.createUser({
|
|
...input,
|
|
username: `other-${suffix}`,
|
|
email: input.email.toUpperCase(),
|
|
}),
|
|
).rejects.toBeInstanceOf(ConflictException);
|
|
});
|
|
|
|
it('normalizes lookup by e-mail or username', async () => {
|
|
const byEmail = await users.findByUsernameOrEmail(input.email.toUpperCase());
|
|
const byName = await users.findByUsernameOrEmail(input.username);
|
|
expect(byEmail?.id).toBe(byName?.id);
|
|
});
|
|
});
|