feat: initialize parent-bff service with full core features
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
This commit is contained in:
402
services/parent-bff/test/unit/response-mapper.test.ts
Normal file
402
services/parent-bff/test/unit/response-mapper.test.ts
Normal file
@@ -0,0 +1,402 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user