200 lines
6.8 KiB
TypeScript
200 lines
6.8 KiB
TypeScript
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)
|
|
})
|
|
})
|