import { CourtService } from '../../../src/services/court.service'; import { ApiError } from '../../../src/middleware/errorHandler'; import prisma from '../../../src/config/database'; import { CourtType } from '../../../src/utils/constants'; // Mock dependencies jest.mock('../../../src/config/database', () => ({ __esModule: true, default: { court: { findMany: jest.fn(), findUnique: jest.fn(), findFirst: jest.fn(), create: jest.fn(), update: jest.fn(), }, courtSchedule: { findFirst: jest.fn(), createMany: jest.fn(), }, booking: { findMany: jest.fn(), }, }, })); describe('CourtService', () => { const mockCourt = { id: 'court-123', name: 'Cancha Principal', description: 'Cancha panorámica profesional', type: CourtType.PANORAMIC, isIndoor: false, hasLighting: true, hasParking: true, pricePerHour: 2500, imageUrl: 'https://example.com/court.jpg', isActive: true, createdAt: new Date(), updatedAt: new Date(), schedules: [ { id: 'schedule-1', courtId: 'court-123', dayOfWeek: 1, openTime: '08:00', closeTime: '23:00' }, ], _count: { bookings: 5 }, }; beforeEach(() => { jest.resetAllMocks(); }); describe('getAllCourts', () => { it('should return all active courts by default', async () => { // Arrange const mockCourts = [mockCourt]; (prisma.court.findMany as jest.Mock).mockResolvedValue(mockCourts); // Act const result = await CourtService.getAllCourts(); // Assert expect(prisma.court.findMany).toHaveBeenCalledWith({ where: { isActive: true }, include: { schedules: true, _count: { select: { bookings: { where: { status: { in: ['PENDING', 'CONFIRMED'] }, }, }, }, }, }, orderBy: { name: 'asc' }, }); expect(result).toEqual(mockCourts); }); it('should return all courts including inactive when specified', async () => { // Arrange const mockCourts = [mockCourt, { ...mockCourt, id: 'court-456', isActive: false }]; (prisma.court.findMany as jest.Mock).mockResolvedValue(mockCourts); // Act const result = await CourtService.getAllCourts(true); // Assert expect(prisma.court.findMany).toHaveBeenCalledWith({ where: {}, include: expect.any(Object), orderBy: { name: 'asc' }, }); expect(result).toEqual(mockCourts); }); }); describe('getCourtById', () => { it('should return court by id', async () => { // Arrange (prisma.court.findUnique as jest.Mock).mockResolvedValue(mockCourt); // Act const result = await CourtService.getCourtById('court-123'); // Assert expect(prisma.court.findUnique).toHaveBeenCalledWith({ where: { id: 'court-123' }, include: { schedules: true, _count: { select: { bookings: { where: { status: { in: ['PENDING', 'CONFIRMED'] }, }, }, }, }, }, }); expect(result).toEqual(mockCourt); }); it('should throw error when court not found', async () => { // Arrange (prisma.court.findUnique as jest.Mock).mockResolvedValue(null); // Act & Assert await expect(CourtService.getCourtById('non-existent')).rejects.toThrow(ApiError); await expect(CourtService.getCourtById('non-existent')).rejects.toThrow('Cancha no encontrada'); }); }); describe('createCourt', () => { const validCourtInput = { name: 'Nueva Cancha', description: 'Descripción de prueba', type: CourtType.INDOOR, isIndoor: true, hasLighting: true, hasParking: false, pricePerHour: 3000, imageUrl: 'https://example.com/new-court.jpg', }; it('should create a new court successfully', async () => { // Arrange (prisma.court.findUnique as jest.Mock).mockResolvedValue(null); (prisma.court.create as jest.Mock).mockResolvedValue(mockCourt); (prisma.courtSchedule.createMany as jest.Mock).mockResolvedValue({ count: 7 }); // Mock getCourtById call at the end of createCourt const mockGetById = { ...mockCourt, schedules: [] }; (prisma.court.findUnique as jest.Mock) .mockResolvedValueOnce(null) // First call for name check .mockResolvedValueOnce(mockGetById); // Second call for getCourtById // Act const result = await CourtService.createCourt(validCourtInput); // Assert expect(prisma.court.findUnique).toHaveBeenCalledWith({ where: { name: validCourtInput.name }, }); expect(prisma.court.create).toHaveBeenCalledWith({ data: { name: validCourtInput.name, description: validCourtInput.description, type: validCourtInput.type, isIndoor: validCourtInput.isIndoor, hasLighting: validCourtInput.hasLighting, hasParking: validCourtInput.hasParking, pricePerHour: validCourtInput.pricePerHour, imageUrl: validCourtInput.imageUrl, }, include: { schedules: true }, }); expect(prisma.courtSchedule.createMany).toHaveBeenCalled(); }); it('should use default values when optional fields not provided', async () => { // Arrange const minimalInput = { name: 'Cancha Mínima' }; const createdCourt = { ...mockCourt, name: minimalInput.name, type: CourtType.PANORAMIC, isIndoor: false, hasLighting: true, hasParking: false, pricePerHour: 2000, }; (prisma.court.findUnique as jest.Mock) .mockResolvedValueOnce(null) // First call for name check .mockResolvedValueOnce({ ...createdCourt, schedules: [] }); // Second call for getCourtById (prisma.court.create as jest.Mock).mockResolvedValue(createdCourt); (prisma.courtSchedule.createMany as jest.Mock).mockResolvedValue({ count: 7 }); // Act await CourtService.createCourt(minimalInput); // Assert expect(prisma.court.create).toHaveBeenCalledWith({ data: { name: minimalInput.name, description: undefined, type: CourtType.PANORAMIC, isIndoor: false, hasLighting: true, hasParking: false, pricePerHour: 2000, imageUrl: undefined, }, include: { schedules: true }, }); }); it('should throw error when court name already exists', async () => { // Arrange (prisma.court.findUnique as jest.Mock).mockResolvedValue(mockCourt); // Act & Assert await expect(CourtService.createCourt(validCourtInput)).rejects.toThrow(ApiError); await expect(CourtService.createCourt(validCourtInput)).rejects.toThrow('Ya existe una cancha con ese nombre'); expect(prisma.court.create).not.toHaveBeenCalled(); }); }); describe('updateCourt', () => { const updateData = { name: 'Cancha Actualizada', pricePerHour: 3500, }; it('should update court successfully', async () => { // Arrange (prisma.court.findUnique as jest.Mock) .mockResolvedValueOnce(mockCourt) // First call for existence check .mockResolvedValueOnce(mockCourt); // Second call for name check (prisma.court.findFirst as jest.Mock).mockResolvedValue(null); (prisma.court.update as jest.Mock).mockResolvedValue({ ...mockCourt, ...updateData, }); // Act const result = await CourtService.updateCourt('court-123', updateData); // Assert expect(prisma.court.update).toHaveBeenCalledWith({ where: { id: 'court-123' }, data: updateData, include: { schedules: true }, }); expect(result.name).toBe(updateData.name); expect(result.pricePerHour).toBe(updateData.pricePerHour); }); it('should throw error when court not found', async () => { // Arrange (prisma.court.findUnique as jest.Mock).mockResolvedValue(null); // Act & Assert await expect(CourtService.updateCourt('non-existent-id-123', updateData)).rejects.toThrow(ApiError); await expect(CourtService.updateCourt('non-existent-id-123', updateData)).rejects.toThrow('Cancha no encontrada'); }); it('should throw error when new name conflicts with existing court', async () => { // Arrange (prisma.court.findUnique as jest.Mock) .mockResolvedValueOnce(mockCourt) // First call for existence check .mockResolvedValueOnce(mockCourt); // Second call for name check (prisma.court.findFirst as jest.Mock).mockResolvedValue({ id: 'other-court', name: 'Cancha Actualizada' }); // Act & Assert await expect(CourtService.updateCourt('court-123', updateData)).rejects.toThrow(ApiError); await expect(CourtService.updateCourt('court-123', updateData)).rejects.toThrow('Ya existe otra cancha con ese nombre'); }); it('should allow keeping the same name', async () => { // Arrange const sameNameUpdate = { name: 'Cancha Principal', pricePerHour: 3500 }; (prisma.court.findUnique as jest.Mock) .mockResolvedValueOnce(mockCourt) // First call for existence check .mockResolvedValueOnce(mockCourt); // Second call for name check (returns same court with same name) (prisma.court.findFirst as jest.Mock).mockResolvedValue(null); (prisma.court.update as jest.Mock).mockResolvedValue({ ...mockCourt, ...sameNameUpdate, }); // Act const result = await CourtService.updateCourt('court-123', sameNameUpdate); // Assert expect(result.pricePerHour).toBe(3500); }); }); describe('deleteCourt', () => { it('should deactivate court successfully', async () => { // Arrange (prisma.court.findUnique as jest.Mock).mockResolvedValue(mockCourt); (prisma.court.update as jest.Mock).mockResolvedValue({ ...mockCourt, isActive: false, }); // Act const result = await CourtService.deleteCourt('court-123'); // Assert expect(prisma.court.update).toHaveBeenCalledWith({ where: { id: 'court-123' }, data: { isActive: false }, }); expect(result.isActive).toBe(false); }); it('should throw error when court not found', async () => { // Arrange (prisma.court.findUnique as jest.Mock).mockResolvedValue(null); // Act & Assert await expect(CourtService.deleteCourt('non-existent')).rejects.toThrow(ApiError); await expect(CourtService.deleteCourt('non-existent')).rejects.toThrow('Cancha no encontrada'); }); }); describe('getAvailability', () => { const testDate = new Date('2026-02-02'); // Monday it('should return availability for a court on a specific date', async () => { // Arrange const mockBookings = [ { startTime: '10:00', endTime: '11:00' }, { startTime: '14:00', endTime: '15:00' }, ]; (prisma.court.findUnique as jest.Mock).mockResolvedValue(mockCourt); (prisma.booking.findMany as jest.Mock).mockResolvedValue(mockBookings); // Act const result = await CourtService.getAvailability('court-123', testDate); // Assert expect(prisma.court.findUnique).toHaveBeenCalledWith({ where: { id: 'court-123' }, include: { schedules: true, _count: { select: { bookings: { where: { status: { in: ['PENDING', 'CONFIRMED'] }, }, }, }, }, }, }); expect(prisma.booking.findMany).toHaveBeenCalledWith({ where: { courtId: 'court-123', date: testDate, status: { in: ['PENDING', 'CONFIRMED'] }, }, select: { startTime: true, endTime: true }, }); expect(result).toHaveProperty('courtId', 'court-123'); expect(result).toHaveProperty('date', testDate); expect(result).toHaveProperty('openTime'); expect(result).toHaveProperty('closeTime'); expect(result).toHaveProperty('slots'); expect(Array.isArray(result.slots)).toBe(true); }); it('should return unavailable when no schedule for day', async () => { // Arrange const courtWithoutSchedule = { ...mockCourt, schedules: [], }; (prisma.court.findUnique as jest.Mock).mockResolvedValue(courtWithoutSchedule); // Act const result = await CourtService.getAvailability('court-123', testDate); // Assert expect(result).toHaveProperty('available', false); expect(result).toHaveProperty('reason', 'La cancha no tiene horario para este día'); }); it('should mark booked slots as unavailable', async () => { // Arrange const mockBookings = [{ startTime: '10:00', endTime: '11:00' }]; (prisma.court.findUnique as jest.Mock).mockResolvedValue(mockCourt); (prisma.booking.findMany as jest.Mock).mockResolvedValue(mockBookings); // Act const result = await CourtService.getAvailability('court-123', testDate); // Assert expect(result).toHaveProperty('slots'); const slots = (result as any).slots; const tenAmSlot = slots.find((s: any) => s.time === '10:00'); expect(tenAmSlot).toBeDefined(); expect(tenAmSlot!.available).toBe(false); const elevenAmSlot = slots.find((s: any) => s.time === '11:00'); expect(elevenAmSlot).toBeDefined(); expect(elevenAmSlot!.available).toBe(true); }); it('should throw error when court not found', async () => { // Arrange (prisma.court.findUnique as jest.Mock).mockResolvedValue(null); // Act & Assert await expect(CourtService.getAvailability('non-existent', testDate)).rejects.toThrow(ApiError); }); }); });