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
426 lines
14 KiB
TypeScript
426 lines
14 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|