integration tests
Some checks failed
CI / lint-typecheck (pull_request) Failing after 4m50s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
Some checks failed
CI / lint-typecheck (pull_request) Failing after 4m50s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
This commit is contained in:
@@ -1,662 +0,0 @@
|
||||
/**
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
86
tests/integration/website/WebsiteTestContext.ts
Normal file
86
tests/integration/website/WebsiteTestContext.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { vi } from 'vitest';
|
||||
import { MockLeaguesApiClient } from './mocks/MockLeaguesApiClient';
|
||||
import { CircuitBreakerRegistry } from '../../../apps/website/lib/api/base/RetryHandler';
|
||||
|
||||
export class WebsiteTestContext {
|
||||
public mockLeaguesApiClient: MockLeaguesApiClient;
|
||||
private originalFetch: typeof global.fetch;
|
||||
|
||||
private fetchMock = vi.fn();
|
||||
|
||||
constructor() {
|
||||
this.mockLeaguesApiClient = new MockLeaguesApiClient();
|
||||
this.originalFetch = global.fetch;
|
||||
}
|
||||
|
||||
static create() {
|
||||
return new WebsiteTestContext();
|
||||
}
|
||||
|
||||
setup() {
|
||||
this.originalFetch = global.fetch;
|
||||
global.fetch = this.fetchMock;
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL = 'http://localhost:3001';
|
||||
process.env.API_BASE_URL = 'http://localhost:3001';
|
||||
vi.stubEnv('NODE_ENV', 'test');
|
||||
CircuitBreakerRegistry.getInstance().resetAll();
|
||||
}
|
||||
|
||||
teardown() {
|
||||
global.fetch = this.originalFetch;
|
||||
this.fetchMock.mockClear();
|
||||
this.mockLeaguesApiClient.clearMocks();
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
CircuitBreakerRegistry.getInstance().resetAll();
|
||||
// Reset environment variables
|
||||
delete process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
delete process.env.API_BASE_URL;
|
||||
}
|
||||
|
||||
mockFetchResponse(data: any, status = 200, ok = true) {
|
||||
this.fetchMock.mockResolvedValueOnce(this.createMockResponse(data, status, ok));
|
||||
}
|
||||
|
||||
mockFetchError(error: Error) {
|
||||
this.fetchMock.mockRejectedValueOnce(error);
|
||||
}
|
||||
|
||||
mockFetchComplex(handler: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>) {
|
||||
this.fetchMock.mockImplementation(handler);
|
||||
}
|
||||
|
||||
createMockResponse(data: any, status = 200, ok = true): Response {
|
||||
return {
|
||||
ok,
|
||||
status,
|
||||
statusText: ok ? 'OK' : 'Error',
|
||||
headers: new Headers(),
|
||||
json: async () => data,
|
||||
text: async () => (typeof data === 'string' ? data : JSON.stringify(data)),
|
||||
blob: async () => new Blob(),
|
||||
arrayBuffer: async () => new ArrayBuffer(0),
|
||||
formData: async () => new FormData(),
|
||||
clone: () => this.createMockResponse(data, status, ok),
|
||||
body: null,
|
||||
bodyUsed: false,
|
||||
} as Response;
|
||||
}
|
||||
|
||||
createMockErrorResponse(status: number, statusText: string, body: string): Response {
|
||||
return {
|
||||
ok: false,
|
||||
status,
|
||||
statusText,
|
||||
headers: new Headers(),
|
||||
text: async () => body,
|
||||
json: async () => ({ message: body }),
|
||||
blob: async () => new Blob(),
|
||||
arrayBuffer: async () => new ArrayBuffer(0),
|
||||
formData: async () => new FormData(),
|
||||
clone: () => this.createMockErrorResponse(status, statusText, body),
|
||||
body: null,
|
||||
bodyUsed: false,
|
||||
} as Response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { LeagueDetailPageQuery } from '../../../../apps/website/lib/page-queries/LeagueDetailPageQuery';
|
||||
import { WebsiteTestContext } from '../WebsiteTestContext';
|
||||
|
||||
// Mock data factories
|
||||
const createMockLeagueData = (leagueId: string = 'league-1') => ({
|
||||
leagues: [
|
||||
{
|
||||
id: leagueId,
|
||||
name: 'Test League',
|
||||
description: 'A test league',
|
||||
ownerId: 'driver-1',
|
||||
createdAt: new Date().toISOString(),
|
||||
usedSlots: 5,
|
||||
settings: {
|
||||
maxDrivers: 10,
|
||||
},
|
||||
scoring: {
|
||||
gameId: 'game-1',
|
||||
gameName: 'Test Game',
|
||||
primaryChampionshipType: 'driver' as const,
|
||||
scoringPresetId: 'preset-1',
|
||||
scoringPresetName: 'Test Preset',
|
||||
dropPolicySummary: 'No drops',
|
||||
scoringPatternSummary: 'Standard scoring',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createMockMembershipsData = () => ({
|
||||
members: [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'Driver 1',
|
||||
},
|
||||
role: 'owner',
|
||||
joinedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createMockRacesData = (leagueId: string = 'league-1') => ({
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
track: 'Test Track',
|
||||
car: 'Test Car',
|
||||
scheduledAt: new Date().toISOString(),
|
||||
leagueId: leagueId,
|
||||
leagueName: 'Test League',
|
||||
status: 'scheduled',
|
||||
strengthOfField: 50,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createMockDriverData = () => ({
|
||||
id: 'driver-1',
|
||||
name: 'Test Driver',
|
||||
avatarUrl: 'https://example.com/avatar.png',
|
||||
});
|
||||
|
||||
const createMockConfigData = () => ({
|
||||
form: {
|
||||
scoring: {
|
||||
presetId: 'preset-1',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('LeagueDetailPageQuery Integration', () => {
|
||||
const ctx = WebsiteTestContext.create();
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.setup();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ctx.teardown();
|
||||
});
|
||||
|
||||
describe('Happy Path', () => {
|
||||
it('should return valid league detail data when API returns success', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
ctx.mockFetchResponse(createMockLeagueData(leagueId)); // For getAllWithCapacityAndScoring
|
||||
ctx.mockFetchResponse(createMockMembershipsData()); // For getMemberships
|
||||
ctx.mockFetchResponse(createMockRacesData(leagueId)); // For getPageData
|
||||
ctx.mockFetchResponse(createMockDriverData()); // For getDriver
|
||||
ctx.mockFetchResponse(createMockConfigData()); // For getLeagueConfig
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
|
||||
expect(data.leagueId).toBe(leagueId);
|
||||
expect(data.name).toBe('Test League');
|
||||
expect(data.ownerSummary).toBeDefined();
|
||||
expect(data.ownerSummary?.driverName).toBe('Test Driver');
|
||||
});
|
||||
|
||||
it('should handle league without owner', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-2';
|
||||
const leagueData = createMockLeagueData(leagueId);
|
||||
leagueData.leagues[0].ownerId = ''; // No owner
|
||||
|
||||
ctx.mockFetchResponse(leagueData); // getAllWithCapacityAndScoring
|
||||
ctx.mockFetchResponse(createMockMembershipsData()); // getMemberships
|
||||
ctx.mockFetchResponse(createMockRacesData(leagueId)); // getPageData
|
||||
ctx.mockFetchResponse(createMockConfigData()); // getLeagueConfig
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.ownerSummary).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle league with no races', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-3';
|
||||
ctx.mockFetchResponse(createMockLeagueData(leagueId)); // getAllWithCapacityAndScoring
|
||||
ctx.mockFetchResponse(createMockMembershipsData()); // getMemberships
|
||||
ctx.mockFetchResponse({ races: [] }); // getPageData
|
||||
ctx.mockFetchResponse(createMockDriverData()); // getDriver
|
||||
ctx.mockFetchResponse(createMockConfigData()); // getLeagueConfig
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.info.racesCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle 404 error when league not found', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'non-existent-league';
|
||||
ctx.mockFetchResponse({ leagues: [] }); // getAllWithCapacityAndScoring
|
||||
ctx.mockFetchResponse(createMockMembershipsData()); // getMemberships
|
||||
ctx.mockFetchResponse(createMockRacesData(leagueId)); // getPageData
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('notFound');
|
||||
});
|
||||
|
||||
it('should handle 500 error when API server error', async () => {
|
||||
// Arrange
|
||||
ctx.mockFetchResponse({ message: 'Internal Server Error' }, 500, false);
|
||||
ctx.mockFetchResponse({ message: 'Internal Server Error' }, 500, false);
|
||||
ctx.mockFetchResponse({ message: 'Internal Server Error' }, 500, false);
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute('league-1');
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('serverError');
|
||||
});
|
||||
|
||||
it('should handle network error', async () => {
|
||||
// Arrange
|
||||
ctx.mockFetchError(new Error('Network error: Unable to reach the API server'));
|
||||
ctx.mockFetchError(new Error('Network error: Unable to reach the API server'));
|
||||
ctx.mockFetchError(new Error('Network error: Unable to reach the API server'));
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute('league-1');
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('serverError');
|
||||
});
|
||||
|
||||
it('should handle timeout error', async () => {
|
||||
// Arrange
|
||||
const timeoutError = new Error('Request timed out after 30 seconds');
|
||||
timeoutError.name = 'AbortError';
|
||||
ctx.mockFetchError(timeoutError);
|
||||
ctx.mockFetchError(timeoutError);
|
||||
ctx.mockFetchError(timeoutError);
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute('league-1');
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('serverError');
|
||||
});
|
||||
|
||||
it('should handle unauthorized error', async () => {
|
||||
// Arrange
|
||||
ctx.mockFetchResponse({ message: 'Unauthorized' }, 401, false);
|
||||
ctx.mockFetchResponse({ message: 'Unauthorized' }, 401, false);
|
||||
ctx.mockFetchResponse({ message: 'Unauthorized' }, 401, false);
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute('league-1');
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('unauthorized');
|
||||
});
|
||||
|
||||
it('should handle forbidden error', async () => {
|
||||
// Arrange
|
||||
ctx.mockFetchResponse({ message: 'Forbidden' }, 403, false);
|
||||
ctx.mockFetchResponse({ message: 'Forbidden' }, 403, false);
|
||||
ctx.mockFetchResponse({ message: 'Forbidden' }, 403, false);
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute('league-1');
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('unauthorized');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Missing Data', () => {
|
||||
it('should handle API returning partial data (missing memberships)', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
ctx.mockFetchResponse(createMockLeagueData(leagueId));
|
||||
ctx.mockFetchResponse(null); // Missing memberships
|
||||
ctx.mockFetchResponse(createMockRacesData(leagueId));
|
||||
ctx.mockFetchResponse(createMockDriverData());
|
||||
ctx.mockFetchResponse(createMockConfigData());
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.info.membersCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle API returning partial data (missing races)', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
ctx.mockFetchResponse(createMockLeagueData(leagueId));
|
||||
ctx.mockFetchResponse(createMockMembershipsData());
|
||||
ctx.mockFetchResponse(null); // Missing races
|
||||
ctx.mockFetchResponse(createMockDriverData());
|
||||
ctx.mockFetchResponse(createMockConfigData());
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.info.racesCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle API returning partial data (missing scoring config)', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
ctx.mockFetchResponse(createMockLeagueData(leagueId));
|
||||
ctx.mockFetchResponse(createMockMembershipsData());
|
||||
ctx.mockFetchResponse(createMockRacesData(leagueId));
|
||||
ctx.mockFetchResponse(createMockDriverData());
|
||||
ctx.mockFetchResponse({ message: 'Config not found' }, 404, false); // Missing config
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.info.scoring).toBe('Standard');
|
||||
});
|
||||
|
||||
it('should handle API returning partial data (missing owner)', async () => {
|
||||
// Arrange
|
||||
const leagueId = 'league-1';
|
||||
ctx.mockFetchResponse(createMockLeagueData(leagueId));
|
||||
ctx.mockFetchResponse(createMockMembershipsData());
|
||||
ctx.mockFetchResponse(createMockRacesData(leagueId));
|
||||
ctx.mockFetchResponse(null); // Missing owner
|
||||
ctx.mockFetchResponse(createMockConfigData());
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute(leagueId);
|
||||
|
||||
// Assert
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.ownerSummary).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle API returning empty leagues array', async () => {
|
||||
// Arrange
|
||||
ctx.mockFetchResponse({ leagues: [] });
|
||||
ctx.mockFetchResponse(createMockMembershipsData());
|
||||
ctx.mockFetchResponse(createMockRacesData('league-1'));
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute('league-1');
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('notFound');
|
||||
});
|
||||
|
||||
it('should handle API returning null data', async () => {
|
||||
// Arrange
|
||||
ctx.mockFetchResponse(null);
|
||||
ctx.mockFetchResponse(createMockMembershipsData());
|
||||
ctx.mockFetchResponse(createMockRacesData('league-1'));
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute('league-1');
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('notFound');
|
||||
});
|
||||
|
||||
it('should handle API returning malformed data', async () => {
|
||||
// Arrange
|
||||
ctx.mockFetchResponse({ someOtherKey: [] });
|
||||
ctx.mockFetchResponse(createMockMembershipsData());
|
||||
ctx.mockFetchResponse(createMockRacesData('league-1'));
|
||||
|
||||
// Act
|
||||
const result = await LeagueDetailPageQuery.execute('league-1');
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.getError()).toBe('notFound');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,6 @@
|
||||
/**
|
||||
* Integration Tests for LeaguesPageQuery
|
||||
*
|
||||
* Tests the LeaguesPageQuery with mocked API clients to verify:
|
||||
* - Happy path: API returns valid leagues data
|
||||
* - Error handling: 404 when leagues endpoint not found
|
||||
* - Error handling: 500 when API server error
|
||||
* - Empty results: API returns empty leagues list
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { LeaguesPageQuery } from '@/lib/page-queries/LeaguesPageQuery';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { LeaguesPageQuery } from '../../../../apps/website/lib/page-queries/LeaguesPageQuery';
|
||||
import { WebsiteTestContext } from '../WebsiteTestContext';
|
||||
|
||||
// Mock data factories
|
||||
const createMockLeaguesData = () => ({
|
||||
@@ -63,27 +54,21 @@ const createMockEmptyLeaguesData = () => ({
|
||||
});
|
||||
|
||||
describe('LeaguesPageQuery Integration', () => {
|
||||
let originalFetch: typeof global.fetch;
|
||||
const ctx = WebsiteTestContext.create();
|
||||
|
||||
beforeEach(() => {
|
||||
// Store original fetch to restore later
|
||||
originalFetch = global.fetch;
|
||||
ctx.setup();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original fetch
|
||||
global.fetch = originalFetch;
|
||||
ctx.teardown();
|
||||
});
|
||||
|
||||
describe('Happy Path', () => {
|
||||
it('should return valid leagues data when API returns success', async () => {
|
||||
// Arrange
|
||||
const mockData = createMockLeaguesData();
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockData,
|
||||
text: async () => JSON.stringify(mockData),
|
||||
});
|
||||
ctx.mockFetchResponse(mockData);
|
||||
|
||||
// Act
|
||||
const result = await LeaguesPageQuery.execute();
|
||||
@@ -99,14 +84,14 @@ describe('LeaguesPageQuery Integration', () => {
|
||||
// Verify first league
|
||||
expect(viewData.leagues[0].id).toBe('league-1');
|
||||
expect(viewData.leagues[0].name).toBe('Test League 1');
|
||||
expect(viewData.leagues[0].settings.maxDrivers).toBe(10);
|
||||
expect(viewData.leagues[0].usedSlots).toBe(5);
|
||||
expect(viewData.leagues[0].maxDrivers).toBe(10);
|
||||
expect(viewData.leagues[0].usedDriverSlots).toBe(5);
|
||||
|
||||
// Verify second league
|
||||
expect(viewData.leagues[1].id).toBe('league-2');
|
||||
expect(viewData.leagues[1].name).toBe('Test League 2');
|
||||
expect(viewData.leagues[1].settings.maxDrivers).toBe(20);
|
||||
expect(viewData.leagues[1].usedSlots).toBe(15);
|
||||
expect(viewData.leagues[1].maxDrivers).toBe(20);
|
||||
expect(viewData.leagues[1].usedDriverSlots).toBe(15);
|
||||
});
|
||||
|
||||
it('should handle single league correctly', async () => {
|
||||
@@ -135,11 +120,7 @@ describe('LeaguesPageQuery Integration', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockData,
|
||||
text: async () => JSON.stringify(mockData),
|
||||
});
|
||||
ctx.mockFetchResponse(mockData);
|
||||
|
||||
// Act
|
||||
const result = await LeaguesPageQuery.execute();
|
||||
@@ -158,11 +139,7 @@ describe('LeaguesPageQuery Integration', () => {
|
||||
it('should handle empty leagues list from API', async () => {
|
||||
// Arrange
|
||||
const mockData = createMockEmptyLeaguesData();
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockData,
|
||||
text: async () => JSON.stringify(mockData),
|
||||
});
|
||||
ctx.mockFetchResponse(mockData);
|
||||
|
||||
// Act
|
||||
const result = await LeaguesPageQuery.execute();
|
||||
@@ -180,12 +157,7 @@ describe('LeaguesPageQuery Integration', () => {
|
||||
describe('Error Handling', () => {
|
||||
it('should handle 404 error when leagues endpoint not found', async () => {
|
||||
// Arrange
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
text: async () => 'Leagues not found',
|
||||
});
|
||||
ctx.mockFetchResponse({ message: 'Leagues not found' }, 404, false);
|
||||
|
||||
// Act
|
||||
const result = await LeaguesPageQuery.execute();
|
||||
@@ -193,17 +165,12 @@ describe('LeaguesPageQuery Integration', () => {
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error).toBe('LEAGUES_FETCH_FAILED');
|
||||
expect(error).toBe('notFound');
|
||||
});
|
||||
|
||||
it('should handle 500 error when API server error', async () => {
|
||||
// Arrange
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
text: async () => 'Internal Server Error',
|
||||
});
|
||||
ctx.mockFetchResponse({ message: 'Internal Server Error' }, 500, false);
|
||||
|
||||
// Act
|
||||
const result = await LeaguesPageQuery.execute();
|
||||
@@ -216,7 +183,7 @@ describe('LeaguesPageQuery Integration', () => {
|
||||
|
||||
it('should handle network error', async () => {
|
||||
// Arrange
|
||||
global.fetch = vi.fn().mockRejectedValue(new Error('Network error: Unable to reach the API server'));
|
||||
ctx.mockFetchError(new Error('Network error: Unable to reach the API server'));
|
||||
|
||||
// Act
|
||||
const result = await LeaguesPageQuery.execute();
|
||||
@@ -231,7 +198,7 @@ describe('LeaguesPageQuery Integration', () => {
|
||||
// Arrange
|
||||
const timeoutError = new Error('Request timed out after 30 seconds');
|
||||
timeoutError.name = 'AbortError';
|
||||
global.fetch = vi.fn().mockRejectedValue(timeoutError);
|
||||
ctx.mockFetchError(timeoutError);
|
||||
|
||||
// Act
|
||||
const result = await LeaguesPageQuery.execute();
|
||||
@@ -244,12 +211,7 @@ describe('LeaguesPageQuery Integration', () => {
|
||||
|
||||
it('should handle unauthorized error (redirect)', async () => {
|
||||
// Arrange
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
text: async () => 'Unauthorized',
|
||||
});
|
||||
ctx.mockFetchResponse({ message: 'Unauthorized' }, 401, false);
|
||||
|
||||
// Act
|
||||
const result = await LeaguesPageQuery.execute();
|
||||
@@ -262,12 +224,7 @@ describe('LeaguesPageQuery Integration', () => {
|
||||
|
||||
it('should handle forbidden error (redirect)', async () => {
|
||||
// Arrange
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 403,
|
||||
statusText: 'Forbidden',
|
||||
text: async () => 'Forbidden',
|
||||
});
|
||||
ctx.mockFetchResponse({ message: 'Forbidden' }, 403, false);
|
||||
|
||||
// Act
|
||||
const result = await LeaguesPageQuery.execute();
|
||||
@@ -280,12 +237,22 @@ describe('LeaguesPageQuery Integration', () => {
|
||||
|
||||
it('should handle unknown error type', async () => {
|
||||
// Arrange
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 999,
|
||||
statusText: 'Unknown Error',
|
||||
text: async () => 'Unknown error',
|
||||
});
|
||||
ctx.mockFetchResponse({ message: 'Unknown error' }, 999, false);
|
||||
|
||||
// Act
|
||||
const result = await LeaguesPageQuery.execute();
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error).toBe('LEAGUES_FETCH_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle API returning null or undefined data', async () => {
|
||||
// Arrange
|
||||
ctx.mockFetchResponse({ leagues: null });
|
||||
|
||||
// Act
|
||||
const result = await LeaguesPageQuery.execute();
|
||||
@@ -295,25 +262,6 @@ describe('LeaguesPageQuery Integration', () => {
|
||||
const error = result.getError();
|
||||
expect(error).toBe('UNKNOWN_ERROR');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle API returning null or undefined data', async () => {
|
||||
// Arrange
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => null,
|
||||
text: async () => 'null',
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await LeaguesPageQuery.execute();
|
||||
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error).toBe('LEAGUES_FETCH_FAILED');
|
||||
});
|
||||
|
||||
it('should handle API returning malformed data', async () => {
|
||||
// Arrange
|
||||
@@ -321,10 +269,7 @@ describe('LeaguesPageQuery Integration', () => {
|
||||
// Missing 'leagues' property
|
||||
someOtherProperty: 'value',
|
||||
};
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockData,
|
||||
});
|
||||
ctx.mockFetchResponse(mockData);
|
||||
|
||||
// Act
|
||||
const result = await LeaguesPageQuery.execute();
|
||||
@@ -332,7 +277,7 @@ describe('LeaguesPageQuery Integration', () => {
|
||||
// Assert
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error).toBe('LEAGUES_FETCH_FAILED');
|
||||
expect(error).toBe('UNKNOWN_ERROR');
|
||||
});
|
||||
|
||||
it('should handle API returning leagues with missing required fields', async () => {
|
||||
@@ -343,13 +288,13 @@ describe('LeaguesPageQuery Integration', () => {
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
// Missing other required fields
|
||||
settings: { maxDrivers: 10 },
|
||||
usedSlots: 5,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockData,
|
||||
});
|
||||
ctx.mockFetchResponse(mockData);
|
||||
|
||||
// Act
|
||||
const result = await LeaguesPageQuery.execute();
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getWebsiteRouteContracts, ScenarioRole } from '../../shared/website/RouteContractSpec';
|
||||
import { WebsiteRouteManager } from '../../shared/website/WebsiteRouteManager';
|
||||
import { RouteScenarioMatrix } from '../../shared/website/RouteScenarioMatrix';
|
||||
import { getWebsiteRouteContracts, ScenarioRole } from '../../../shared/website/RouteContractSpec';
|
||||
import { WebsiteRouteManager } from '../../../shared/website/WebsiteRouteManager';
|
||||
import { RouteScenarioMatrix } from '../../../shared/website/RouteScenarioMatrix';
|
||||
|
||||
describe('RouteContractSpec', () => {
|
||||
const contracts = getWebsiteRouteContracts();
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, test, beforeAll, afterAll } from 'vitest';
|
||||
import { routes } from '../../../apps/website/lib/routing/RouteConfig';
|
||||
import { WebsiteServerHarness } from '../harness/WebsiteServerHarness';
|
||||
import { ApiServerHarness } from '../harness/ApiServerHarness';
|
||||
import { HttpDiagnostics } from '../../shared/website/HttpDiagnostics';
|
||||
import { routes } from '../../../../apps/website/lib/routing/RouteConfig';
|
||||
import { WebsiteServerHarness } from '../../harness/WebsiteServerHarness';
|
||||
import { ApiServerHarness } from '../../harness/ApiServerHarness';
|
||||
import { HttpDiagnostics } from '../../../shared/website/HttpDiagnostics';
|
||||
|
||||
const WEBSITE_BASE_URL = process.env.WEBSITE_BASE_URL || 'http://localhost:3000';
|
||||
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3001';
|
||||
@@ -142,12 +142,6 @@ describe('Route Protection Matrix', () => {
|
||||
headers['Cookie'] = cookie;
|
||||
}
|
||||
|
||||
const url = `${WEBSITE_BASE_URL}${path}`;
|
||||
const response = await fetch(url, {
|
||||
headers,
|
||||
redirect: 'manual',
|
||||
});
|
||||
|
||||
const status = response.status;
|
||||
const location = response.headers.get('location');
|
||||
const html = status >= 400 ? await response.text() : undefined;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, test, beforeAll, afterAll, expect } from 'vitest';
|
||||
import { getWebsiteRouteContracts, RouteContract } from '../../shared/website/RouteContractSpec';
|
||||
import { WebsiteServerHarness } from '../harness/WebsiteServerHarness';
|
||||
import { ApiServerHarness } from '../harness/ApiServerHarness';
|
||||
import { HttpDiagnostics } from '../../shared/website/HttpDiagnostics';
|
||||
import { getWebsiteRouteContracts, RouteContract } from '../../../shared/website/RouteContractSpec';
|
||||
import { WebsiteServerHarness } from '../../harness/WebsiteServerHarness';
|
||||
import { ApiServerHarness } from '../../harness/ApiServerHarness';
|
||||
import { HttpDiagnostics } from '../../../shared/website/HttpDiagnostics';
|
||||
|
||||
const WEBSITE_BASE_URL = process.env.WEBSITE_BASE_URL || 'http://localhost:3005';
|
||||
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3006';
|
||||
@@ -60,6 +60,74 @@ describe('Website SSR Integration', () => {
|
||||
const location = response.headers.get('location');
|
||||
const html = await response.text();
|
||||
|
||||
if (status === 500) {
|
||||
console.error(`[WebsiteSSR] 500 Error at ${contract.path}. HTML:`, html.substring(0, 10000));
|
||||
const errorMatch = html.match(/<pre[^>]*>([\s\S]*?)<\/pre>/);
|
||||
if (errorMatch) {
|
||||
console.error(`[WebsiteSSR] Error details from HTML:`, errorMatch[1]);
|
||||
}
|
||||
const nextDataMatch = html.match(/<script id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/);
|
||||
if (nextDataMatch) {
|
||||
console.error(`[WebsiteSSR] NEXT_DATA:`, nextDataMatch[1]);
|
||||
}
|
||||
// Look for Next.js 13+ flight data or error markers
|
||||
const flightDataMatch = html.match(/self\.__next_f\.push\(\[1,"([^"]+)"\]\)/g);
|
||||
if (flightDataMatch) {
|
||||
console.error(`[WebsiteSSR] Flight Data found, checking for errors...`);
|
||||
flightDataMatch.forEach(m => {
|
||||
if (m.includes('Error') || m.includes('failed')) {
|
||||
console.error(`[WebsiteSSR] Potential error in flight data:`, m);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Check for specific error message in the body
|
||||
if (html.includes('Error:')) {
|
||||
const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/);
|
||||
if (bodyMatch) {
|
||||
console.error(`[WebsiteSSR] Body content:`, bodyMatch[1].substring(0, 1000));
|
||||
}
|
||||
}
|
||||
// Check for Next.js 14+ error markers
|
||||
const nextErrorMatch = html.match(/<meta name="next-error" content="([^"]+)"\/>/);
|
||||
if (nextErrorMatch) {
|
||||
console.error(`[WebsiteSSR] Next.js Error Marker:`, nextErrorMatch[1]);
|
||||
}
|
||||
// Check for "digest" error markers
|
||||
const digestMatch = html.match(/"digest":"([^"]+)"/);
|
||||
if (digestMatch) {
|
||||
console.error(`[WebsiteSSR] Error Digest:`, digestMatch[1]);
|
||||
}
|
||||
// Check for "notFound" in flight data
|
||||
if (html.includes('notFound')) {
|
||||
console.error(`[WebsiteSSR] "notFound" found in HTML source`);
|
||||
}
|
||||
// Check for "NEXT_NOT_FOUND"
|
||||
if (html.includes('NEXT_NOT_FOUND')) {
|
||||
console.error(`[WebsiteSSR] "NEXT_NOT_FOUND" found in HTML source`);
|
||||
}
|
||||
// Check for "Invariant: notFound() called in shell"
|
||||
if (html.includes('Invariant: notFound() called in shell')) {
|
||||
console.error(`[WebsiteSSR] "Invariant: notFound() called in shell" found in HTML source`);
|
||||
}
|
||||
// Check for "Error: notFound()"
|
||||
if (html.includes('Error: notFound()')) {
|
||||
console.error(`[WebsiteSSR] "Error: notFound()" found in HTML source`);
|
||||
}
|
||||
// Check for "DIGEST"
|
||||
if (html.includes('DIGEST')) {
|
||||
console.error(`[WebsiteSSR] "DIGEST" found in HTML source`);
|
||||
}
|
||||
// Check for "NEXT_REDIRECT"
|
||||
if (html.includes('NEXT_REDIRECT')) {
|
||||
console.error(`[WebsiteSSR] "NEXT_REDIRECT" found in HTML source`);
|
||||
}
|
||||
// Check for "Error: "
|
||||
const genericErrorMatch = html.match(/Error: ([^<]+)/);
|
||||
if (genericErrorMatch) {
|
||||
console.error(`[WebsiteSSR] Generic Error Match:`, genericErrorMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
const failureContext = {
|
||||
url,
|
||||
status,
|
||||
Reference in New Issue
Block a user