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 & { 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 & { 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 > = 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 > = 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 > = 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 > = 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 > = 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'); }); });