import { describe, test, expect, beforeAll, afterAll, mock } from "bun:test"; import { db } from "@/db"; import { annotators, quizSessions } from "@/db/schema"; import { eq } from "drizzle-orm"; // Mock cookie store that captures set/delete calls const cookieJar = new Map(); const mockCookieStore = { get(name: string) { const value = cookieJar.get(name); return value ? { value } : undefined; }, set(name: string, value: string, _opts?: unknown) { cookieJar.set(name, value); }, delete(name: string) { cookieJar.delete(name); }, }; // Mock next/headers before importing route handlers mock.module("next/headers", () => ({ cookies: async () => mockCookieStore, })); // Set up a valid session using the same auth module (it writes to mocked cookies) import { createSession } from "@/lib/auth"; import { QUIZ_QUESTIONS } from "@/lib/quiz-questions"; // Import route handlers after mock is set up const { GET, POST } = await import("../route"); const TEST_USER = { id: "test-quiz-user", displayName: "Quiz Tester", password: "test", }; beforeAll(async () => { await db .delete(quizSessions) .where(eq(quizSessions.annotatorId, TEST_USER.id)); await db.delete(annotators).where(eq(annotators.id, TEST_USER.id)); await db.insert(annotators).values(TEST_USER); await createSession(TEST_USER.id); }); afterAll(async () => { await db .delete(quizSessions) .where(eq(quizSessions.annotatorId, TEST_USER.id)); await db.delete(annotators).where(eq(annotators.id, TEST_USER.id)); }); function makeRequest(body: unknown): Request { return new Request("http://localhost:3000/api/quiz", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); } describe("POST /api/quiz {action: 'start'}", () => { test("returns quizSessionId and 8 questions", async () => { const res = await POST(makeRequest({ action: "start" })); const data = await res.json(); expect(res.status).toBe(200); expect(data.quizSessionId).toBeDefined(); expect(typeof data.quizSessionId).toBe("string"); expect(data.questions).toHaveLength(8); expect(data.questions[0]).toHaveProperty("id"); expect(data.questions[0]).toHaveProperty("paragraphText"); expect(data.questions[0]).toHaveProperty("options"); expect(data.questions[0]).toHaveProperty("correctAnswer"); }); }); describe("POST /api/quiz {action: 'answer'}", () => { let quizSessionId: string; let questions: Array<{ id: string; correctAnswer: string; explanation: string; }>; beforeAll(async () => { const res = await POST(makeRequest({ action: "start" })); const data = await res.json(); quizSessionId = data.quizSessionId; questions = data.questions; }); test("returns correct: true for a correct answer", async () => { const q = questions[0]; const res = await POST( makeRequest({ action: "answer", quizSessionId, questionId: q.id, answer: q.correctAnswer, }), ); const data = await res.json(); expect(res.status).toBe(200); expect(data.correct).toBe(true); expect(data.explanation).toBe(q.explanation); expect(data.completed).toBe(false); }); test("returns correct: false for a wrong answer", async () => { const q = questions[1]; // Pick an answer that is NOT the correct one const wrongAnswer = q.correctAnswer === q.correctAnswer ? QUIZ_QUESTIONS.find((qq) => qq.id === q.id)!.options.find( (o) => o.value !== q.correctAnswer, )!.value : q.correctAnswer; const res = await POST( makeRequest({ action: "answer", quizSessionId, questionId: q.id, answer: wrongAnswer, }), ); const data = await res.json(); expect(res.status).toBe(200); expect(data.correct).toBe(false); expect(data.correctAnswer).toBe(q.correctAnswer); expect(data.explanation).toBeDefined(); expect(data.completed).toBe(false); }); test("rejects duplicate answer for same question", async () => { const q = questions[0]; // already answered above const res = await POST( makeRequest({ action: "answer", quizSessionId, questionId: q.id, answer: q.correctAnswer, }), ); expect(res.status).toBe(400); const data = await res.json(); expect(data.error).toBe("Question already answered"); }); }); describe("Full quiz flow - all correct", () => { let quizSessionId: string; let questions: Array<{ id: string; correctAnswer: string; }>; test("answering all 8 correctly returns passed: true", async () => { const startRes = await POST(makeRequest({ action: "start" })); const startData = await startRes.json(); quizSessionId = startData.quizSessionId; questions = startData.questions; let lastData: { score?: number; passed?: boolean; completed: boolean } = { completed: false, }; for (let i = 0; i < questions.length; i++) { const q = questions[i]; const res = await POST( makeRequest({ action: "answer", quizSessionId, questionId: q.id, answer: q.correctAnswer, }), ); lastData = await res.json(); } expect(lastData.completed).toBe(true); expect(lastData.score).toBe(8); expect(lastData.passed).toBe(true); }); test("GET returns hasPassedQuiz: true after passing", async () => { const res = await GET(); const data = await res.json(); expect(res.status).toBe(200); expect(data.hasPassedQuiz).toBe(true); expect(data.quizSessionId).toBeDefined(); }); }); describe("Full quiz flow - failing score", () => { test("answering with 5 wrong returns passed: false", async () => { // Clean up passed quizzes so GET doesn't short-circuit await db .delete(quizSessions) .where(eq(quizSessions.annotatorId, TEST_USER.id)); const startRes = await POST(makeRequest({ action: "start" })); const startData = await startRes.json(); const quizSessionId = startData.quizSessionId; const questions: Array<{ id: string; correctAnswer: string }> = startData.questions; let lastData: { score?: number; passed?: boolean; completed: boolean } = { completed: false, }; for (let i = 0; i < questions.length; i++) { const q = questions[i]; const fullQ = QUIZ_QUESTIONS.find((qq) => qq.id === q.id)!; // Get wrong answer for first 5, correct for last 3 let answer: string; if (i < 5) { const wrongOption = fullQ.options.find( (o) => o.value !== q.correctAnswer, ); answer = wrongOption ? wrongOption.value : q.correctAnswer; } else { answer = q.correctAnswer; } const res = await POST( makeRequest({ action: "answer", quizSessionId, questionId: q.id, answer, }), ); lastData = await res.json(); } expect(lastData.completed).toBe(true); expect(lastData.score).toBe(3); expect(lastData.passed).toBe(false); }); test("GET returns hasPassedQuiz: false after failing", async () => { const res = await GET(); const data = await res.json(); expect(res.status).toBe(200); expect(data.hasPassedQuiz).toBe(false); }); });