Files
Edu/services/parent-bff/test/unit/response-mapper.test.ts
SpecialX 2229309a1e 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
2026-07-10 18:49:06 +08:00

403 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 → GradeTypescore 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();
});
});
});