import { beforeEach, describe, expect, it, vi } from "vitest" const mocks = vi.hoisted(() => { const authMock = vi.fn() const revalidatePathMock = vi.fn() const createIdMock = vi.fn() const ensureLimitMock = vi.fn() const ensureWhereMock = vi.fn(() => ({ limit: ensureLimitMock })) const ensureInnerJoinSecondMock = vi.fn(() => ({ where: ensureWhereMock })) const ensureInnerJoinFirstMock = vi.fn(() => ({ innerJoin: ensureInnerJoinSecondMock })) const countWhereMock = vi.fn() const selectFromMock = vi.fn(() => ({ innerJoin: ensureInnerJoinFirstMock, where: countWhereMock })) const selectMock = vi.fn(() => ({ from: selectFromMock })) const assignmentFindFirstMock = vi.fn() const assignmentTargetFindFirstMock = vi.fn() const submissionFindFirstMock = vi.fn() const txAnswerFindFirstMock = vi.fn() const insertValuesMock = vi.fn() const insertMock = vi.fn(() => ({ values: insertValuesMock })) const txInsertValuesMock = vi.fn() const txInsertMock = vi.fn(() => ({ values: txInsertValuesMock })) const updateWhereMock = vi.fn() const updateSetMock = vi.fn(() => ({ where: updateWhereMock })) const updateMock = vi.fn(() => ({ set: updateSetMock })) const txUpdateWhereMock = vi.fn() const txUpdateSetMock = vi.fn(() => ({ where: txUpdateWhereMock })) const txUpdateMock = vi.fn(() => ({ set: txUpdateSetMock })) const transactionMock = vi.fn(async (callback: (tx: unknown) => Promise) => callback({ query: { homeworkAnswers: { findFirst: txAnswerFindFirstMock } }, update: txUpdateMock, insert: txInsertMock, }) ) return { authMock, revalidatePathMock, createIdMock, ensureLimitMock, countWhereMock, selectMock, assignmentFindFirstMock, assignmentTargetFindFirstMock, submissionFindFirstMock, txAnswerFindFirstMock, insertValuesMock, insertMock, txInsertValuesMock, txInsertMock, updateSetMock, updateMock, txUpdateSetMock, txUpdateMock, transactionMock, } }) vi.mock("@/auth", () => ({ auth: mocks.authMock, })) vi.mock("next/cache", () => ({ revalidatePath: mocks.revalidatePathMock, })) vi.mock("@paralleldrive/cuid2", () => ({ createId: mocks.createIdMock, })) vi.mock("@/shared/db", () => ({ db: { select: mocks.selectMock, query: { homeworkAssignments: { findFirst: mocks.assignmentFindFirstMock }, homeworkAssignmentTargets: { findFirst: mocks.assignmentTargetFindFirstMock }, homeworkSubmissions: { findFirst: mocks.submissionFindFirstMock }, }, insert: mocks.insertMock, update: mocks.updateMock, transaction: mocks.transactionMock, }, })) vi.mock("@/shared/db/schema", () => ({ classes: { id: "id", teacherId: "teacherId" }, classEnrollments: { classId: "classId", studentId: "studentId", status: "status" }, classSubjectTeachers: { classId: "classId", teacherId: "teacherId", subjectId: "subjectId" }, exams: { id: "id" }, homeworkAnswers: { id: "id", submissionId: "submissionId", questionId: "questionId" }, homeworkAssignmentQuestions: { assignmentId: "assignmentId" }, homeworkAssignmentTargets: { assignmentId: "assignmentId", studentId: "studentId" }, homeworkAssignments: { id: "id", status: "status" }, homeworkSubmissions: { id: "id", assignmentId: "assignmentId", studentId: "studentId", status: "status", submittedAt: "submittedAt", isLate: "isLate", updatedAt: "updatedAt", score: "score", }, roles: { id: "id", name: "name" }, users: { id: "id" }, usersToRoles: { userId: "userId", roleId: "roleId" }, })) import { gradeHomeworkSubmissionAction, saveHomeworkAnswerAction, startHomeworkSubmissionAction, submitHomeworkAction, } from "@/modules/homework/actions" describe("homework action flow", () => { beforeEach(() => { vi.resetAllMocks() }) it("starts submission for assigned student", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_student" } }) mocks.ensureLimitMock.mockResolvedValue([{ id: "u_student" }]) mocks.assignmentFindFirstMock.mockResolvedValue({ id: "a_1", status: "published", availableAt: null, maxAttempts: 2, }) mocks.assignmentTargetFindFirstMock.mockResolvedValue({ assignmentId: "a_1", studentId: "u_student" }) mocks.countWhereMock.mockResolvedValue([{ c: 0 }]) mocks.createIdMock.mockReturnValue("sub_1") const formData = new FormData() formData.set("assignmentId", "a_1") const result = await startHomeworkSubmissionAction(null, formData) expect(result).toEqual({ success: true, message: "Started", data: "sub_1" }) expect(mocks.insertValuesMock).toHaveBeenCalledWith( expect.objectContaining({ id: "sub_1", assignmentId: "a_1", studentId: "u_student", attemptNo: 1, status: "started", }) ) expect(mocks.revalidatePathMock).toHaveBeenCalledWith("/student/learning/assignments") }) it("blocks submission when assignment is past due", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_student" } }) mocks.ensureLimitMock.mockResolvedValue([{ id: "u_student" }]) mocks.submissionFindFirstMock.mockResolvedValue({ id: "sub_1", studentId: "u_student", status: "started", assignment: { dueAt: new Date(Date.now() - 60_000), allowLate: false, lateDueAt: null, }, }) const formData = new FormData() formData.set("submissionId", "sub_1") const result = await submitHomeworkAction(null, formData) expect(result).toEqual({ success: false, message: "Past due" }) }) it("submits started homework before due time", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_student" } }) mocks.ensureLimitMock.mockResolvedValue([{ id: "u_student" }]) mocks.submissionFindFirstMock.mockResolvedValue({ id: "sub_2", studentId: "u_student", status: "started", assignment: { dueAt: new Date(Date.now() + 60_000), allowLate: false, lateDueAt: null, }, }) const formData = new FormData() formData.set("submissionId", "sub_2") const result = await submitHomeworkAction(null, formData) expect(result).toEqual({ success: true, message: "Submitted", data: "sub_2" }) expect(mocks.updateSetMock).toHaveBeenCalledWith(expect.objectContaining({ status: "submitted", isLate: false })) expect(mocks.revalidatePathMock).toHaveBeenCalledWith("/teacher/homework/submissions") expect(mocks.revalidatePathMock).toHaveBeenCalledWith("/student/learning/assignments") }) it("blocks start when attempts are exhausted", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_student" } }) mocks.ensureLimitMock.mockResolvedValue([{ id: "u_student" }]) mocks.assignmentFindFirstMock.mockResolvedValue({ id: "a_2", status: "published", availableAt: null, maxAttempts: 1, }) mocks.assignmentTargetFindFirstMock.mockResolvedValue({ assignmentId: "a_2", studentId: "u_student" }) mocks.countWhereMock.mockResolvedValue([{ c: 1 }]) const formData = new FormData() formData.set("assignmentId", "a_2") const result = await startHomeworkSubmissionAction(null, formData) expect(result).toEqual({ success: false, message: "No attempts left" }) }) it("grades submission and writes total score", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_teacher" } }) mocks.ensureLimitMock.mockResolvedValue([{ id: "u_teacher", role: "teacher" }]) const formData = new FormData() formData.set("submissionId", "sub_1") formData.set( "answersJson", JSON.stringify([ { id: "ans_1", score: 5, feedback: "good" }, { id: "ans_2", score: 3, feedback: "" }, ]) ) const result = await gradeHomeworkSubmissionAction(null, formData) expect(result).toEqual({ success: true, message: "Grading saved" }) expect(mocks.updateMock).toHaveBeenCalledTimes(3) expect(mocks.updateSetMock).toHaveBeenCalledWith(expect.objectContaining({ score: 8, status: "graded" })) expect(mocks.revalidatePathMock).toHaveBeenCalledWith("/teacher/homework/submissions") }) it("saves new answer for started submission", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_student" } }) mocks.ensureLimitMock.mockResolvedValue([{ id: "u_student" }]) mocks.submissionFindFirstMock.mockResolvedValue({ id: "sub_3", studentId: "u_student", status: "started", assignment: {}, }) mocks.txAnswerFindFirstMock.mockResolvedValue(null) mocks.createIdMock.mockReturnValue("ans_new") const formData = new FormData() formData.set("submissionId", "sub_3") formData.set("questionId", "q_3") formData.set("answerJson", JSON.stringify({ text: "answer content" })) const result = await saveHomeworkAnswerAction(null, formData) expect(result).toEqual({ success: true, message: "Saved", data: "sub_3" }) expect(mocks.txInsertValuesMock).toHaveBeenCalledWith( expect.objectContaining({ id: "ans_new", submissionId: "sub_3", questionId: "q_3", }) ) }) it("updates existing answer for started submission", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_student" } }) mocks.ensureLimitMock.mockResolvedValue([{ id: "u_student" }]) mocks.submissionFindFirstMock.mockResolvedValue({ id: "sub_4", studentId: "u_student", status: "started", assignment: {}, }) mocks.txAnswerFindFirstMock.mockResolvedValue({ id: "ans_existing" }) const formData = new FormData() formData.set("submissionId", "sub_4") formData.set("questionId", "q_4") formData.set("answerJson", JSON.stringify({ text: "updated answer" })) const result = await saveHomeworkAnswerAction(null, formData) expect(result).toEqual({ success: true, message: "Saved", data: "sub_4" }) expect(mocks.txUpdateMock).toHaveBeenCalledTimes(1) expect(mocks.txUpdateSetMock).toHaveBeenCalledWith(expect.objectContaining({ answerContent: { text: "updated answer" } })) }) })