=test_update_homework_tests_and_work_log
Some checks failed
CI / build-deploy (push) Has been cancelled
Some checks failed
CI / build-deploy (push) Has been cancelled
This commit is contained in:
84
tests/integration/api-ai-chat.route.test.ts
Normal file
84
tests/integration/api-ai-chat.route.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
authMock: vi.fn(),
|
||||
parseAiChatPayloadMock: vi.fn(),
|
||||
createAiChatCompletionMock: vi.fn(),
|
||||
getAiErrorMessageMock: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("@/auth", () => ({
|
||||
auth: mocks.authMock,
|
||||
}))
|
||||
|
||||
vi.mock("@/shared/lib/ai", () => ({
|
||||
parseAiChatPayload: mocks.parseAiChatPayloadMock,
|
||||
createAiChatCompletion: mocks.createAiChatCompletionMock,
|
||||
getAiErrorMessage: mocks.getAiErrorMessageMock,
|
||||
}))
|
||||
|
||||
import { POST } from "@/app/api/ai/chat/route"
|
||||
|
||||
describe("POST /api/ai/chat", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
it("returns 401 when session is missing", async () => {
|
||||
mocks.authMock.mockResolvedValue(null)
|
||||
const req = new Request("http://localhost/api/ai/chat", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ messages: [] }),
|
||||
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 ai response content for valid input", async () => {
|
||||
mocks.authMock.mockResolvedValue({ user: { id: "u_1" } })
|
||||
mocks.parseAiChatPayloadMock.mockReturnValue({ messages: [{ role: "user", content: "hello" }] })
|
||||
mocks.createAiChatCompletionMock.mockResolvedValue({
|
||||
content: "mocked-answer",
|
||||
usage: { totalTokens: 10, promptTokens: 5, completionTokens: 5 },
|
||||
})
|
||||
|
||||
const req = new Request("http://localhost/api/ai/chat", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ messages: [{ role: "user", content: "hello" }] }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.success).toBe(true)
|
||||
expect(data.content).toBe("mocked-answer")
|
||||
expect(mocks.createAiChatCompletionMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("maps invalid payload errors to 400", async () => {
|
||||
mocks.authMock.mockResolvedValue({ user: { id: "u_1" } })
|
||||
mocks.parseAiChatPayloadMock.mockImplementation(() => {
|
||||
throw new Error("bad payload")
|
||||
})
|
||||
mocks.getAiErrorMessageMock.mockReturnValue("Invalid payload")
|
||||
|
||||
const req = new Request("http://localhost/api/ai/chat", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ messages: "bad" }),
|
||||
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" })
|
||||
})
|
||||
})
|
||||
199
tests/integration/api-onboarding-complete.route.test.ts
Normal file
199
tests/integration/api-onboarding-complete.route.test.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
78
tests/integration/api-onboarding-status.route.test.ts
Normal file
78
tests/integration/api-onboarding-status.route.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
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 }))
|
||||
const selectMock = vi.fn(() => ({ from: fromMock }))
|
||||
return {
|
||||
authMock: vi.fn(),
|
||||
findFirstMock: vi.fn(),
|
||||
whereMock,
|
||||
selectMock,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock("@/auth", () => ({
|
||||
auth: mocks.authMock,
|
||||
}))
|
||||
|
||||
vi.mock("@/shared/db", () => ({
|
||||
db: {
|
||||
query: {
|
||||
users: {
|
||||
findFirst: mocks.findFirstMock,
|
||||
},
|
||||
},
|
||||
select: mocks.selectMock,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@/shared/db/schema", () => ({
|
||||
roles: { name: "name", id: "id" },
|
||||
users: { id: "id" },
|
||||
usersToRoles: { roleId: "roleId", userId: "userId" },
|
||||
}))
|
||||
|
||||
import { GET } from "@/app/api/onboarding/status/route"
|
||||
|
||||
describe("GET /api/onboarding/status", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
it("returns not required when user is unauthenticated", async () => {
|
||||
mocks.authMock.mockResolvedValue(null)
|
||||
|
||||
const response = await GET()
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data).toEqual({ required: false })
|
||||
expect(mocks.findFirstMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("returns teacher role for grade head and requires onboarding when missing onboardedAt", async () => {
|
||||
mocks.authMock.mockResolvedValue({ user: { id: "u_1" } })
|
||||
mocks.findFirstMock.mockResolvedValue({ onboardedAt: null })
|
||||
mocks.whereMock.mockResolvedValue([{ name: "grade_head" }])
|
||||
|
||||
const response = await GET()
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data).toEqual({ required: true, role: "teacher" })
|
||||
})
|
||||
|
||||
it("prioritizes admin role and marks not required when onboarded", async () => {
|
||||
mocks.authMock.mockResolvedValue({ user: { id: "u_2" } })
|
||||
mocks.findFirstMock.mockResolvedValue({ onboardedAt: new Date("2026-01-01") })
|
||||
mocks.whereMock.mockResolvedValue([{ name: "student" }, { name: "admin" }])
|
||||
|
||||
const response = await GET()
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data).toEqual({ required: false, role: "admin" })
|
||||
})
|
||||
})
|
||||
67
tests/integration/dashboard-routing.test.ts
Normal file
67
tests/integration/dashboard-routing.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
authMock: vi.fn(),
|
||||
getUserProfileMock: vi.fn(),
|
||||
redirectMock: vi.fn((target: string) => {
|
||||
throw new Error(`REDIRECT:${target}`)
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock("@/auth", () => ({
|
||||
auth: mocks.authMock,
|
||||
}))
|
||||
|
||||
vi.mock("@/modules/users/data-access", () => ({
|
||||
getUserProfile: mocks.getUserProfileMock,
|
||||
}))
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: mocks.redirectMock,
|
||||
}))
|
||||
|
||||
import DashboardPage from "@/app/(dashboard)/dashboard/page"
|
||||
|
||||
describe("dashboard route dispatcher", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
mocks.redirectMock.mockImplementation((target: string) => {
|
||||
throw new Error(`REDIRECT:${target}`)
|
||||
})
|
||||
})
|
||||
|
||||
it("redirects to login when session is missing", async () => {
|
||||
mocks.authMock.mockResolvedValue(null)
|
||||
await expect(DashboardPage()).rejects.toThrow("REDIRECT:/login")
|
||||
})
|
||||
|
||||
it("redirects to login when user profile is missing", async () => {
|
||||
mocks.authMock.mockResolvedValue({ user: { id: "u_1" } })
|
||||
mocks.getUserProfileMock.mockResolvedValue(null)
|
||||
await expect(DashboardPage()).rejects.toThrow("REDIRECT:/login")
|
||||
})
|
||||
|
||||
it("redirects admin to admin dashboard", async () => {
|
||||
mocks.authMock.mockResolvedValue({ user: { id: "u_admin" } })
|
||||
mocks.getUserProfileMock.mockResolvedValue({ role: "admin" })
|
||||
await expect(DashboardPage()).rejects.toThrow("REDIRECT:/admin/dashboard")
|
||||
})
|
||||
|
||||
it("redirects student to student dashboard", async () => {
|
||||
mocks.authMock.mockResolvedValue({ user: { id: "u_student" } })
|
||||
mocks.getUserProfileMock.mockResolvedValue({ role: "student" })
|
||||
await expect(DashboardPage()).rejects.toThrow("REDIRECT:/student/dashboard")
|
||||
})
|
||||
|
||||
it("redirects parent to parent dashboard", async () => {
|
||||
mocks.authMock.mockResolvedValue({ user: { id: "u_parent" } })
|
||||
mocks.getUserProfileMock.mockResolvedValue({ role: "parent" })
|
||||
await expect(DashboardPage()).rejects.toThrow("REDIRECT:/parent/dashboard")
|
||||
})
|
||||
|
||||
it("falls back to student dashboard when role is unknown", async () => {
|
||||
mocks.authMock.mockResolvedValue({ user: { id: "u_teacher" } })
|
||||
mocks.getUserProfileMock.mockResolvedValue({ role: "" })
|
||||
await expect(DashboardPage()).rejects.toThrow("REDIRECT:/student/dashboard")
|
||||
})
|
||||
})
|
||||
290
tests/integration/homework-actions.test.ts
Normal file
290
tests/integration/homework-actions.test.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
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<unknown>) =>
|
||||
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" } }))
|
||||
})
|
||||
})
|
||||
298
tests/integration/homework-create-assignment.test.ts
Normal file
298
tests/integration/homework-create-assignment.test.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
46
tests/integration/proxy-guard.test.ts
Normal file
46
tests/integration/proxy-guard.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
|
||||
vi.mock("@/auth", () => ({
|
||||
auth: (handler: (req: unknown) => unknown) => handler,
|
||||
}))
|
||||
|
||||
import proxy from "@/proxy"
|
||||
|
||||
type SessionRole = "admin" | "teacher" | "student" | "parent"
|
||||
|
||||
const createRequest = (pathname: string, role?: SessionRole) => ({
|
||||
nextUrl: {
|
||||
pathname,
|
||||
clone: () => new URL(`http://localhost${pathname}`),
|
||||
},
|
||||
auth: role ? { user: { role } } : null,
|
||||
url: `http://localhost${pathname}`,
|
||||
})
|
||||
|
||||
describe("proxy route guard", () => {
|
||||
it("redirects unauthenticated requests to login with callback", async () => {
|
||||
const response = await proxy(createRequest("/teacher/dashboard") as never)
|
||||
expect(response.status).toBe(307)
|
||||
const location = response.headers.get("location") ?? ""
|
||||
expect(location).toContain("/login")
|
||||
expect(location).toContain("callbackUrl=%2Fteacher%2Fdashboard")
|
||||
})
|
||||
|
||||
it("redirects student away from admin routes", async () => {
|
||||
const response = await proxy(createRequest("/admin/dashboard", "student") as never)
|
||||
expect(response.status).toBe(307)
|
||||
expect(response.headers.get("location")).toContain("/student/dashboard")
|
||||
})
|
||||
|
||||
it("redirects parent away from management routes", async () => {
|
||||
const response = await proxy(createRequest("/management/grade/insights", "parent") as never)
|
||||
expect(response.status).toBe(307)
|
||||
expect(response.headers.get("location")).toContain("/parent/dashboard")
|
||||
})
|
||||
|
||||
it("allows teacher access to management routes", async () => {
|
||||
const response = await proxy(createRequest("/management/grade/insights", "teacher") as never)
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get("location")).toBeNull()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user