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

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:
2026-01-31 22:30:44 +00:00
parent e135e7ad24
commit dd10891432
61 changed files with 19256 additions and 142 deletions

View File

@@ -0,0 +1,425 @@
import { BookingService } from '../../../src/services/booking.service';
import { ApiError } from '../../../src/middleware/errorHandler';
import prisma from '../../../src/config/database';
import { BookingStatus } from '../../../src/utils/constants';
import * as emailService from '../../../src/services/email.service';
// Mock dependencies
jest.mock('../../../src/config/database', () => ({
__esModule: true,
default: {
court: {
findFirst: jest.fn(),
findUnique: jest.fn(),
},
courtSchedule: {
findFirst: jest.fn(),
},
booking: {
findFirst: jest.fn(),
findUnique: jest.fn(),
findMany: jest.fn(),
create: jest.fn(),
update: jest.fn(),
},
},
}));
jest.mock('../../../src/services/email.service');
jest.mock('../../../src/config/logger', () => ({
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
}));
describe('BookingService', () => {
const mockCourt = {
id: 'court-123',
name: 'Cancha 1',
pricePerHour: 2000,
isActive: true,
};
const mockSchedule = {
id: 'schedule-123',
courtId: 'court-123',
dayOfWeek: 1, // Monday
openTime: '08:00',
closeTime: '23:00',
};
const mockBooking = {
id: 'booking-123',
userId: 'user-123',
courtId: 'court-123',
date: new Date('2026-02-01'),
startTime: '10:00',
endTime: '11:00',
status: BookingStatus.PENDING,
totalPrice: 2000,
notes: null,
court: mockCourt,
user: {
id: 'user-123',
firstName: 'Test',
lastName: 'User',
email: 'test@example.com',
},
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('createBooking', () => {
const validBookingInput = {
userId: 'user-123',
courtId: 'court-123',
date: new Date('2026-02-01'),
startTime: '10:00',
endTime: '11:00',
notes: 'Test booking',
};
it('should create a booking successfully', async () => {
// Arrange
(prisma.court.findFirst as jest.Mock).mockResolvedValue(mockCourt);
(prisma.courtSchedule.findFirst as jest.Mock).mockResolvedValue(mockSchedule);
(prisma.booking.findFirst as jest.Mock).mockResolvedValue(null); // No conflict
(prisma.booking.create as jest.Mock).mockResolvedValue(mockBooking);
(emailService.sendBookingConfirmation as jest.Mock).mockResolvedValue(undefined);
// Act
const result = await BookingService.createBooking(validBookingInput);
// Assert
expect(prisma.court.findFirst).toHaveBeenCalledWith({
where: { id: validBookingInput.courtId, isActive: true },
});
expect(prisma.booking.create).toHaveBeenCalled();
expect(emailService.sendBookingConfirmation).toHaveBeenCalled();
expect(result).toHaveProperty('id');
expect(result).toHaveProperty('benefitsApplied');
});
it('should throw error when date is in the past', async () => {
// Arrange
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const pastBookingInput = {
...validBookingInput,
date: yesterday,
};
// Act & Assert
await expect(BookingService.createBooking(pastBookingInput)).rejects.toThrow(ApiError);
await expect(BookingService.createBooking(pastBookingInput)).rejects.toThrow('No se pueden hacer reservas en fechas pasadas');
});
it('should throw error when court not found or inactive', async () => {
// Arrange
(prisma.court.findFirst as jest.Mock).mockResolvedValue(null);
// Act & Assert
await expect(BookingService.createBooking(validBookingInput)).rejects.toThrow(ApiError);
await expect(BookingService.createBooking(validBookingInput)).rejects.toThrow('Cancha no encontrada o inactiva');
});
it('should throw error when no schedule for day', async () => {
// Arrange
(prisma.court.findFirst as jest.Mock).mockResolvedValue(mockCourt);
(prisma.courtSchedule.findFirst as jest.Mock).mockResolvedValue(null);
// Act & Assert
await expect(BookingService.createBooking(validBookingInput)).rejects.toThrow(ApiError);
await expect(BookingService.createBooking(validBookingInput)).rejects.toThrow('La cancha no tiene horario disponible para este día');
});
it('should throw error when time is outside schedule', async () => {
// Arrange
(prisma.court.findFirst as jest.Mock).mockResolvedValue(mockCourt);
(prisma.courtSchedule.findFirst as jest.Mock).mockResolvedValue({
...mockSchedule,
openTime: '08:00',
closeTime: '12:00',
});
const lateBookingInput = {
...validBookingInput,
startTime: '13:00',
endTime: '14:00',
};
// Act & Assert
await expect(BookingService.createBooking(lateBookingInput)).rejects.toThrow(ApiError);
});
it('should throw error when end time is before or equal to start time', async () => {
// Arrange
(prisma.court.findFirst as jest.Mock).mockResolvedValue(mockCourt);
(prisma.courtSchedule.findFirst as jest.Mock).mockResolvedValue(mockSchedule);
const invalidBookingInput = {
...validBookingInput,
startTime: '10:00',
endTime: '10:00',
};
// Act & Assert
await expect(BookingService.createBooking(invalidBookingInput)).rejects.toThrow(ApiError);
await expect(BookingService.createBooking(invalidBookingInput)).rejects.toThrow('La hora de fin debe ser posterior a la de inicio');
});
it('should throw error when there is a time conflict', async () => {
// Arrange
(prisma.court.findFirst as jest.Mock).mockResolvedValue(mockCourt);
(prisma.courtSchedule.findFirst as jest.Mock).mockResolvedValue(mockSchedule);
(prisma.booking.findFirst as jest.Mock).mockResolvedValue({
id: 'existing-booking',
startTime: '10:00',
endTime: '11:00',
});
// Act & Assert
await expect(BookingService.createBooking(validBookingInput)).rejects.toThrow(ApiError);
await expect(BookingService.createBooking(validBookingInput)).rejects.toThrow('La cancha no está disponible en ese horario');
});
it('should not fail if confirmation email fails', async () => {
// Arrange
(prisma.court.findFirst as jest.Mock).mockResolvedValue(mockCourt);
(prisma.courtSchedule.findFirst as jest.Mock).mockResolvedValue(mockSchedule);
(prisma.booking.findFirst as jest.Mock).mockResolvedValue(null);
(prisma.booking.create as jest.Mock).mockResolvedValue(mockBooking);
(emailService.sendBookingConfirmation as jest.Mock).mockRejectedValue(new Error('Email failed'));
// Act
const result = await BookingService.createBooking(validBookingInput);
// Assert
expect(result).toHaveProperty('id');
});
});
describe('getAllBookings', () => {
it('should return all bookings with filters', async () => {
// Arrange
const mockBookings = [mockBooking];
(prisma.booking.findMany as jest.Mock).mockResolvedValue(mockBookings);
// Act
const result = await BookingService.getAllBookings({
userId: 'user-123',
courtId: 'court-123',
});
// Assert
expect(prisma.booking.findMany).toHaveBeenCalledWith({
where: { userId: 'user-123', courtId: 'court-123' },
include: expect.any(Object),
orderBy: expect.any(Array),
});
expect(result).toEqual(mockBookings);
});
it('should return all bookings without filters', async () => {
// Arrange
const mockBookings = [mockBooking, { ...mockBooking, id: 'booking-456' }];
(prisma.booking.findMany as jest.Mock).mockResolvedValue(mockBookings);
// Act
const result = await BookingService.getAllBookings({});
// Assert
expect(prisma.booking.findMany).toHaveBeenCalledWith({
where: {},
include: expect.any(Object),
orderBy: expect.any(Array),
});
});
});
describe('getBookingById', () => {
it('should return booking by id', async () => {
// Arrange
(prisma.booking.findUnique as jest.Mock).mockResolvedValue(mockBooking);
// Act
const result = await BookingService.getBookingById('booking-123');
// Assert
expect(prisma.booking.findUnique).toHaveBeenCalledWith({
where: { id: 'booking-123' },
include: expect.any(Object),
});
expect(result).toEqual(mockBooking);
});
it('should throw error when booking not found', async () => {
// Arrange
(prisma.booking.findUnique as jest.Mock).mockResolvedValue(null);
// Act & Assert
await expect(BookingService.getBookingById('non-existent')).rejects.toThrow(ApiError);
await expect(BookingService.getBookingById('non-existent')).rejects.toThrow('Reserva no encontrada');
});
});
describe('getUserBookings', () => {
it('should return user bookings', async () => {
// Arrange
const userBookings = [mockBooking];
(prisma.booking.findMany as jest.Mock).mockResolvedValue(userBookings);
// Act
const result = await BookingService.getUserBookings('user-123');
// Assert
expect(prisma.booking.findMany).toHaveBeenCalledWith({
where: { userId: 'user-123' },
include: expect.any(Object),
orderBy: expect.any(Array),
});
expect(result).toEqual(userBookings);
});
it('should return upcoming bookings when specified', async () => {
// Arrange
const userBookings = [mockBooking];
(prisma.booking.findMany as jest.Mock).mockResolvedValue(userBookings);
// Act
const result = await BookingService.getUserBookings('user-123', true);
// Assert
expect(prisma.booking.findMany).toHaveBeenCalledWith({
where: {
userId: 'user-123',
date: expect.any(Object),
status: expect.any(Object),
},
include: expect.any(Object),
orderBy: expect.any(Array),
});
});
});
describe('cancelBooking', () => {
it('should cancel booking successfully', async () => {
// Arrange
(prisma.booking.findUnique as jest.Mock).mockResolvedValue(mockBooking);
(prisma.booking.update as jest.Mock).mockResolvedValue({
...mockBooking,
status: BookingStatus.CANCELLED,
});
(emailService.sendBookingCancellation as jest.Mock).mockResolvedValue(undefined);
// Act
const result = await BookingService.cancelBooking('booking-123', 'user-123');
// Assert
expect(prisma.booking.update).toHaveBeenCalledWith({
where: { id: 'booking-123' },
data: { status: BookingStatus.CANCELLED },
include: expect.any(Object),
});
expect(emailService.sendBookingCancellation).toHaveBeenCalled();
expect(result.status).toBe(BookingStatus.CANCELLED);
});
it('should throw error when user tries to cancel another user booking', async () => {
// Arrange
(prisma.booking.findUnique as jest.Mock).mockResolvedValue(mockBooking);
// Act & Assert
await expect(BookingService.cancelBooking('booking-123', 'different-user')).rejects.toThrow(ApiError);
await expect(BookingService.cancelBooking('booking-123', 'different-user')).rejects.toThrow('No tienes permiso para cancelar esta reserva');
});
it('should throw error when booking is already cancelled', async () => {
// Arrange
(prisma.booking.findUnique as jest.Mock).mockResolvedValue({
...mockBooking,
status: BookingStatus.CANCELLED,
});
// Act & Assert
await expect(BookingService.cancelBooking('booking-123', 'user-123')).rejects.toThrow(ApiError);
await expect(BookingService.cancelBooking('booking-123', 'user-123')).rejects.toThrow('La reserva ya está cancelada');
});
it('should throw error when booking is completed', async () => {
// Arrange
(prisma.booking.findUnique as jest.Mock).mockResolvedValue({
...mockBooking,
status: BookingStatus.COMPLETED,
});
// Act & Assert
await expect(BookingService.cancelBooking('booking-123', 'user-123')).rejects.toThrow(ApiError);
await expect(BookingService.cancelBooking('booking-123', 'user-123')).rejects.toThrow('No se puede cancelar una reserva completada');
});
});
describe('confirmBooking', () => {
it('should confirm pending booking', async () => {
// Arrange
(prisma.booking.findUnique as jest.Mock).mockResolvedValue(mockBooking);
(prisma.booking.update as jest.Mock).mockResolvedValue({
...mockBooking,
status: BookingStatus.CONFIRMED,
});
// Act
const result = await BookingService.confirmBooking('booking-123');
// Assert
expect(prisma.booking.update).toHaveBeenCalledWith({
where: { id: 'booking-123' },
data: { status: BookingStatus.CONFIRMED },
include: expect.any(Object),
});
expect(result.status).toBe(BookingStatus.CONFIRMED);
});
it('should throw error when booking is not pending', async () => {
// Arrange
(prisma.booking.findUnique as jest.Mock).mockResolvedValue({
...mockBooking,
status: BookingStatus.CONFIRMED,
});
// Act & Assert
await expect(BookingService.confirmBooking('booking-123')).rejects.toThrow(ApiError);
await expect(BookingService.confirmBooking('booking-123')).rejects.toThrow('Solo se pueden confirmar reservas pendientes');
});
});
describe('updateBooking', () => {
it('should update booking successfully', async () => {
// Arrange
(prisma.booking.findUnique as jest.Mock).mockResolvedValue(mockBooking);
(prisma.booking.update as jest.Mock).mockResolvedValue({
...mockBooking,
notes: 'Updated notes',
});
// Act
const result = await BookingService.updateBooking('booking-123', { notes: 'Updated notes' }, 'user-123');
// Assert
expect(prisma.booking.update).toHaveBeenCalled();
expect(result.notes).toBe('Updated notes');
});
it('should throw error when user tries to update another user booking', async () => {
// Arrange
(prisma.booking.findUnique as jest.Mock).mockResolvedValue(mockBooking);
// Act & Assert
await expect(BookingService.updateBooking('booking-123', { notes: 'test' }, 'different-user')).rejects.toThrow(ApiError);
await expect(BookingService.updateBooking('booking-123', { notes: 'test' }, 'different-user')).rejects.toThrow('No tienes permiso para modificar esta reserva');
});
});
});