add complete parent-bff implementation including: - GraphQL endpoint with depth/cost validation - ChildGuard越权校验 with redis cache and singleflight - parallel orchestration with partial failure fallback - three-level cache fallback strategy (Redis + LRU + downstream) - Kafka consumer for cache invalidation and notification push - opossum circuit breaker for downstream services - Prometheus metrics and SLO alerts - Helm chart for k8s deployment with multi-environment configs - Grafana dashboard for observability - complete unit and integration tests
403 lines
12 KiB
TypeScript
403 lines
12 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
||
import {
|
||
mapParent,
|
||
mapChild,
|
||
mapClassInfo,
|
||
mapGrade,
|
||
mapHomework,
|
||
mapExam,
|
||
mapViewport,
|
||
mapWeakness,
|
||
mapTrend,
|
||
mapAnalytics,
|
||
} from "../../src/aggregation/response-mapper.js";
|
||
import type {
|
||
UserInfoDto,
|
||
ChildDto,
|
||
GradeDto,
|
||
HomeworkDto,
|
||
ExamDto,
|
||
ViewportDto,
|
||
StudentWeaknessDto,
|
||
LearningTrendDto,
|
||
ClassPerformanceDto,
|
||
} from "../../src/clients/dtos.js";
|
||
|
||
describe("Response Mapper", () => {
|
||
describe("mapParent", () => {
|
||
it("正确映射 UserInfoDto → ParentType", () => {
|
||
const dto: UserInfoDto = {
|
||
id: "parent-001",
|
||
email: "parent@example.com",
|
||
name: "王家长",
|
||
roles: ["parent"],
|
||
permissions: [],
|
||
};
|
||
const result = mapParent(dto);
|
||
expect(result.id).toBe("parent-001");
|
||
expect(result.email).toBe("parent@example.com");
|
||
expect(result.name).toBe("王家长");
|
||
expect(result.avatar).toBeNull();
|
||
expect(result.roles).toEqual(["parent"]);
|
||
expect(result.dataScope).toBe("CHILDREN");
|
||
});
|
||
});
|
||
|
||
describe("mapChild", () => {
|
||
it("正确映射 ChildDto → ChildType", () => {
|
||
const dto: ChildDto = {
|
||
id: "student-001",
|
||
name: "李同学",
|
||
grade: "三年级",
|
||
classId: "class-001",
|
||
className: "三年级1班",
|
||
gradeId: "grade-003",
|
||
};
|
||
const result = mapChild(dto);
|
||
expect(result.id).toBe("student-001");
|
||
expect(result.name).toBe("李同学");
|
||
expect(result.grade).toBe("三年级");
|
||
expect(result.class.id).toBe("class-001");
|
||
expect(result.class.name).toBe("三年级1班");
|
||
expect(result.class.gradeId).toBe("grade-003");
|
||
});
|
||
});
|
||
|
||
describe("mapClassInfo", () => {
|
||
it("正确映射 ClassInfo", () => {
|
||
const result = mapClassInfo({
|
||
id: "class-001",
|
||
name: "三年级1班",
|
||
gradeId: "grade-003",
|
||
});
|
||
expect(result.id).toBe("class-001");
|
||
expect(result.name).toBe("三年级1班");
|
||
expect(result.gradeId).toBe("grade-003");
|
||
});
|
||
});
|
||
|
||
describe("mapGrade", () => {
|
||
it("正确映射 GradeDto → GradeType(score string → number)", () => {
|
||
const dto: GradeDto = {
|
||
id: "grade-001",
|
||
studentId: "student-001",
|
||
examId: "exam-001",
|
||
homeworkId: "",
|
||
score: "85.5",
|
||
feedback: "数学表现良好",
|
||
gradedBy: "teacher-001",
|
||
createdAt: "2026-01-01T00:00:00.000Z",
|
||
updatedAt: "2026-01-02T00:00:00.000Z",
|
||
};
|
||
const result = mapGrade(dto);
|
||
expect(result.id).toBe("grade-001");
|
||
expect(result.examId).toBe("exam-001");
|
||
expect(result.score).toBe(85.5);
|
||
expect(result.subject).toBe("数学");
|
||
expect(result.examTitle).toBe("数学表现良好");
|
||
expect(result.rank).toBeNull();
|
||
expect(result.gradedAt).toBe("2026-01-02T00:00:00.000Z");
|
||
});
|
||
|
||
it("feedback 为空时 examTitle 为默认值", () => {
|
||
const dto: GradeDto = {
|
||
id: "grade-001",
|
||
studentId: "student-001",
|
||
examId: "exam-001",
|
||
homeworkId: "",
|
||
score: "90",
|
||
feedback: "",
|
||
gradedBy: "teacher-001",
|
||
createdAt: "2026-01-01T00:00:00.000Z",
|
||
updatedAt: "2026-01-02T00:00:00.000Z",
|
||
};
|
||
const result = mapGrade(dto);
|
||
expect(result.examTitle).toBe("未命名考试");
|
||
expect(result.subject).toBe("未知科目");
|
||
});
|
||
|
||
it("score 非数字时返回 0", () => {
|
||
const dto: GradeDto = {
|
||
id: "grade-001",
|
||
studentId: "student-001",
|
||
examId: "exam-001",
|
||
homeworkId: "",
|
||
score: "invalid",
|
||
feedback: "语文表现良好",
|
||
gradedBy: "teacher-001",
|
||
createdAt: "2026-01-01T00:00:00.000Z",
|
||
updatedAt: "2026-01-02T00:00:00.000Z",
|
||
};
|
||
const result = mapGrade(dto);
|
||
expect(result.score).toBe(0);
|
||
expect(result.subject).toBe("语文");
|
||
});
|
||
|
||
it("feedback 无科目前缀时 subject 为综合", () => {
|
||
const dto: GradeDto = {
|
||
id: "grade-001",
|
||
studentId: "student-001",
|
||
examId: "exam-001",
|
||
homeworkId: "",
|
||
score: "80",
|
||
feedback: "表现不错",
|
||
gradedBy: "teacher-001",
|
||
createdAt: "2026-01-01T00:00:00.000Z",
|
||
updatedAt: "2026-01-02T00:00:00.000Z",
|
||
};
|
||
const result = mapGrade(dto);
|
||
expect(result.subject).toBe("综合");
|
||
});
|
||
});
|
||
|
||
describe("mapHomework", () => {
|
||
it("SUBMITTED 状态返回 submittedAt", () => {
|
||
const dto: HomeworkDto = {
|
||
id: "hw-001",
|
||
classId: "class-001",
|
||
title: "数学练习",
|
||
description: "",
|
||
dueDate: "2026-01-05T00:00:00.000Z",
|
||
status: "SUBMITTED",
|
||
createdBy: "teacher-001",
|
||
createdAt: "2026-01-01T00:00:00.000Z",
|
||
updatedAt: "2026-01-02T00:00:00.000Z",
|
||
};
|
||
const result = mapHomework(dto);
|
||
expect(result.status).toBe("SUBMITTED");
|
||
expect(result.submittedAt).toBe("2026-01-02T00:00:00.000Z");
|
||
});
|
||
|
||
it("GRADED 状态返回 submittedAt", () => {
|
||
const dto: HomeworkDto = {
|
||
id: "hw-001",
|
||
classId: "class-001",
|
||
title: "数学练习",
|
||
description: "",
|
||
dueDate: "2026-01-05T00:00:00.000Z",
|
||
status: "GRADED",
|
||
createdBy: "teacher-001",
|
||
createdAt: "2026-01-01T00:00:00.000Z",
|
||
updatedAt: "2026-01-03T00:00:00.000Z",
|
||
};
|
||
const result = mapHomework(dto);
|
||
expect(result.submittedAt).toBe("2026-01-03T00:00:00.000Z");
|
||
});
|
||
|
||
it("NOT_SUBMITTED 状态 submittedAt 为 null", () => {
|
||
const dto: HomeworkDto = {
|
||
id: "hw-001",
|
||
classId: "class-001",
|
||
title: "数学练习",
|
||
description: "",
|
||
dueDate: "2026-01-05T00:00:00.000Z",
|
||
status: "NOT_SUBMITTED",
|
||
createdBy: "teacher-001",
|
||
createdAt: "2026-01-01T00:00:00.000Z",
|
||
updatedAt: "2026-01-02T00:00:00.000Z",
|
||
};
|
||
const result = mapHomework(dto);
|
||
expect(result.submittedAt).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("mapExam", () => {
|
||
it("PUBLISHED 状态返回 publishedAt", () => {
|
||
const dto: ExamDto = {
|
||
id: "exam-001",
|
||
classId: "class-001",
|
||
title: "数学期中考试",
|
||
description: "",
|
||
examDate: "2026-02-01T00:00:00.000Z",
|
||
duration: "90",
|
||
totalScore: "100",
|
||
status: "PUBLISHED",
|
||
createdBy: "teacher-001",
|
||
createdAt: "2026-01-01T00:00:00.000Z",
|
||
updatedAt: "2026-01-15T00:00:00.000Z",
|
||
};
|
||
const result = mapExam(dto);
|
||
expect(result.status).toBe("PUBLISHED");
|
||
expect(result.publishedAt).toBe("2026-01-15T00:00:00.000Z");
|
||
});
|
||
|
||
it("DRAFT 状态 publishedAt 为 null", () => {
|
||
const dto: ExamDto = {
|
||
id: "exam-001",
|
||
classId: "class-001",
|
||
title: "数学期中考试",
|
||
description: "",
|
||
examDate: "2026-02-01T00:00:00.000Z",
|
||
duration: "90",
|
||
totalScore: "100",
|
||
status: "DRAFT",
|
||
createdBy: "teacher-001",
|
||
createdAt: "2026-01-01T00:00:00.000Z",
|
||
updatedAt: "2026-01-15T00:00:00.000Z",
|
||
};
|
||
const result = mapExam(dto);
|
||
expect(result.publishedAt).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("mapViewport", () => {
|
||
it("正确映射 ViewportDto", () => {
|
||
const dto: ViewportDto = {
|
||
key: "dashboard",
|
||
label: "首页",
|
||
route: "/parent/dashboard",
|
||
icon: "home",
|
||
sortOrder: "1",
|
||
requiredPermission: "parent:dashboard:view",
|
||
};
|
||
const result = mapViewport(dto);
|
||
expect(result.key).toBe("dashboard");
|
||
expect(result.label).toBe("首页");
|
||
expect(result.icon).toBe("home");
|
||
expect(result.requiredPermission).toBe("parent:dashboard:view");
|
||
});
|
||
|
||
it("icon 和 requiredPermission 可选", () => {
|
||
const dto: ViewportDto = {
|
||
key: "settings",
|
||
label: "设置",
|
||
route: "/parent/settings",
|
||
sortOrder: "5",
|
||
};
|
||
const result = mapViewport(dto);
|
||
expect(result.icon).toBeNull();
|
||
expect(result.requiredPermission).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("mapWeakness", () => {
|
||
it("正确映射薄弱点列表", () => {
|
||
const dto: StudentWeaknessDto = {
|
||
studentId: "student-001",
|
||
weakPoints: [
|
||
{ knowledgePointId: "kp-1", title: "分数加减法", mastery: 0.45 },
|
||
{ knowledgePointId: "kp-2", title: "阅读理解", mastery: 0.62 },
|
||
],
|
||
};
|
||
const result = mapWeakness(dto);
|
||
expect(result).toHaveLength(2);
|
||
expect(result[0]!.knowledgePointId).toBe("kp-1");
|
||
expect(result[0]!.name).toBe("分数加减法");
|
||
expect(result[0]!.masteryRate).toBe(0.45);
|
||
expect(result[0]!.subject).toBe("综合");
|
||
});
|
||
|
||
it("空薄弱点列表", () => {
|
||
const dto: StudentWeaknessDto = {
|
||
studentId: "student-001",
|
||
weakPoints: [],
|
||
};
|
||
const result = mapWeakness(dto);
|
||
expect(result).toHaveLength(0);
|
||
});
|
||
});
|
||
|
||
describe("mapTrend", () => {
|
||
it("正确映射趋势点(int64 ms → ISO string)", () => {
|
||
const dto: LearningTrendDto = {
|
||
studentId: "student-001",
|
||
points: [
|
||
{ date: 1735689600000, score: 80 },
|
||
{ date: 1735776000000, score: 85 },
|
||
],
|
||
};
|
||
const result = mapTrend(dto);
|
||
expect(result).toHaveLength(2);
|
||
expect(result[0]!.score).toBe(80);
|
||
expect(typeof result[0]!.date).toBe("string");
|
||
expect(new Date(result[0]!.date).getTime()).toBe(1735689600000);
|
||
expect(result[0]!.subject).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("mapAnalytics", () => {
|
||
it("正确计算 classRank(降序排名)", () => {
|
||
const weakness: StudentWeaknessDto = {
|
||
studentId: "student-001",
|
||
weakPoints: [],
|
||
};
|
||
const trend: LearningTrendDto = {
|
||
studentId: "student-001",
|
||
points: [],
|
||
};
|
||
const classPerf: ClassPerformanceDto = {
|
||
classId: "class-001",
|
||
averageScore: 82.5,
|
||
passRate: 0.9,
|
||
scores: [
|
||
{ studentId: "student-001", score: 85, grade: "A" },
|
||
{ studentId: "student-002", score: 92, grade: "A" },
|
||
{ studentId: "student-003", score: 65, grade: "C" },
|
||
],
|
||
};
|
||
const result = mapAnalytics("student-001", weakness, trend, classPerf);
|
||
expect(result.childId).toBe("student-001");
|
||
expect(result.classAverage).toBe(82.5);
|
||
const sorted = [...classPerf.scores].sort((a, b) => b.score - a.score);
|
||
const idx = sorted.findIndex((s) => s.studentId === "student-001");
|
||
expect(result.classRank).toBe(idx + 1);
|
||
});
|
||
|
||
it("classPerf 为 null 时 classRank 和 classAverage 为 null", () => {
|
||
const weakness: StudentWeaknessDto = {
|
||
studentId: "student-001",
|
||
weakPoints: [],
|
||
};
|
||
const trend: LearningTrendDto = {
|
||
studentId: "student-001",
|
||
points: [],
|
||
};
|
||
const result = mapAnalytics("student-001", weakness, trend, null);
|
||
expect(result.classRank).toBeNull();
|
||
expect(result.classAverage).toBeNull();
|
||
});
|
||
|
||
it("childId 不在 scores 中时 classRank 为 null", () => {
|
||
const weakness: StudentWeaknessDto = {
|
||
studentId: "student-001",
|
||
weakPoints: [],
|
||
};
|
||
const trend: LearningTrendDto = {
|
||
studentId: "student-001",
|
||
points: [],
|
||
};
|
||
const classPerf: ClassPerformanceDto = {
|
||
classId: "class-001",
|
||
averageScore: 80,
|
||
passRate: 0.9,
|
||
scores: [
|
||
{ studentId: "student-002", score: 90, grade: "A" },
|
||
{ studentId: "student-003", score: 70, grade: "B" },
|
||
],
|
||
};
|
||
const result = mapAnalytics("student-001", weakness, trend, classPerf);
|
||
expect(result.classRank).toBeNull();
|
||
expect(result.classAverage).toBe(80);
|
||
});
|
||
|
||
it("scores 为空数组时 classRank 为 null", () => {
|
||
const weakness: StudentWeaknessDto = {
|
||
studentId: "student-001",
|
||
weakPoints: [],
|
||
};
|
||
const trend: LearningTrendDto = {
|
||
studentId: "student-001",
|
||
points: [],
|
||
};
|
||
const classPerf: ClassPerformanceDto = {
|
||
classId: "class-001",
|
||
averageScore: 0,
|
||
passRate: 0,
|
||
scores: [],
|
||
};
|
||
const result = mapAnalytics("student-001", weakness, trend, classPerf);
|
||
expect(result.classRank).toBeNull();
|
||
});
|
||
});
|
||
});
|