import superjson from 'superjson'; import { beforeEach, describe, expect, it } from 'vitest'; import { newClient } from '../helpers/api'; import { resetDatabase } from '../helpers/database'; import { createAnswer, createHunt, createHuntMember, createQuest, createTeam, createUser, TEST_PASSWORD } from '../helpers/factories'; import { prisma } from './setup'; beforeEach(resetDatabase); /** Hunt with one quest, one team and a submitted (non-final) answer. */ async function seedAnswer() { const creator = await createUser(); const hunt = await createHunt(creator.id); const quest = await createQuest(hunt.id); const owner = await createUser(); const team = await createTeam(hunt.id, owner.id); const answer = await createAnswer({ teamId: team.id, questId: quest.id, memberId: owner.id }); return { creator, hunt, quest, owner, team, answer }; } const reviewBody = { score: 42, review: 'Nice shot!', showcase: true, internalNote: 'check exposure', rankingValue: 7 }; describe('review answer (POST /api/admin/answer/[answerId]/review)', () => { it('requires a session', async () => { const res = await newClient().post('/api/admin/answer/1/review', { ...reviewBody, showcase: false }); expect(res.status).toBe(401); }); it('returns 404 for unknown answers', async () => { const { hunt } = await seedAnswer(); const organizer = await createUser({ email: 'organizer@test.dev' }); await createHuntMember(hunt.id, organizer.id); const client = newClient(); await client.login('organizer@test.dev', TEST_PASSWORD); const res = await client.post('/api/admin/answer/999999/review', { ...reviewBody, showcase: false }); expect(res.status).toBe(404); }); it('denies non-members of the hunt with 404', async () => { const { answer } = await seedAnswer(); await createUser({ email: 'stranger@test.dev' }); const client = newClient(); await client.login('stranger@test.dev', TEST_PASSWORD); const res = await client.post( `/api/admin/answer/${answer.id}/review`, reviewBody ); expect(res.status).toBe(404); expect(await prisma.review.count()).toBe(0); }); it('creates a review as a hunt member', async () => { const { hunt, answer } = await seedAnswer(); const organizer = await createUser({ email: 'organizer@test.dev' }); await createHuntMember(hunt.id, organizer.id); const client = newClient(); await client.login('organizer@test.dev', TEST_PASSWORD); const res = await client.post( `/api/admin/answer/${answer.id}/review`, reviewBody ); expect(res.status).toBe(200); expect(await res.json()).toEqual({ id: answer.id }); const review = await prisma.review.findUniqueOrThrow({ where: { answerId: answer.id } }); expect(review).toMatchObject({ score: 42, review: 'Nice shot!', showcase: true, internalNote: 'check exposure', rankingValue: 7, reviewerId: organizer.id }); }); it('updates the existing review on a second pass', async () => { const { hunt, answer } = await seedAnswer(); const first = await createUser({ email: 'first@test.dev' }); const second = await createUser({ email: 'second@test.dev' }); await createHuntMember(hunt.id, first.id); await createHuntMember(hunt.id, second.id); const firstClient = newClient(); await firstClient.login('first@test.dev', TEST_PASSWORD); await firstClient.post(`/api/admin/answer/${answer.id}/review`, reviewBody); const secondClient = newClient(); await secondClient.login('second@test.dev', TEST_PASSWORD); const res = await secondClient.post( `/api/admin/answer/${answer.id}/review`, { ...reviewBody, score: 100, showcase: false } ); expect(res.status).toBe(200); const reviews = await prisma.review.findMany({ where: { answerId: answer.id } }); expect(reviews).toHaveLength(1); expect(reviews[0]).toMatchObject({ score: 100, showcase: false, reviewerId: second.id }); }); it('rejects invalid scores with 400', async () => { const { hunt, answer } = await seedAnswer(); const organizer = await createUser({ email: 'organizer@test.dev' }); await createHuntMember(hunt.id, organizer.id); const client = newClient(); await client.login('organizer@test.dev', TEST_PASSWORD); const res = await client.post(`/api/admin/answer/${answer.id}/review`, { ...reviewBody, score: -5 }); expect(res.status).toBe(400); }); }); describe('hunt admin detail (GET /api/admin/hunt/[huntId])', () => { it('denies non-members with 404', async () => { const { hunt } = await seedAnswer(); await createUser({ email: 'stranger@test.dev' }); const client = newClient(); await client.login('stranger@test.dev', TEST_PASSWORD); const res = await client.get(`/api/admin/hunt/${hunt.id}`); expect(res.status).toBe(404); }); it('responds with a SuperJSON envelope containing real Dates', async () => { const { hunt } = await seedAnswer(); const organizer = await createUser({ email: 'organizer@test.dev' }); await createHuntMember(hunt.id, organizer.id); const start = new Date('2026-09-01T12:00:00Z'); await prisma.hunt.update({ where: { id: hunt.id }, data: { start } }); const client = newClient(); await client.login('organizer@test.dev', TEST_PASSWORD); const res = await client.get(`/api/admin/hunt/${hunt.id}`); expect(res.status).toBe(200); const raw = await res.text(); const envelope = JSON.parse(raw); // SuperJSON wraps the payload in { json, meta } expect(envelope.json).toBeDefined(); expect(envelope.meta).toBeDefined(); const huntDto = superjson.parse<{ start: Date; id: number }>(raw); expect(huntDto.id).toBe(hunt.id); expect(huntDto.start).toBeInstanceOf(Date); expect(huntDto.start.toISOString()).toBe(start.toISOString()); }); });