Files
gridpilot.gg/core/racing/application/use-cases/TransferLeagueOwnershipUseCase.test.ts
2025-12-21 00:43:42 +01:00

259 lines
8.8 KiB
TypeScript

import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
import {
TransferLeagueOwnershipUseCase,
type TransferLeagueOwnershipInput,
type TransferLeagueOwnershipResult,
type TransferLeagueOwnershipErrorCode,
} from './TransferLeagueOwnershipUseCase';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { UseCaseOutputPort, Logger } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { Result } from '@core/shared/application/Result';
describe('TransferLeagueOwnershipUseCase', () => {
let leagueRepository: ILeagueRepository;
let membershipRepository: ILeagueMembershipRepository;
let logger: Logger & { error: Mock };
let output: UseCaseOutputPort<TransferLeagueOwnershipResult> & { present: Mock };
let useCase: TransferLeagueOwnershipUseCase;
beforeEach(() => {
leagueRepository = {
findById: vi.fn(),
update: vi.fn(),
} as unknown as ILeagueRepository;
membershipRepository = {
getMembership: vi.fn(),
saveMembership: vi.fn(),
} as unknown as ILeagueMembershipRepository;
logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Logger & { error: Mock };
output = {
present: vi.fn(),
} as unknown as UseCaseOutputPort<TransferLeagueOwnershipResult> & { present: Mock };
useCase = new TransferLeagueOwnershipUseCase(
leagueRepository,
membershipRepository,
logger,
output,
);
});
it('transfers ownership successfully', async () => {
const mockLeague = {
id: 'league-1',
ownerId: { toString: () => 'owner-1' },
update: vi.fn().mockReturnValue({}),
} as unknown as { id: string; ownerId: { toString: () => string }; update: Mock };
const mockNewOwnerMembership = {
leagueId: 'league-1',
driverId: 'owner-2',
status: { toString: () => 'active' },
role: 'member',
} as unknown as { leagueId: string; driverId: string; status: { toString: () => string }; role: string };
const mockCurrentOwnerMembership = {
leagueId: 'league-1',
driverId: 'owner-1',
status: { toString: () => 'active' },
role: 'owner',
} as unknown as { leagueId: string; driverId: string; status: { toString: () => string }; role: string };
(leagueRepository.findById as unknown as Mock).mockResolvedValue(mockLeague);
(membershipRepository.getMembership as unknown as Mock)
.mockResolvedValueOnce(mockNewOwnerMembership)
.mockResolvedValueOnce(mockCurrentOwnerMembership);
const input: TransferLeagueOwnershipInput = {
leagueId: 'league-1',
currentOwnerId: 'owner-1',
newOwnerId: 'owner-2',
};
const result: Result<
void,
ApplicationErrorCode<TransferLeagueOwnershipErrorCode, { message: string }>
> = await useCase.execute(input);
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeUndefined();
expect(leagueRepository.findById).toHaveBeenCalledWith('league-1');
expect(membershipRepository.getMembership).toHaveBeenCalledWith('league-1', 'owner-2');
expect(membershipRepository.getMembership).toHaveBeenCalledWith('league-1', 'owner-1');
expect(membershipRepository.saveMembership).toHaveBeenCalledTimes(2);
const saveMembershipMock = membershipRepository.saveMembership as unknown as Mock;
const firstSaveCall = saveMembershipMock.mock.calls[0]![0] as { role: string };
const secondSaveCall = saveMembershipMock.mock.calls[1]![0] as { role: string };
expect(firstSaveCall.role).toBe('owner');
expect(secondSaveCall.role).toBe('admin');
expect(mockLeague.update).toHaveBeenCalledWith({ ownerId: 'owner-2' });
expect(leagueRepository.update).toHaveBeenCalledWith(expect.anything());
expect(output.present).toHaveBeenCalledTimes(1);
const presentMock = output.present as Mock;
const presented = presentMock.mock.calls[0]![0] as TransferLeagueOwnershipResult;
expect(presented).toEqual({
leagueId: 'league-1',
previousOwnerId: 'owner-1',
newOwnerId: 'owner-2',
});
});
it('returns LEAGUE_NOT_FOUND when league does not exist', async () => {
(leagueRepository.findById as unknown as Mock).mockResolvedValue(null);
const input: TransferLeagueOwnershipInput = {
leagueId: 'non-existent',
currentOwnerId: 'owner-1',
newOwnerId: 'owner-2',
};
const result: Result<
void,
ApplicationErrorCode<TransferLeagueOwnershipErrorCode, { message: string }>
> = await useCase.execute(input);
expect(result.isErr()).toBe(true);
const error = result.unwrapErr() as ApplicationErrorCode<
TransferLeagueOwnershipErrorCode,
{ message: string }
>;
expect(error.code).toBe('LEAGUE_NOT_FOUND');
expect(error.details?.message).toContain('non-existent');
expect(output.present).not.toHaveBeenCalled();
});
it('returns NOT_LEAGUE_OWNER when current owner does not match', async () => {
const mockLeague = {
id: 'league-1',
ownerId: { toString: () => 'other-owner' },
update: vi.fn(),
} as unknown as { id: string; ownerId: { toString: () => string }; update: Mock };
(leagueRepository.findById as unknown as Mock).mockResolvedValue(mockLeague);
const input: TransferLeagueOwnershipInput = {
leagueId: 'league-1',
currentOwnerId: 'owner-1',
newOwnerId: 'owner-2',
};
const result: Result<
void,
ApplicationErrorCode<TransferLeagueOwnershipErrorCode, { message: string }>
> = await useCase.execute(input);
expect(result.isErr()).toBe(true);
const error = result.unwrapErr() as ApplicationErrorCode<
TransferLeagueOwnershipErrorCode,
{ message: string }
>;
expect(error.code).toBe('NOT_LEAGUE_OWNER');
expect(output.present).not.toHaveBeenCalled();
});
it('returns NEW_OWNER_NOT_MEMBER when new owner is not an active member', async () => {
const mockLeague = {
id: 'league-1',
ownerId: { toString: () => 'owner-1' },
update: vi.fn(),
} as unknown as { id: string; ownerId: { toString: () => string }; update: Mock };
(leagueRepository.findById as unknown as Mock).mockResolvedValue(mockLeague);
(membershipRepository.getMembership as unknown as Mock).mockResolvedValue(null);
const input: TransferLeagueOwnershipInput = {
leagueId: 'league-1',
currentOwnerId: 'owner-1',
newOwnerId: 'owner-2',
};
const result: Result<
void,
ApplicationErrorCode<TransferLeagueOwnershipErrorCode, { message: string }>
> = await useCase.execute(input);
expect(result.isErr()).toBe(true);
const error = result.unwrapErr() as ApplicationErrorCode<
TransferLeagueOwnershipErrorCode,
{ message: string }
>;
expect(error.code).toBe('NEW_OWNER_NOT_MEMBER');
expect(output.present).not.toHaveBeenCalled();
});
it('wraps repository errors in REPOSITORY_ERROR and logs the error', async () => {
const mockLeague = {
id: 'league-1',
ownerId: { toString: () => 'owner-1' },
update: vi.fn().mockReturnValue({}),
} as unknown as { id: string; ownerId: { toString: () => string }; update: Mock };
(leagueRepository.findById as unknown as Mock).mockResolvedValue(mockLeague);
const mockNewOwnerMembership = {
leagueId: 'league-1',
driverId: 'owner-2',
status: { toString: () => 'active' },
role: 'member',
} as unknown as { leagueId: string; driverId: string; status: { toString: () => string }; role: string };
(membershipRepository.getMembership as unknown as Mock)
.mockResolvedValueOnce(mockNewOwnerMembership)
.mockResolvedValueOnce(null);
const updateError = new Error('update failed');
(leagueRepository.update as unknown as Mock).mockRejectedValue(updateError);
const input: TransferLeagueOwnershipInput = {
leagueId: 'league-1',
currentOwnerId: 'owner-1',
newOwnerId: 'owner-2',
};
const result: Result<
void,
ApplicationErrorCode<TransferLeagueOwnershipErrorCode, { message: string }>
> = await useCase.execute(input);
expect(result.isErr()).toBe(true);
const error = result.unwrapErr() as ApplicationErrorCode<
TransferLeagueOwnershipErrorCode,
{ message: string }
>;
expect(error.code).toBe('REPOSITORY_ERROR');
expect(error.details?.message).toBe('update failed');
expect(output.present).not.toHaveBeenCalled();
expect(logger.error).toHaveBeenCalled();
const errorMock = logger.error as Mock;
const calls = errorMock.mock.calls;
expect(calls.length).toBeGreaterThan(0);
const loggedMessage = calls[0]?.[0] as string;
expect(loggedMessage).toContain('update failed');
});
});