import { describe, expect, it, vi } from "vitest" import { NextRequest } from "next/server" import { Permissions } from "@/shared/types/permissions" import { encodePermissionsBitmap } from "@/shared/lib/permission-bitmap" const { getTokenMock } = vi.hoisted(() => ({ getTokenMock: vi.fn(), })) vi.mock("next-auth/jwt", () => ({ getToken: getTokenMock, })) import { proxy } from "@/proxy" const createRequest = (pathname: string) => ({ nextUrl: { pathname, clone: () => new URL(`http://localhost${pathname}`), }, url: `http://localhost${pathname}`, headers: new Headers(), }) describe("proxy route guard", () => { it("redirects unauthenticated requests to login with callback", async () => { getTokenMock.mockResolvedValue(null) 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=") expect(decodeURIComponent(location)).toContain("/teacher/dashboard") }) it("redirects user without school:manage permission away from admin routes", async () => { getTokenMock.mockResolvedValue({ onboarded: true, roles: ["student"], permissionsBitmap: encodePermissionsBitmap([Permissions.HOMEWORK_SUBMIT]), }) const response = await proxy(createRequest("/admin/dashboard") as never) expect(response.status).toBe(307) expect(response.headers.get("location")).toContain("/student/dashboard") }) it("redirects user without grade:manage permission away from management routes", async () => { getTokenMock.mockResolvedValue({ onboarded: true, roles: ["parent"], permissionsBitmap: encodePermissionsBitmap([Permissions.DASHBOARD_PARENT_READ]), }) const response = await proxy(createRequest("/management/grade/insights") as never) expect(response.status).toBe(307) expect(response.headers.get("location")).toContain("/parent/dashboard") }) it("allows user with grade:manage permission to access management routes", async () => { getTokenMock.mockResolvedValue({ onboarded: true, roles: ["teacher"], permissionsBitmap: encodePermissionsBitmap([ Permissions.GRADE_MANAGE, Permissions.EXAM_CREATE, ]), }) const response = await proxy(createRequest("/management/grade/insights") as never) expect(response.status).toBe(200) expect(response.headers.get("location")).toBeNull() }) }) describe("proxy requestId 注入", () => { it("未带 x-request-id 请求头时,response 头包含新生成的 x-request-id", async () => { getTokenMock.mockResolvedValue({ onboarded: true, roles: ["teacher"], permissionsBitmap: encodePermissionsBitmap([Permissions.EXAM_CREATE]), }) const req = new NextRequest(new URL("/dashboard", "http://localhost:3000")) const res = await proxy(req) expect(res.headers.get("x-request-id")).toBeDefined() expect(res.headers.get("x-request-id")).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i) }) it("带 x-request-id 请求头时,response 头沿用请求头中的 id", async () => { const existingId = "client-supplied-id" getTokenMock.mockResolvedValue({ onboarded: true, roles: ["teacher"], permissionsBitmap: encodePermissionsBitmap([Permissions.EXAM_CREATE]), }) const req = new NextRequest(new URL("/dashboard", "http://localhost:3000"), { headers: { "x-request-id": existingId }, }) const res = await proxy(req) expect(res.headers.get("x-request-id")).toBe(existingId) }) })