refactor racing use cases
This commit is contained in:
@@ -1,152 +1,258 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { TransferLeagueOwnershipUseCase } from './TransferLeagueOwnershipUseCase';
|
||||
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: 'owner-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: 'active',
|
||||
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: 'active',
|
||||
status: { toString: () => 'active' },
|
||||
role: 'owner',
|
||||
};
|
||||
} as unknown as { leagueId: string; driverId: string; status: { toString: () => string }; role: string };
|
||||
|
||||
const mockLeagueRepository = {
|
||||
findById: vi.fn().mockResolvedValue(mockLeague),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
} as unknown as ILeagueRepository;
|
||||
(leagueRepository.findById as unknown as Mock).mockResolvedValue(mockLeague);
|
||||
|
||||
const mockMembershipRepository = {
|
||||
getMembership: vi.fn()
|
||||
.mockResolvedValueOnce(mockNewOwnerMembership)
|
||||
.mockResolvedValueOnce(mockCurrentOwnerMembership),
|
||||
saveMembership: vi.fn().mockResolvedValue(undefined),
|
||||
} as unknown as ILeagueMembershipRepository;
|
||||
(membershipRepository.getMembership as unknown as Mock)
|
||||
.mockResolvedValueOnce(mockNewOwnerMembership)
|
||||
.mockResolvedValueOnce(mockCurrentOwnerMembership);
|
||||
|
||||
const useCase = new TransferLeagueOwnershipUseCase(
|
||||
mockLeagueRepository,
|
||||
mockMembershipRepository,
|
||||
);
|
||||
|
||||
const command = {
|
||||
const input: TransferLeagueOwnershipInput = {
|
||||
leagueId: 'league-1',
|
||||
currentOwnerId: 'owner-1',
|
||||
newOwnerId: 'owner-2',
|
||||
};
|
||||
|
||||
const result = await useCase.execute(command);
|
||||
const result: Result<
|
||||
void,
|
||||
ApplicationErrorCode<TransferLeagueOwnershipErrorCode, { message: string }>
|
||||
> = await useCase.execute(input);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(mockLeagueRepository.findById).toHaveBeenCalledWith('league-1');
|
||||
expect(mockMembershipRepository.getMembership).toHaveBeenCalledWith('league-1', 'owner-2');
|
||||
expect(mockMembershipRepository.getMembership).toHaveBeenCalledWith('league-1', 'owner-1');
|
||||
expect(mockMembershipRepository.saveMembership).toHaveBeenCalledWith({
|
||||
...mockNewOwnerMembership,
|
||||
role: 'owner',
|
||||
});
|
||||
expect(mockMembershipRepository.saveMembership).toHaveBeenCalledWith({
|
||||
...mockCurrentOwnerMembership,
|
||||
role: 'admin',
|
||||
});
|
||||
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(mockLeagueRepository.update).toHaveBeenCalledWith({});
|
||||
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 error when league not found', async () => {
|
||||
const mockLeagueRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
} as unknown as ILeagueRepository;
|
||||
it('returns LEAGUE_NOT_FOUND when league does not exist', async () => {
|
||||
(leagueRepository.findById as unknown as Mock).mockResolvedValue(null);
|
||||
|
||||
const mockMembershipRepository = {} as unknown as ILeagueMembershipRepository;
|
||||
|
||||
const useCase = new TransferLeagueOwnershipUseCase(
|
||||
mockLeagueRepository,
|
||||
mockMembershipRepository,
|
||||
);
|
||||
|
||||
const command = {
|
||||
leagueId: 'league-1',
|
||||
const input: TransferLeagueOwnershipInput = {
|
||||
leagueId: 'non-existent',
|
||||
currentOwnerId: 'owner-1',
|
||||
newOwnerId: 'owner-2',
|
||||
};
|
||||
|
||||
const result = await useCase.execute(command);
|
||||
const result: Result<
|
||||
void,
|
||||
ApplicationErrorCode<TransferLeagueOwnershipErrorCode, { message: string }>
|
||||
> = await useCase.execute(input);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual({ code: 'LEAGUE_NOT_FOUND' });
|
||||
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 error when not current owner', async () => {
|
||||
it('returns NOT_LEAGUE_OWNER when current owner does not match', async () => {
|
||||
const mockLeague = {
|
||||
id: 'league-1',
|
||||
ownerId: 'owner-2',
|
||||
};
|
||||
ownerId: { toString: () => 'other-owner' },
|
||||
update: vi.fn(),
|
||||
} as unknown as { id: string; ownerId: { toString: () => string }; update: Mock };
|
||||
|
||||
const mockLeagueRepository = {
|
||||
findById: vi.fn().mockResolvedValue(mockLeague),
|
||||
} as unknown as ILeagueRepository;
|
||||
(leagueRepository.findById as unknown as Mock).mockResolvedValue(mockLeague);
|
||||
|
||||
const mockMembershipRepository = {} as unknown as ILeagueMembershipRepository;
|
||||
|
||||
const useCase = new TransferLeagueOwnershipUseCase(
|
||||
mockLeagueRepository,
|
||||
mockMembershipRepository,
|
||||
);
|
||||
|
||||
const command = {
|
||||
const input: TransferLeagueOwnershipInput = {
|
||||
leagueId: 'league-1',
|
||||
currentOwnerId: 'owner-1',
|
||||
newOwnerId: 'owner-2',
|
||||
};
|
||||
|
||||
const result = await useCase.execute(command);
|
||||
const result: Result<
|
||||
void,
|
||||
ApplicationErrorCode<TransferLeagueOwnershipErrorCode, { message: string }>
|
||||
> = await useCase.execute(input);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual({ code: 'NOT_CURRENT_OWNER' });
|
||||
const error = result.unwrapErr() as ApplicationErrorCode<
|
||||
TransferLeagueOwnershipErrorCode,
|
||||
{ message: string }
|
||||
>;
|
||||
|
||||
expect(error.code).toBe('NOT_LEAGUE_OWNER');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns error when new owner is not active member', async () => {
|
||||
it('returns NEW_OWNER_NOT_MEMBER when new owner is not an active member', async () => {
|
||||
const mockLeague = {
|
||||
id: 'league-1',
|
||||
ownerId: 'owner-1',
|
||||
};
|
||||
ownerId: { toString: () => 'owner-1' },
|
||||
update: vi.fn(),
|
||||
} as unknown as { id: string; ownerId: { toString: () => string }; update: Mock };
|
||||
|
||||
const mockLeagueRepository = {
|
||||
findById: vi.fn().mockResolvedValue(mockLeague),
|
||||
} as unknown as ILeagueRepository;
|
||||
(leagueRepository.findById as unknown as Mock).mockResolvedValue(mockLeague);
|
||||
|
||||
const mockMembershipRepository = {
|
||||
getMembership: vi.fn().mockResolvedValue(null),
|
||||
} as unknown as ILeagueMembershipRepository;
|
||||
(membershipRepository.getMembership as unknown as Mock).mockResolvedValue(null);
|
||||
|
||||
const useCase = new TransferLeagueOwnershipUseCase(
|
||||
mockLeagueRepository,
|
||||
mockMembershipRepository,
|
||||
);
|
||||
|
||||
const command = {
|
||||
const input: TransferLeagueOwnershipInput = {
|
||||
leagueId: 'league-1',
|
||||
currentOwnerId: 'owner-1',
|
||||
newOwnerId: 'owner-2',
|
||||
};
|
||||
|
||||
const result = await useCase.execute(command);
|
||||
const result: Result<
|
||||
void,
|
||||
ApplicationErrorCode<TransferLeagueOwnershipErrorCode, { message: string }>
|
||||
> = await useCase.execute(input);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual({ code: 'NEW_OWNER_NOT_ACTIVE_MEMBER' });
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user