website refactor
This commit is contained in:
@@ -0,0 +1,662 @@
|
||||
/**
|
||||
* Integration Tests for LeagueDetailPageQuery
|
||||
*
|
||||
* Tests the LeagueDetailPageQuery with mocked API clients to verify:
|
||||
* - Happy path: API returns valid league detail data
|
||||
* - Error handling: 404 when league not found
|
||||
* - Error handling: 500 when API server error
|
||||
* - Missing data: API returns partial data
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { LeagueDetailPageQuery } from '@/lib/page-queries/LeagueDetailPageQuery';
|
||||
import { MockLeaguesApiClient } from './mocks/MockLeaguesApiClient';
|
||||
import { ApiError } from '../../../apps/website/lib/api/base/ApiError';
|
||||
|
||||
// Mock data factories
|
||||
const createMockLeagueDetailData = () => ({
|
||||
leagues: [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'A test league',
|
||||
capacity: 10,
|
||||
currentMembers: 5,
|
||||
ownerId: 'driver-1',
|
||||
status: 'active' as const,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createMockMembershipsData = () => ({
|
||||
members: [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
iracingId: '12345',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
joinedAt: new Date().toISOString(),
|
||||
},
|
||||
role: 'owner' as const,
|
||||
status: 'active' as const,
|
||||
joinedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createMockRacesPageData = () => ({
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
track: 'Test Track',
|
||||
car: 'Test Car',
|
||||
scheduledAt: new Date().toISOString(),
|
||||
leagueName: 'Test League',
|
||||
status: 'scheduled' as const,
|
||||
strengthOfField: 50,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createMockDriverData = () => ({
|
||||
id: 'driver-1',
|
||||
iracingId: '12345',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
joinedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const createMockLeagueConfigData = () => ({
|
||||
form: {
|
||||
scoring: {
|
||||
presetId: 'preset-1',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('LeagueDetailPageQuery Integration', () => {
|
||||
let mockLeaguesApiClient: MockLeaguesApiClient;
|
||||
|
||||
beforeEach(() => {
|
||||
mockLeaguesApiClient = new MockLeaguesApiClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockLeaguesApiClient.clearMocks();
|
||||
});
|
||||
|
||||
describe('Happy Path', () => {
|
||||
it('should return valid league detail data when API returns success', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
const mockLeaguesData = createMockLeagueDetailData();
|
||||
const mockMembershipsData = createMockMembershipsData();
|
||||
const mockRacesPageData = createMockRacesPageData();
|
||||
const mockDriverData = createMockDriverData();
|
||||
const mockLeagueConfigData = createMockLeagueConfigData();
|
||||
|
||||
// Mock fetch to return different data based on the URL
|
||||
global.fetch = vi.fn((url: string) => {
|
||||
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
|
||||
return Promise.resolve(createMockResponse(mockLeaguesData));
|
||||
}
|
||||
if (url.includes('/memberships')) {
|
||||
return Promise.resolve(createMockResponse(mockMembershipsData));
|
||||
}
|
||||
if (url.includes('/races/page-data')) {
|
||||
return Promise.resolve(createMockResponse(mockRacesPageData));
|
||||
}
|
||||
if (url.includes('/drivers/driver-1')) {
|
||||
return Promise.resolve(createMockResponse(mockDriverData));
|
||||
}
|
||||
if (url.includes('/config')) {
|
||||
return Promise.resolve(createMockResponse(mockLeagueConfigData));
|
||||
}
|
||||
return Promise.resolve(createMockErrorResponse(404, 'Not Found', 'Not Found'));
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.league).toBeDefined();
|
||||
expect(data.league.id).toBe('league-1');
|
||||
expect(data.league.name).toBe('Test League');
|
||||
expect(data.league.capacity).toBe(10);
|
||||
expect(data.league.currentMembers).toBe(5);
|
||||
|
||||
expect(data.owner).toBeDefined();
|
||||
expect(data.owner?.id).toBe('driver-1');
|
||||
expect(data.owner?.name).toBe('Test Driver');
|
||||
|
||||
expect(data.memberships).toBeDefined();
|
||||
expect(data.memberships.members).toBeDefined();
|
||||
expect(data.memberships.members.length).toBe(1);
|
||||
|
||||
expect(data.races).toBeDefined();
|
||||
expect(data.races.length).toBe(1);
|
||||
expect(data.races[0].id).toBe('race-1');
|
||||
expect(data.races[0].name).toBe('Test Track - Test Car');
|
||||
|
||||
expect(data.scoringConfig).toBeDefined();
|
||||
expect(data.scoringConfig?.scoringPresetId).toBe('preset-1');
|
||||
});
|
||||
|
||||
it('should handle league without owner', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-2';
|
||||
const mockLeaguesData = {
|
||||
leagues: [
|
||||
{
|
||||
id: 'league-2',
|
||||
name: 'League Without Owner',
|
||||
description: 'A league without an owner',
|
||||
capacity: 15,
|
||||
currentMembers: 8,
|
||||
// No ownerId
|
||||
status: 'active' as const,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
const mockMembershipsData = createMockMembershipsData();
|
||||
const mockRacesPageData = createMockRacesPageData();
|
||||
|
||||
global.fetch = vi.fn((url: string) => {
|
||||
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
|
||||
return Promise.resolve(createMockResponse(mockLeaguesData));
|
||||
}
|
||||
if (url.includes('/memberships')) {
|
||||
return Promise.resolve(createMockResponse(mockMembershipsData));
|
||||
}
|
||||
if (url.includes('/races/page-data')) {
|
||||
return Promise.resolve(createMockResponse(mockRacesPageData));
|
||||
}
|
||||
return Promise.resolve(createMockErrorResponse(404, 'Not Found', 'Not Found'));
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
|
||||
expect(data.owner).toBeNull();
|
||||
expect(data.league.id).toBe('league-2');
|
||||
expect(data.league.name).toBe('League Without Owner');
|
||||
});
|
||||
|
||||
it('should handle league with no races', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-3';
|
||||
const mockLeaguesData = createMockLeagueDetailData();
|
||||
const mockMembershipsData = createMockMembershipsData();
|
||||
const mockRacesPageData = { races: [] };
|
||||
|
||||
global.fetch = vi.fn((url: string) => {
|
||||
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
|
||||
return Promise.resolve(createMockResponse(mockLeaguesData));
|
||||
}
|
||||
if (url.includes('/memberships')) {
|
||||
return Promise.resolve(createMockResponse(mockMembershipsData));
|
||||
}
|
||||
if (url.includes('/races/page-data')) {
|
||||
return Promise.resolve(createMockResponse(mockRacesPageData));
|
||||
}
|
||||
return Promise.resolve(createMockErrorResponse(404, 'Not Found', 'Not Found'));
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
|
||||
expect(data.races).toBeDefined();
|
||||
expect(data.races.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle 404 error when league not found', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'non-existent-league';
|
||||
|
||||
global.fetch = vi.fn((url: string) => {
|
||||
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
|
||||
return Promise.resolve(createMockResponse({ leagues: [] }));
|
||||
}
|
||||
return Promise.resolve(createMockErrorResponse(404, 'Not Found', 'League not found'));
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error).toBe('notFound');
|
||||
});
|
||||
|
||||
it('should handle 500 error when API server error', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
|
||||
global.fetch = vi.fn((url: string) => {
|
||||
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
|
||||
return Promise.resolve(createMockErrorResponse(500, 'Internal Server Error', 'Internal Server Error'));
|
||||
}
|
||||
return Promise.resolve(createMockErrorResponse(500, 'Internal Server Error', 'Internal Server Error'));
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error).toBe('serverError');
|
||||
});
|
||||
|
||||
it('should handle network error', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
|
||||
global.fetch = vi.fn().mockRejectedValue(new Error('Network error: Unable to reach the API server'));
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error).toBe('serverError');
|
||||
});
|
||||
|
||||
it('should handle timeout error', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
const timeoutError = new Error('Request timed out after 30 seconds');
|
||||
timeoutError.name = 'AbortError';
|
||||
|
||||
global.fetch = vi.fn().mockRejectedValue(timeoutError);
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error).toBe('serverError');
|
||||
});
|
||||
|
||||
it('should handle unauthorized error', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
|
||||
global.fetch = vi.fn((url: string) => {
|
||||
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
text: async () => 'Unauthorized',
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
text: async () => 'Unauthorized',
|
||||
});
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error).toBe('unauthorized');
|
||||
});
|
||||
|
||||
it('should handle forbidden error', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
|
||||
global.fetch = vi.fn((url: string) => {
|
||||
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
status: 403,
|
||||
statusText: 'Forbidden',
|
||||
text: async () => 'Forbidden',
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
status: 403,
|
||||
statusText: 'Forbidden',
|
||||
text: async () => 'Forbidden',
|
||||
});
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error).toBe('unauthorized');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Missing Data', () => {
|
||||
it('should handle API returning partial data (missing memberships)', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
const mockLeaguesData = createMockLeagueDetailData();
|
||||
const mockRacesPageData = createMockRacesPageData();
|
||||
|
||||
global.fetch = vi.fn((url: string) => {
|
||||
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: async () => mockLeaguesData,
|
||||
});
|
||||
}
|
||||
if (url.includes('/memberships')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: async () => ({ members: [] }),
|
||||
});
|
||||
}
|
||||
if (url.includes('/races/page-data')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: async () => mockRacesPageData,
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
text: async () => 'Not Found',
|
||||
});
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
|
||||
expect(data.memberships).toBeDefined();
|
||||
expect(data.memberships.members).toBeDefined();
|
||||
expect(data.memberships.members.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle API returning partial data (missing races)', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
const mockLeaguesData = createMockLeagueDetailData();
|
||||
const mockMembershipsData = createMockMembershipsData();
|
||||
|
||||
global.fetch = vi.fn((url: string) => {
|
||||
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: async () => mockLeaguesData,
|
||||
});
|
||||
}
|
||||
if (url.includes('/memberships')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: async () => mockMembershipsData,
|
||||
});
|
||||
}
|
||||
if (url.includes('/races/page-data')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: async () => ({ races: [] }),
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
text: async () => 'Not Found',
|
||||
});
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
|
||||
expect(data.races).toBeDefined();
|
||||
expect(data.races.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle API returning partial data (missing scoring config)', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
const mockLeaguesData = createMockLeagueDetailData();
|
||||
const mockMembershipsData = createMockMembershipsData();
|
||||
const mockRacesPageData = createMockRacesPageData();
|
||||
|
||||
global.fetch = vi.fn((url: string) => {
|
||||
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: async () => mockLeaguesData,
|
||||
});
|
||||
}
|
||||
if (url.includes('/memberships')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: async () => mockMembershipsData,
|
||||
});
|
||||
}
|
||||
if (url.includes('/races/page-data')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: async () => mockRacesPageData,
|
||||
});
|
||||
}
|
||||
if (url.includes('/config')) {
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
text: async () => 'Config not found',
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
text: async () => 'Not Found',
|
||||
});
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
|
||||
expect(data.scoringConfig).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle API returning partial data (missing owner)', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
const mockLeaguesData = {
|
||||
leagues: [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'A test league',
|
||||
capacity: 10,
|
||||
currentMembers: 5,
|
||||
ownerId: 'driver-1',
|
||||
status: 'active' as const,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
const mockMembershipsData = createMockMembershipsData();
|
||||
const mockRacesPageData = createMockRacesPageData();
|
||||
|
||||
global.fetch = vi.fn((url: string) => {
|
||||
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: async () => mockLeaguesData,
|
||||
});
|
||||
}
|
||||
if (url.includes('/memberships')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: async () => mockMembershipsData,
|
||||
});
|
||||
}
|
||||
if (url.includes('/races/page-data')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: async () => mockRacesPageData,
|
||||
});
|
||||
}
|
||||
if (url.includes('/drivers/driver-1')) {
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
text: async () => 'Driver not found',
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
text: async () => 'Not Found',
|
||||
});
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
|
||||
expect(data.owner).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle API returning empty leagues array', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
|
||||
global.fetch = vi.fn((url: string) => {
|
||||
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: async () => ({ leagues: [] }),
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
text: async () => 'Not Found',
|
||||
});
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error.type).toBe('notFound');
|
||||
expect(error.message).toContain('Leagues not found');
|
||||
});
|
||||
|
||||
it('should handle API returning null data', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
|
||||
global.fetch = vi.fn((url: string) => {
|
||||
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: async () => null,
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
text: async () => 'Not Found',
|
||||
});
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error.type).toBe('notFound');
|
||||
expect(error.message).toContain('Leagues not found');
|
||||
});
|
||||
|
||||
it('should handle API returning malformed data', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
|
||||
global.fetch = vi.fn((url: string) => {
|
||||
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: async () => ({ someOtherProperty: 'value' }),
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
text: async () => 'Not Found',
|
||||
});
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error.type).toBe('notFound');
|
||||
expect(error.message).toContain('Leagues not found');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user