Download packages/api/src/__tests__/attempt.integration.test.ts from Atmint/cpns: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/spaces/Atmint/cpns/resolve/main/packages/api/src/__tests__/attempt.integration.test.ts
- Command line
-
hf download hf://spaces/Atmint/cpns/packages/api/src/__tests__/attempt.integration.test.ts
-
curl -L -o attempt.integration.test.ts https://huggingface.co/spaces/Atmint/cpns/resolve/main/packages/api/src/__tests__/attempt.integration.test.ts
10.4 kB
| import { describe, expect, it, beforeAll, afterAll, mock } from "bun:test"; | |
| import { drizzle } from "drizzle-orm/pglite"; | |
| import * as schema from "../../../db/src/schema"; | |
| import { closeTestPGlite, getTestPGlite } from "./test-setup"; | |
| mock.module("@pram/env/server", () => ({ | |
| env: { | |
| DATABASE_URL: "postgres://localhost:5432/test", | |
| BETTER_AUTH_SECRET: "a".repeat(32), | |
| BETTER_AUTH_URL: "http://localhost:3000", | |
| CORS_ORIGIN: "http://localhost:5173", | |
| API_KEY_ENCRYPTION_KEY: "z".repeat(32), | |
| REDIS_URL: "redis://localhost:6379", | |
| SMTP_HOST: "localhost", | |
| SMTP_USER: "test", | |
| SMTP_PASS: "test", | |
| SMTP_FROM: "test@test.com", | |
| }, | |
| })); | |
| describe("attempt router", () => { | |
| let attemptRouter: any; | |
| let packageId: string; | |
| let questionIds: string[]; | |
| let testDb: any; | |
| function makeCaller(userId: string) { | |
| return attemptRouter.createCaller({ | |
| session: { user: { id: userId }, expiresAt: new Date() }, | |
| auth: null, | |
| }); | |
| } | |
| async function createUserAndCaller(label: string) { | |
| const id = `user-${label}-${Date.now()}`; | |
| await testDb.insert(schema.user).values({ id, name: `User ${label}`, email: `${label}@test.com` }); | |
| return { id, caller: makeCaller(id) }; | |
| } | |
| beforeAll(async () => { | |
| const pg = await getTestPGlite(); | |
| testDb = drizzle(pg, { schema }); | |
| mock.module("@pram/db", () => ({ | |
| __esModule: true, | |
| ...schema, | |
| db: testDb, | |
| })); | |
| const mod = await import("../routers/attempt"); | |
| attemptRouter = mod.attemptRouter; | |
| await testDb.insert(schema.examType).values([ | |
| { id: "IELTS", name: "IELTS", language: "English" }, | |
| ]); | |
| await testDb.insert(schema.sectionType).values([ | |
| { id: "READING", name: "Reading" }, | |
| ]); | |
| const ownerId = "owner-id"; | |
| await testDb.insert(schema.user).values({ id: ownerId, name: "Owner", email: "owner@test.com" }); | |
| const [pkg] = await testDb.insert(schema.testPackage).values({ | |
| title: "IELTS Reading Test", | |
| examTypeId: "IELTS", | |
| creatorUserId: ownerId, | |
| isPublic: true, | |
| }).returning(); | |
| packageId = pkg.id; | |
| const [sec] = await testDb.insert(schema.packageSection).values({ | |
| packageId: pkg.id, | |
| sectionTypeId: "READING", | |
| title: "Reading Section", | |
| orderIndex: 0, | |
| }).returning(); | |
| const qs = await testDb.insert(schema.question).values([ | |
| { | |
| examTypeId: "IELTS", sectionTypeId: "READING", format: "multiple_choice", | |
| passageText: "P".repeat(60), questionText: "What is the main idea?", | |
| options: [{ key: "A", text: "Option A" }, { key: "B", text: "Option B" }, { key: "C", text: "Option C" }, { key: "D", text: "Option D" }], | |
| correctAnswer: "A", explanation: "Because A is correct", difficulty: 2, | |
| skillTags: ["main_idea"], creatorUserId: ownerId, isPublic: true, | |
| }, | |
| { | |
| examTypeId: "IELTS", sectionTypeId: "READING", format: "true_false_not_given", | |
| passageText: "P".repeat(60), questionText: "The author states that...", | |
| correctAnswer: "TRUE", explanation: "It says so", difficulty: 2, | |
| skillTags: ["detail"], creatorUserId: ownerId, isPublic: true, | |
| }, | |
| { | |
| examTypeId: "IELTS", sectionTypeId: "READING", format: "fill_blank", | |
| passageText: "P".repeat(60), questionText: "The answer is ___", | |
| correctAnswer: "paradigm", explanation: "Context clue", difficulty: 3, | |
| skillTags: ["vocabulary"], creatorUserId: ownerId, isPublic: true, | |
| }, | |
| ]).returning(); | |
| questionIds = qs.map((q: any) => q.id); | |
| await testDb.insert(schema.sectionQuestion).values([ | |
| { sectionId: sec.id, questionId: qs[0]!.id, orderIndex: 0 }, | |
| { sectionId: sec.id, questionId: qs[1]!.id, orderIndex: 1 }, | |
| { sectionId: sec.id, questionId: qs[2]!.id, orderIndex: 2 }, | |
| ]); | |
| }); | |
| afterAll(async () => { | |
| await closeTestPGlite(); | |
| }); | |
| it("start creates a new attempt", async () => { | |
| const { caller } = await createUserAndCaller("start"); | |
| const result = await caller.start({ packageId }); | |
| expect(result).toHaveProperty("attemptId"); | |
| }); | |
| it("start rejects duplicate in-progress attempt", { timeout: 15000 }, async () => { | |
| const { caller } = await createUserAndCaller("dup"); | |
| await caller.start({ packageId }); | |
| await Bun.sleep(3100); // wait for rate limit window (3000ms) | |
| expect(caller.start({ packageId })).rejects.toThrow("sedang berjalan"); | |
| }); | |
| it("getById returns attempt with sections and questions", async () => { | |
| const { caller } = await createUserAndCaller("getid"); | |
| const { attemptId } = await caller.start({ packageId }); | |
| const result = await caller.getById({ id: attemptId }); | |
| expect(result.status).toBe("in_progress"); | |
| expect(result.sections).toHaveLength(1); | |
| expect(result.sections[0].questions).toHaveLength(3); | |
| }); | |
| it("getById strips correctAnswer during in_progress", async () => { | |
| const { caller } = await createUserAndCaller("strip"); | |
| const { attemptId } = await caller.start({ packageId }); | |
| const result = await caller.getById({ id: attemptId }); | |
| for (const s of result.sections) { | |
| for (const q of s.questions) { | |
| expect(q.correctAnswer).toBeUndefined(); | |
| expect(q.explanation).toBeUndefined(); | |
| } | |
| } | |
| }); | |
| it("submitAnswer stores user answer", async () => { | |
| const { caller } = await createUserAndCaller("submit"); | |
| const { attemptId } = await caller.start({ packageId }); | |
| const attempt = await caller.getById({ id: attemptId }); | |
| const sectionResultId = attempt.sections[0].sectionResultId; | |
| const result = await caller.submitAnswer({ | |
| attemptId, sectionResultId, | |
| questionId: questionIds[0]!, userAnswer: "A", | |
| }); | |
| expect(result.success).toBe(true); | |
| }); | |
| it("finish scores correct answers", { timeout: 30000 }, async () => { | |
| const { caller } = await createUserAndCaller("score"); | |
| const { attemptId } = await caller.start({ packageId }); | |
| await Bun.sleep(4500); | |
| const attempt = await caller.getById({ id: attemptId }); | |
| const sectionResultId = attempt.sections[0].sectionResultId; | |
| await caller.submitAnswer({ attemptId, sectionResultId, questionId: questionIds[0]!, userAnswer: "A" }); | |
| await Bun.sleep(600); | |
| await caller.submitAnswer({ attemptId, sectionResultId, questionId: questionIds[1]!, userAnswer: "FALSE" }); | |
| await Bun.sleep(600); | |
| await caller.submitAnswer({ attemptId, sectionResultId, questionId: questionIds[2]!, userAnswer: "wrong" }); | |
| const result = await caller.finish({ attemptId }); | |
| expect(result.totalScore).toBe(1); | |
| expect(result.maxScore).toBe(3); | |
| expect(result.percentage).toBe(33); | |
| }); | |
| it("finish sums maxScore across multiple sections", { timeout: 30000 }, async () => { | |
| const ownerId = "multi-section-owner"; | |
| await testDb.insert(schema.user).values({ | |
| id: ownerId, | |
| name: "Multi Section Owner", | |
| email: `multi-section-${Date.now()}@test.com`, | |
| }).onConflictDoNothing(); | |
| const [pkg] = await testDb.insert(schema.testPackage).values({ | |
| title: "Multi Section Test", | |
| examTypeId: "IELTS", | |
| creatorUserId: ownerId, | |
| isPublic: true, | |
| }).returning(); | |
| const sections = await testDb.insert(schema.packageSection).values([ | |
| { | |
| packageId: pkg.id, | |
| sectionTypeId: "READING", | |
| title: "Reading Part 1", | |
| orderIndex: 0, | |
| }, | |
| { | |
| packageId: pkg.id, | |
| sectionTypeId: "READING", | |
| title: "Reading Part 2", | |
| orderIndex: 1, | |
| }, | |
| ]).returning(); | |
| const makeQuestion = (idx: number) => ({ | |
| examTypeId: "IELTS", | |
| sectionTypeId: "READING", | |
| format: "multiple_choice" as const, | |
| passageText: `Passage ${idx} `.repeat(10), | |
| questionText: `Question ${idx}?`, | |
| options: [ | |
| { key: "A", text: "Option A" }, | |
| { key: "B", text: "Option B" }, | |
| { key: "C", text: "Option C" }, | |
| { key: "D", text: "Option D" }, | |
| ], | |
| correctAnswer: "A", | |
| explanation: "Because A is correct", | |
| difficulty: 2, | |
| skillTags: ["main_idea"], | |
| creatorUserId: ownerId, | |
| isPublic: true, | |
| }); | |
| const sectionOneQuestions = await testDb.insert(schema.question).values( | |
| Array.from({ length: 10 }, (_, idx) => makeQuestion(idx + 1)), | |
| ).returning(); | |
| const sectionTwoQuestions = await testDb.insert(schema.question).values( | |
| Array.from({ length: 10 }, (_, idx) => makeQuestion(idx + 11)), | |
| ).returning(); | |
| await testDb.insert(schema.sectionQuestion).values([ | |
| ...sectionOneQuestions.map((q, orderIndex) => ({ | |
| sectionId: sections[0]!.id, | |
| questionId: q.id, | |
| orderIndex, | |
| })), | |
| ...sectionTwoQuestions.map((q, orderIndex) => ({ | |
| sectionId: sections[1]!.id, | |
| questionId: q.id, | |
| orderIndex, | |
| })), | |
| ]); | |
| const { caller } = await createUserAndCaller("multisection"); | |
| const { attemptId } = await caller.start({ packageId: pkg.id }); | |
| await Bun.sleep(4500); | |
| const result = await caller.finish({ attemptId }); | |
| expect(result.maxScore).toBe(20); | |
| expect(typeof result.maxScore).toBe("number"); | |
| }); | |
| it("finish rejects attempts under 5 seconds", async () => { | |
| const { caller } = await createUserAndCaller("fastfinish"); | |
| const { attemptId } = await caller.start({ packageId }); | |
| expect(caller.finish({ attemptId })).rejects.toThrow("terlalu cepat"); | |
| }); | |
| it("getActiveAttempt returns in-progress attempt", async () => { | |
| const { caller } = await createUserAndCaller("active"); | |
| const { attemptId } = await caller.start({ packageId }); | |
| const result = await caller.getActiveAttempt({ packageId }); | |
| expect(result).not.toBeNull(); | |
| expect(result.id).toBe(attemptId); | |
| expect(result.status).toBe("in_progress"); | |
| }); | |
| it("abandon marks attempt as abandoned", async () => { | |
| const { caller } = await createUserAndCaller("abandon"); | |
| const { attemptId } = await caller.start({ packageId }); | |
| const result = await caller.abandon({ attemptId }); | |
| expect(result.success).toBe(true); | |
| }); | |
| it("myAttempts returns paginated list", async () => { | |
| const { caller } = await createUserAndCaller("mylist"); | |
| await caller.start({ packageId }); | |
| const result = await caller.myAttempts(); | |
| expect(result.attempts.length).toBeGreaterThan(0); | |
| expect(result.total).toBeGreaterThan(0); | |
| }); | |
| }); | |