175 lines
4.8 KiB
TypeScript
175 lines
4.8 KiB
TypeScript
/**
|
||
* MSW Handlers - 拦截所有 HTTP / GraphQL 请求返回 mock 数据
|
||
*
|
||
* 拦截路径(contract.md §2.3 + §4.2):
|
||
* - POST /api/auth/login → 返回固定 JWT + UserInfo
|
||
* - POST /api/v1/teacher/graphql → 按 operationName 返回对应 GraphQL 响应
|
||
*
|
||
* 切换:NEXT_PUBLIC_API_MOCKING=enabled 时启用(见 src/mocks/index.ts)
|
||
*
|
||
* 维护者:ai13(teacher-portal)
|
||
*/
|
||
|
||
import { http, HttpResponse } from "msw";
|
||
import { mockUser, mockUserInfo, mockToken } from "./fixtures/user";
|
||
import { mockViewports } from "./fixtures/viewports";
|
||
import { mockClasses, findClass } from "./fixtures/classes";
|
||
import { generateMockStudents } from "./fixtures/students";
|
||
import { generateMockExams } from "./fixtures/exams";
|
||
import { generateMockHomework } from "./fixtures/homework";
|
||
import { generateMockGrades } from "./fixtures/grades";
|
||
|
||
/** ActionState 信封(coord-final-decisions F9 + 总裁 §3.4 方案 B) */
|
||
function envelope<T>(data: T) {
|
||
return { success: true, errors: null, data };
|
||
}
|
||
|
||
/** GraphQL 响应体(urql 期望 { data: ... }) */
|
||
function graphqlData<T>(data: T) {
|
||
return HttpResponse.json({ data });
|
||
}
|
||
|
||
/**
|
||
* 根据 operationName 路由 GraphQL mock 响应
|
||
*
|
||
* 对应 graphql.ts 的 5 个 P2 Query + 4 个 P3 Query + 1 个 Mutation。
|
||
*/
|
||
function handleGraphQL(
|
||
operationName: string,
|
||
variables: Record<string, unknown>,
|
||
) {
|
||
switch (operationName) {
|
||
case "Dashboard":
|
||
return graphqlData({
|
||
dashboard: {
|
||
user: mockUser,
|
||
classes: mockClasses,
|
||
viewports: mockViewports,
|
||
stats: {
|
||
totalExams: 8,
|
||
pendingGrading: 12,
|
||
todayHomework: 3,
|
||
},
|
||
},
|
||
});
|
||
|
||
case "Viewports":
|
||
return graphqlData({ viewports: mockViewports });
|
||
|
||
case "Me":
|
||
return graphqlData({ me: mockUser });
|
||
|
||
case "Classes":
|
||
return graphqlData({ classes: mockClasses });
|
||
|
||
case "Class": {
|
||
const id = variables.id as string;
|
||
return graphqlData({ class: findClass(id) });
|
||
}
|
||
|
||
case "ClassStudents": {
|
||
const classId = variables.classId as string;
|
||
return graphqlData({ classStudents: generateMockStudents(classId) });
|
||
}
|
||
|
||
case "ClassExams": {
|
||
const classId = variables.classId as string;
|
||
return graphqlData({ classExams: generateMockExams(classId) });
|
||
}
|
||
|
||
case "ClassHomework": {
|
||
const classId = variables.classId as string;
|
||
return graphqlData({ classHomework: generateMockHomework(classId) });
|
||
}
|
||
|
||
case "ExamGrades": {
|
||
const examId = variables.examId as string;
|
||
return graphqlData({ examGrades: generateMockGrades(examId) });
|
||
}
|
||
|
||
case "UpdateUser": {
|
||
const input = variables.input as { name?: string; email?: string };
|
||
return graphqlData({
|
||
updateUser: { ...mockUser, ...input },
|
||
});
|
||
}
|
||
|
||
default:
|
||
return HttpResponse.json(
|
||
{
|
||
errors: [
|
||
{
|
||
message: `Mock 未覆盖的 GraphQL operation: ${operationName}`,
|
||
extensions: { code: "BFF_TEACHER_NOT_IMPLEMENTED" },
|
||
},
|
||
],
|
||
},
|
||
{ status: 200 },
|
||
);
|
||
}
|
||
}
|
||
|
||
export const handlers = [
|
||
// POST /api/auth/login → 登录
|
||
http.post("*/api/auth/login", async ({ request }) => {
|
||
const body = (await request.json()) as {
|
||
email?: string;
|
||
password?: string;
|
||
};
|
||
if (!body.email || !body.password) {
|
||
return HttpResponse.json(
|
||
{
|
||
success: false,
|
||
error: { code: "GW_UNAUTHORIZED", message: "邮箱和密码必填" },
|
||
},
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
return HttpResponse.json(
|
||
envelope({
|
||
user: mockUserInfo,
|
||
tokens: { accessToken: mockToken, refreshToken: "mock-refresh" },
|
||
}),
|
||
);
|
||
}),
|
||
|
||
// POST /api/v1/teacher/graphql → GraphQL endpoint
|
||
http.post("*/api/v1/teacher/graphql", async ({ request }) => {
|
||
const body = (await request.json()) as {
|
||
operationName?: string;
|
||
query?: string;
|
||
variables?: Record<string, unknown>;
|
||
};
|
||
|
||
const operationName =
|
||
body.operationName ?? extractOperationName(body.query);
|
||
const variables = body.variables ?? {};
|
||
|
||
return handleGraphQL(operationName, variables);
|
||
}),
|
||
|
||
// GET /api/health → liveness
|
||
http.get("*/api/health", () =>
|
||
HttpResponse.json({ status: "ok", service: "teacher-portal" }),
|
||
),
|
||
|
||
// GET /api/ready → readiness
|
||
http.get("*/api/ready", () =>
|
||
HttpResponse.json({
|
||
status: "ok",
|
||
service: "teacher-portal",
|
||
checks: { msw: true },
|
||
}),
|
||
),
|
||
];
|
||
|
||
/**
|
||
* 从 query 字符串提取 operationName
|
||
* 兜底:当客户端未传 operationName 时从 query 文本提取
|
||
*/
|
||
function extractOperationName(query?: string): string {
|
||
if (!query) return "Unknown";
|
||
const match = query.match(/(?:query|mutation)\s+(\w+)/);
|
||
return match?.[1] ?? "Unknown";
|
||
}
|