235 lines
7.1 KiB
TypeScript
235 lines
7.1 KiB
TypeScript
import type { Logger } from '@core/shared/domain/Logger';
|
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
|
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
|
import {
|
|
TransferLeagueOwnershipUseCase,
|
|
type TransferLeagueOwnershipErrorCode,
|
|
type TransferLeagueOwnershipInput
|
|
} from './TransferLeagueOwnershipUseCase';
|
|
import type { LeagueRepository } from '../../domain/repositories/LeagueRepository';
|
|
import type { LeagueMembershipRepository } from '../../domain/repositories/LeagueMembershipRepository';
|
|
import type { League } from '../../domain/entities/League';
|
|
import type { LeagueMembership } from '../../domain/entities/LeagueMembership';
|
|
|
|
describe('TransferLeagueOwnershipUseCase', () => {
|
|
let leagueRepository: {
|
|
findById: Mock;
|
|
update: Mock;
|
|
};
|
|
let membershipRepository: {
|
|
getMembership: Mock;
|
|
saveMembership: Mock;
|
|
};
|
|
let logger: Logger & { error: Mock };
|
|
let useCase: TransferLeagueOwnershipUseCase;
|
|
|
|
beforeEach(() => {
|
|
leagueRepository = {
|
|
findById: vi.fn(),
|
|
update: vi.fn(),
|
|
};
|
|
|
|
membershipRepository = {
|
|
getMembership: vi.fn(),
|
|
saveMembership: vi.fn(),
|
|
};
|
|
|
|
logger = {
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
} as unknown as Logger & { error: Mock };
|
|
|
|
useCase = new TransferLeagueOwnershipUseCase(
|
|
leagueRepository as unknown as LeagueRepository,
|
|
membershipRepository as unknown as LeagueMembershipRepository,
|
|
logger,
|
|
);
|
|
});
|
|
|
|
it('transfers ownership successfully', async () => {
|
|
const mockLeague = {
|
|
id: 'league-1',
|
|
ownerId: { toString: () => 'owner-1' },
|
|
update: vi.fn().mockReturnValue({}),
|
|
} as unknown as League;
|
|
|
|
const mockNewOwnerMembership = {
|
|
leagueId: 'league-1',
|
|
driverId: 'owner-2',
|
|
status: { toString: () => 'active' },
|
|
role: 'member',
|
|
} as unknown as LeagueMembership;
|
|
|
|
const mockCurrentOwnerMembership = {
|
|
leagueId: 'league-1',
|
|
driverId: 'owner-1',
|
|
status: { toString: () => 'active' },
|
|
role: 'owner',
|
|
} as unknown as LeagueMembership;
|
|
|
|
leagueRepository.findById.mockResolvedValue(mockLeague);
|
|
|
|
membershipRepository.getMembership
|
|
.mockResolvedValueOnce(mockNewOwnerMembership)
|
|
.mockResolvedValueOnce(mockCurrentOwnerMembership);
|
|
|
|
const input: TransferLeagueOwnershipInput = {
|
|
leagueId: 'league-1',
|
|
currentOwnerId: 'owner-1',
|
|
newOwnerId: 'owner-2',
|
|
};
|
|
|
|
const result = await useCase.execute(input);
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
const successResult = result.unwrap();
|
|
expect(successResult).toEqual({
|
|
leagueId: 'league-1',
|
|
previousOwnerId: 'owner-1',
|
|
newOwnerId: 'owner-2',
|
|
});
|
|
|
|
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 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());
|
|
});
|
|
|
|
it('returns LEAGUE_NOT_FOUND when league does not exist', async () => {
|
|
leagueRepository.findById.mockResolvedValue(null);
|
|
|
|
const input: TransferLeagueOwnershipInput = {
|
|
leagueId: 'non-existent',
|
|
currentOwnerId: 'owner-1',
|
|
newOwnerId: 'owner-2',
|
|
};
|
|
|
|
const result = 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');
|
|
});
|
|
|
|
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 League;
|
|
|
|
leagueRepository.findById.mockResolvedValue(mockLeague);
|
|
|
|
const input: TransferLeagueOwnershipInput = {
|
|
leagueId: 'league-1',
|
|
currentOwnerId: 'owner-1',
|
|
newOwnerId: 'owner-2',
|
|
};
|
|
|
|
const result = 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');
|
|
});
|
|
|
|
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 League;
|
|
|
|
leagueRepository.findById.mockResolvedValue(mockLeague);
|
|
|
|
membershipRepository.getMembership.mockResolvedValue(null);
|
|
|
|
const input: TransferLeagueOwnershipInput = {
|
|
leagueId: 'league-1',
|
|
currentOwnerId: 'owner-1',
|
|
newOwnerId: 'owner-2',
|
|
};
|
|
|
|
const result = 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');
|
|
});
|
|
|
|
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 League;
|
|
|
|
leagueRepository.findById.mockResolvedValue(mockLeague);
|
|
|
|
const mockNewOwnerMembership = {
|
|
leagueId: 'league-1',
|
|
driverId: 'owner-2',
|
|
status: { toString: () => 'active' },
|
|
role: 'member',
|
|
} as unknown as LeagueMembership;
|
|
|
|
membershipRepository.getMembership
|
|
.mockResolvedValueOnce(mockNewOwnerMembership)
|
|
.mockResolvedValueOnce(null);
|
|
|
|
const updateError = new Error('update failed');
|
|
leagueRepository.update.mockRejectedValue(updateError);
|
|
|
|
const input: TransferLeagueOwnershipInput = {
|
|
leagueId: 'league-1',
|
|
currentOwnerId: 'owner-1',
|
|
newOwnerId: 'owner-2',
|
|
};
|
|
|
|
const result = 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(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');
|
|
});
|
|
});
|