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, testEvent, testToken1, testToken2, testToken3; let testMatch; // Setup test data beforeAll(async () => { // Clean up await prisma.rating.deleteMany({}); await prisma.message.deleteMany({}); await prisma.match.deleteMany({}); await prisma.chatRoom.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_moves', 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 }); // Create test event testEvent = await prisma.event.create({ data: { name: 'Test Dance Festival', slug: 'test-dance-festival', location: 'Test City', startDate: new Date('2025-06-01'), endDate: new Date('2025-06-03'), description: 'Test event', worldsdcId: '12345', }, }); // Add users as participants await prisma.eventParticipant.createMany({ data: [ { userId: testUser1.id, eventId: testEvent.id }, { userId: testUser2.id, eventId: testEvent.id }, { userId: testUser3.id, eventId: testEvent.id }, ], }); }); afterAll(async () => { // Clean up await prisma.rating.deleteMany({}); await prisma.message.deleteMany({}); await prisma.match.deleteMany({}); await prisma.chatRoom.deleteMany({}); await prisma.eventParticipant.deleteMany({}); await prisma.event.deleteMany({}); await prisma.user.deleteMany({}); await prisma.$disconnect(); }); describe('Matches API Tests', () => { describe('POST /api/matches', () => { it('should create a match request successfully', async () => { const response = await request(app) .post('/api/matches') .set('Authorization', `Bearer ${testToken1}`) .send({ targetUserId: testUser2.id, eventSlug: testEvent.slug, }) .expect('Content-Type', /json/) .expect(201); expect(response.body).toHaveProperty('success', true); expect(response.body.data).toHaveProperty('slug'); expect(response.body.data).toHaveProperty('status', 'pending'); expect(response.body.data.slug).toMatch(/^[a-z0-9]+$/); // CUID format // Save for later tests testMatch = response.body.data; }); it('should reject match request without authentication', async () => { const response = await request(app) .post('/api/matches') .send({ targetUserId: testUser2.id, eventSlug: testEvent.slug, }) .expect('Content-Type', /json/) .expect(401); expect(response.body).toHaveProperty('success', false); }); it('should reject match request to yourself', async () => { const response = await request(app) .post('/api/matches') .set('Authorization', `Bearer ${testToken1}`) .send({ targetUserId: testUser1.id, // Same as authenticated user eventSlug: testEvent.slug, }) .expect('Content-Type', /json/) .expect(400); expect(response.body).toHaveProperty('success', false); expect(response.body.error).toContain('yourself'); }); it('should reject match request with invalid event', async () => { const response = await request(app) .post('/api/matches') .set('Authorization', `Bearer ${testToken1}`) .send({ targetUserId: testUser2.id, eventSlug: 'non-existent-event', }) .expect('Content-Type', /json/) .expect(404); expect(response.body).toHaveProperty('success', false); }); }); describe('GET /api/matches', () => { it('should list all matches for authenticated user', async () => { const response = await request(app) .get('/api/matches') .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); const match = response.body.data[0]; expect(match).toHaveProperty('slug'); expect(match).toHaveProperty('partner'); expect(match).toHaveProperty('event'); expect(match).toHaveProperty('status'); }); it('should filter matches by event slug', async () => { const response = await request(app) .get(`/api/matches?eventSlug=${testEvent.slug}`) .set('Authorization', `Bearer ${testToken1}`) .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body.data).toBeInstanceOf(Array); if (response.body.data.length > 0) { expect(response.body.data[0].event.slug).toBe(testEvent.slug); } }); it('should filter matches by status', async () => { const response = await request(app) .get('/api/matches?status=pending') .set('Authorization', `Bearer ${testToken1}`) .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body.data).toBeInstanceOf(Array); response.body.data.forEach(match => { expect(match.status).toBe('pending'); }); }); }); describe('GET /api/matches/:slug', () => { it('should get match details by slug', async () => { const response = await request(app) .get(`/api/matches/${testMatch.slug}`) .set('Authorization', `Bearer ${testToken1}`) .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body.data).toHaveProperty('slug', testMatch.slug); expect(response.body.data).toHaveProperty('partner'); expect(response.body.data).toHaveProperty('event'); expect(response.body.data).toHaveProperty('hasRated', false); expect(response.body.data).toHaveProperty('isInitiator'); }); it('should reject access to match by non-participant', async () => { // Create third user const testUser3 = await prisma.user.create({ data: { username: 'outsider', email: 'outsider@example.com', passwordHash: await hashPassword('password123'), emailVerified: true, }, }); const testToken3 = generateToken({ userId: testUser3.id }); const response = await request(app) .get(`/api/matches/${testMatch.slug}`) .set('Authorization', `Bearer ${testToken3}`) .expect('Content-Type', /json/) .expect(403); expect(response.body).toHaveProperty('success', false); // Clean up await prisma.user.delete({ where: { id: testUser3.id } }); }); it('should return 404 for non-existent match slug', async () => { const response = await request(app) .get('/api/matches/nonexistentslug123') .set('Authorization', `Bearer ${testToken1}`) .expect('Content-Type', /json/) .expect(404); expect(response.body).toHaveProperty('success', false); }); }); describe('PUT /api/matches/:slug/accept', () => { it('should accept a match request', async () => { const response = await request(app) .put(`/api/matches/${testMatch.slug}/accept`) .set('Authorization', `Bearer ${testToken2}`) // User 2 accepts .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body.data).toHaveProperty('status', 'accepted'); expect(response.body.data).toHaveProperty('roomId'); }); it('should reject accepting already accepted match', async () => { const response = await request(app) .put(`/api/matches/${testMatch.slug}/accept`) .set('Authorization', `Bearer ${testToken2}`) .expect('Content-Type', /json/) .expect(400); expect(response.body).toHaveProperty('success', false); expect(response.body.error).toContain('already accepted'); }); it('should reject accepting match by initiator', async () => { // Create new match for this test (with testUser3) const newMatchRes = await request(app) .post('/api/matches') .set('Authorization', `Bearer ${testToken1}`) .send({ targetUserId: testUser3.id, eventSlug: testEvent.slug, }) .expect(201); const newMatch = newMatchRes.body.data; expect(newMatch).toHaveProperty('slug'); const response = await request(app) .put(`/api/matches/${newMatch.slug}/accept`) .set('Authorization', `Bearer ${testToken1}`) // Initiator tries to accept .expect('Content-Type', /json/) .expect(403); expect(response.body).toHaveProperty('success', false); // Clean up await prisma.match.delete({ where: { slug: newMatch.slug } }); }); }); describe('GET /api/matches/:slug/messages', () => { it('should get match messages (empty initially)', async () => { const response = await request(app) .get(`/api/matches/${testMatch.slug}/messages`) .set('Authorization', `Bearer ${testToken1}`) .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body.data).toBeInstanceOf(Array); }); }); describe('DELETE /api/matches/:slug', () => { it('should delete/reject a match request', async () => { // Create new match for deletion test (with a different user) const newMatchRes = await request(app) .post('/api/matches') .set('Authorization', `Bearer ${testToken2}`) .send({ targetUserId: testUser3.id, eventSlug: testEvent.slug, }) .expect(201); const newMatch = newMatchRes.body.data; expect(newMatch).toHaveProperty('slug'); const response = await request(app) .delete(`/api/matches/${newMatch.slug}`) .set('Authorization', `Bearer ${testToken2}`) .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body).toHaveProperty('message'); // Verify deletion const match = await prisma.match.findUnique({ where: { slug: newMatch.slug }, }); expect(match).toBeNull(); }); }); }); describe('Ratings API Tests', () => { describe('POST /api/matches/:slug/ratings', () => { it('should create a rating successfully', async () => { const response = await request(app) .post(`/api/matches/${testMatch.slug}/ratings`) .set('Authorization', `Bearer ${testToken1}`) .send({ score: 5, comment: 'Great partner!', wouldCollaborateAgain: true, }) .expect('Content-Type', /json/) .expect(201); expect(response.body).toHaveProperty('success', true); expect(response.body.data).toHaveProperty('score', 5); expect(response.body.data).toHaveProperty('comment', 'Great partner!'); expect(response.body.data).toHaveProperty('wouldCollaborateAgain', true); }); it('should reject duplicate rating from same user', async () => { const response = await request(app) .post(`/api/matches/${testMatch.slug}/ratings`) .set('Authorization', `Bearer ${testToken1}`) // Same user tries again .send({ score: 4, comment: 'Second rating', wouldCollaborateAgain: false, }) .expect('Content-Type', /json/) .expect(400); expect(response.body).toHaveProperty('success', false); expect(response.body.error).toContain('already rated'); }); it('should reject rating with invalid score', async () => { const response = await request(app) .post(`/api/matches/${testMatch.slug}/ratings`) .set('Authorization', `Bearer ${testToken2}`) .send({ score: 6, // Invalid (must be 1-5) comment: 'Test', wouldCollaborateAgain: true, }) .expect('Content-Type', /json/) .expect(400); expect(response.body).toHaveProperty('success', false); }); it('should create rating without comment (optional)', async () => { const response = await request(app) .post(`/api/matches/${testMatch.slug}/ratings`) .set('Authorization', `Bearer ${testToken2}`) .send({ score: 4, wouldCollaborateAgain: true, }) .expect('Content-Type', /json/) .expect(201); expect(response.body).toHaveProperty('success', true); expect(response.body.data).toHaveProperty('score', 4); expect(response.body.data).toHaveProperty('comment', null); }); it('should auto-complete match when both users rate', async () => { // Both users have now rated, check match status const matchRes = await request(app) .get(`/api/matches/${testMatch.slug}`) .set('Authorization', `Bearer ${testToken1}`) .expect(200); expect(matchRes.body.data.status).toBe('completed'); }); it('should update hasRated flag after rating', async () => { const response = await request(app) .get(`/api/matches/${testMatch.slug}`) .set('Authorization', `Bearer ${testToken1}`) .expect(200); expect(response.body.data.hasRated).toBe(true); }); }); describe('GET /api/users/:username/ratings', () => { it('should get user ratings', async () => { const response = await request(app) .get(`/api/users/${testUser2.username}/ratings`) .set('Authorization', `Bearer ${testToken1}`) .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body.data).toHaveProperty('username', testUser2.username); expect(response.body.data).toHaveProperty('averageRating'); expect(response.body.data).toHaveProperty('ratingsCount'); expect(response.body.data).toHaveProperty('ratings'); expect(response.body.data.ratings).toBeInstanceOf(Array); if (response.body.data.ratings.length > 0) { const rating = response.body.data.ratings[0]; expect(rating).toHaveProperty('score'); expect(rating).toHaveProperty('rater'); expect(rating).toHaveProperty('event'); expect(rating).toHaveProperty('createdAt'); } }); it('should calculate average rating correctly', async () => { const response = await request(app) .get(`/api/users/${testUser2.username}/ratings`) .set('Authorization', `Bearer ${testToken1}`) .expect(200); const { averageRating, ratingsCount } = response.body.data; if (ratingsCount > 0) { expect(parseFloat(averageRating)).toBeGreaterThan(0); expect(parseFloat(averageRating)).toBeLessThanOrEqual(5); } }); it('should return 404 for non-existent user', async () => { const response = await request(app) .get('/api/users/nonexistentuser/ratings') .set('Authorization', `Bearer ${testToken1}`) .expect('Content-Type', /json/) .expect(404); expect(response.body).toHaveProperty('success', false); }); }); });