Files
app-padel/backend/tests/unit/services/court.service.test.ts
Ivan Alcaraz dd10891432
Some checks failed
CI/CD Pipeline / 🧪 Tests (push) Has been cancelled
CI/CD Pipeline / 🏗️ Build (push) Has been cancelled
CI/CD Pipeline / 🚀 Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / 🚀 Deploy to Production (push) Has been cancelled
CI/CD Pipeline / 🏷️ Create Release (push) Has been cancelled
CI/CD Pipeline / 🧹 Cleanup (push) Has been cancelled
FASE 7 COMPLETADA: Testing y Lanzamiento - PROYECTO FINALIZADO
Implementados 4 módulos con agent swarm:

1. TESTING FUNCIONAL (Jest)
   - Configuración Jest + ts-jest
   - Tests unitarios: auth, booking, court (55 tests)
   - Tests integración: routes (56 tests)
   - Factories y utilidades de testing
   - Coverage configurado (70% servicios)
   - Scripts: test, test:watch, test:coverage

2. TESTING DE USUARIO (Beta)
   - Sistema de beta testers
   - Feedback con categorías y severidad
   - Beta issues tracking
   - 8 testers de prueba creados
   - API completa para gestión de feedback

3. DOCUMENTACIÓN COMPLETA
   - API.md - 150+ endpoints documentados
   - SETUP.md - Guía de instalación
   - DEPLOY.md - Deploy en VPS
   - ARCHITECTURE.md - Arquitectura del sistema
   - APP_STORE.md - Material para stores
   - Postman Collection completa
   - PM2 ecosystem config
   - Nginx config con SSL

4. GO LIVE Y PRODUCCIÓN
   - Sistema de monitoreo (logs, health checks)
   - Servicio de alertas multi-canal
   - Pre-deploy check script
   - Docker + docker-compose producción
   - Backup automatizado
   - CI/CD GitHub Actions
   - Launch checklist completo

ESTADÍSTICAS FINALES:
- Fases completadas: 7/7
- Archivos creados: 250+
- Líneas de código: 60,000+
- Endpoints API: 150+
- Tests: 110+
- Documentación: 5,000+ líneas

PROYECTO COMPLETO Y LISTO PARA PRODUCCIÓN
2026-01-31 22:30:44 +00:00

424 lines
14 KiB
TypeScript

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);
});
});
});