✅ 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:
304
backend/tests/integration/routes/auth.routes.test.ts
Normal file
304
backend/tests/integration/routes/auth.routes.test.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
import request from 'supertest';
|
||||
import app from '../../../src/app';
|
||||
import { setupTestDb, teardownTestDb, resetDatabase } from '../../utils/testDb';
|
||||
import { createUser } from '../../utils/factories';
|
||||
import { generateTokens } from '../../utils/auth';
|
||||
|
||||
describe('Auth Routes Integration Tests', () => {
|
||||
beforeAll(async () => {
|
||||
await setupTestDb();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await teardownTestDb();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await resetDatabase();
|
||||
});
|
||||
|
||||
describe('POST /api/v1/auth/register', () => {
|
||||
const validRegisterData = {
|
||||
email: 'test@example.com',
|
||||
password: 'Password123!',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
phone: '+1234567890',
|
||||
playerLevel: 'BEGINNER',
|
||||
handPreference: 'RIGHT',
|
||||
positionPreference: 'BOTH',
|
||||
};
|
||||
|
||||
it('should register a new user successfully', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/auth/register')
|
||||
.send(validRegisterData);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body.data).toHaveProperty('user');
|
||||
expect(response.body.data).toHaveProperty('accessToken');
|
||||
expect(response.body.data).toHaveProperty('refreshToken');
|
||||
expect(response.body.data.user).toHaveProperty('email', validRegisterData.email);
|
||||
expect(response.body.data.user).toHaveProperty('firstName', validRegisterData.firstName);
|
||||
expect(response.body.data.user).not.toHaveProperty('password');
|
||||
});
|
||||
|
||||
it('should return 409 when email already exists', async () => {
|
||||
// Arrange
|
||||
await createUser({ email: validRegisterData.email });
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/auth/register')
|
||||
.send(validRegisterData);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(409);
|
||||
expect(response.body).toHaveProperty('success', false);
|
||||
expect(response.body).toHaveProperty('message', 'El email ya está registrado');
|
||||
});
|
||||
|
||||
it('should return 400 when email is invalid', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/auth/register')
|
||||
.send({ ...validRegisterData, email: 'invalid-email' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty('success', false);
|
||||
});
|
||||
|
||||
it('should return 400 when password is too short', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/auth/register')
|
||||
.send({ ...validRegisterData, password: '123' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty('success', false);
|
||||
});
|
||||
|
||||
it('should return 400 when required fields are missing', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/auth/register')
|
||||
.send({ email: 'test@example.com', password: 'Password123!' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty('success', false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/auth/login', () => {
|
||||
const validLoginData = {
|
||||
email: 'login@example.com',
|
||||
password: 'Password123!',
|
||||
};
|
||||
|
||||
it('should login with valid credentials', async () => {
|
||||
// Arrange
|
||||
await createUser({
|
||||
email: validLoginData.email,
|
||||
password: validLoginData.password,
|
||||
firstName: 'Login',
|
||||
lastName: 'Test',
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/auth/login')
|
||||
.send(validLoginData);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body.data).toHaveProperty('user');
|
||||
expect(response.body.data).toHaveProperty('accessToken');
|
||||
expect(response.body.data).toHaveProperty('refreshToken');
|
||||
expect(response.body.data.user).toHaveProperty('email', validLoginData.email);
|
||||
expect(response.body.data.user).not.toHaveProperty('password');
|
||||
});
|
||||
|
||||
it('should return 401 with invalid password', async () => {
|
||||
// Arrange
|
||||
await createUser({
|
||||
email: validLoginData.email,
|
||||
password: validLoginData.password,
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/auth/login')
|
||||
.send({ ...validLoginData, password: 'WrongPassword123!' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body).toHaveProperty('success', false);
|
||||
expect(response.body).toHaveProperty('message', 'Email o contraseña incorrectos');
|
||||
});
|
||||
|
||||
it('should return 401 when user not found', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/auth/login')
|
||||
.send({ email: 'nonexistent@example.com', password: 'Password123!' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body).toHaveProperty('success', false);
|
||||
expect(response.body).toHaveProperty('message', 'Email o contraseña incorrectos');
|
||||
});
|
||||
|
||||
it('should return 401 when user is inactive', async () => {
|
||||
// Arrange
|
||||
await createUser({
|
||||
email: validLoginData.email,
|
||||
password: validLoginData.password,
|
||||
isActive: false,
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/auth/login')
|
||||
.send(validLoginData);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body).toHaveProperty('success', false);
|
||||
expect(response.body).toHaveProperty('message', 'Usuario desactivado');
|
||||
});
|
||||
|
||||
it('should return 400 with invalid email format', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/auth/login')
|
||||
.send({ email: 'invalid-email', password: 'Password123!' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty('success', false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/auth/me', () => {
|
||||
it('should return user profile when authenticated', async () => {
|
||||
// Arrange
|
||||
const user = await createUser({
|
||||
email: 'profile@example.com',
|
||||
firstName: 'Profile',
|
||||
lastName: 'User',
|
||||
});
|
||||
const { accessToken } = generateTokens({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get('/api/v1/auth/me')
|
||||
.set('Authorization', `Bearer ${accessToken}`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body.data).toHaveProperty('id', user.id);
|
||||
expect(response.body.data).toHaveProperty('email', user.email);
|
||||
expect(response.body.data).toHaveProperty('firstName', 'Profile');
|
||||
expect(response.body.data).not.toHaveProperty('password');
|
||||
});
|
||||
|
||||
it('should return 401 when no token provided', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get('/api/v1/auth/me');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body).toHaveProperty('success', false);
|
||||
expect(response.body).toHaveProperty('message', 'Token de autenticación no proporcionado');
|
||||
});
|
||||
|
||||
it('should return 401 with invalid token', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get('/api/v1/auth/me')
|
||||
.set('Authorization', 'Bearer invalid-token');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body).toHaveProperty('success', false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/auth/refresh', () => {
|
||||
it('should refresh access token with valid refresh token', async () => {
|
||||
// Arrange
|
||||
const user = await createUser({ email: 'refresh@example.com' });
|
||||
const { refreshToken } = generateTokens({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/auth/refresh')
|
||||
.send({ refreshToken });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body.data).toHaveProperty('accessToken');
|
||||
});
|
||||
|
||||
it('should return 400 when refresh token is missing', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/auth/refresh')
|
||||
.send({});
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty('success', false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/auth/logout', () => {
|
||||
it('should logout successfully', async () => {
|
||||
// Arrange
|
||||
const user = await createUser({ email: 'logout@example.com' });
|
||||
const { accessToken } = generateTokens({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/auth/logout')
|
||||
.set('Authorization', `Bearer ${accessToken}`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body).toHaveProperty('message', 'Logout exitoso');
|
||||
});
|
||||
|
||||
it('should allow logout without authentication', async () => {
|
||||
// Act - logout endpoint doesn't require authentication
|
||||
const response = await request(app)
|
||||
.post('/api/v1/auth/logout');
|
||||
|
||||
// Assert - logout is allowed without auth (just returns success)
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
});
|
||||
});
|
||||
});
|
||||
428
backend/tests/integration/routes/booking.routes.test.ts
Normal file
428
backend/tests/integration/routes/booking.routes.test.ts
Normal file
@@ -0,0 +1,428 @@
|
||||
import request from 'supertest';
|
||||
import app from '../../../src/app';
|
||||
import { setupTestDb, teardownTestDb, resetDatabase } from '../../utils/testDb';
|
||||
import { createUser, createCourtWithSchedules, createBooking } from '../../utils/factories';
|
||||
import { generateTokens } from '../../utils/auth';
|
||||
import { UserRole, BookingStatus } from '../../../src/utils/constants';
|
||||
|
||||
describe('Booking Routes Integration Tests', () => {
|
||||
let testUser: any;
|
||||
let testCourt: any;
|
||||
let userToken: string;
|
||||
let adminToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupTestDb();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await teardownTestDb();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await resetDatabase();
|
||||
|
||||
// Setup test data
|
||||
testUser = await createUser({
|
||||
email: 'bookinguser@example.com',
|
||||
firstName: 'Booking',
|
||||
lastName: 'User',
|
||||
});
|
||||
|
||||
testCourt = await createCourtWithSchedules({
|
||||
name: 'Test Court',
|
||||
pricePerHour: 2000,
|
||||
});
|
||||
|
||||
const tokens = generateTokens({
|
||||
userId: testUser.id,
|
||||
email: testUser.email,
|
||||
role: testUser.role,
|
||||
});
|
||||
userToken = tokens.accessToken;
|
||||
|
||||
const adminUser = await createUser({
|
||||
email: 'admin@example.com',
|
||||
role: UserRole.ADMIN,
|
||||
});
|
||||
const adminTokens = generateTokens({
|
||||
userId: adminUser.id,
|
||||
email: adminUser.email,
|
||||
role: adminUser.role,
|
||||
});
|
||||
adminToken = adminTokens.accessToken;
|
||||
});
|
||||
|
||||
describe('POST /api/v1/bookings', () => {
|
||||
const getTomorrowDate = () => {
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
return tomorrow.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
const getValidBookingData = () => ({
|
||||
courtId: testCourt.id,
|
||||
date: getTomorrowDate(),
|
||||
startTime: '10:00',
|
||||
endTime: '11:00',
|
||||
notes: 'Test booking notes',
|
||||
});
|
||||
|
||||
it('should create a booking successfully', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/bookings')
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.send(getValidBookingData());
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body.data).toHaveProperty('id');
|
||||
expect(response.body.data).toHaveProperty('courtId', testCourt.id);
|
||||
expect(response.body.data).toHaveProperty('userId', testUser.id);
|
||||
expect(response.body.data).toHaveProperty('status', BookingStatus.PENDING);
|
||||
expect(response.body.data).toHaveProperty('benefitsApplied');
|
||||
});
|
||||
|
||||
it('should return 401 when not authenticated', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/bookings')
|
||||
.send(getValidBookingData());
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
|
||||
it('should return 400 with invalid court ID', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/bookings')
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.send({ ...getValidBookingData(), courtId: 'invalid-uuid' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty('success', false);
|
||||
});
|
||||
|
||||
it('should return 400 with invalid date format', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/bookings')
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.send({ ...getValidBookingData(), date: 'invalid-date' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 400 with invalid time format', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/bookings')
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.send({ ...getValidBookingData(), startTime: '25:00' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 404 when court is not found', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/bookings')
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.send({ ...getValidBookingData(), courtId: '00000000-0000-0000-0000-000000000000' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toHaveProperty('message', 'Cancha no encontrada o inactiva');
|
||||
});
|
||||
|
||||
it('should return 409 when time slot is already booked', async () => {
|
||||
// Arrange - Create existing booking
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
await createBooking({
|
||||
userId: testUser.id,
|
||||
courtId: testCourt.id,
|
||||
date: tomorrow,
|
||||
startTime: '10:00',
|
||||
endTime: '11:00',
|
||||
status: BookingStatus.CONFIRMED,
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/bookings')
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.send(getValidBookingData());
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(409);
|
||||
expect(response.body).toHaveProperty('message', 'La cancha no está disponible en ese horario');
|
||||
});
|
||||
|
||||
it('should return 400 with past date', async () => {
|
||||
// Arrange
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/bookings')
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.send({
|
||||
...getValidBookingData(),
|
||||
date: yesterday.toISOString().split('T')[0],
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty('message', 'No se pueden hacer reservas en fechas pasadas');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/bookings/my-bookings', () => {
|
||||
it('should return user bookings', async () => {
|
||||
// Arrange
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
await createBooking({
|
||||
userId: testUser.id,
|
||||
courtId: testCourt.id,
|
||||
date: tomorrow,
|
||||
startTime: '10:00',
|
||||
endTime: '11:00',
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get('/api/v1/bookings/my-bookings')
|
||||
.set('Authorization', `Bearer ${userToken}`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body.data).toBeInstanceOf(Array);
|
||||
expect(response.body.data.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should return 401 when not authenticated', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get('/api/v1/bookings/my-bookings');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/bookings/:id', () => {
|
||||
it('should return booking by id', async () => {
|
||||
// Arrange
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
const booking = await createBooking({
|
||||
userId: testUser.id,
|
||||
courtId: testCourt.id,
|
||||
date: tomorrow,
|
||||
startTime: '10:00',
|
||||
endTime: '11:00',
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get(`/api/v1/bookings/${booking.id}`)
|
||||
.set('Authorization', `Bearer ${userToken}`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body.data).toHaveProperty('id', booking.id);
|
||||
expect(response.body.data).toHaveProperty('court');
|
||||
expect(response.body.data).toHaveProperty('user');
|
||||
});
|
||||
|
||||
it('should return 404 when booking not found', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get('/api/v1/bookings/00000000-0000-0000-0000-000000000000')
|
||||
.set('Authorization', `Bearer ${userToken}`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toHaveProperty('message', 'Reserva no encontrada');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /api/v1/bookings/:id', () => {
|
||||
it('should update booking notes', async () => {
|
||||
// Arrange
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
const booking = await createBooking({
|
||||
userId: testUser.id,
|
||||
courtId: testCourt.id,
|
||||
date: tomorrow,
|
||||
startTime: '10:00',
|
||||
endTime: '11:00',
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.put(`/api/v1/bookings/${booking.id}`)
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.send({ notes: 'Updated notes' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body.data).toHaveProperty('notes', 'Updated notes');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /api/v1/bookings/:id', () => {
|
||||
it('should cancel booking successfully', async () => {
|
||||
// Arrange
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
const booking = await createBooking({
|
||||
userId: testUser.id,
|
||||
courtId: testCourt.id,
|
||||
date: tomorrow,
|
||||
startTime: '10:00',
|
||||
endTime: '11:00',
|
||||
status: BookingStatus.PENDING,
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.delete(`/api/v1/bookings/${booking.id}`)
|
||||
.set('Authorization', `Bearer ${userToken}`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body.data).toHaveProperty('status', BookingStatus.CANCELLED);
|
||||
});
|
||||
|
||||
it('should return 403 when trying to cancel another user booking', async () => {
|
||||
// Arrange
|
||||
const otherUser = await createUser({ email: 'other@example.com' });
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
const booking = await createBooking({
|
||||
userId: otherUser.id,
|
||||
courtId: testCourt.id,
|
||||
date: tomorrow,
|
||||
startTime: '10:00',
|
||||
endTime: '11:00',
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.delete(`/api/v1/bookings/${booking.id}`)
|
||||
.set('Authorization', `Bearer ${userToken}`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body).toHaveProperty('message', 'No tienes permiso para cancelar esta reserva');
|
||||
});
|
||||
|
||||
it('should return 400 when booking is already cancelled', async () => {
|
||||
// Arrange
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
const booking = await createBooking({
|
||||
userId: testUser.id,
|
||||
courtId: testCourt.id,
|
||||
date: tomorrow,
|
||||
startTime: '10:00',
|
||||
endTime: '11:00',
|
||||
status: BookingStatus.CANCELLED,
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.delete(`/api/v1/bookings/${booking.id}`)
|
||||
.set('Authorization', `Bearer ${userToken}`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty('message', 'La reserva ya está cancelada');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/bookings (Admin)', () => {
|
||||
it('should return all bookings for admin', async () => {
|
||||
// Arrange
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
await createBooking({
|
||||
userId: testUser.id,
|
||||
courtId: testCourt.id,
|
||||
date: tomorrow,
|
||||
startTime: '10:00',
|
||||
endTime: '11:00',
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get('/api/v1/bookings')
|
||||
.set('Authorization', `Bearer ${adminToken}`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body.data).toBeInstanceOf(Array);
|
||||
});
|
||||
|
||||
it('should return 403 for non-admin user', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get('/api/v1/bookings')
|
||||
.set('Authorization', `Bearer ${userToken}`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /api/v1/bookings/:id/confirm (Admin)', () => {
|
||||
it('should confirm booking for admin', async () => {
|
||||
// Arrange
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
const booking = await createBooking({
|
||||
userId: testUser.id,
|
||||
courtId: testCourt.id,
|
||||
date: tomorrow,
|
||||
startTime: '10:00',
|
||||
endTime: '11:00',
|
||||
status: BookingStatus.PENDING,
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.put(`/api/v1/bookings/${booking.id}/confirm`)
|
||||
.set('Authorization', `Bearer ${adminToken}`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body.data).toHaveProperty('status', BookingStatus.CONFIRMED);
|
||||
});
|
||||
});
|
||||
});
|
||||
396
backend/tests/integration/routes/courts.routes.test.ts
Normal file
396
backend/tests/integration/routes/courts.routes.test.ts
Normal file
@@ -0,0 +1,396 @@
|
||||
import request from 'supertest';
|
||||
import app from '../../../src/app';
|
||||
import { setupTestDb, teardownTestDb, resetDatabase } from '../../utils/testDb';
|
||||
import { createUser, createCourt, createCourtWithSchedules, createBooking } from '../../utils/factories';
|
||||
import { generateTokens } from '../../utils/auth';
|
||||
import { UserRole, BookingStatus } from '../../../src/utils/constants';
|
||||
|
||||
describe('Courts Routes Integration Tests', () => {
|
||||
let testUser: any;
|
||||
let adminUser: any;
|
||||
let userToken: string;
|
||||
let adminToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupTestDb();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await teardownTestDb();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await resetDatabase();
|
||||
|
||||
testUser = await createUser({
|
||||
email: 'courtuser@example.com',
|
||||
firstName: 'Court',
|
||||
lastName: 'User',
|
||||
});
|
||||
|
||||
adminUser = await createUser({
|
||||
email: 'courtadmin@example.com',
|
||||
role: UserRole.ADMIN,
|
||||
});
|
||||
|
||||
const userTokens = generateTokens({
|
||||
userId: testUser.id,
|
||||
email: testUser.email,
|
||||
role: testUser.role,
|
||||
});
|
||||
userToken = userTokens.accessToken;
|
||||
|
||||
const adminTokens = generateTokens({
|
||||
userId: adminUser.id,
|
||||
email: adminUser.email,
|
||||
role: adminUser.role,
|
||||
});
|
||||
adminToken = adminTokens.accessToken;
|
||||
});
|
||||
|
||||
describe('GET /api/v1/courts', () => {
|
||||
it('should return all active courts', async () => {
|
||||
// Arrange
|
||||
await createCourt({ name: 'Cancha 1', isActive: true });
|
||||
await createCourt({ name: 'Cancha 2', isActive: true });
|
||||
await createCourt({ name: 'Cancha 3', isActive: false });
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get('/api/v1/courts');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body.data).toBeInstanceOf(Array);
|
||||
expect(response.body.data.length).toBe(2); // Only active courts
|
||||
});
|
||||
|
||||
it('should return courts with schedules', async () => {
|
||||
// Arrange
|
||||
await createCourtWithSchedules({ name: 'Cancha Con Horario' });
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get('/api/v1/courts');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.data[0]).toHaveProperty('schedules');
|
||||
expect(response.body.data[0].schedules).toBeInstanceOf(Array);
|
||||
});
|
||||
|
||||
it('should return courts with booking counts', async () => {
|
||||
// Arrange
|
||||
const court = await createCourtWithSchedules({ name: 'Cancha Con Reservas' });
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
await createBooking({
|
||||
userId: testUser.id,
|
||||
courtId: court.id,
|
||||
date: tomorrow,
|
||||
status: BookingStatus.CONFIRMED,
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get('/api/v1/courts');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.data[0]).toHaveProperty('_count');
|
||||
expect(response.body.data[0]._count).toHaveProperty('bookings');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/courts/:id', () => {
|
||||
it('should return court by id', async () => {
|
||||
// Arrange
|
||||
const court = await createCourtWithSchedules({
|
||||
name: 'Cancha Específica',
|
||||
description: 'Descripción de prueba',
|
||||
pricePerHour: 2500,
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get(`/api/v1/courts/${court.id}`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body.data).toHaveProperty('id', court.id);
|
||||
expect(response.body.data).toHaveProperty('name', 'Cancha Específica');
|
||||
expect(response.body.data).toHaveProperty('description', 'Descripción de prueba');
|
||||
expect(response.body.data).toHaveProperty('pricePerHour', 2500);
|
||||
expect(response.body.data).toHaveProperty('schedules');
|
||||
});
|
||||
|
||||
it('should return 404 when court not found', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get('/api/v1/courts/00000000-0000-0000-0000-000000000000');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toHaveProperty('success', false);
|
||||
expect(response.body).toHaveProperty('message', 'Cancha no encontrada');
|
||||
});
|
||||
|
||||
it('should return 400 for invalid court id format', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get('/api/v1/courts/invalid-id');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/courts/:id/availability', () => {
|
||||
it('should return availability for a court', async () => {
|
||||
// Arrange
|
||||
const court = await createCourtWithSchedules({
|
||||
name: 'Cancha Disponible',
|
||||
pricePerHour: 2000,
|
||||
});
|
||||
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const formattedDate = tomorrow.toISOString().split('T')[0];
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get(`/api/v1/courts/${court.id}/availability`)
|
||||
.query({ date: formattedDate });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body.data).toHaveProperty('courtId', court.id);
|
||||
expect(response.body.data).toHaveProperty('date');
|
||||
expect(response.body.data).toHaveProperty('openTime');
|
||||
expect(response.body.data).toHaveProperty('closeTime');
|
||||
expect(response.body.data).toHaveProperty('slots');
|
||||
expect(response.body.data.slots).toBeInstanceOf(Array);
|
||||
expect(response.body.data.slots.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should mark booked slots as unavailable', async () => {
|
||||
// Arrange
|
||||
const court = await createCourtWithSchedules({
|
||||
name: 'Cancha Con Reserva',
|
||||
pricePerHour: 2000,
|
||||
});
|
||||
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(0, 0, 0, 0);
|
||||
|
||||
// Create a booking at 10:00
|
||||
await createBooking({
|
||||
userId: testUser.id,
|
||||
courtId: court.id,
|
||||
date: tomorrow,
|
||||
startTime: '10:00',
|
||||
endTime: '11:00',
|
||||
status: BookingStatus.CONFIRMED,
|
||||
});
|
||||
|
||||
const formattedDate = tomorrow.toISOString().split('T')[0];
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get(`/api/v1/courts/${court.id}/availability`)
|
||||
.query({ date: formattedDate });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
const tenAmSlot = response.body.data.slots.find((s: any) => s.time === '10:00');
|
||||
expect(tenAmSlot).toBeDefined();
|
||||
expect(tenAmSlot.available).toBe(false);
|
||||
});
|
||||
|
||||
it('should return 404 when court not found', async () => {
|
||||
// Arrange
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const formattedDate = tomorrow.toISOString().split('T')[0];
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get('/api/v1/courts/00000000-0000-0000-0000-000000000000/availability')
|
||||
.query({ date: formattedDate });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should return 400 when date is missing', async () => {
|
||||
// Arrange
|
||||
const court = await createCourtWithSchedules({ name: 'Test Court' });
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.get(`/api/v1/courts/${court.id}/availability`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/courts (Admin only)', () => {
|
||||
it('should create a new court as admin', async () => {
|
||||
// Arrange
|
||||
const newCourtData = {
|
||||
name: 'Nueva Cancha Admin',
|
||||
description: 'Cancha creada por admin',
|
||||
type: 'PANORAMIC',
|
||||
isIndoor: false,
|
||||
hasLighting: true,
|
||||
hasParking: true,
|
||||
pricePerHour: 3000,
|
||||
};
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/courts')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send(newCourtData);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body.data).toHaveProperty('id');
|
||||
expect(response.body.data).toHaveProperty('name', newCourtData.name);
|
||||
expect(response.body.data).toHaveProperty('schedules');
|
||||
expect(response.body.data.schedules).toBeInstanceOf(Array);
|
||||
expect(response.body.data.schedules.length).toBe(7); // One for each day
|
||||
});
|
||||
|
||||
it('should return 401 when not authenticated', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/courts')
|
||||
.send({ name: 'Unauthorized Court' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
|
||||
it('should return 403 when not admin', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/courts')
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.send({ name: 'Forbidden Court' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
it('should return 409 when court name already exists', async () => {
|
||||
// Arrange
|
||||
await createCourt({ name: 'Duplicate Court' });
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.post('/api/v1/courts')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({ name: 'Duplicate Court' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(409);
|
||||
expect(response.body).toHaveProperty('message', 'Ya existe una cancha con ese nombre');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /api/v1/courts/:id (Admin only)', () => {
|
||||
it('should update court as admin', async () => {
|
||||
// Arrange
|
||||
const court = await createCourt({ name: 'Court To Update' });
|
||||
const updateData = {
|
||||
name: 'Updated Court Name',
|
||||
pricePerHour: 3500,
|
||||
};
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.put(`/api/v1/courts/${court.id}`)
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send(updateData);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body.data).toHaveProperty('name', updateData.name);
|
||||
expect(response.body.data).toHaveProperty('pricePerHour', updateData.pricePerHour);
|
||||
});
|
||||
|
||||
it('should return 404 when court not found', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.put('/api/v1/courts/00000000-0000-0000-0000-000000000000')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({ name: 'New Name' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should return 403 for non-admin user', async () => {
|
||||
// Arrange
|
||||
const court = await createCourt({ name: 'Protected Court' });
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.put(`/api/v1/courts/${court.id}`)
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.send({ name: 'Hacked Name' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /api/v1/courts/:id (Admin only)', () => {
|
||||
it('should deactivate court as admin', async () => {
|
||||
// Arrange
|
||||
const court = await createCourt({ name: 'Court To Delete', isActive: true });
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.delete(`/api/v1/courts/${court.id}`)
|
||||
.set('Authorization', `Bearer ${adminToken}`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('success', true);
|
||||
expect(response.body.data).toHaveProperty('isActive', false);
|
||||
});
|
||||
|
||||
it('should return 404 when court not found', async () => {
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.delete('/api/v1/courts/00000000-0000-0000-0000-000000000000')
|
||||
.set('Authorization', `Bearer ${adminToken}`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should return 403 for non-admin user', async () => {
|
||||
// Arrange
|
||||
const court = await createCourt({ name: 'Protected Court' });
|
||||
|
||||
// Act
|
||||
const response = await request(app)
|
||||
.delete(`/api/v1/courts/${court.id}`)
|
||||
.set('Authorization', `Bearer ${userToken}`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user