import { beforeEach, describe, expect, it } from 'vitest'; import { newClient } from '../helpers/api'; import { resetDatabase } from '../helpers/database'; import { createAnswer, createHunt, createQuest, createTeam, createUser, TEST_PASSWORD } from '../helpers/factories'; import { prisma } from './setup'; beforeEach(resetDatabase); /** Hunt (with revealed quests), a quest and a team owned by the given user. */ async function seedPlayground(ownerEmail: string) { const creator = await createUser(); const hunt = await createHunt(creator.id, { revealQuests: true }); const quest = await createQuest(hunt.id, { textRequired: true }); const owner = await createUser({ email: ownerEmail }); const team = await createTeam(hunt.id, owner.id); return { hunt, quest, owner, team }; } function form(fields: Record) { const data = new FormData(); for (const [key, value] of Object.entries(fields)) { data.append(key, value); } return data; } describe('submit answer (POST /api/quest/[questId]/submit)', () => { it('requires a session', async () => { const res = await newClient().post( '/api/quest/1/submit', form({ answer: 'hello' }) ); expect(res.status).toBe(401); }); it('returns 404 for unknown quests', async () => { await createUser({ email: 'owner@test.dev' }); const client = newClient(); await client.login('owner@test.dev', TEST_PASSWORD); const res = await client.post( '/api/quest/999999/submit', form({ answer: 'hello' }) ); expect(res.status).toBe(404); }); it('returns 403 when the user has no team in the hunt', async () => { const { quest } = await seedPlayground('owner@test.dev'); const loner = await createUser({ email: 'loner@test.dev' }); const client = newClient(); await client.login('loner@test.dev', TEST_PASSWORD); const res = await client.post( `/api/quest/${quest.id}/submit`, form({ answer: 'hello' }) ); expect(res.status).toBe(403); expect( await prisma.questAnswer.count({ where: { memberId: loner.id } }) ).toBe(0); }); it('stores a text-only answer', async () => { const { quest, team, owner } = await seedPlayground('owner@test.dev'); const client = newClient(); await client.login('owner@test.dev', TEST_PASSWORD); const res = await client.post( `/api/quest/${quest.id}/submit`, form({ answer: 'the answer', lang: 'en' }) ); expect(res.status).toBe(200); expect(await res.json()).toEqual({ ok: true }); const answer = await prisma.questAnswer.findFirst({ where: { teamId: team.id, questId: quest.id } }); expect(answer).toMatchObject({ text: 'the answer', lang: 'en', final: false, memberId: owner.id }); }); it('exposes the stored answer with ISO dates via the quest endpoint', async () => { const { quest } = await seedPlayground('owner@test.dev'); const client = newClient(); await client.login('owner@test.dev', TEST_PASSWORD); await client.post( `/api/quest/${quest.id}/submit`, form({ answer: 'the answer' }) ); const res = await client.get(`/api/quest/${quest.id}`); expect(res.status).toBe(200); const body = await res.json(); expect(body.answer.text).toBe('the answer'); expect(new Date(body.answer.createdAt).getTime()).not.toBeNaN(); expect(new Date(body.answer.updatedAt).getTime()).not.toBeNaN(); }); it('updates an existing answer when updatedAt is current', async () => { const { quest, team } = await seedPlayground('owner@test.dev'); const client = newClient(); await client.login('owner@test.dev', TEST_PASSWORD); await client.post( `/api/quest/${quest.id}/submit`, form({ answer: 'first try' }) ); const existing = await prisma.questAnswer.findFirstOrThrow({ where: { teamId: team.id, questId: quest.id } }); const res = await client.post( `/api/quest/${quest.id}/submit`, form({ answer: 'final try', updatedAt: existing.updatedAt.toISOString(), final: 'true' }) ); expect(res.status).toBe(200); const updated = await prisma.questAnswer.findFirstOrThrow({ where: { teamId: team.id, questId: quest.id } }); expect(updated.id).toBe(existing.id); expect(updated.text).toBe('final try'); expect(updated.final).toBe(true); }); it('rejects updates based on stale data with 409', async () => { const { quest } = await seedPlayground('owner@test.dev'); const client = newClient(); await client.login('owner@test.dev', TEST_PASSWORD); await client.post( `/api/quest/${quest.id}/submit`, form({ answer: 'first try' }) ); // missing updatedAt although an answer exists expect( ( await client.post( `/api/quest/${quest.id}/submit`, form({ answer: 'second try' }) ) ).status ).toBe(409); // outdated updatedAt expect( ( await client.post( `/api/quest/${quest.id}/submit`, form({ answer: 'second try', updatedAt: new Date(Date.now() - 60_000).toISOString() }) ) ).status ).toBe(409); }); it('rejects submissions for finalized answers with 418', async () => { const { quest, team, owner } = await seedPlayground('owner@test.dev'); await createAnswer( { teamId: team.id, questId: quest.id, memberId: owner.id }, { text: 'locked in', final: true } ); const client = newClient(); await client.login('owner@test.dev', TEST_PASSWORD); const res = await client.post( `/api/quest/${quest.id}/submit`, form({ answer: 'too late', updatedAt: new Date().toISOString() }) ); expect(res.status).toBe(418); }); });