This commit is contained in:
2025-12-18 14:26:17 +01:00
parent 61f675d991
commit bf1f09c774
9 changed files with 387 additions and 30 deletions

View File

@@ -200,7 +200,7 @@ describe('LeagueService', () => {
});
describe('createLeague', () => {
it('should call apiClient.create and return CreateLeagueViewModel', async () => {
it('should call apiClient.create', async () => {
const input: CreateLeagueInputDTO = {
name: 'New League',
description: 'A new league',
@@ -213,31 +213,9 @@ describe('LeagueService', () => {
mockApiClient.create.mockResolvedValue(mockDto);
const result = await service.createLeague(input);
await service.createLeague(input);
expect(mockApiClient.create).toHaveBeenCalledWith(input);
expect(result).toBeInstanceOf(CreateLeagueViewModel);
expect(result.leagueId).toBe('new-league-id');
expect(result.success).toBe(true);
});
it('should handle unsuccessful creation', async () => {
const input: CreateLeagueInputDTO = {
name: 'New League',
description: 'A new league',
};
const mockDto: CreateLeagueOutputDTO = {
leagueId: '',
success: false,
};
mockApiClient.create.mockResolvedValue(mockDto);
const result = await service.createLeague(input);
expect(result.success).toBe(false);
expect(result.successMessage).toBe('Failed to create league.');
});
it('should throw error when apiClient.create fails', async () => {
@@ -251,6 +229,52 @@ describe('LeagueService', () => {
await expect(service.createLeague(input)).rejects.toThrow('API call failed');
});
it('should not call apiClient.create when submitBlocker is blocked', async () => {
const input: CreateLeagueInputDTO = {
name: 'New League',
description: 'A new league',
};
// First call should succeed
const mockDto: CreateLeagueOutputDTO = {
leagueId: 'new-league-id',
success: true,
};
mockApiClient.create.mockResolvedValue(mockDto);
await service.createLeague(input); // This should block the submitBlocker
// Reset mock to check calls
mockApiClient.create.mockClear();
// Second call should not call API
await service.createLeague(input);
expect(mockApiClient.create).not.toHaveBeenCalled();
});
it('should not call apiClient.create when throttle is active', async () => {
const input: CreateLeagueInputDTO = {
name: 'New League',
description: 'A new league',
};
// First call
const mockDto: CreateLeagueOutputDTO = {
leagueId: 'new-league-id',
success: true,
};
mockApiClient.create.mockResolvedValue(mockDto);
await service.createLeague(input); // This blocks throttle for 500ms
// Reset mock
mockApiClient.create.mockClear();
// Immediate second call should not call API due to throttle
await service.createLeague(input);
expect(mockApiClient.create).not.toHaveBeenCalled();
});
});
describe('removeMember', () => {

View File

@@ -8,6 +8,7 @@ import { LeagueStandingsViewModel } from "@/lib/view-models/LeagueStandingsViewM
import { LeagueStatsViewModel } from "@/lib/view-models/LeagueStatsViewModel";
import { LeagueSummaryViewModel } from "@/lib/view-models/LeagueSummaryViewModel";
import { RemoveMemberViewModel } from "@/lib/view-models/RemoveMemberViewModel";
import { SubmitBlocker, ThrottleBlocker } from "@/lib/blockers";
/**
@@ -17,6 +18,9 @@ import { RemoveMemberViewModel } from "@/lib/view-models/RemoveMemberViewModel";
* All dependencies are injected via constructor.
*/
export class LeagueService {
private readonly submitBlocker = new SubmitBlocker();
private readonly throttle = new ThrottleBlocker(500);
constructor(
private readonly apiClient: LeaguesApiClient
) {}
@@ -68,12 +72,19 @@ export class LeagueService {
}
/**
* Create a new league
*/
async createLeague(input: CreateLeagueInputDTO): Promise<CreateLeagueViewModel> {
const dto = await this.apiClient.create(input);
return new CreateLeagueViewModel(dto);
}
* Create a new league
*/
async createLeague(input: CreateLeagueInputDTO): Promise<void> {
if (!this.submitBlocker.canExecute() || !this.throttle.canExecute()) return;
this.submitBlocker.block();
this.throttle.block();
try {
await this.apiClient.create(input);
} finally {
this.submitBlocker.release();
}
}
/**
* Remove a member from league