test: add comprehensive test suite for Matches & Ratings API
- Created matches.test.js with 24 tests covering: * Match creation and validation * Match listing and filtering * Match acceptance workflow * Match deletion * Rating creation and validation * User ratings display - Fixed Jest ES module issues: * Added mock for jsdom to bypass parse5 compatibility * Added mock for dompurify for test environment * Updated package.json with moduleNameMapper Test results: 19/24 passing (79%) Remaining: 5 tests need investigation
This commit is contained in:
478
backend/src/__tests__/matches.test.js
Normal file
478
backend/src/__tests__/matches.test.js
Normal file
@@ -0,0 +1,478 @@
|
||||
const request = require('supertest');
|
||||
const app = require('../app');
|
||||
const { prisma } = require('../utils/db');
|
||||
const { hashPassword, generateToken } = require('../utils/auth');
|
||||
|
||||
// Test data
|
||||
let testUser1, testUser2, testEvent, testToken1, testToken2;
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
||||
// Generate tokens
|
||||
testToken1 = generateToken({ userId: testUser1.id });
|
||||
testToken2 = generateToken({ userId: testUser2.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 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
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(404);
|
||||
|
||||
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
|
||||
const newMatchRes = await request(app)
|
||||
.post('/api/matches')
|
||||
.set('Authorization', `Bearer ${testToken1}`)
|
||||
.send({
|
||||
targetUserId: testUser2.id,
|
||||
eventSlug: testEvent.slug,
|
||||
});
|
||||
|
||||
const newMatch = newMatchRes.body.data;
|
||||
|
||||
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
|
||||
const newMatchRes = await request(app)
|
||||
.post('/api/matches')
|
||||
.set('Authorization', `Bearer ${testToken1}`)
|
||||
.send({
|
||||
targetUserId: testUser2.id,
|
||||
eventSlug: testEvent.slug,
|
||||
});
|
||||
|
||||
const newMatch = newMatchRes.body.data;
|
||||
|
||||
const response = await request(app)
|
||||
.delete(`/api/matches/${newMatch.slug}`)
|
||||
.set('Authorization', `Bearer ${testToken1}`)
|
||||
.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).toHaveProperty('message');
|
||||
expect(response.body.data.rating).toHaveProperty('score', 5);
|
||||
expect(response.body.data.rating).toHaveProperty('comment', 'Great partner!');
|
||||
expect(response.body.data.rating).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.rating).toHaveProperty('score', 4);
|
||||
expect(response.body.data.rating).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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user