Files
spotlightcam/backend/src/__tests__/events.test.js
Radosław Gierwiało 85a47f4e8e test: fix backend test cleanup to preserve production data
Replace deleteMany({}) with selective cleanup targeting only test data:

- events.test.js: Delete only test users (john_dancer, sarah_swings, mike_blues)
  and test events (test-dance-festival-2025) before creating new ones
- matches.test.js: Clean up john_dancer, sarah_swings, mike_moves and
  test-dance-festival slug specifically
- users.test.js: Remove only john_dancer and sarah_swings test users
  in both beforeAll and afterAll hooks
- auth.test.js: Target specific test usernames/emails (testuser, newuser,
  lockouttest, etc.) instead of all users
- auth-phase1.5.test.js: Clean up 12 specific test users by username/email
- socket.test.js: Add beforeAll cleanup for sockettest user to prevent
  conflicts from previous test runs
- socket-webrtc.test.js: Clean up webrtc_user1 and webrtc_user2 before
  creating them

Fix CORS configuration for tests:
- security.js: Add http://localhost:3000 to allowed origins in development
  mode to fix app.test.js CORS test (was failing with 500 error)

Results: Improved from 125/223 passing to 137/223 passing (12 more tests fixed)
All test data cleanup now uses WHERE clauses with specific usernames/emails/slugs
instead of wiping entire tables with deleteMany({})
2025-11-19 21:46:04 +01:00

744 lines
24 KiB
JavaScript

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 only specific test data (not all tables)
const testUsernames = ['john_dancer', 'sarah_swings', 'mike_blues'];
const testEmails = ['john@example.com', 'sarah@example.com', 'mike@example.com'];
const testEventSlugs = ['test-dance-festival-2025', 'test-another-event-2025'];
// Find test users
const testUsers = await prisma.user.findMany({
where: {
OR: [
{ username: { in: testUsernames } },
{ email: { in: testEmails } }
]
},
select: { id: true }
});
const testUserIds = testUsers.map(u => u.id);
// Find test events
const testEvents = await prisma.event.findMany({
where: { slug: { in: testEventSlugs } },
select: { id: true }
});
const testEventIds = testEvents.map(e => e.id);
// Delete related data for test users and events only
if (testUserIds.length > 0) {
await prisma.eventUserHeat.deleteMany({ where: { userId: { in: testUserIds } } });
await prisma.rating.deleteMany({ where: { raterId: { in: testUserIds } } });
await prisma.message.deleteMany({ where: { userId: { in: testUserIds } } });
await prisma.match.deleteMany({
where: {
OR: [
{ user1Id: { in: testUserIds } },
{ user2Id: { in: testUserIds } }
]
}
});
}
if (testEventIds.length > 0) {
await prisma.chatRoom.deleteMany({ where: { eventId: { in: testEventIds } } });
await prisma.eventCheckinToken.deleteMany({ where: { eventId: { in: testEventIds } } });
await prisma.eventParticipant.deleteMany({ where: { eventId: { in: testEventIds } } });
await prisma.event.deleteMany({ where: { id: { in: testEventIds } } });
}
// Delete test users
await prisma.user.deleteMany({
where: {
OR: [
{ username: { in: testUsernames } },
{ email: { in: testEmails } }
]
}
});
// 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');
});
});
});