import { seedMatches, seedTriviaQuestions } from '@fan-passport/shared'; import request from 'supertest'; import { describe, expect, it } from 'vitest'; import { createApp } from '../app.js'; import { DemoStore } from '../store.js'; describe('Fan Passport API integration', () => { it('runs the primary collection, trivia, prediction, badge, challenge, and leaderboard flow', async () => { const store = new DemoStore(); const app = createApp(store); const createResponse = await request(app) .post('/api/users') .send({ displayName: 'Integration Fan', country: 'England' }) .expect(201); const userId = createResponse.body.user.id as string; expect(userId).toMatch(/^user-/); expect(createResponse.body.passport.user.points).toBe(0); const firstRank = createResponse.body.passport.stats.rank as number; await request(app) .post('/api/collect') .send({ userId, itemType: 'team', contentId: 'england' }) .expect(200) .expect((response) => { expect(response.body.passport.collections).toHaveLength(1); expect(response.body.passport.user.points).toBeGreaterThan(0); }); await request(app) .post('/api/collect') .send({ userId, itemType: 'team', contentId: 'england' }) .expect(200) .expect((response) => { expect(response.body.achievementEvents).toHaveLength(0); expect(response.body.message).toMatch(/already/i); expect(response.body.passport.collections).toHaveLength(1); }); for (const teamId of ['usa', 'iran', 'wales']) { await request(app) .post('/api/collect') .send({ userId, itemType: 'team', contentId: teamId }) .expect(200); } const groupPassport = await request(app).get(`/api/passport/${userId}`).expect(200); const groupChallenge = groupPassport.body.challengeProgress.find( (progress: { challengeId: string }) => progress.challengeId === 'challenge-group-b-complete' ); expect(groupChallenge).toMatchObject({ complete: true, current: 4, target: 4 }); expect(groupPassport.body.badges.some((badge: { badgeId: string }) => badge.badgeId === 'badge-group-b-complete')).toBe(true); const trivia = seedTriviaQuestions[0]; await request(app) .post(`/api/trivia/${trivia.id}/answer`) .send({ userId, selectedOptionId: trivia.correctOptionId }) .expect(200) .expect((response) => { expect(response.body.message).toMatch(/Correct/i); expect(response.body.passport.triviaAnswers).toHaveLength(1); }); const englandUsa = seedMatches.find((match) => match.id === 'match-england-usa'); expect(englandUsa?.underdogTeamId).toBe('usa'); await request(app) .post('/api/predictions') .send({ userId, matchId: 'match-england-usa', predictedWinnerTeamId: 'usa', confidence: 5 }) .expect(200) .expect((response) => { expect(response.body.passport.predictions).toHaveLength(1); expect(response.body.passport.predictions[0].giantKilling).toBe(true); expect(response.body.passport.badges.some((badge: { badgeId: string }) => badge.badgeId === 'badge-giant-killer')).toBe(true); }); await request(app) .post('/api/memories') .send({ userId, title: 'A night for the underdog', note: 'The crowd noise after backing the upset made the passport feel alive.', matchId: 'match-england-usa', mood: 'shock' }) .expect(200) .expect((response) => { expect(response.body.passport.memories).toHaveLength(1); expect(response.body.passport.badges.some((badge: { badgeId: string }) => badge.badgeId === 'badge-memory-keeper')).toBe(true); }); const finalPassport = await request(app).get(`/api/passport/${userId}`).expect(200); expect(finalPassport.body.stats.rank).toBeLessThan(firstRank); expect(finalPassport.body.achievements.length).toBeGreaterThanOrEqual(7); }); it('returns useful validation and not-found errors', async () => { const store = new DemoStore(); const app = createApp(store); await request(app) .post('/api/users') .send({ displayName: 'A' }) .expect(400) .expect((response) => { expect(response.body.error.code).toBe('VALIDATION_ERROR'); }); await request(app) .get('/api/passport/missing-user') .expect(404) .expect((response) => { expect(response.body.error.code).toBe('USER_NOT_FOUND'); }); }); });