import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest'; import type { Logger } from '@core/shared/application/Logger'; import { Result } from '@core/shared/application/Result'; import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase'; import { GetTeamDetailsUseCase } from '@core/racing/application/use-cases/GetTeamDetailsUseCase'; import { GetTeamMembersUseCase } from '@core/racing/application/use-cases/GetTeamMembersUseCase'; import { GetTeamJoinRequestsUseCase } from '@core/racing/application/use-cases/GetTeamJoinRequestsUseCase'; import { CreateTeamUseCase } from '@core/racing/application/use-cases/CreateTeamUseCase'; import { UpdateTeamUseCase } from '@core/racing/application/use-cases/UpdateTeamUseCase'; import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase'; import { GetTeamMembershipUseCase } from '@core/racing/application/use-cases/GetTeamMembershipUseCase'; import type { CreateTeamInputDTO } from './dtos/CreateTeamInputDTO'; import type { UpdateTeamInputDTO } from './dtos/UpdateTeamInputDTO'; import { TeamService } from './TeamService'; type ValueObjectStub = { props: string; toString(): string }; type TeamEntityStub = { id: string; name: ValueObjectStub; tag: ValueObjectStub; description: ValueObjectStub; ownerId: ValueObjectStub; leagues: ValueObjectStub[]; createdAt: { toDate(): Date }; update: Mock; }; describe('TeamService', () => { const makeValueObject = (value: string): ValueObjectStub => ({ props: value, toString() { return value; }, }); const makeTeam = (overrides?: Partial>): TeamEntityStub => { const base: Omit = { id: 'team-1', name: makeValueObject('Team One'), tag: makeValueObject('T1'), description: makeValueObject('Desc'), ownerId: makeValueObject('owner-1'), leagues: [makeValueObject('league-1')], createdAt: { toDate: () => new Date('2023-01-01T00:00:00.000Z') }, }; const team: TeamEntityStub = { ...base, ...overrides, update: vi.fn((updates: Partial<{ name: string; tag: string; description: string }>) => { const next: Partial> = { ...(overrides ?? {}), ...(updates.name !== undefined ? { name: makeValueObject(updates.name) } : {}), ...(updates.tag !== undefined ? { tag: makeValueObject(updates.tag) } : {}), ...(updates.description !== undefined ? { description: makeValueObject(updates.description) } : {}), }; return makeTeam(next); }), }; return team; }; let teamRepository: { findAll: Mock; findById: Mock; create: Mock; update: Mock; }; let membershipRepository: { countByTeamId: Mock; getActiveMembershipForDriver: Mock; getMembership: Mock; getTeamMembers: Mock; getJoinRequests: Mock; saveMembership: Mock; }; let driverRepository: { findById: Mock; }; let logger: Logger; let service: TeamService; beforeEach(() => { teamRepository = { findAll: vi.fn(), findById: vi.fn(), create: vi.fn(), update: vi.fn(), }; membershipRepository = { countByTeamId: vi.fn(), getActiveMembershipForDriver: vi.fn(), getMembership: vi.fn(), getTeamMembers: vi.fn(), getJoinRequests: vi.fn(), saveMembership: vi.fn(), }; driverRepository = { findById: vi.fn(), }; logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), } as unknown as Logger; service = new TeamService(teamRepository as unknown as never, membershipRepository as unknown as never, driverRepository as unknown as never, logger); }); it('getAll returns teams and totalCount on success', async () => { teamRepository.findAll.mockResolvedValue([makeTeam()]); membershipRepository.countByTeamId.mockResolvedValue(3); await expect(service.getAll()).resolves.toEqual({ teams: [ { id: 'team-1', name: 'Team One', tag: 'T1', description: 'Desc', memberCount: 3, leagues: ['league-1'], }, ], totalCount: 1, }); }); it('getAll returns empty list when use case error message is empty (covers fallback)', async () => { const executeSpy = vi .spyOn(GetAllTeamsUseCase.prototype, 'execute') .mockResolvedValueOnce(Result.err({ code: 'REPOSITORY_ERROR', details: { message: '' } })); await expect(service.getAll()).resolves.toEqual({ teams: [], totalCount: 0 }); executeSpy.mockRestore(); }); it('getAll returns empty list when repository throws', async () => { teamRepository.findAll.mockRejectedValue(new Error('boom')); await expect(service.getAll()).resolves.toEqual({ teams: [], totalCount: 0 }); }); it('getDetails returns DTO on success', async () => { teamRepository.findById.mockResolvedValue(makeTeam()); membershipRepository.getMembership.mockResolvedValue({ teamId: 'team-1', driverId: 'driver-1', role: 'driver', status: 'active', joinedAt: new Date('2023-02-01T00:00:00.000Z'), }); await expect(service.getDetails('team-1', 'driver-1')).resolves.toEqual({ team: { id: 'team-1', name: 'Team One', tag: 'T1', description: 'Desc', ownerId: 'owner-1', leagues: ['league-1'], createdAt: '2023-01-01T00:00:00.000Z', }, membership: { role: 'member', joinedAt: '2023-02-01T00:00:00.000Z', isActive: true, }, canManage: false, }); }); it('getDetails passes empty driverId when userId missing', async () => { const executeSpy = vi .spyOn(GetTeamDetailsUseCase.prototype, 'execute') .mockImplementationOnce(async (input) => { expect(input).toEqual({ teamId: 'team-1', driverId: '' }); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: '' } }); }); await expect(service.getDetails('team-1')).resolves.toBeNull(); executeSpy.mockRestore(); }); it('getDetails returns null on TEAM_NOT_FOUND', async () => { teamRepository.findById.mockResolvedValue(null); await expect(service.getDetails('team-1', 'driver-1')).resolves.toBeNull(); }); it('getMembers returns DTO on success (filters missing drivers)', async () => { teamRepository.findById.mockResolvedValue(makeTeam()); membershipRepository.getTeamMembers.mockResolvedValue([ { teamId: 'team-1', driverId: 'd1', role: 'driver', status: 'active', joinedAt: new Date('2023-02-01T00:00:00.000Z'), }, { teamId: 'team-1', driverId: 'd2', role: 'owner', status: 'active', joinedAt: new Date('2023-02-02T00:00:00.000Z'), }, ]); driverRepository.findById.mockImplementation(async (id: string) => { if (id === 'd1') return { id: 'd1', name: makeValueObject('Driver One') }; return null; }); await expect(service.getMembers('team-1')).resolves.toEqual({ members: [ { driverId: 'd1', driverName: 'Driver One', role: 'member', joinedAt: '2023-02-01T00:00:00.000Z', isActive: true, avatarUrl: '', }, ], totalCount: 1, ownerCount: 1, managerCount: 0, memberCount: 1, }); }); it('getMembers returns empty DTO on error', async () => { teamRepository.findById.mockResolvedValue(null); await expect(service.getMembers('team-1')).resolves.toEqual({ members: [], totalCount: 0, ownerCount: 0, managerCount: 0, memberCount: 0, }); }); it('getMembers returns empty DTO when use case error message is empty (covers fallback)', async () => { const executeSpy = vi .spyOn(GetTeamMembersUseCase.prototype, 'execute') .mockResolvedValueOnce(Result.err({ code: 'REPOSITORY_ERROR', details: { message: '' } })); await expect(service.getMembers('team-1')).resolves.toEqual({ members: [], totalCount: 0, ownerCount: 0, managerCount: 0, memberCount: 0, }); executeSpy.mockRestore(); }); it('getJoinRequests returns DTO on success', async () => { teamRepository.findById.mockResolvedValue(makeTeam()); membershipRepository.getJoinRequests.mockResolvedValue([ { id: 'jr1', teamId: 'team-1', driverId: 'd1', requestedAt: new Date('2023-02-03T00:00:00.000Z'), }, ]); driverRepository.findById.mockResolvedValue({ id: 'd1', name: makeValueObject('Driver One') }); await expect(service.getJoinRequests('team-1')).resolves.toEqual({ requests: [ { requestId: 'jr1', driverId: 'd1', driverName: 'Driver One', teamId: 'team-1', status: 'pending', requestedAt: '2023-02-03T00:00:00.000Z', avatarUrl: '', }, ], pendingCount: 1, totalCount: 1, }); }); it('getJoinRequests returns empty DTO on error', async () => { teamRepository.findById.mockResolvedValue(null); await expect(service.getJoinRequests('team-1')).resolves.toEqual({ requests: [], pendingCount: 0, totalCount: 0, }); }); it('getJoinRequests returns empty DTO when use case error message is empty (covers fallback)', async () => { const executeSpy = vi .spyOn(GetTeamJoinRequestsUseCase.prototype, 'execute') .mockResolvedValueOnce(Result.err({ code: 'REPOSITORY_ERROR', details: { message: '' } })); await expect(service.getJoinRequests('team-1')).resolves.toEqual({ requests: [], pendingCount: 0, totalCount: 0, }); executeSpy.mockRestore(); }); it('create returns success on success', async () => { membershipRepository.getActiveMembershipForDriver.mockResolvedValue(null); teamRepository.create.mockImplementation(async (team: unknown) => team); const input: CreateTeamInputDTO = { name: 'N', tag: 'T', description: 'D' }; await expect(service.create(input, 'owner-1')).resolves.toEqual({ id: expect.any(String), success: true, }); expect(membershipRepository.saveMembership).toHaveBeenCalledTimes(1); }); it('create returns failure DTO on error', async () => { membershipRepository.getActiveMembershipForDriver.mockResolvedValue({ teamId: 'team-1' }); const input: CreateTeamInputDTO = { name: 'N', tag: 'T' }; await expect(service.create(input, 'owner-1')).resolves.toEqual({ id: '', success: false, }); }); it('create uses empty description and ownerId when missing', async () => { const executeSpy = vi .spyOn(CreateTeamUseCase.prototype, 'execute') .mockImplementationOnce(async (command) => { expect(command).toMatchObject({ description: '', ownerId: '' }); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: '' } }); }); const input: CreateTeamInputDTO = { name: 'N', tag: 'T' }; await expect(service.create(input)).resolves.toEqual({ id: '', success: false }); executeSpy.mockRestore(); }); it('update returns success on success', async () => { membershipRepository.getMembership.mockResolvedValue({ role: 'owner' }); teamRepository.findById.mockResolvedValue(makeTeam()); teamRepository.update.mockResolvedValue(undefined); const input: UpdateTeamInputDTO = { name: 'New Name' }; await expect(service.update('team-1', input, 'owner-1')).resolves.toEqual({ success: true }); expect(teamRepository.update).toHaveBeenCalledTimes(1); }); it('update returns failure DTO on error', async () => { membershipRepository.getMembership.mockResolvedValue(null); const input: UpdateTeamInputDTO = { name: 'New Name' }; await expect(service.update('team-1', input, 'owner-1')).resolves.toEqual({ success: false }); }); it('update uses empty updatedBy when userId missing', async () => { const executeSpy = vi .spyOn(UpdateTeamUseCase.prototype, 'execute') .mockImplementationOnce(async (command) => { expect(command.updatedBy).toBe(''); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: '' } }); }); const input: UpdateTeamInputDTO = { name: 'New Name' }; await expect(service.update('team-1', input)).resolves.toEqual({ success: false }); executeSpy.mockRestore(); }); it('update includes name when provided', async () => { const executeSpy = vi .spyOn(UpdateTeamUseCase.prototype, 'execute') .mockImplementationOnce(async (command) => { expect(command.updates).toEqual({ name: 'New Name' }); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: '' } }); }); const input: UpdateTeamInputDTO = { name: 'New Name' }; await expect(service.update('team-1', input, 'owner-1')).resolves.toEqual({ success: false }); executeSpy.mockRestore(); }); it('update includes tag when provided', async () => { const executeSpy = vi .spyOn(UpdateTeamUseCase.prototype, 'execute') .mockImplementationOnce(async (command) => { expect(command.updates).toEqual({ tag: 'NEW' }); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: '' } }); }); const input: UpdateTeamInputDTO = { tag: 'NEW' }; await expect(service.update('team-1', input, 'owner-1')).resolves.toEqual({ success: false }); executeSpy.mockRestore(); }); it('update includes description when provided', async () => { const executeSpy = vi .spyOn(UpdateTeamUseCase.prototype, 'execute') .mockImplementationOnce(async (command) => { expect(command.updates).toEqual({ description: 'D' }); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: '' } }); }); const input: UpdateTeamInputDTO = { description: 'D' }; await expect(service.update('team-1', input, 'owner-1')).resolves.toEqual({ success: false }); executeSpy.mockRestore(); }); it('update includes no updates when no fields are provided', async () => { const executeSpy = vi .spyOn(UpdateTeamUseCase.prototype, 'execute') .mockImplementationOnce(async (command) => { expect(command.updates).toEqual({}); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: '' } }); }); const input: UpdateTeamInputDTO = {}; await expect(service.update('team-1', input, 'owner-1')).resolves.toEqual({ success: false }); executeSpy.mockRestore(); }); it('getDriverTeam returns driver team DTO on success', async () => { membershipRepository.getActiveMembershipForDriver.mockResolvedValue({ teamId: 'team-1', role: 'driver', status: 'active', joinedAt: new Date('2023-02-01T00:00:00.000Z'), }); teamRepository.findById.mockResolvedValue(makeTeam()); await expect(service.getDriverTeam('driver-1')).resolves.toEqual({ team: { id: 'team-1', name: 'Team One', tag: 'T1', description: 'Desc', ownerId: 'owner-1', leagues: ['league-1'], createdAt: '2023-01-01T00:00:00.000Z', }, membership: { role: 'member', joinedAt: '2023-02-01T00:00:00.000Z', isActive: true, }, isOwner: false, canManage: false, }); }); it('getDriverTeam returns null when membership is missing', async () => { membershipRepository.getActiveMembershipForDriver.mockResolvedValue(null); await expect(service.getDriverTeam('driver-1')).resolves.toBeNull(); expect(teamRepository.findById).not.toHaveBeenCalled(); }); it('getDriverTeam returns null when use case error message is empty (covers fallback)', async () => { const executeSpy = vi .spyOn(GetDriverTeamUseCase.prototype, 'execute') .mockResolvedValueOnce(Result.err({ code: 'REPOSITORY_ERROR', details: { message: '' } })); await expect(service.getDriverTeam('driver-1')).resolves.toBeNull(); executeSpy.mockRestore(); }); it('getMembership returns presenter model on success', async () => { membershipRepository.getMembership.mockResolvedValue({ teamId: 'team-1', driverId: 'd1', role: 'driver', status: 'active', joinedAt: new Date('2023-02-01T00:00:00.000Z'), }); await expect(service.getMembership('team-1', 'd1')).resolves.toEqual({ role: 'member', joinedAt: '2023-02-01T00:00:00.000Z', isActive: true, }); }); it('getMembership returns null when missing', async () => { membershipRepository.getMembership.mockResolvedValue(null); await expect(service.getMembership('team-1', 'd1')).resolves.toBeNull(); }); it('getMembership returns null when use case error message is empty (covers fallback)', async () => { const executeSpy = vi .spyOn(GetTeamMembershipUseCase.prototype, 'execute') .mockResolvedValueOnce(Result.err({ code: 'REPOSITORY_ERROR', details: { message: '' } })); await expect(service.getMembership('team-1', 'd1')).resolves.toBeNull(); executeSpy.mockRestore(); }); });