view models

This commit is contained in:
2025-12-18 13:48:35 +01:00
parent cc2553876a
commit 91adbb9c83
71 changed files with 3119 additions and 359 deletions

View File

@@ -0,0 +1,146 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { AuthService } from './AuthService';
import { AuthApiClient } from '../../api/auth/AuthApiClient';
import { SessionViewModel } from '../../view-models/SessionViewModel';
describe('AuthService', () => {
let mockApiClient: Mocked<AuthApiClient>;
let service: AuthService;
beforeEach(() => {
mockApiClient = {
signup: vi.fn(),
login: vi.fn(),
logout: vi.fn(),
getIracingAuthUrl: vi.fn(),
} as Mocked<AuthApiClient>;
service = new AuthService(mockApiClient);
});
describe('signup', () => {
it('should call apiClient.signup and return SessionViewModel', async () => {
const params = {
email: 'test@example.com',
password: 'password123',
displayName: 'Test User',
};
const mockResponse = {
token: 'jwt-token',
user: {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
},
};
mockApiClient.signup.mockResolvedValue(mockResponse);
const result = await service.signup(params);
expect(mockApiClient.signup).toHaveBeenCalledWith(params);
expect(result).toBeInstanceOf(SessionViewModel);
expect(result.userId).toBe('user-123');
expect(result.email).toBe('test@example.com');
expect(result.displayName).toBe('Test User');
expect(result.isAuthenticated).toBe(true);
});
it('should throw error when apiClient.signup fails', async () => {
const params = {
email: 'test@example.com',
password: 'password123',
displayName: 'Test User',
};
const error = new Error('Signup failed');
mockApiClient.signup.mockRejectedValue(error);
await expect(service.signup(params)).rejects.toThrow('Signup failed');
});
});
describe('login', () => {
it('should call apiClient.login and return SessionViewModel', async () => {
const params = {
email: 'test@example.com',
password: 'password123',
};
const mockResponse = {
token: 'jwt-token',
user: {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
},
};
mockApiClient.login.mockResolvedValue(mockResponse);
const result = await service.login(params);
expect(mockApiClient.login).toHaveBeenCalledWith(params);
expect(result).toBeInstanceOf(SessionViewModel);
expect(result.userId).toBe('user-123');
expect(result.email).toBe('test@example.com');
expect(result.displayName).toBe('Test User');
expect(result.isAuthenticated).toBe(true);
});
it('should throw error when apiClient.login fails', async () => {
const params = {
email: 'test@example.com',
password: 'password123',
};
const error = new Error('Login failed');
mockApiClient.login.mockRejectedValue(error);
await expect(service.login(params)).rejects.toThrow('Login failed');
});
});
describe('logout', () => {
it('should call apiClient.logout', async () => {
mockApiClient.logout.mockResolvedValue(undefined);
await service.logout();
expect(mockApiClient.logout).toHaveBeenCalled();
});
it('should throw error when apiClient.logout fails', async () => {
const error = new Error('Logout failed');
mockApiClient.logout.mockRejectedValue(error);
await expect(service.logout()).rejects.toThrow('Logout failed');
});
});
describe('getIracingAuthUrl', () => {
it('should call apiClient.getIracingAuthUrl with returnTo', () => {
const returnTo = '/dashboard';
const expectedUrl = 'https://api.example.com/auth/iracing/start?returnTo=%2Fdashboard';
mockApiClient.getIracingAuthUrl.mockReturnValue(expectedUrl);
const result = service.getIracingAuthUrl(returnTo);
expect(mockApiClient.getIracingAuthUrl).toHaveBeenCalledWith(returnTo);
expect(result).toBe(expectedUrl);
});
it('should call apiClient.getIracingAuthUrl without returnTo', () => {
const expectedUrl = 'https://api.example.com/auth/iracing/start';
mockApiClient.getIracingAuthUrl.mockReturnValue(expectedUrl);
const result = service.getIracingAuthUrl();
expect(mockApiClient.getIracingAuthUrl).toHaveBeenCalledWith(undefined);
expect(result).toBe(expectedUrl);
});
});
});

View File

@@ -1,9 +1,9 @@
import { AuthApiClient } from '../../api/auth/AuthApiClient';
import { SessionViewModel } from '../../view-models/SessionViewModel';
// TODO: Move these types to apps/website/lib/types/generated when available
// TODO: Create DTOs for login/signup params in apps/website/lib/types/generated
type LoginParamsDto = { email: string; password: string };
type SignupParamsDto = { email: string; password: string; displayName: string };
type SessionDataDto = { userId: string; email: string; displayName: string; token: string };
/**
* Auth Service
@@ -19,9 +19,10 @@ export class AuthService {
/**
* Sign up a new user
*/
async signup(params: SignupParamsDto): Promise<SessionDataDto> {
async signup(params: SignupParamsDto): Promise<SessionViewModel> {
try {
return await this.apiClient.signup(params);
const dto = await this.apiClient.signup(params);
return new SessionViewModel(dto.user);
} catch (error) {
throw error;
}
@@ -30,9 +31,10 @@ export class AuthService {
/**
* Log in an existing user
*/
async login(params: LoginParamsDto): Promise<SessionDataDto> {
async login(params: LoginParamsDto): Promise<SessionViewModel> {
try {
return await this.apiClient.login(params);
const dto = await this.apiClient.login(params);
return new SessionViewModel(dto.user);
} catch (error) {
throw error;
}

View File

@@ -0,0 +1,57 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { SessionService } from './SessionService';
import { AuthApiClient } from '../../api/auth/AuthApiClient';
import { SessionViewModel } from '../../view-models/SessionViewModel';
describe('SessionService', () => {
let mockApiClient: Mocked<AuthApiClient>;
let service: SessionService;
beforeEach(() => {
mockApiClient = {
getSession: vi.fn(),
} as Mocked<AuthApiClient>;
service = new SessionService(mockApiClient);
});
describe('getSession', () => {
it('should call apiClient.getSession and return SessionViewModel when session exists', async () => {
const mockResponse = {
token: 'jwt-token',
user: {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
},
};
mockApiClient.getSession.mockResolvedValue(mockResponse);
const result = await service.getSession();
expect(mockApiClient.getSession).toHaveBeenCalled();
expect(result).toBeInstanceOf(SessionViewModel);
expect(result?.userId).toBe('user-123');
expect(result?.email).toBe('test@example.com');
expect(result?.displayName).toBe('Test User');
expect(result?.isAuthenticated).toBe(true);
});
it('should return null when apiClient.getSession returns null', async () => {
mockApiClient.getSession.mockResolvedValue(null);
const result = await service.getSession();
expect(mockApiClient.getSession).toHaveBeenCalled();
expect(result).toBeNull();
});
it('should throw error when apiClient.getSession fails', async () => {
const error = new Error('Get session failed');
mockApiClient.getSession.mockRejectedValue(error);
await expect(service.getSession()).rejects.toThrow('Get session failed');
});
});
});

View File

@@ -1,5 +1,5 @@
import { SessionViewModel } from '@/lib/view-models/SessionViewModel';
import { AuthApiClient } from '../../api/auth/AuthApiClient';
import { SessionViewModel } from '../../view-models';
/**
* Session Service
@@ -17,6 +17,6 @@ export class SessionService {
*/
async getSession(): Promise<SessionViewModel | null> {
const dto = await this.apiClient.getSession();
return dto ? new SessionViewModel(dto) : null;
return dto ? new SessionViewModel(dto.user) : null;
}
}