const request = require('supertest'); const app = require('../app'); const { prisma } = require('../utils/db'); const { hashPassword, generateToken } = require('../utils/auth'); // Test data let testUser1, testUser2, testUser3; let testToken1, testToken2, testToken3; let testEvent, testEvent2; let testDivision, testCompetitionType; let checkinToken; // Setup test data beforeAll(async () => { // Clean up await prisma.eventUserHeat.deleteMany({}); await prisma.rating.deleteMany({}); await prisma.message.deleteMany({}); await prisma.match.deleteMany({}); await prisma.chatRoom.deleteMany({}); await prisma.eventCheckinToken.deleteMany({}); await prisma.eventParticipant.deleteMany({}); await prisma.event.deleteMany({}); await prisma.user.deleteMany({}); // Create test users testUser1 = await prisma.user.create({ data: { username: 'john_dancer', email: 'john@example.com', passwordHash: await hashPassword('password123'), emailVerified: true, firstName: 'John', lastName: 'Doe', }, }); testUser2 = await prisma.user.create({ data: { username: 'sarah_swings', email: 'sarah@example.com', passwordHash: await hashPassword('password123'), emailVerified: true, firstName: 'Sarah', lastName: 'Smith', }, }); testUser3 = await prisma.user.create({ data: { username: 'mike_blues', email: 'mike@example.com', passwordHash: await hashPassword('password123'), emailVerified: true, firstName: 'Mike', lastName: 'Johnson', }, }); // Generate tokens testToken1 = generateToken({ userId: testUser1.id }); testToken2 = generateToken({ userId: testUser2.id }); testToken3 = generateToken({ userId: testUser3.id }); // Get or create division and competition type testDivision = await prisma.division.findFirst(); testCompetitionType = await prisma.competitionType.findFirst(); // If they don't exist, create them if (!testDivision) { testDivision = await prisma.division.create({ data: { name: 'Intermediate', abbreviation: 'INT', displayOrder: 1, }, }); } if (!testCompetitionType) { testCompetitionType = await prisma.competitionType.create({ data: { name: 'Jack & Jill', abbreviation: 'J&J', }, }); } // Create test events testEvent = await prisma.event.create({ data: { name: 'Test Dance Festival 2025', slug: 'test-dance-festival-2025', location: 'Test City', startDate: new Date('2025-06-01'), endDate: new Date('2025-06-03'), description: 'Test event description', worldsdcId: 'test-2025', }, }); testEvent2 = await prisma.event.create({ data: { name: 'Another Dance Event', slug: 'another-dance-event', location: 'Another City', startDate: new Date('2025-07-15'), endDate: new Date('2025-07-17'), description: 'Another test event', worldsdcId: 'another-2025', }, }); // Create checkin token for testEvent checkinToken = await prisma.eventCheckinToken.create({ data: { eventId: testEvent.id, token: 'test-checkin-token-123', }, }); // Add testUser1 as participant to testEvent await prisma.eventParticipant.create({ data: { userId: testUser1.id, eventId: testEvent.id, }, }); // Create chat room for testEvent await prisma.chatRoom.create({ data: { eventId: testEvent.id, type: 'event', }, }); }, 30000); afterAll(async () => { // Clean up await prisma.eventUserHeat.deleteMany({}); await prisma.rating.deleteMany({}); await prisma.message.deleteMany({}); await prisma.match.deleteMany({}); await prisma.chatRoom.deleteMany({}); await prisma.eventCheckinToken.deleteMany({}); await prisma.eventParticipant.deleteMany({}); await prisma.event.deleteMany({}); await prisma.user.deleteMany({}); await prisma.$disconnect(); }); describe('Events API Tests', () => { describe('GET /api/events', () => { it('should list all events for authenticated user', async () => { const response = await request(app) .get('/api/events') .set('Authorization', `Bearer ${testToken1}`) .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body).toHaveProperty('count'); expect(response.body.data).toBeInstanceOf(Array); expect(response.body.data.length).toBeGreaterThan(0); const event = response.body.data[0]; expect(event).toHaveProperty('slug'); expect(event).toHaveProperty('name'); expect(event).toHaveProperty('location'); expect(event).toHaveProperty('startDate'); expect(event).toHaveProperty('isJoined'); expect(event).toHaveProperty('participantsCount'); }); it('should show joined events first', async () => { const response = await request(app) .get('/api/events') .set('Authorization', `Bearer ${testToken1}`) .expect(200); // testUser1 is participant of testEvent, so it should be first const joinedEvents = response.body.data.filter(e => e.isJoined); expect(joinedEvents.length).toBeGreaterThan(0); expect(response.body.data[0].isJoined).toBe(true); }); it('should reject request without authentication', async () => { const response = await request(app) .get('/api/events') .expect('Content-Type', /json/) .expect(401); expect(response.body).toHaveProperty('success', false); }); }); describe('GET /api/events/:slug', () => { it('should get event details by slug', async () => { const response = await request(app) .get(`/api/events/${testEvent.slug}`) .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body.data).toHaveProperty('slug', testEvent.slug); expect(response.body.data).toHaveProperty('name', testEvent.name); expect(response.body.data).toHaveProperty('location'); expect(response.body.data).toHaveProperty('startDate'); expect(response.body.data).toHaveProperty('endDate'); }); it('should return 404 for non-existent event', async () => { const response = await request(app) .get('/api/events/non-existent-slug') .expect('Content-Type', /json/) .expect(404); expect(response.body).toHaveProperty('success', false); expect(response.body.error).toContain('not found'); }); it('should work without authentication', async () => { const response = await request(app) .get(`/api/events/${testEvent.slug}`) .expect(200); expect(response.body.success).toBe(true); }); }); describe('POST /api/events/checkin/:token', () => { it('should check-in user to event successfully', async () => { const response = await request(app) .post(`/api/events/checkin/${checkinToken.token}`) .set('Authorization', `Bearer ${testToken2}`) .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body.data).toHaveProperty('event'); expect(response.body.data.event.slug).toBe(testEvent.slug); // Verify user is now a participant const participant = await prisma.eventParticipant.findUnique({ where: { userId_eventId: { userId: testUser2.id, eventId: testEvent.id, }, }, }); expect(participant).toBeTruthy(); }); it('should handle already checked-in user', async () => { const response = await request(app) .post(`/api/events/checkin/${checkinToken.token}`) .set('Authorization', `Bearer ${testToken1}`) // testUser1 is already participant .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body).toHaveProperty('alreadyCheckedIn', true); }); it('should reject invalid checkin token', async () => { const response = await request(app) .post('/api/events/checkin/invalid-token-xyz') .set('Authorization', `Bearer ${testToken3}`) .expect('Content-Type', /json/) .expect(404); expect(response.body).toHaveProperty('success', false); expect(response.body.error).toContain('Invalid check-in token'); }); it('should reject checkin without authentication', async () => { const response = await request(app) .post(`/api/events/checkin/${checkinToken.token}`) .expect(401); expect(response.body).toHaveProperty('success', false); }); }); describe('GET /api/events/:slug/details', () => { it('should get detailed event information for participant', async () => { const response = await request(app) .get(`/api/events/${testEvent.slug}/details`) .set('Authorization', `Bearer ${testToken1}`) .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body.data).toHaveProperty('event'); expect(response.body.data.event.slug).toBe(testEvent.slug); expect(response.body.data).toHaveProperty('participants'); }); it('should return 404 for non-existent event', async () => { const response = await request(app) .get('/api/events/non-existent/details') .set('Authorization', `Bearer ${testToken1}`) .expect(404); expect(response.body).toHaveProperty('success', false); }); it('should work for any authenticated user (no participant check)', async () => { const response = await request(app) .get(`/api/events/${testEvent.slug}/details`) .set('Authorization', `Bearer ${testToken3}`) // testUser3 is not participant .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body.data).toHaveProperty('event'); expect(response.body.data).toHaveProperty('participants'); }); }); describe('DELETE /api/events/:slug/leave', () => { it('should allow participant to leave event', async () => { // testUser1 is already a participant from setup, use them instead const response = await request(app) .delete(`/api/events/${testEvent.slug}/leave`) .set('Authorization', `Bearer ${testToken1}`) .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body).toHaveProperty('message'); // Verify user is no longer a participant const participant = await prisma.eventParticipant.findUnique({ where: { userId_eventId: { userId: testUser1.id, eventId: testEvent.id, }, }, }); expect(participant).toBeNull(); // Re-add testUser1 for subsequent tests await prisma.eventParticipant.create({ data: { userId: testUser1.id, eventId: testEvent.id, }, }); }); it('should return 404 for non-existent event', async () => { const response = await request(app) .delete('/api/events/non-existent/leave') .set('Authorization', `Bearer ${testToken1}`) .expect(404); expect(response.body).toHaveProperty('success', false); }); it('should return 400 if user is not a participant', async () => { const response = await request(app) .delete(`/api/events/${testEvent2.slug}/leave`) .set('Authorization', `Bearer ${testToken1}`) .expect(400); expect(response.body).toHaveProperty('success', false); }); }); describe('GET /api/events/:slug/messages', () => { it('should get event chat messages (empty initially)', async () => { const response = await request(app) .get(`/api/events/${testEvent.slug}/messages`) .set('Authorization', `Bearer ${testToken1}`) .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body.data).toBeInstanceOf(Array); expect(response.body).toHaveProperty('hasMore'); }); it('should return 404 for non-existent event', async () => { const response = await request(app) .get('/api/events/non-existent/messages') .set('Authorization', `Bearer ${testToken1}`) .expect(404); expect(response.body).toHaveProperty('success', false); expect(response.body.error).toContain('not found'); }); it('should reject without authentication', async () => { const response = await request(app) .get(`/api/events/${testEvent.slug}/messages`) .expect(401); expect(response.body).toHaveProperty('success', false); }); }); describe('POST /api/events/:slug/heats', () => { it('should add heats for participant', async () => { const heats = [ { divisionId: testDivision.id, competitionTypeId: testCompetitionType.id, heatNumber: 1, role: 'Leader', }, { divisionId: testDivision.id, competitionTypeId: testCompetitionType.id, heatNumber: 2, role: 'Follower', }, ]; const response = await request(app) .post(`/api/events/${testEvent.slug}/heats`) .set('Authorization', `Bearer ${testToken1}`) .send({ heats }) .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body.data).toBeInstanceOf(Array); expect(response.body.data.length).toBe(2); expect(response.body.data[0]).toHaveProperty('heatNumber'); expect(response.body.data[0]).toHaveProperty('role'); }); it('should reject invalid heats array', async () => { const response = await request(app) .post(`/api/events/${testEvent.slug}/heats`) .set('Authorization', `Bearer ${testToken1}`) .send({ heats: [] }) .expect(400); expect(response.body).toHaveProperty('success', false); expect(response.body.error).toContain('non-empty array'); }); it('should reject heats with missing fields', async () => { const heats = [ { divisionId: testDivision.id, // missing competitionTypeId and heatNumber }, ]; const response = await request(app) .post(`/api/events/${testEvent.slug}/heats`) .set('Authorization', `Bearer ${testToken1}`) .send({ heats }) .expect(400); expect(response.body).toHaveProperty('success', false); }); it('should reject invalid heat number', async () => { const heats = [ { divisionId: testDivision.id, competitionTypeId: testCompetitionType.id, heatNumber: 10, // Invalid (must be 1-9) }, ]; const response = await request(app) .post(`/api/events/${testEvent.slug}/heats`) .set('Authorization', `Bearer ${testToken1}`) .send({ heats }) .expect(400); expect(response.body).toHaveProperty('success', false); expect(response.body.error).toContain('between 1 and 9'); }); it('should reject invalid role', async () => { const heats = [ { divisionId: testDivision.id, competitionTypeId: testCompetitionType.id, heatNumber: 1, role: 'InvalidRole', }, ]; const response = await request(app) .post(`/api/events/${testEvent.slug}/heats`) .set('Authorization', `Bearer ${testToken1}`) .send({ heats }) .expect(400); expect(response.body).toHaveProperty('success', false); expect(response.body.error).toContain('Leader or Follower'); }); it('should return 403 for non-participant', async () => { const heats = [ { divisionId: testDivision.id, competitionTypeId: testCompetitionType.id, heatNumber: 1, }, ]; const response = await request(app) .post(`/api/events/${testEvent.slug}/heats`) .set('Authorization', `Bearer ${testToken3}`) .send({ heats }) .expect(403); expect(response.body).toHaveProperty('success', false); expect(response.body.error).toContain('must be a participant'); }); }); describe('GET /api/events/:slug/heats/me', () => { it('should get user\'s heats for event', async () => { const response = await request(app) .get(`/api/events/${testEvent.slug}/heats/me`) .set('Authorization', `Bearer ${testToken1}`) .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body.data).toBeInstanceOf(Array); // testUser1 has heats from previous test expect(response.body.data.length).toBeGreaterThan(0); }); it('should return empty array for user without heats', async () => { // Add testUser3 as participant first await prisma.eventParticipant.create({ data: { userId: testUser3.id, eventId: testEvent.id, }, }); const response = await request(app) .get(`/api/events/${testEvent.slug}/heats/me`) .set('Authorization', `Bearer ${testToken3}`) .expect(200); expect(response.body.success).toBe(true); expect(response.body.data).toEqual([]); }); it('should work for any authenticated user (no participant check)', async () => { const response = await request(app) .get(`/api/events/${testEvent2.slug}/heats/me`) .set('Authorization', `Bearer ${testToken1}`) .expect(200); expect(response.body).toHaveProperty('success', true); // testUser1 has no heats for testEvent2, so data should be empty expect(response.body.data).toEqual([]); expect(response.body.count).toBe(0); }); }); describe('GET /api/events/:slug/heats/all', () => { it('should get all heats for event', async () => { const response = await request(app) .get(`/api/events/${testEvent.slug}/heats/all`) .set('Authorization', `Bearer ${testToken1}`) .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body.data).toBeInstanceOf(Array); expect(response.body.data.length).toBeGreaterThan(0); if (response.body.data.length > 0) { const userHeats = response.body.data[0]; // Response is grouped by user expect(userHeats).toHaveProperty('userId'); expect(userHeats).toHaveProperty('username'); expect(userHeats).toHaveProperty('heats'); expect(userHeats.heats).toBeInstanceOf(Array); if (userHeats.heats.length > 0) { const heat = userHeats.heats[0]; expect(heat).toHaveProperty('division'); expect(heat).toHaveProperty('competitionType'); expect(heat).toHaveProperty('heatNumber'); } } }); it('should filter by division', async () => { const response = await request(app) .get(`/api/events/${testEvent.slug}/heats/all?divisionId=${testDivision.id}`) .set('Authorization', `Bearer ${testToken1}`) .expect(200); expect(response.body.success).toBe(true); // Check that all heats in all users have the correct division response.body.data.forEach(userHeats => { userHeats.heats.forEach(heat => { expect(heat.division.id).toBe(testDivision.id); }); }); }); it('should work for any authenticated user (no participant check)', async () => { const response = await request(app) .get(`/api/events/${testEvent2.slug}/heats/all`) .set('Authorization', `Bearer ${testToken1}`) .expect(200); expect(response.body).toHaveProperty('success', true); // testEvent2 has no heats, so data should be empty expect(response.body.data).toEqual([]); }); }); describe('DELETE /api/events/:slug/heats/:id', () => { let heatToDelete; beforeAll(async () => { // Get a heat ID for deletion const heats = await prisma.eventUserHeat.findMany({ where: { userId: testUser1.id, eventId: testEvent.id, }, }); heatToDelete = heats[0]; }); it('should delete user\'s heat', async () => { const response = await request(app) .delete(`/api/events/${testEvent.slug}/heats/${heatToDelete.id}`) .set('Authorization', `Bearer ${testToken1}`) .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body).toHaveProperty('message'); // Verify heat is deleted const heat = await prisma.eventUserHeat.findUnique({ where: { id: heatToDelete.id }, }); expect(heat).toBeNull(); }); it('should return 404 for non-existent heat', async () => { const response = await request(app) .delete(`/api/events/${testEvent.slug}/heats/99999`) .set('Authorization', `Bearer ${testToken1}`) .expect(404); expect(response.body).toHaveProperty('success', false); }); it('should reject deleting another user\'s heat', async () => { // Create a heat for testUser3 await prisma.eventParticipant.upsert({ where: { userId_eventId: { userId: testUser3.id, eventId: testEvent.id, }, }, create: { userId: testUser3.id, eventId: testEvent.id, }, update: {}, }); const heat3 = await prisma.eventUserHeat.create({ data: { userId: testUser3.id, eventId: testEvent.id, divisionId: testDivision.id, competitionTypeId: testCompetitionType.id, heatNumber: 5, }, }); const response = await request(app) .delete(`/api/events/${testEvent.slug}/heats/${heat3.id}`) .set('Authorization', `Bearer ${testToken1}`) // testUser1 tries to delete testUser3's heat .expect(403); expect(response.body).toHaveProperty('success', false); expect(response.body.error).toContain('your own heats'); }); }); });