view models
This commit is contained in:
@@ -1,204 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { AuthService } from './AuthService';
|
||||
import { AuthApiClient } from '../../api/auth/AuthApiClient';
|
||||
import type { LoginParamsDto, SignupParamsDto, SessionDataDto } from '../../dtos';
|
||||
|
||||
describe('AuthService', () => {
|
||||
let mockApiClient: AuthApiClient;
|
||||
let service: AuthService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiClient = {
|
||||
signup: vi.fn(),
|
||||
login: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
getSession: vi.fn(),
|
||||
getIracingAuthUrl: vi.fn(),
|
||||
} as unknown as AuthApiClient;
|
||||
|
||||
service = new AuthService(mockApiClient);
|
||||
});
|
||||
|
||||
describe('signup', () => {
|
||||
it('should sign up user via API client', async () => {
|
||||
// Arrange
|
||||
const params: SignupParamsDto = {
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
displayName: 'Test User',
|
||||
};
|
||||
|
||||
const expectedSession: SessionDataDto = {
|
||||
userId: 'user-1',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
isAuthenticated: true,
|
||||
};
|
||||
|
||||
vi.mocked(mockApiClient.signup).mockResolvedValue(expectedSession);
|
||||
|
||||
// Act
|
||||
const result = await service.signup(params);
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.signup).toHaveBeenCalledWith(params);
|
||||
expect(mockApiClient.signup).toHaveBeenCalledTimes(1);
|
||||
expect(result).toBe(expectedSession);
|
||||
});
|
||||
|
||||
it('should propagate API client errors', async () => {
|
||||
// Arrange
|
||||
const params: SignupParamsDto = {
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
};
|
||||
|
||||
const error = new Error('API Error: Failed to sign up');
|
||||
vi.mocked(mockApiClient.signup).mockRejectedValue(error);
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.signup(params)).rejects.toThrow(
|
||||
'API Error: Failed to sign up'
|
||||
);
|
||||
|
||||
expect(mockApiClient.signup).toHaveBeenCalledWith(params);
|
||||
expect(mockApiClient.signup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should log in user via API client', async () => {
|
||||
// Arrange
|
||||
const params: LoginParamsDto = {
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
};
|
||||
|
||||
const expectedSession: SessionDataDto = {
|
||||
userId: 'user-1',
|
||||
email: 'test@example.com',
|
||||
isAuthenticated: true,
|
||||
};
|
||||
|
||||
vi.mocked(mockApiClient.login).mockResolvedValue(expectedSession);
|
||||
|
||||
// Act
|
||||
const result = await service.login(params);
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.login).toHaveBeenCalledWith(params);
|
||||
expect(mockApiClient.login).toHaveBeenCalledTimes(1);
|
||||
expect(result).toBe(expectedSession);
|
||||
});
|
||||
|
||||
it('should propagate API client errors', async () => {
|
||||
// Arrange
|
||||
const params: LoginParamsDto = {
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
};
|
||||
|
||||
const error = new Error('API Error: Invalid credentials');
|
||||
vi.mocked(mockApiClient.login).mockRejectedValue(error);
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.login(params)).rejects.toThrow(
|
||||
'API Error: Invalid credentials'
|
||||
);
|
||||
|
||||
expect(mockApiClient.login).toHaveBeenCalledWith(params);
|
||||
expect(mockApiClient.login).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
it('should log out user via API client', async () => {
|
||||
// Arrange
|
||||
vi.mocked(mockApiClient.logout).mockResolvedValue(undefined);
|
||||
|
||||
// Act
|
||||
await service.logout();
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.logout).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should propagate API client errors', async () => {
|
||||
// Arrange
|
||||
const error = new Error('API Error: Failed to logout');
|
||||
vi.mocked(mockApiClient.logout).mockRejectedValue(error);
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.logout()).rejects.toThrow(
|
||||
'API Error: Failed to logout'
|
||||
);
|
||||
|
||||
expect(mockApiClient.logout).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIracingAuthUrl', () => {
|
||||
it('should get iRacing auth URL via API client', () => {
|
||||
// Arrange
|
||||
const returnTo = '/dashboard';
|
||||
const expectedUrl = 'http://localhost:3001/auth/iracing/start?returnTo=%2Fdashboard';
|
||||
|
||||
vi.mocked(mockApiClient.getIracingAuthUrl).mockReturnValue(expectedUrl);
|
||||
|
||||
// Act
|
||||
const result = service.getIracingAuthUrl(returnTo);
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getIracingAuthUrl).toHaveBeenCalledWith(returnTo);
|
||||
expect(mockApiClient.getIracingAuthUrl).toHaveBeenCalledTimes(1);
|
||||
expect(result).toBe(expectedUrl);
|
||||
});
|
||||
|
||||
it('should handle undefined returnTo', () => {
|
||||
// Arrange
|
||||
const expectedUrl = 'http://localhost:3001/auth/iracing/start';
|
||||
|
||||
vi.mocked(mockApiClient.getIracingAuthUrl).mockReturnValue(expectedUrl);
|
||||
|
||||
// Act
|
||||
const result = service.getIracingAuthUrl();
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getIracingAuthUrl).toHaveBeenCalledWith(undefined);
|
||||
expect(result).toBe(expectedUrl);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Constructor Dependency Injection', () => {
|
||||
it('should require apiClient', () => {
|
||||
// This test verifies the constructor signature
|
||||
expect(() => {
|
||||
new AuthService(mockApiClient);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should use injected apiClient', async () => {
|
||||
// Arrange
|
||||
const customApiClient = {
|
||||
signup: vi.fn().mockResolvedValue({ userId: 'user-1', email: 'test@example.com', isAuthenticated: true }),
|
||||
login: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
getSession: vi.fn(),
|
||||
getIracingAuthUrl: vi.fn(),
|
||||
} as unknown as AuthApiClient;
|
||||
|
||||
const customService = new AuthService(customApiClient);
|
||||
|
||||
const params: SignupParamsDto = {
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
};
|
||||
|
||||
// Act
|
||||
await customService.signup(params);
|
||||
|
||||
// Assert
|
||||
expect(customApiClient.signup).toHaveBeenCalledWith(params);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
import { AuthApiClient } from '../../api/auth/AuthApiClient';
|
||||
import type { LoginParamsDto, SignupParamsDto, SessionDataDto } from '../../dtos';
|
||||
|
||||
// TODO: Move these types to apps/website/lib/types/generated when available
|
||||
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
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { SessionService } from './SessionService';
|
||||
import { AuthApiClient } from '../../api/auth/AuthApiClient';
|
||||
import { SessionPresenter } from '../../presenters/SessionPresenter';
|
||||
import { SessionViewModel } from '../../view-models';
|
||||
import type { SessionDataDto } from '../../dtos';
|
||||
|
||||
describe('SessionService', () => {
|
||||
let mockApiClient: AuthApiClient;
|
||||
let mockPresenter: SessionPresenter;
|
||||
let service: SessionService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiClient = {
|
||||
getSession: vi.fn(),
|
||||
signup: vi.fn(),
|
||||
login: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
getIracingAuthUrl: vi.fn(),
|
||||
} as unknown as AuthApiClient;
|
||||
|
||||
mockPresenter = {
|
||||
presentSession: vi.fn(),
|
||||
} as unknown as SessionPresenter;
|
||||
|
||||
service = new SessionService(mockApiClient, mockPresenter);
|
||||
});
|
||||
|
||||
describe('getSession', () => {
|
||||
it('should get session via API client and present it', async () => {
|
||||
// Arrange
|
||||
const dto: SessionDataDto = {
|
||||
userId: 'user-1',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
driverId: 'driver-1',
|
||||
isAuthenticated: true,
|
||||
};
|
||||
|
||||
const expectedViewModel = new SessionViewModel(dto);
|
||||
|
||||
vi.mocked(mockApiClient.getSession).mockResolvedValue(dto);
|
||||
vi.mocked(mockPresenter.presentSession).mockReturnValue(expectedViewModel);
|
||||
|
||||
// Act
|
||||
const result = await service.getSession();
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getSession).toHaveBeenCalledTimes(1);
|
||||
expect(mockPresenter.presentSession).toHaveBeenCalledWith(dto);
|
||||
expect(mockPresenter.presentSession).toHaveBeenCalledTimes(1);
|
||||
expect(result).toBe(expectedViewModel);
|
||||
});
|
||||
|
||||
it('should return null when session is null', async () => {
|
||||
// Arrange
|
||||
vi.mocked(mockApiClient.getSession).mockResolvedValue(null);
|
||||
vi.mocked(mockPresenter.presentSession).mockReturnValue(null);
|
||||
|
||||
// Act
|
||||
const result = await service.getSession();
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getSession).toHaveBeenCalledTimes(1);
|
||||
expect(mockPresenter.presentSession).toHaveBeenCalledWith(null);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should propagate API client errors', async () => {
|
||||
// Arrange
|
||||
const error = new Error('API Error: Failed to get session');
|
||||
vi.mocked(mockApiClient.getSession).mockRejectedValue(error);
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.getSession()).rejects.toThrow(
|
||||
'API Error: Failed to get session'
|
||||
);
|
||||
|
||||
expect(mockApiClient.getSession).toHaveBeenCalledTimes(1);
|
||||
expect(mockPresenter.presentSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle different session data', async () => {
|
||||
// Arrange
|
||||
const dto: SessionDataDto = {
|
||||
userId: 'user-2',
|
||||
email: 'another@example.com',
|
||||
isAuthenticated: false,
|
||||
};
|
||||
|
||||
const expectedViewModel = new SessionViewModel(dto);
|
||||
|
||||
vi.mocked(mockApiClient.getSession).mockResolvedValue(dto);
|
||||
vi.mocked(mockPresenter.presentSession).mockReturnValue(expectedViewModel);
|
||||
|
||||
// Act
|
||||
const result = await service.getSession();
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getSession).toHaveBeenCalledTimes(1);
|
||||
expect(mockPresenter.presentSession).toHaveBeenCalledWith(dto);
|
||||
expect(result).toBe(expectedViewModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Constructor Dependency Injection', () => {
|
||||
it('should require apiClient and presenter', () => {
|
||||
// This test verifies the constructor signature
|
||||
expect(() => {
|
||||
new SessionService(mockApiClient, mockPresenter);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should use injected dependencies', async () => {
|
||||
// Arrange
|
||||
const customApiClient = {
|
||||
getSession: vi.fn().mockResolvedValue({ userId: 'user-1', email: 'test@example.com', isAuthenticated: true }),
|
||||
signup: vi.fn(),
|
||||
login: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
getIracingAuthUrl: vi.fn(),
|
||||
} as unknown as AuthApiClient;
|
||||
|
||||
const customPresenter = {
|
||||
presentSession: vi.fn().mockReturnValue(new SessionViewModel({ userId: 'user-1', email: 'test@example.com', isAuthenticated: true })),
|
||||
} as unknown as SessionPresenter;
|
||||
|
||||
const customService = new SessionService(customApiClient, customPresenter);
|
||||
|
||||
// Act
|
||||
await customService.getSession();
|
||||
|
||||
// Assert
|
||||
expect(customApiClient.getSession).toHaveBeenCalledTimes(1);
|
||||
expect(customPresenter.presentSession).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,28 +1,22 @@
|
||||
import { AuthApiClient } from '../../api/auth/AuthApiClient';
|
||||
import { SessionPresenter } from '../../presenters/SessionPresenter';
|
||||
import type { SessionViewModel } from '../../view-models';
|
||||
import { SessionViewModel } from '../../view-models';
|
||||
|
||||
/**
|
||||
* Session Service
|
||||
*
|
||||
* Orchestrates session operations by coordinating API calls and presentation logic.
|
||||
* Orchestrates session operations by coordinating API calls and view model creation.
|
||||
* All dependencies are injected via constructor.
|
||||
*/
|
||||
export class SessionService {
|
||||
constructor(
|
||||
private readonly apiClient: AuthApiClient,
|
||||
private readonly presenter: SessionPresenter
|
||||
private readonly apiClient: AuthApiClient
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get current user session with presentation transformation
|
||||
* Get current user session with view model transformation
|
||||
*/
|
||||
async getSession(): Promise<SessionViewModel | null> {
|
||||
try {
|
||||
const dto = await this.apiClient.getSession();
|
||||
return this.presenter.presentSession(dto);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const dto = await this.apiClient.getSession();
|
||||
return dto ? new SessionViewModel(dto) : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user