tests cleanup
This commit is contained in:
262
core/ports/media/MediaResolverPort.test.ts
Normal file
262
core/ports/media/MediaResolverPort.test.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* TDD Tests for MediaResolverPort interface contract
|
||||
*
|
||||
* Tests cover:
|
||||
* - Interface contract compliance
|
||||
* - Method signatures
|
||||
* - Return types
|
||||
* - Error handling behavior
|
||||
*/
|
||||
|
||||
import { MediaReference } from '@core/domain/media/MediaReference';
|
||||
|
||||
// Mock interface for testing
|
||||
interface MediaResolverPort {
|
||||
resolve(ref: MediaReference, baseUrl: string): Promise<string | null>;
|
||||
}
|
||||
|
||||
describe('MediaResolverPort', () => {
|
||||
let mockResolver: MediaResolverPort;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a mock implementation for testing
|
||||
mockResolver = {
|
||||
resolve: jest.fn(async (ref: MediaReference, baseUrl: string): Promise<string | null> => {
|
||||
// Mock implementation that returns different URLs based on type
|
||||
switch (ref.type) {
|
||||
case 'system-default':
|
||||
return `${baseUrl}/defaults/${ref.variant}`;
|
||||
case 'generated':
|
||||
return `${baseUrl}/generated/${ref.generationRequestId}`;
|
||||
case 'uploaded':
|
||||
return `${baseUrl}/media/${ref.mediaId}`;
|
||||
case 'none':
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
describe('Interface Contract', () => {
|
||||
it('should have a resolve method', () => {
|
||||
expect(typeof mockResolver.resolve).toBe('function');
|
||||
});
|
||||
|
||||
it('should accept MediaReference and string parameters', async () => {
|
||||
const ref = MediaReference.createSystemDefault('avatar');
|
||||
const baseUrl = 'https://api.example.com';
|
||||
|
||||
await expect(mockResolver.resolve(ref, baseUrl)).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it('should return Promise<string | null>', async () => {
|
||||
const ref = MediaReference.createSystemDefault('avatar');
|
||||
const baseUrl = 'https://api.example.com';
|
||||
|
||||
const result = await mockResolver.resolve(ref, baseUrl);
|
||||
expect(result === null || typeof result === 'string').toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('System Default Resolution', () => {
|
||||
it('should resolve system-default avatar to correct URL', async () => {
|
||||
const ref = MediaReference.createSystemDefault('avatar');
|
||||
const baseUrl = 'https://api.example.com';
|
||||
|
||||
const result = await mockResolver.resolve(ref, baseUrl);
|
||||
|
||||
expect(result).toBe('https://api.example.com/defaults/avatar');
|
||||
});
|
||||
|
||||
it('should resolve system-default logo to correct URL', async () => {
|
||||
const ref = MediaReference.createSystemDefault('logo');
|
||||
const baseUrl = 'https://api.example.com';
|
||||
|
||||
const result = await mockResolver.resolve(ref, baseUrl);
|
||||
|
||||
expect(result).toBe('https://api.example.com/defaults/logo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Generated Resolution', () => {
|
||||
it('should resolve generated reference to correct URL', async () => {
|
||||
const ref = MediaReference.createGenerated('req-123');
|
||||
const baseUrl = 'https://api.example.com';
|
||||
|
||||
const result = await mockResolver.resolve(ref, baseUrl);
|
||||
|
||||
expect(result).toBe('https://api.example.com/generated/req-123');
|
||||
});
|
||||
|
||||
it('should handle generated reference with special characters', async () => {
|
||||
const ref = MediaReference.createGenerated('req-abc-123_XYZ');
|
||||
const baseUrl = 'https://api.example.com';
|
||||
|
||||
const result = await mockResolver.resolve(ref, baseUrl);
|
||||
|
||||
expect(result).toBe('https://api.example.com/generated/req-abc-123_XYZ');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Uploaded Resolution', () => {
|
||||
it('should resolve uploaded reference to correct URL', async () => {
|
||||
const ref = MediaReference.createUploaded('media-456');
|
||||
const baseUrl = 'https://api.example.com';
|
||||
|
||||
const result = await mockResolver.resolve(ref, baseUrl);
|
||||
|
||||
expect(result).toBe('https://api.example.com/media/media-456');
|
||||
});
|
||||
|
||||
it('should handle uploaded reference with special characters', async () => {
|
||||
const ref = MediaReference.createUploaded('media-abc-456_XYZ');
|
||||
const baseUrl = 'https://api.example.com';
|
||||
|
||||
const result = await mockResolver.resolve(ref, baseUrl);
|
||||
|
||||
expect(result).toBe('https://api.example.com/media/media-abc-456_XYZ');
|
||||
});
|
||||
});
|
||||
|
||||
describe('None Resolution', () => {
|
||||
it('should resolve none reference to null', async () => {
|
||||
const ref = MediaReference.createNone();
|
||||
const baseUrl = 'https://api.example.com';
|
||||
|
||||
const result = await mockResolver.resolve(ref, baseUrl);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Base URL Handling', () => {
|
||||
it('should handle base URL without trailing slash', async () => {
|
||||
const ref = MediaReference.createSystemDefault('avatar');
|
||||
const baseUrl = 'https://api.example.com';
|
||||
|
||||
const result = await mockResolver.resolve(ref, baseUrl);
|
||||
|
||||
expect(result).toBe('https://api.example.com/defaults/avatar');
|
||||
});
|
||||
|
||||
it('should handle base URL with trailing slash', async () => {
|
||||
const ref = MediaReference.createSystemDefault('avatar');
|
||||
const baseUrl = 'https://api.example.com/';
|
||||
|
||||
const result = await mockResolver.resolve(ref, baseUrl);
|
||||
|
||||
// Implementation should handle this consistently
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handle localhost URLs', async () => {
|
||||
const ref = MediaReference.createSystemDefault('avatar');
|
||||
const baseUrl = 'http://localhost:3000';
|
||||
|
||||
const result = await mockResolver.resolve(ref, baseUrl);
|
||||
|
||||
expect(result).toBe('http://localhost:3000/defaults/avatar');
|
||||
});
|
||||
|
||||
it('should handle relative URLs', async () => {
|
||||
const ref = MediaReference.createSystemDefault('avatar');
|
||||
const baseUrl = '/api';
|
||||
|
||||
const result = await mockResolver.resolve(ref, baseUrl);
|
||||
|
||||
expect(result).toBe('/api/defaults/avatar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle null baseUrl gracefully', async () => {
|
||||
const ref = MediaReference.createSystemDefault('avatar');
|
||||
|
||||
// This should not throw but handle gracefully
|
||||
await expect(mockResolver.resolve(ref, null as any)).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle empty baseUrl gracefully', async () => {
|
||||
const ref = MediaReference.createSystemDefault('avatar');
|
||||
|
||||
// This should not throw but handle gracefully
|
||||
await expect(mockResolver.resolve(ref, '')).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle undefined baseUrl gracefully', async () => {
|
||||
const ref = MediaReference.createSystemDefault('avatar');
|
||||
|
||||
// This should not throw but handle gracefully
|
||||
await expect(mockResolver.resolve(ref, undefined as any)).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle very long media IDs', async () => {
|
||||
const longId = 'a'.repeat(1000);
|
||||
const ref = MediaReference.createUploaded(longId);
|
||||
const baseUrl = 'https://api.example.com';
|
||||
|
||||
const result = await mockResolver.resolve(ref, baseUrl);
|
||||
|
||||
expect(result).toBe(`https://api.example.com/media/${longId}`);
|
||||
});
|
||||
|
||||
it('should handle Unicode characters in IDs', async () => {
|
||||
const ref = MediaReference.createUploaded('media-日本語-123');
|
||||
const baseUrl = 'https://api.example.com';
|
||||
|
||||
const result = await mockResolver.resolve(ref, baseUrl);
|
||||
|
||||
expect(result).toBe('https://api.example.com/media/media-日本語-123');
|
||||
});
|
||||
|
||||
it('should handle multiple calls with different references', async () => {
|
||||
const refs = [
|
||||
MediaReference.createSystemDefault('avatar'),
|
||||
MediaReference.createGenerated('req-123'),
|
||||
MediaReference.createUploaded('media-456'),
|
||||
MediaReference.createNone()
|
||||
];
|
||||
const baseUrl = 'https://api.example.com';
|
||||
|
||||
const results = await Promise.all(refs.map(ref => mockResolver.resolve(ref, baseUrl)));
|
||||
|
||||
expect(results).toEqual([
|
||||
'https://api.example.com/defaults/avatar',
|
||||
'https://api.example.com/generated/req-123',
|
||||
'https://api.example.com/media/media-456',
|
||||
null
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance Considerations', () => {
|
||||
it('should resolve quickly for simple cases', async () => {
|
||||
const ref = MediaReference.createSystemDefault('avatar');
|
||||
const baseUrl = 'https://api.example.com';
|
||||
|
||||
const start = Date.now();
|
||||
await mockResolver.resolve(ref, baseUrl);
|
||||
const duration = Date.now() - start;
|
||||
|
||||
expect(duration).toBeLessThan(100); // Should be very fast
|
||||
});
|
||||
|
||||
it('should handle concurrent resolutions', async () => {
|
||||
const refs = Array.from({ length: 100 }, (_, i) =>
|
||||
MediaReference.createUploaded(`media-${i}`)
|
||||
);
|
||||
const baseUrl = 'https://api.example.com';
|
||||
|
||||
const start = Date.now();
|
||||
const results = await Promise.all(refs.map(ref => mockResolver.resolve(ref, baseUrl)));
|
||||
const duration = Date.now() - start;
|
||||
|
||||
expect(results.length).toBe(100);
|
||||
expect(duration).toBeLessThan(1000); // Should handle 100 concurrent calls quickly
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,11 +10,28 @@ import {
|
||||
SeasonDropPolicy,
|
||||
} from '@core/racing/domain/value-objects/SeasonDropPolicy';
|
||||
import { SeasonStewardingConfig } from '@core/racing/domain/value-objects/SeasonStewardingConfig';
|
||||
import {
|
||||
createMinimalSeason,
|
||||
createBaseSeason,
|
||||
} from '@core/testing/factories/racing/SeasonFactory';
|
||||
import { Season, SeasonStatus } from '@core/racing/domain/entities/season/Season';
|
||||
|
||||
const createMinimalSeason = (overrides?: { status?: SeasonStatus }) =>
|
||||
Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'iracing',
|
||||
name: 'Test Season',
|
||||
status: overrides?.status ?? 'planned',
|
||||
});
|
||||
|
||||
const createBaseSeason = () =>
|
||||
Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'iracing',
|
||||
name: 'Config Season',
|
||||
status: 'planned',
|
||||
startDate: new Date('2025-01-01T00:00:00Z'),
|
||||
endDate: undefined,
|
||||
maxDrivers: 24,
|
||||
});
|
||||
|
||||
describe('Season aggregate lifecycle', () => {
|
||||
it('transitions Planned → Active → Completed → Archived with timestamps', () => {
|
||||
@@ -205,5 +222,3 @@ describe('Season configuration updates', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,63 @@ import { EventScoringService } from '@core/racing/domain/services/EventScoringSe
|
||||
import type { BonusRule } from '@core/racing/domain/types/BonusRule';
|
||||
import { Result } from '@core/racing/domain/entities/result/Result';
|
||||
import type { Penalty } from '@core/racing/domain/entities/Penalty';
|
||||
import { makeChampionshipConfig } from '../../../testing/factories/racing/ChampionshipConfigFactory';
|
||||
import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig';
|
||||
import type { SessionType } from '@core/racing/domain/types/SessionType';
|
||||
import { PointsTable } from '@core/racing/domain/value-objects/PointsTable';
|
||||
|
||||
const makeChampionshipConfig = (params: {
|
||||
id: string;
|
||||
name: string;
|
||||
sessionTypes: SessionType[];
|
||||
mainPoints: number[];
|
||||
sprintPoints?: number[];
|
||||
mainBonusRules?: BonusRule[];
|
||||
}): ChampionshipConfig => {
|
||||
const { id, name, sessionTypes, mainPoints, sprintPoints, mainBonusRules } = params;
|
||||
|
||||
const pointsTableBySessionType: Record<SessionType, PointsTable> = {} as Record<SessionType, PointsTable>;
|
||||
|
||||
sessionTypes.forEach((sessionType) => {
|
||||
if (sessionType === 'main') {
|
||||
pointsTableBySessionType[sessionType] = new PointsTable(
|
||||
mainPoints.reduce((acc, points, index) => {
|
||||
acc[index + 1] = points;
|
||||
return acc;
|
||||
}, {} as Record<number, number>),
|
||||
);
|
||||
} else if (sessionType === 'sprint' && sprintPoints) {
|
||||
pointsTableBySessionType[sessionType] = new PointsTable(
|
||||
sprintPoints.reduce((acc, points, index) => {
|
||||
acc[index + 1] = points;
|
||||
return acc;
|
||||
}, {} as Record<number, number>),
|
||||
);
|
||||
} else {
|
||||
pointsTableBySessionType[sessionType] = new PointsTable({});
|
||||
}
|
||||
});
|
||||
|
||||
const bonusRulesBySessionType: Record<SessionType, BonusRule[]> = {} as Record<SessionType, BonusRule[]>;
|
||||
sessionTypes.forEach((sessionType) => {
|
||||
if (sessionType === 'main' && mainBonusRules) {
|
||||
bonusRulesBySessionType[sessionType] = mainBonusRules;
|
||||
} else {
|
||||
bonusRulesBySessionType[sessionType] = [];
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
type: 'driver',
|
||||
sessionTypes,
|
||||
pointsTableBySessionType,
|
||||
bonusRulesBySessionType,
|
||||
dropScorePolicy: {
|
||||
strategy: 'none',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('EventScoringService', () => {
|
||||
const seasonId = 'season-1';
|
||||
@@ -201,4 +256,4 @@ describe('EventScoringService', () => {
|
||||
(mapWithBonus.get('driver-3')?.penaltyPoints || 0),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import type { ChampionshipConfig } from '../../../racing/domain/types/ChampionshipConfig';
|
||||
import type { ChampionshipType } from '../../../racing/domain/types/ChampionshipType';
|
||||
import type { SessionType } from '../../../racing/domain/types/SessionType';
|
||||
import type { BonusRule } from '../../../racing/domain/types/BonusRule';
|
||||
import type { DropScorePolicy, DropScoreStrategy } from '../../../racing/domain/types/DropScorePolicy';
|
||||
import { PointsTable } from '../../../racing/domain/value-objects/PointsTable';
|
||||
|
||||
interface ChampionshipConfigInput {
|
||||
id: string;
|
||||
name: string;
|
||||
sessionTypes: SessionType[];
|
||||
mainPoints?: number[];
|
||||
mainBonusRules?: BonusRule[];
|
||||
type?: ChampionshipType;
|
||||
dropScorePolicy?: DropScorePolicy;
|
||||
strategy?: DropScoreStrategy;
|
||||
}
|
||||
|
||||
export function makeChampionshipConfig(input: ChampionshipConfigInput): ChampionshipConfig {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
sessionTypes,
|
||||
mainPoints = [25, 18, 15, 12, 10, 8, 6, 4, 2, 1],
|
||||
mainBonusRules = [],
|
||||
type = 'driver',
|
||||
dropScorePolicy = { strategy: 'none' },
|
||||
} = input;
|
||||
|
||||
const pointsTableBySessionType: Record<SessionType, PointsTable> = {} as Record<SessionType, PointsTable>;
|
||||
|
||||
// Convert array format to PointsTable for each session type
|
||||
sessionTypes.forEach(sessionType => {
|
||||
const pointsArray = mainPoints;
|
||||
const pointsMap: Record<number, number> = {};
|
||||
pointsArray.forEach((points, index) => {
|
||||
pointsMap[index + 1] = points;
|
||||
});
|
||||
pointsTableBySessionType[sessionType] = new PointsTable(pointsMap);
|
||||
});
|
||||
|
||||
const bonusRulesBySessionType: Record<SessionType, BonusRule[]> = {} as Record<SessionType, BonusRule[]>;
|
||||
|
||||
// Add bonus rules for each session type
|
||||
sessionTypes.forEach(sessionType => {
|
||||
bonusRulesBySessionType[sessionType] = mainBonusRules;
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
sessionTypes,
|
||||
pointsTableBySessionType,
|
||||
bonusRulesBySessionType,
|
||||
dropScorePolicy,
|
||||
};
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { ParticipantRef } from '../../../racing/domain/types/ParticipantRef';
|
||||
|
||||
export function makeDriverRef(driverId: string): ParticipantRef {
|
||||
return {
|
||||
id: driverId,
|
||||
type: 'driver',
|
||||
};
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { PointsTable } from '../../../racing/domain/value-objects/PointsTable';
|
||||
|
||||
export function makePointsTable(points: number[]): PointsTable {
|
||||
return new PointsTable(points);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Season } from '@core/racing/domain/entities/season/Season';
|
||||
import type { SeasonStatusValue } from '@core/racing/domain/value-objects/SeasonStatus';
|
||||
|
||||
export const createMinimalSeason = (overrides?: { status?: SeasonStatusValue }) =>
|
||||
Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'iracing',
|
||||
name: 'Test Season',
|
||||
status: overrides?.status ?? 'planned',
|
||||
});
|
||||
|
||||
export const createBaseSeason = () =>
|
||||
Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'iracing',
|
||||
name: 'Config Season',
|
||||
status: 'planned',
|
||||
startDate: new Date('2025-01-01T00:00:00Z'),
|
||||
endDate: undefined,
|
||||
maxDrivers: 24,
|
||||
});
|
||||
Reference in New Issue
Block a user