257 lines
7.3 KiB
TypeScript
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);
|
|
});
|
|
});
|