Files
spotlightcam/backend/src/__tests__/events.test.js
Radosław Gierwiało 1747bf2d91 test: add comprehensive test suite for Events API
- Created events.test.js with 34 tests covering all 10 endpoints:
  * GET /api/events - list events with join status
  * GET /api/events/:slug - event details
  * GET /api/events/:slug/messages - event chat messages
  * POST /api/events/checkin/:token - QR code checkin
  * GET /api/events/:slug/details - detailed event info
  * DELETE /api/events/:slug/leave - leave event
  * POST /api/events/:slug/heats - add/update user heats
  * GET /api/events/:slug/heats/me - get user's heats
  * GET /api/events/:slug/heats/all - get all heats
  * DELETE /api/events/:slug/heats/:id - delete heat

- All 34 tests passing (100%)
- events.js coverage: 82.02% (up from 8.98%)
- Branch coverage: 75%
- Function coverage: 88.23%
2025-11-14 23:30:34 +01:00

699 lines
22 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
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');
});
});
});