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 classLimitMock = vi.fn() const enrollmentWhereMock = vi.fn() const subjectTeacherWhereMock = vi.fn() const examFindFirstMock = vi.fn() const txInsertValuesMock = vi.fn() const txInsertMock = vi.fn(() => ({ values: txInsertValuesMock })) const transactionMock = vi.fn(async (callback: (tx: { insert: typeof txInsertMock }) => unknown) => callback({ insert: txInsertMock }) ) const schema = { classes: { id: "id", teacherId: "teacherId" }, classEnrollments: { classId: "classId", studentId: "studentId", status: "status" }, classSubjectTeachers: { classId: "classId", teacherId: "teacherId", subjectId: "subjectId" }, exams: { id: "id", subjectId: "subjectId", title: "title", structure: "structure" }, homeworkAnswers: { id: "id" }, homeworkAssignmentQuestions: { assignmentId: "assignmentId", questionId: "questionId" }, homeworkAssignmentTargets: { assignmentId: "assignmentId", studentId: "studentId" }, homeworkAssignments: { id: "id" }, homeworkSubmissions: { id: "id" }, roles: { id: "id", name: "name" }, users: { id: "id" }, usersToRoles: { userId: "userId", roleId: "roleId" }, } return { authMock, revalidatePathMock, createIdMock, ensureLimitMock, classLimitMock, enrollmentWhereMock, subjectTeacherWhereMock, examFindFirstMock, txInsertValuesMock, txInsertMock, transactionMock, schema, } }) vi.mock("@/auth", () => ({ auth: mocks.authMock, })) vi.mock("next/cache", () => ({ revalidatePath: mocks.revalidatePathMock, })) vi.mock("@paralleldrive/cuid2", () => ({ createId: mocks.createIdMock, })) vi.mock("@/shared/db/schema", () => mocks.schema) vi.mock("@/shared/db", () => ({ db: { select: () => ({ from: (table: unknown) => { if (table === mocks.schema.users) { return { innerJoin: () => ({ innerJoin: () => ({ where: () => ({ limit: mocks.ensureLimitMock, }), }), }), } } if (table === mocks.schema.classes) { return { where: () => ({ limit: mocks.classLimitMock, }), } } if (table === mocks.schema.classEnrollments) { return { innerJoin: () => ({ where: mocks.enrollmentWhereMock, }), } } if (table === mocks.schema.classSubjectTeachers) { return { where: mocks.subjectTeacherWhereMock, } } return { where: () => ({ limit: vi.fn().mockResolvedValue([]), }), } }, }), query: { exams: { findFirst: mocks.examFindFirstMock, }, }, transaction: mocks.transactionMock, }, })) import { createHomeworkAssignmentAction } from "@/modules/homework/actions" describe("createHomeworkAssignmentAction", () => { beforeEach(() => { vi.resetAllMocks() }) it("creates published assignment from exam with targets", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_admin" } }) mocks.ensureLimitMock.mockResolvedValue([{ id: "u_admin", role: "admin" }]) mocks.classLimitMock.mockResolvedValue([{ id: "class_1", teacherId: "teacher_1" }]) mocks.examFindFirstMock.mockResolvedValue({ id: "exam_1", title: "Exam A", subjectId: "subject_1", structure: { sections: [] }, questions: [{ questionId: "q_1", score: 10, order: 1 }], }) mocks.enrollmentWhereMock.mockResolvedValue([{ studentId: "stu_1" }, { studentId: "stu_2" }]) mocks.createIdMock.mockReturnValue("assignment_1") const formData = new FormData() formData.set("sourceExamId", "exam_1") formData.set("classId", "class_1") formData.set("publish", "true") const result = await createHomeworkAssignmentAction(null, formData) expect(result).toEqual({ success: true, message: "Assignment created", data: "assignment_1" }) expect(mocks.txInsertValuesMock).toHaveBeenCalledTimes(3) expect(mocks.revalidatePathMock).toHaveBeenCalledWith("/teacher/homework/assignments") expect(mocks.revalidatePathMock).toHaveBeenCalledWith("/teacher/homework/submissions") }) it("returns not found when source exam does not exist", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_admin" } }) mocks.ensureLimitMock.mockResolvedValue([{ id: "u_admin", role: "admin" }]) mocks.classLimitMock.mockResolvedValue([{ id: "class_1", teacherId: "teacher_1" }]) mocks.examFindFirstMock.mockResolvedValue(null) const formData = new FormData() formData.set("sourceExamId", "missing_exam") formData.set("classId", "class_1") formData.set("publish", "true") const result = await createHomeworkAssignmentAction(null, formData) expect(result).toEqual({ success: false, message: "Exam not found" }) expect(mocks.transactionMock).not.toHaveBeenCalled() }) it("blocks publish when class has no active students", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_admin" } }) mocks.ensureLimitMock.mockResolvedValue([{ id: "u_admin", role: "admin" }]) mocks.classLimitMock.mockResolvedValue([{ id: "class_2", teacherId: "teacher_2" }]) mocks.examFindFirstMock.mockResolvedValue({ id: "exam_2", title: "Exam B", subjectId: "subject_2", structure: { sections: [] }, questions: [{ questionId: "q_2", score: 5, order: 1 }], }) mocks.enrollmentWhereMock.mockResolvedValue([]) const formData = new FormData() formData.set("sourceExamId", "exam_2") formData.set("classId", "class_2") formData.set("publish", "true") const result = await createHomeworkAssignmentAction(null, formData) expect(result).toEqual({ success: false, message: "No active students in this class" }) expect(mocks.transactionMock).not.toHaveBeenCalled() }) it("blocks teacher when not assigned to class", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_teacher" } }) mocks.ensureLimitMock.mockResolvedValue([{ id: "u_teacher", role: "teacher" }]) mocks.classLimitMock.mockResolvedValue([{ id: "class_3", teacherId: "owner_teacher" }]) mocks.examFindFirstMock.mockResolvedValue({ id: "exam_3", title: "Exam C", subjectId: "subject_3", structure: { sections: [] }, questions: [{ questionId: "q_3", score: 5, order: 1 }], }) mocks.subjectTeacherWhereMock.mockResolvedValue([]) const formData = new FormData() formData.set("sourceExamId", "exam_3") formData.set("classId", "class_3") formData.set("publish", "true") const result = await createHomeworkAssignmentAction(null, formData) expect(result).toEqual({ success: false, message: "Not assigned to this class" }) expect(mocks.transactionMock).not.toHaveBeenCalled() }) it("blocks teacher when exam subject is not assigned", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_teacher" } }) mocks.ensureLimitMock.mockResolvedValue([{ id: "u_teacher", role: "teacher" }]) mocks.classLimitMock.mockResolvedValue([{ id: "class_4", teacherId: "owner_teacher" }]) mocks.examFindFirstMock.mockResolvedValue({ id: "exam_4", title: "Exam D", subjectId: "subject_math", structure: { sections: [] }, questions: [{ questionId: "q_4", score: 10, order: 1 }], }) mocks.subjectTeacherWhereMock.mockResolvedValue([{ subjectId: "subject_english" }]) const formData = new FormData() formData.set("sourceExamId", "exam_4") formData.set("classId", "class_4") formData.set("publish", "true") const result = await createHomeworkAssignmentAction(null, formData) expect(result).toEqual({ success: false, message: "Not assigned to this subject" }) expect(mocks.transactionMock).not.toHaveBeenCalled() }) it("allows teacher assigned subject to publish", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_teacher" } }) mocks.ensureLimitMock.mockResolvedValue([{ id: "u_teacher", role: "teacher" }]) mocks.classLimitMock.mockResolvedValue([{ id: "class_5", teacherId: "owner_teacher" }]) mocks.examFindFirstMock.mockResolvedValue({ id: "exam_5", title: "Exam E", subjectId: "subject_science", structure: { sections: [] }, questions: [{ questionId: "q_5", score: 8, order: 1 }], }) mocks.subjectTeacherWhereMock.mockResolvedValue([{ subjectId: "subject_science" }]) mocks.enrollmentWhereMock.mockResolvedValue([{ studentId: "stu_5" }]) mocks.createIdMock.mockReturnValue("assignment_5") const formData = new FormData() formData.set("sourceExamId", "exam_5") formData.set("classId", "class_5") formData.set("publish", "true") const result = await createHomeworkAssignmentAction(null, formData) expect(result).toEqual({ success: true, message: "Assignment created", data: "assignment_5" }) expect(mocks.txInsertValuesMock).toHaveBeenCalledTimes(3) }) it("returns exam subject missing for teacher-assigned class", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_teacher" } }) mocks.ensureLimitMock.mockResolvedValue([{ id: "u_teacher", role: "teacher" }]) mocks.classLimitMock.mockResolvedValue([{ id: "class_6", teacherId: "owner_teacher" }]) mocks.examFindFirstMock.mockResolvedValue({ id: "exam_6", title: "Exam F", subjectId: null, structure: { sections: [] }, questions: [{ questionId: "q_6", score: 10, order: 1 }], }) mocks.subjectTeacherWhereMock.mockResolvedValue([{ subjectId: "subject_history" }]) const formData = new FormData() formData.set("sourceExamId", "exam_6") formData.set("classId", "class_6") formData.set("publish", "true") const result = await createHomeworkAssignmentAction(null, formData) expect(result).toEqual({ success: false, message: "Exam subject not set" }) expect(mocks.transactionMock).not.toHaveBeenCalled() }) it("returns class not found when class is missing", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_admin" } }) mocks.ensureLimitMock.mockResolvedValue([{ id: "u_admin", role: "admin" }]) mocks.classLimitMock.mockResolvedValue([]) const formData = new FormData() formData.set("sourceExamId", "exam_7") formData.set("classId", "missing_class") const result = await createHomeworkAssignmentAction(null, formData) expect(result).toEqual({ success: false, message: "Class not found" }) expect(mocks.transactionMock).not.toHaveBeenCalled() }) })