import { beforeEach, describe, expect, it, vi } from "vitest" const mocks = vi.hoisted(() => { const whereMock = vi.fn() const innerJoinMock = vi.fn(() => ({ where: whereMock })) const fromMock = vi.fn(() => ({ innerJoin: innerJoinMock, where: whereMock })) const selectMock = vi.fn(() => ({ from: fromMock })) const updateWhereMock = vi.fn() const updateSetMock = vi.fn(() => ({ where: updateWhereMock })) const updateMock = vi.fn(() => ({ set: updateSetMock })) const insertOnDuplicateKeyUpdateMock = vi.fn() const insertValuesMock = vi.fn(() => ({ onDuplicateKeyUpdate: insertOnDuplicateKeyUpdateMock })) const insertMock = vi.fn(() => ({ values: insertValuesMock })) const enrollStudentByInvitationCodeMock = vi.fn() return { authMock: vi.fn(), whereMock, fromMock, selectMock, roleFindFirstMock: vi.fn(), updateWhereMock, updateSetMock, updateMock, insertOnDuplicateKeyUpdateMock, insertValuesMock, insertMock, enrollStudentByInvitationCodeMock, } }) vi.mock("@/auth", () => ({ auth: mocks.authMock, })) vi.mock("@/shared/db", () => ({ db: { select: mocks.selectMock, query: { roles: { findFirst: mocks.roleFindFirstMock, }, }, update: mocks.updateMock, insert: mocks.insertMock, }, })) vi.mock("@/shared/db/schema", () => ({ classes: { id: "id", invitationCode: "invitationCode" }, classSubjectTeachers: { classId: "classId", subjectId: "subjectId", teacherId: "teacherId", updatedAt: "updatedAt" }, roles: { id: "id", name: "name" }, users: { id: "id", onboardedAt: "onboardedAt", name: "name", phone: "phone", address: "address" }, usersToRoles: { userId: "userId", roleId: "roleId" }, subjects: { id: "id", name: "name" }, })) vi.mock("@/modules/classes/data-access", () => ({ enrollStudentByInvitationCode: mocks.enrollStudentByInvitationCodeMock, })) import { POST } from "@/app/api/onboarding/complete/route" describe("POST /api/onboarding/complete", () => { beforeEach(() => { vi.resetAllMocks() mocks.whereMock.mockResolvedValue([]) mocks.roleFindFirstMock.mockResolvedValue({ id: "role_student" }) }) it("returns 401 when session is missing", async () => { mocks.authMock.mockResolvedValue(null) const req = new Request("http://localhost/api/onboarding/complete", { method: "POST", body: JSON.stringify({ role: "student", name: "A" }), headers: { "Content-Type": "application/json" }, }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(401) expect(data).toEqual({ success: false, message: "Unauthorized" }) }) it("returns 400 for invalid payload structure", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_1" } }) const req = new Request("http://localhost/api/onboarding/complete", { method: "POST", body: "not-json", headers: { "Content-Type": "application/json" }, }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(400) expect(data).toEqual({ success: false, message: "Invalid payload" }) }) it("returns 400 for invalid role", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_1" } }) const req = new Request("http://localhost/api/onboarding/complete", { method: "POST", body: JSON.stringify({ role: "guest", name: "A" }), headers: { "Content-Type": "application/json" }, }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(400) expect(data).toEqual({ success: false, message: "Invalid role" }) }) it("returns 403 when non-admin selects admin role", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_2" } }) mocks.whereMock.mockResolvedValue([{ name: "teacher" }]) const req = new Request("http://localhost/api/onboarding/complete", { method: "POST", body: JSON.stringify({ role: "admin", name: "Admin User" }), headers: { "Content-Type": "application/json" }, }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(403) expect(data).toEqual({ success: false, message: "Forbidden" }) }) it("returns 400 when name is missing", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_3" } }) const req = new Request("http://localhost/api/onboarding/complete", { method: "POST", body: JSON.stringify({ role: "student", name: " " }), headers: { "Content-Type": "application/json" }, }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(400) expect(data).toEqual({ success: false, message: "Name is required" }) }) it("completes student onboarding and enrolls deduplicated class codes", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_student" } }) const req = new Request("http://localhost/api/onboarding/complete", { method: "POST", body: JSON.stringify({ role: "student", name: "Student A", classCodes: "C1, C1;C2 C3", }), headers: { "Content-Type": "application/json" }, }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(200) expect(data).toEqual({ success: true }) expect(mocks.enrollStudentByInvitationCodeMock).toHaveBeenCalledTimes(3) expect(mocks.enrollStudentByInvitationCodeMock).toHaveBeenNthCalledWith(1, "u_student", "C1") expect(mocks.enrollStudentByInvitationCodeMock).toHaveBeenNthCalledWith(2, "u_student", "C2") expect(mocks.enrollStudentByInvitationCodeMock).toHaveBeenNthCalledWith(3, "u_student", "C3") expect(mocks.updateSetMock).toHaveBeenCalled() expect(mocks.insertValuesMock).toHaveBeenCalled() }) it("completes teacher onboarding and upserts class-subject mapping", async () => { mocks.authMock.mockResolvedValue({ user: { id: "u_teacher" } }) mocks.whereMock .mockResolvedValueOnce([]) .mockResolvedValueOnce([{ id: "class_1", invitationCode: "T1" }]) .mockResolvedValueOnce([ { id: "sub_math", name: "数学" }, { id: "sub_music", name: "音乐" }, ]) const req = new Request("http://localhost/api/onboarding/complete", { method: "POST", body: JSON.stringify({ role: "teacher", name: "Teacher A", classCodes: "T1", teacherSubjects: ["数学", "音乐", "invalid"], }), headers: { "Content-Type": "application/json" }, }) const response = await POST(req) const data = await response.json() expect(response.status).toBe(200) expect(data).toEqual({ success: true }) expect(mocks.enrollStudentByInvitationCodeMock).not.toHaveBeenCalled() expect(mocks.insertOnDuplicateKeyUpdateMock).toHaveBeenCalledTimes(3) }) })