refactor: RBAC权限系统重构 + UI组件拆分 + 测试修复 + 架构文档
Some checks failed
CI / build-deploy (push) Has been cancelled
Some checks failed
CI / build-deploy (push) Has been cancelled
- RBAC: 新增30个权限点、DataScope行级权限、requirePermission守卫,所有57+ Server Action接入权限校验 - UI拆分: exam-form(1623行→11文件)、textbook-reader(744行→7文件),均降至300行以内 - 测试: 新增5个单元测试文件(19用例),修复4个集成测试文件(38用例全部通过) - 架构文档: 新增架构影响地图(004/005)、标准功能清单(006)、差距审计报告(007) - 项目规则: 架构图优先规则,改码必同步图 - 安全: rehype-sanitize净化、AES加密API Key、权限路由守卫 - 无障碍: skip-link、aria-label、prefers-reduced-motion - 性能: next/font优化、next/image、代码分割
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const authMock = vi.fn()
|
||||
const requirePermissionMock = vi.fn()
|
||||
const revalidatePathMock = vi.fn()
|
||||
const createIdMock = vi.fn()
|
||||
|
||||
@@ -38,7 +38,7 @@ const mocks = vi.hoisted(() => {
|
||||
)
|
||||
|
||||
return {
|
||||
authMock,
|
||||
requirePermissionMock,
|
||||
revalidatePathMock,
|
||||
createIdMock,
|
||||
ensureLimitMock,
|
||||
@@ -60,8 +60,14 @@ const mocks = vi.hoisted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock("@/auth", () => ({
|
||||
auth: mocks.authMock,
|
||||
vi.mock("@/shared/lib/auth-guard", () => ({
|
||||
requirePermission: mocks.requirePermissionMock,
|
||||
PermissionDeniedError: class PermissionDeniedError extends Error {
|
||||
constructor(permission: string) {
|
||||
super(`Permission denied: ${permission}`)
|
||||
this.name = "PermissionDeniedError"
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("next/cache", () => ({
|
||||
@@ -105,9 +111,6 @@ vi.mock("@/shared/db/schema", () => ({
|
||||
updatedAt: "updatedAt",
|
||||
score: "score",
|
||||
},
|
||||
roles: { id: "id", name: "name" },
|
||||
users: { id: "id" },
|
||||
usersToRoles: { userId: "userId", roleId: "roleId" },
|
||||
}))
|
||||
|
||||
import {
|
||||
@@ -117,14 +120,31 @@ import {
|
||||
submitHomeworkAction,
|
||||
} from "@/modules/homework/actions"
|
||||
|
||||
function studentCtx(userId = "u_student") {
|
||||
return {
|
||||
userId,
|
||||
roles: ["student"],
|
||||
permissions: ["homework:submit"],
|
||||
dataScope: { type: "class_members" as const },
|
||||
}
|
||||
}
|
||||
|
||||
function teacherCtx(userId = "u_teacher") {
|
||||
return {
|
||||
userId,
|
||||
roles: ["teacher"],
|
||||
permissions: ["homework:grade"],
|
||||
dataScope: { type: "class_taught" as const, classIds: [] },
|
||||
}
|
||||
}
|
||||
|
||||
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.requirePermissionMock.mockResolvedValue(studentCtx())
|
||||
mocks.assignmentFindFirstMock.mockResolvedValue({
|
||||
id: "a_1",
|
||||
status: "published",
|
||||
@@ -153,8 +173,7 @@ describe("homework action flow", () => {
|
||||
})
|
||||
|
||||
it("blocks submission when assignment is past due", async () => {
|
||||
mocks.authMock.mockResolvedValue({ user: { id: "u_student" } })
|
||||
mocks.ensureLimitMock.mockResolvedValue([{ id: "u_student" }])
|
||||
mocks.requirePermissionMock.mockResolvedValue(studentCtx())
|
||||
mocks.submissionFindFirstMock.mockResolvedValue({
|
||||
id: "sub_1",
|
||||
studentId: "u_student",
|
||||
@@ -174,8 +193,7 @@ describe("homework action flow", () => {
|
||||
})
|
||||
|
||||
it("submits started homework before due time", async () => {
|
||||
mocks.authMock.mockResolvedValue({ user: { id: "u_student" } })
|
||||
mocks.ensureLimitMock.mockResolvedValue([{ id: "u_student" }])
|
||||
mocks.requirePermissionMock.mockResolvedValue(studentCtx())
|
||||
mocks.submissionFindFirstMock.mockResolvedValue({
|
||||
id: "sub_2",
|
||||
studentId: "u_student",
|
||||
@@ -198,8 +216,7 @@ describe("homework action flow", () => {
|
||||
})
|
||||
|
||||
it("blocks start when attempts are exhausted", async () => {
|
||||
mocks.authMock.mockResolvedValue({ user: { id: "u_student" } })
|
||||
mocks.ensureLimitMock.mockResolvedValue([{ id: "u_student" }])
|
||||
mocks.requirePermissionMock.mockResolvedValue(studentCtx())
|
||||
mocks.assignmentFindFirstMock.mockResolvedValue({
|
||||
id: "a_2",
|
||||
status: "published",
|
||||
@@ -217,8 +234,7 @@ describe("homework action flow", () => {
|
||||
})
|
||||
|
||||
it("grades submission and writes total score", async () => {
|
||||
mocks.authMock.mockResolvedValue({ user: { id: "u_teacher" } })
|
||||
mocks.ensureLimitMock.mockResolvedValue([{ id: "u_teacher", role: "teacher" }])
|
||||
mocks.requirePermissionMock.mockResolvedValue(teacherCtx())
|
||||
|
||||
const formData = new FormData()
|
||||
formData.set("submissionId", "sub_1")
|
||||
@@ -239,8 +255,7 @@ describe("homework action flow", () => {
|
||||
})
|
||||
|
||||
it("saves new answer for started submission", async () => {
|
||||
mocks.authMock.mockResolvedValue({ user: { id: "u_student" } })
|
||||
mocks.ensureLimitMock.mockResolvedValue([{ id: "u_student" }])
|
||||
mocks.requirePermissionMock.mockResolvedValue(studentCtx())
|
||||
mocks.submissionFindFirstMock.mockResolvedValue({
|
||||
id: "sub_3",
|
||||
studentId: "u_student",
|
||||
@@ -267,8 +282,7 @@ describe("homework action flow", () => {
|
||||
})
|
||||
|
||||
it("updates existing answer for started submission", async () => {
|
||||
mocks.authMock.mockResolvedValue({ user: { id: "u_student" } })
|
||||
mocks.ensureLimitMock.mockResolvedValue([{ id: "u_student" }])
|
||||
mocks.requirePermissionMock.mockResolvedValue(studentCtx())
|
||||
mocks.submissionFindFirstMock.mockResolvedValue({
|
||||
id: "sub_4",
|
||||
studentId: "u_student",
|
||||
|
||||
Reference in New Issue
Block a user