2026-03-29 00:32:24 -04:00

257 lines
7.3 KiB
TypeScript

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<string, string>();
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);
});
});