✅ FASE 7 COMPLETADA: Testing y Lanzamiento - PROYECTO FINALIZADO
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
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
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
This commit is contained in:
423
backend/tests/unit/services/court.service.test.ts
Normal file
423
backend/tests/unit/services/court.service.test.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user