resolve todos in website

This commit is contained in:
2025-12-20 12:55:07 +01:00
parent 20588e1c0b
commit 92be9d2e1b
56 changed files with 2476 additions and 78 deletions

View File

@@ -0,0 +1,54 @@
import { describe, it, expect, vi, type Mock } from 'vitest';
import { GetCurrentUserSessionUseCase } from './GetCurrentUserSessionUseCase';
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
describe('GetCurrentUserSessionUseCase', () => {
let sessionPort: {
getCurrentSession: Mock;
createSession: Mock;
clearSession: Mock;
};
let useCase: GetCurrentUserSessionUseCase;
beforeEach(() => {
sessionPort = {
getCurrentSession: vi.fn(),
createSession: vi.fn(),
clearSession: vi.fn(),
};
useCase = new GetCurrentUserSessionUseCase(sessionPort as unknown as IdentitySessionPort);
});
it('returns the current auth session when one exists', async () => {
const session: AuthSessionDTO = {
user: {
id: 'user-1',
email: 'test@example.com',
displayName: 'Test User',
primaryDriverId: 'driver-1',
},
issuedAt: Date.now(),
expiresAt: Date.now() + 1000,
token: 'token-123',
};
sessionPort.getCurrentSession.mockResolvedValue(session);
const result = await useCase.execute();
expect(sessionPort.getCurrentSession).toHaveBeenCalledTimes(1);
expect(result).toEqual(session);
});
it('returns null when there is no active session', async () => {
sessionPort.getCurrentSession.mockResolvedValue(null);
const result = await useCase.execute();
expect(sessionPort.getCurrentSession).toHaveBeenCalledTimes(1);
expect(result).toBeNull();
});
});

View File

@@ -0,0 +1,48 @@
import { describe, it, expect, vi, type Mock } from 'vitest';
import { GetUserUseCase } from './GetUserUseCase';
import { User } from '../../domain/entities/User';
import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
describe('GetUserUseCase', () => {
let userRepository: {
findById: Mock;
};
let useCase: GetUserUseCase;
beforeEach(() => {
userRepository = {
findById: vi.fn(),
};
useCase = new GetUserUseCase(userRepository as unknown as IUserRepository);
});
it('returns a User when the user exists', async () => {
const storedUser: StoredUser = {
id: 'user-1',
email: 'test@example.com',
displayName: 'Test User',
passwordHash: 'hash',
salt: 'salt',
primaryDriverId: 'driver-1',
createdAt: new Date(),
};
userRepository.findById.mockResolvedValue(storedUser);
const result = await useCase.execute('user-1');
expect(userRepository.findById).toHaveBeenCalledWith('user-1');
expect(result).toBeInstanceOf(User);
expect(result.getId().value).toBe('user-1');
expect(result.getDisplayName()).toBe('Test User');
});
it('throws when the user does not exist', async () => {
userRepository.findById.mockResolvedValue(null);
await expect(useCase.execute('missing-user')).rejects.toThrow('User not found');
expect(userRepository.findById).toHaveBeenCalledWith('missing-user');
});
});

View File

@@ -0,0 +1,65 @@
import { describe, it, expect, vi, type Mock } from 'vitest';
import { HandleAuthCallbackUseCase } from './HandleAuthCallbackUseCase';
import type { IdentityProviderPort } from '../ports/IdentityProviderPort';
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
import type { AuthCallbackCommandDTO } from '../dto/AuthCallbackCommandDTO';
import type { AuthenticatedUserDTO } from '../dto/AuthenticatedUserDTO';
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
describe('HandleAuthCallbackUseCase', () => {
let provider: {
completeAuth: Mock;
};
let sessionPort: {
createSession: Mock;
getCurrentSession: Mock;
clearSession: Mock;
};
let useCase: HandleAuthCallbackUseCase;
beforeEach(() => {
provider = {
completeAuth: vi.fn(),
};
sessionPort = {
createSession: vi.fn(),
getCurrentSession: vi.fn(),
clearSession: vi.fn(),
};
useCase = new HandleAuthCallbackUseCase(
provider as unknown as IdentityProviderPort,
sessionPort as unknown as IdentitySessionPort,
);
});
it('completes auth and creates a session', async () => {
const command: AuthCallbackCommandDTO = {
code: 'auth-code',
state: 'state-123',
redirectUri: 'https://app/callback',
};
const user: AuthenticatedUserDTO = {
id: 'user-1',
email: 'test@example.com',
displayName: 'Test User',
};
const session: AuthSessionDTO = {
user,
issuedAt: Date.now(),
expiresAt: Date.now() + 1000,
token: 'session-token',
};
provider.completeAuth.mockResolvedValue(user);
sessionPort.createSession.mockResolvedValue(session);
const result = await useCase.execute(command);
expect(provider.completeAuth).toHaveBeenCalledWith(command);
expect(sessionPort.createSession).toHaveBeenCalledWith(user);
expect(result).toEqual(session);
});
});

View File

@@ -0,0 +1,81 @@
import { describe, it, expect, vi, type Mock } from 'vitest';
import { LoginUseCase } from './LoginUseCase';
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
import type { IAuthRepository } from '../../domain/repositories/IAuthRepository';
import type { IPasswordHashingService } from '../../domain/services/PasswordHashingService';
import { User } from '../../domain/entities/User';
describe('LoginUseCase', () => {
let authRepo: {
findByEmail: Mock;
};
let passwordService: {
verify: Mock;
};
let useCase: LoginUseCase;
beforeEach(() => {
authRepo = {
findByEmail: vi.fn(),
};
passwordService = {
verify: vi.fn(),
};
useCase = new LoginUseCase(
authRepo as unknown as IAuthRepository,
passwordService as unknown as IPasswordHashingService,
);
});
it('returns the user when credentials are valid', async () => {
const email = 'test@example.com';
const password = 'password123';
const emailVO = EmailAddress.create(email);
const user = User.create({
id: { value: 'user-1' } as any,
displayName: 'Test User',
email: emailVO.value,
});
(user as any).getPasswordHash = () => ({ value: 'stored-hash' });
authRepo.findByEmail.mockResolvedValue(user);
passwordService.verify.mockResolvedValue(true);
const result = await useCase.execute(email, password);
expect(authRepo.findByEmail).toHaveBeenCalledWith(emailVO);
expect(passwordService.verify).toHaveBeenCalledWith(password, 'stored-hash');
expect(result).toBe(user);
});
it('throws when user is not found', async () => {
const email = 'missing@example.com';
authRepo.findByEmail.mockResolvedValue(null);
await expect(useCase.execute(email, 'password')).rejects.toThrow('Invalid credentials');
});
it('throws when password is invalid', async () => {
const email = 'test@example.com';
const password = 'wrong-password';
const emailVO = EmailAddress.create(email);
const user = User.create({
id: { value: 'user-1' } as any,
displayName: 'Test User',
email: emailVO.value,
});
(user as any).getPasswordHash = () => ({ value: 'stored-hash' });
authRepo.findByEmail.mockResolvedValue(user);
passwordService.verify.mockResolvedValue(false);
await expect(useCase.execute(email, password)).rejects.toThrow('Invalid credentials');
expect(authRepo.findByEmail).toHaveBeenCalled();
expect(passwordService.verify).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,108 @@
import { describe, it, expect, vi, type Mock } from 'vitest';
import { LoginWithEmailUseCase, type LoginCommandDTO } from './LoginWithEmailUseCase';
import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
describe('LoginWithEmailUseCase', () => {
let userRepository: {
findByEmail: Mock;
};
let sessionPort: {
createSession: Mock;
getCurrentSession: Mock;
clearSession: Mock;
};
let useCase: LoginWithEmailUseCase;
beforeEach(() => {
userRepository = {
findByEmail: vi.fn(),
};
sessionPort = {
createSession: vi.fn(),
getCurrentSession: vi.fn(),
clearSession: vi.fn(),
};
useCase = new LoginWithEmailUseCase(
userRepository as unknown as IUserRepository,
sessionPort as unknown as IdentitySessionPort,
);
});
it('creates a session for valid credentials', async () => {
const command: LoginCommandDTO = {
email: 'Test@Example.com',
password: 'password123',
};
const storedUser: StoredUser = {
id: 'user-1',
email: 'test@example.com',
displayName: 'Test User',
passwordHash: 'hashed-password',
salt: 'salt',
createdAt: new Date(),
};
const session: AuthSessionDTO = {
user: {
id: storedUser.id,
email: storedUser.email,
displayName: storedUser.displayName,
},
issuedAt: Date.now(),
expiresAt: Date.now() + 1000,
token: 'token-123',
};
userRepository.findByEmail.mockResolvedValue(storedUser);
sessionPort.createSession.mockResolvedValue(session);
const result = await useCase.execute(command);
expect(userRepository.findByEmail).toHaveBeenCalledWith('test@example.com');
expect(sessionPort.createSession).toHaveBeenCalledWith({
id: storedUser.id,
email: storedUser.email,
displayName: storedUser.displayName,
});
expect(result).toEqual(session);
});
it('throws when email or password is missing', async () => {
await expect(useCase.execute({ email: '', password: 'x' })).rejects.toThrow('Email and password are required');
await expect(useCase.execute({ email: 'a@example.com', password: '' })).rejects.toThrow('Email and password are required');
});
it('throws when user does not exist', async () => {
const command: LoginCommandDTO = {
email: 'missing@example.com',
password: 'password',
};
userRepository.findByEmail.mockResolvedValue(null);
await expect(useCase.execute(command)).rejects.toThrow('Invalid email or password');
});
it('throws when password is invalid', async () => {
const command: LoginCommandDTO = {
email: 'test@example.com',
password: 'wrong',
};
const storedUser: StoredUser = {
id: 'user-1',
email: 'test@example.com',
displayName: 'Test User',
passwordHash: 'different-hash',
salt: 'salt',
createdAt: new Date(),
};
userRepository.findByEmail.mockResolvedValue(storedUser);
await expect(useCase.execute(command)).rejects.toThrow('Invalid email or password');
});
});

View File

@@ -0,0 +1,28 @@
import { describe, it, expect, vi, type Mock } from 'vitest';
import { LogoutUseCase } from './LogoutUseCase';
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
describe('LogoutUseCase', () => {
let sessionPort: {
clearSession: Mock;
getCurrentSession: Mock;
createSession: Mock;
};
let useCase: LogoutUseCase;
beforeEach(() => {
sessionPort = {
clearSession: vi.fn(),
getCurrentSession: vi.fn(),
createSession: vi.fn(),
};
useCase = new LogoutUseCase(sessionPort as unknown as IdentitySessionPort);
});
it('clears the current session', async () => {
await useCase.execute();
expect(sessionPort.clearSession).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,71 @@
import { describe, it, expect, vi, type Mock } from 'vitest';
import { SignupUseCase } from './SignupUseCase';
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
import { UserId } from '../../domain/value-objects/UserId';
import { User } from '../../domain/entities/User';
import type { IAuthRepository } from '../../domain/repositories/IAuthRepository';
import type { IPasswordHashingService } from '../../domain/services/PasswordHashingService';
vi.mock('../../domain/value-objects/PasswordHash', () => ({
PasswordHash: {
fromHash: (hash: string) => ({ value: hash }),
},
}));
describe('SignupUseCase', () => {
let authRepo: {
findByEmail: Mock;
save: Mock;
};
let passwordService: {
hash: Mock;
};
let useCase: SignupUseCase;
beforeEach(() => {
authRepo = {
findByEmail: vi.fn(),
save: vi.fn(),
};
passwordService = {
hash: vi.fn(),
};
useCase = new SignupUseCase(
authRepo as unknown as IAuthRepository,
passwordService as unknown as IPasswordHashingService,
);
});
it('creates and saves a new user when email is free', async () => {
const email = 'new@example.com';
const password = 'password123';
const displayName = 'New User';
authRepo.findByEmail.mockResolvedValue(null);
passwordService.hash.mockResolvedValue('hashed-password');
const result = await useCase.execute(email, password, displayName);
expect(authRepo.findByEmail).toHaveBeenCalledWith(EmailAddress.create(email));
expect(passwordService.hash).toHaveBeenCalledWith(password);
expect(authRepo.save).toHaveBeenCalled();
expect(result).toBeInstanceOf(User);
expect(result.getDisplayName()).toBe(displayName);
});
it('throws when user already exists', async () => {
const email = 'existing@example.com';
const existingUser = User.create({
id: UserId.create(),
displayName: 'Existing User',
email,
});
authRepo.findByEmail.mockResolvedValue(existingUser);
await expect(useCase.execute(email, 'password', 'Existing User')).rejects.toThrow('User already exists');
});
});

View File

@@ -0,0 +1,121 @@
import { describe, it, expect, vi, type Mock } from 'vitest';
import { SignupWithEmailUseCase, type SignupCommandDTO } from './SignupWithEmailUseCase';
import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
describe('SignupWithEmailUseCase', () => {
let userRepository: {
findByEmail: Mock;
create: Mock;
};
let sessionPort: {
createSession: Mock;
getCurrentSession: Mock;
clearSession: Mock;
};
let useCase: SignupWithEmailUseCase;
beforeEach(() => {
userRepository = {
findByEmail: vi.fn(),
create: vi.fn(),
};
sessionPort = {
createSession: vi.fn(),
getCurrentSession: vi.fn(),
clearSession: vi.fn(),
};
useCase = new SignupWithEmailUseCase(
userRepository as unknown as IUserRepository,
sessionPort as unknown as IdentitySessionPort,
);
});
it('creates a new user and session for valid input', async () => {
const command: SignupCommandDTO = {
email: 'new@example.com',
password: 'password123',
displayName: 'New User',
};
userRepository.findByEmail.mockResolvedValue(null);
const session: AuthSessionDTO = {
user: {
id: 'user-1',
email: command.email.toLowerCase(),
displayName: command.displayName,
},
issuedAt: Date.now(),
expiresAt: Date.now() + 1000,
token: 'session-token',
};
sessionPort.createSession.mockResolvedValue(session);
const result = await useCase.execute(command);
expect(userRepository.findByEmail).toHaveBeenCalledWith(command.email);
expect(userRepository.create).toHaveBeenCalled();
expect(sessionPort.createSession).toHaveBeenCalledWith({
id: expect.any(String),
email: command.email.toLowerCase(),
displayName: command.displayName,
});
expect(result.session).toEqual(session);
expect(result.isNewUser).toBe(true);
});
it('throws when email format is invalid', async () => {
const command: SignupCommandDTO = {
email: 'invalid-email',
password: 'password123',
displayName: 'User',
};
await expect(useCase.execute(command)).rejects.toThrow('Invalid email format');
});
it('throws when password is too short', async () => {
const command: SignupCommandDTO = {
email: 'valid@example.com',
password: 'short',
displayName: 'User',
};
await expect(useCase.execute(command)).rejects.toThrow('Password must be at least 8 characters');
});
it('throws when display name is too short', async () => {
const command: SignupCommandDTO = {
email: 'valid@example.com',
password: 'password123',
displayName: ' ',
};
await expect(useCase.execute(command)).rejects.toThrow('Display name must be at least 2 characters');
});
it('throws when email already exists', async () => {
const command: SignupCommandDTO = {
email: 'existing@example.com',
password: 'password123',
displayName: 'Existing User',
};
const existingUser: StoredUser = {
id: 'user-1',
email: command.email,
displayName: command.displayName,
passwordHash: 'hash',
salt: 'salt',
createdAt: new Date(),
};
userRepository.findByEmail.mockResolvedValue(existingUser);
await expect(useCase.execute(command)).rejects.toThrow('An account with this email already exists');
});
});

View File

@@ -0,0 +1,35 @@
import { describe, it, expect, vi, type Mock } from 'vitest';
import { StartAuthUseCase } from './StartAuthUseCase';
import type { IdentityProviderPort } from '../ports/IdentityProviderPort';
import type { StartAuthCommandDTO } from '../dto/StartAuthCommandDTO';
describe('StartAuthUseCase', () => {
let provider: {
startAuth: Mock;
};
let useCase: StartAuthUseCase;
beforeEach(() => {
provider = {
startAuth: vi.fn(),
};
useCase = new StartAuthUseCase(provider as unknown as IdentityProviderPort);
});
it('delegates to the identity provider to start auth', async () => {
const command: StartAuthCommandDTO = {
redirectUri: 'https://app/callback',
provider: 'demo',
};
const expected = { redirectUrl: 'https://auth/redirect', state: 'state-123' };
provider.startAuth.mockResolvedValue(expected);
const result = await useCase.execute(command);
expect(provider.startAuth).toHaveBeenCalledWith(command);
expect(result).toEqual(expected);
});
});

View File

@@ -0,0 +1,53 @@
import { describe, it, expect, vi, type Mock } from 'vitest';
import { CreateAchievementUseCase, type IAchievementRepository } from './CreateAchievementUseCase';
import { Achievement } from '@core/identity/domain/entities/Achievement';
describe('CreateAchievementUseCase', () => {
let achievementRepository: {
save: Mock;
findById: Mock;
};
let useCase: CreateAchievementUseCase;
beforeEach(() => {
achievementRepository = {
save: vi.fn(),
findById: vi.fn(),
};
useCase = new CreateAchievementUseCase(achievementRepository as unknown as IAchievementRepository);
});
it('creates an achievement and persists it', async () => {
const props = {
id: 'achv-1',
name: 'First Win',
description: 'Awarded for winning your first race',
category: 'driver' as const,
rarity: 'common' as const,
iconUrl: 'https://example.com/icon.png',
points: 50,
requirements: [
{
type: 'wins',
value: 1,
operator: '>=',
},
],
isSecret: false,
};
achievementRepository.save.mockResolvedValue(undefined);
const result = await useCase.execute(props);
expect(result).toBeInstanceOf(Achievement);
expect(result.id).toBe(props.id);
expect(result.name).toBe(props.name);
expect(result.description).toBe(props.description);
expect(result.category).toBe(props.category);
expect(result.points).toBe(props.points);
expect(result.requirements).toHaveLength(1);
expect(achievementRepository.save).toHaveBeenCalledWith(result);
});
});