/** * Integration tests for Recording Stats Update Mechanism * * Tests the full flow: * 1. Recording suggestion accepted → Match created * 2. Both users rate → Match completed + stats updated */ const request = require('supertest'); const app = require('../app'); const { prisma } = require('../utils/db'); const { hashPassword, generateToken } = require('../utils/auth'); // Test data let dancer, recorder; let dancerToken, recorderToken; let testEvent; let testDivision, testCompetitionType; let testHeat; let testSuggestion; // Setup test data beforeAll(async () => { // Clean up test data const testUsernames = ['stats_dancer', 'stats_recorder']; const testEmails = ['stats_dancer@example.com', 'stats_recorder@example.com']; const testEventSlug = 'stats-test-event-2025'; // Find and delete existing test users const existingUsers = await prisma.user.findMany({ where: { OR: [ { username: { in: testUsernames } }, { email: { in: testEmails } } ] }, select: { id: true } }); const existingUserIds = existingUsers.map(u => u.id); if (existingUserIds.length > 0) { await prisma.eventUserHeat.deleteMany({ where: { userId: { in: existingUserIds } } }); await prisma.rating.deleteMany({ where: { raterId: { in: existingUserIds } } }); await prisma.match.deleteMany({ where: { OR: [ { user1Id: { in: existingUserIds } }, { user2Id: { in: existingUserIds } } ] } }); await prisma.eventParticipant.deleteMany({ where: { userId: { in: existingUserIds } } }); await prisma.user.deleteMany({ where: { id: { in: existingUserIds } } }); } // Delete test event const existingEvent = await prisma.event.findUnique({ where: { slug: testEventSlug }, select: { id: true } }); if (existingEvent) { await prisma.recordingSuggestion.deleteMany({ where: { eventId: existingEvent.id } }); await prisma.eventUserHeat.deleteMany({ where: { eventId: existingEvent.id } }); await prisma.eventParticipant.deleteMany({ where: { eventId: existingEvent.id } }); await prisma.event.delete({ where: { id: existingEvent.id } }); } // Create test users with initial stats = 0 const hashedPassword = await hashPassword('TestPass123!'); dancer = await prisma.user.create({ data: { username: 'stats_dancer', email: 'stats_dancer@example.com', passwordHash: hashedPassword, emailVerified: true, recordingsDone: 0, recordingsReceived: 0, }, }); recorder = await prisma.user.create({ data: { username: 'stats_recorder', email: 'stats_recorder@example.com', passwordHash: hashedPassword, emailVerified: true, recordingsDone: 0, recordingsReceived: 0, }, }); dancerToken = generateToken({ userId: dancer.id }); recorderToken = generateToken({ userId: recorder.id }); // Create test event testEvent = await prisma.event.create({ data: { slug: testEventSlug, name: 'Stats Test Event 2025', location: 'Test City', startDate: new Date('2025-06-01'), endDate: new Date('2025-06-03'), }, }); // Add participants await prisma.eventParticipant.createMany({ data: [ { userId: dancer.id, eventId: testEvent.id, competitorNumber: 100 }, { userId: recorder.id, eventId: testEvent.id }, ], }); // Get or create division and competition type testDivision = await prisma.division.findFirst({ where: { abbreviation: 'INT' }, }); if (!testDivision) { testDivision = await prisma.division.create({ data: { name: 'Intermediate', abbreviation: 'INT', displayOrder: 3, }, }); } testCompetitionType = await prisma.competitionType.findFirst({ where: { abbreviation: 'J&J' }, // Use existing abbreviation }); if (!testCompetitionType) { testCompetitionType = await prisma.competitionType.create({ data: { name: 'Jack & Jill', abbreviation: 'J&J', }, }); } // Create heat for dancer testHeat = await prisma.eventUserHeat.create({ data: { userId: dancer.id, eventId: testEvent.id, divisionId: testDivision.id, competitionTypeId: testCompetitionType.id, heatNumber: 1, role: 'Leader', }, }); // Create recording suggestion testSuggestion = await prisma.recordingSuggestion.create({ data: { eventId: testEvent.id, heatId: testHeat.id, recorderId: recorder.id, status: 'pending', }, }); }); afterAll(async () => { // Cleanup if (dancer && recorder) { const userIds = [dancer.id, recorder.id]; await prisma.eventUserHeat.deleteMany({ where: { userId: { in: userIds } } }); await prisma.rating.deleteMany({ where: { raterId: { in: userIds } } }); await prisma.match.deleteMany({ where: { OR: [ { user1Id: { in: userIds } }, { user2Id: { in: userIds } } ] } }); await prisma.eventParticipant.deleteMany({ where: { userId: { in: userIds } } }); await prisma.user.deleteMany({ where: { id: { in: userIds } } }); } if (testEvent) { await prisma.recordingSuggestion.deleteMany({ where: { eventId: testEvent.id } }); await prisma.eventUserHeat.deleteMany({ where: { eventId: testEvent.id } }); await prisma.eventParticipant.deleteMany({ where: { eventId: testEvent.id } }); await prisma.event.delete({ where: { id: testEvent.id } }); } await prisma.$disconnect(); }); describe('Recording Stats Integration Tests', () => { describe('Full Flow: Suggestion → Match → Ratings → Stats', () => { let createdMatch; it('should create Match with correct fields when suggestion is accepted', async () => { // Recorder accepts the suggestion const response = await request(app) .put(`/api/events/${testEvent.slug}/match-suggestions/${testSuggestion.id}/status`) .set('Authorization', `Bearer ${recorderToken}`) .send({ status: 'accepted' }); expect(response.status).toBe(200); expect(response.body.success).toBe(true); expect(response.body.data.status).toBe('accepted'); expect(response.body.data.matchId).toBeDefined(); expect(response.body.data.matchSlug).toBeDefined(); // Verify Match was created with correct fields const match = await prisma.match.findUnique({ where: { id: response.body.data.matchId }, include: { room: true }, }); expect(match).toBeDefined(); expect(match.user1Id).toBe(dancer.id); // Convention: user1 = dancer expect(match.user2Id).toBe(recorder.id); // Convention: user2 = recorder expect(match.eventId).toBe(testEvent.id); expect(match.suggestionId).toBe(testSuggestion.id); expect(match.source).toBe('auto'); expect(match.status).toBe('accepted'); expect(match.statsApplied).toBe(false); expect(match.roomId).toBeDefined(); // Chat room created expect(match.room).toBeDefined(); expect(match.room.type).toBe('private'); createdMatch = match; }); it('should be idempotent - accepting again should not create duplicate Match', async () => { // Update suggestion back to pending await prisma.recordingSuggestion.update({ where: { id: testSuggestion.id }, data: { status: 'pending' }, }); // Accept again const response = await request(app) .put(`/api/events/${testEvent.slug}/match-suggestions/${testSuggestion.id}/status`) .set('Authorization', `Bearer ${recorderToken}`) .send({ status: 'accepted' }); expect(response.status).toBe(200); expect(response.body.data.matchId).toBe(createdMatch.id); // Same match // Verify only one match exists const matchCount = await prisma.match.count({ where: { suggestionId: testSuggestion.id }, }); expect(matchCount).toBe(1); }); it('should NOT update stats after only one rating', async () => { // Dancer rates recorder const response = await request(app) .post(`/api/matches/${createdMatch.slug}/ratings`) .set('Authorization', `Bearer ${dancerToken}`) .send({ score: 5, comment: 'Great recorder!', wouldCollaborateAgain: true, }); expect(response.status).toBe(201); expect(response.body.success).toBe(true); // Verify match is still 'accepted' (not completed) const match = await prisma.match.findUnique({ where: { id: createdMatch.id }, }); expect(match.status).toBe('accepted'); expect(match.statsApplied).toBe(false); // Verify stats NOT updated yet const dancerStats = await prisma.user.findUnique({ where: { id: dancer.id }, select: { recordingsDone: true, recordingsReceived: true }, }); const recorderStats = await prisma.user.findUnique({ where: { id: recorder.id }, select: { recordingsDone: true, recordingsReceived: true }, }); expect(dancerStats.recordingsDone).toBe(0); expect(dancerStats.recordingsReceived).toBe(0); expect(recorderStats.recordingsDone).toBe(0); expect(recorderStats.recordingsReceived).toBe(0); }); it('should update stats and mark match as completed after both users rate', async () => { // Recorder rates dancer const response = await request(app) .post(`/api/matches/${createdMatch.slug}/ratings`) .set('Authorization', `Bearer ${recorderToken}`) .send({ score: 5, comment: 'Great dancer!', wouldCollaborateAgain: true, }); expect(response.status).toBe(201); expect(response.body.success).toBe(true); // Verify match is now 'completed' with statsApplied const match = await prisma.match.findUnique({ where: { id: createdMatch.id }, }); expect(match.status).toBe('completed'); expect(match.statsApplied).toBe(true); // Verify stats were updated correctly const dancerStats = await prisma.user.findUnique({ where: { id: dancer.id }, select: { recordingsDone: true, recordingsReceived: true }, }); const recorderStats = await prisma.user.findUnique({ where: { id: recorder.id }, select: { recordingsDone: true, recordingsReceived: true }, }); // Dancer: received +1 (was recorded) expect(dancerStats.recordingsDone).toBe(0); expect(dancerStats.recordingsReceived).toBe(1); // Recorder: done +1 (did the recording) expect(recorderStats.recordingsDone).toBe(1); expect(recorderStats.recordingsReceived).toBe(0); }); }); describe('Manual Match - Stats NOT Updated', () => { let manualMatch; let user1, user2; let user1Token, user2Token; beforeAll(async () => { // Create two users for manual match const hashedPassword = await hashPassword('TestPass123!'); user1 = await prisma.user.create({ data: { username: 'stats_manual_user1', email: 'stats_manual1@example.com', passwordHash: hashedPassword, emailVerified: true, recordingsDone: 0, recordingsReceived: 0, }, }); user2 = await prisma.user.create({ data: { username: 'stats_manual_user2', email: 'stats_manual2@example.com', passwordHash: hashedPassword, emailVerified: true, recordingsDone: 0, recordingsReceived: 0, }, }); user1Token = generateToken({ userId: user1.id }); user2Token = generateToken({ userId: user2.id }); // Add participants to event await prisma.eventParticipant.createMany({ data: [ { userId: user1.id, eventId: testEvent.id }, { userId: user2.id, eventId: testEvent.id }, ], }); // Create manual match const response = await request(app) .post('/api/matches') .set('Authorization', `Bearer ${user1Token}`) .send({ targetUserId: user2.id, eventSlug: testEvent.slug, }); expect(response.status).toBe(201); manualMatch = response.body.data; }); afterAll(async () => { if (user1 && user2) { const userIds = [user1.id, user2.id]; await prisma.rating.deleteMany({ where: { raterId: { in: userIds } } }); await prisma.match.deleteMany({ where: { OR: [ { user1Id: { in: userIds } }, { user2Id: { in: userIds } } ] } }); await prisma.eventParticipant.deleteMany({ where: { userId: { in: userIds } } }); await prisma.user.deleteMany({ where: { id: { in: userIds } } }); } }); it('should NOT update stats for manual match after ratings', async () => { // Accept match await request(app) .put(`/api/matches/${manualMatch.slug}/accept`) .set('Authorization', `Bearer ${user2Token}`); // Both users rate each other await request(app) .post(`/api/matches/${manualMatch.slug}/ratings`) .set('Authorization', `Bearer ${user1Token}`) .send({ score: 5 }); await request(app) .post(`/api/matches/${manualMatch.slug}/ratings`) .set('Authorization', `Bearer ${user2Token}`) .send({ score: 5 }); // Verify match is completed const match = await prisma.match.findUnique({ where: { slug: manualMatch.slug }, }); expect(match.status).toBe('completed'); expect(match.source).toBe('manual'); expect(match.statsApplied).toBe(true); // Flag set, but stats not updated // Verify stats were NOT updated (source='manual') const user1Stats = await prisma.user.findUnique({ where: { id: user1.id }, select: { recordingsDone: true, recordingsReceived: true }, }); const user2Stats = await prisma.user.findUnique({ where: { id: user2.id }, select: { recordingsDone: true, recordingsReceived: true }, }); expect(user1Stats.recordingsDone).toBe(0); expect(user1Stats.recordingsReceived).toBe(0); expect(user2Stats.recordingsDone).toBe(0); expect(user2Stats.recordingsReceived).toBe(0); }); }); });