chore(parent-bff): merge parent-bff module into main

Merge feat/parent-bff-ai05 into main, conflicts resolved in favor of feature branch
This commit is contained in:
SpecialX
2026-07-10 19:00:29 +08:00
89 changed files with 11177 additions and 39 deletions

View File

@@ -0,0 +1,329 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import type { IamClient } from "../../src/clients/iam.client.js";
import type { CoreEduClient } from "../../src/clients/core-edu.client.js";
import type { DataAnaClient } from "../../src/clients/data-ana.client.js";
import type {
UserInfoDto,
GradeDto,
StudentWeaknessDto,
ClassInfoDto,
HomeworkDto,
ExamDto,
LearningTrendDto,
ClassPerformanceDto,
} from "../../src/clients/dtos.js";
// Mock safeRedis 以控制 Redis 探针行为
vi.mock("../../src/shared/cache/redis.client.js", () => ({
safeRedis: vi.fn().mockResolvedValue("PONG"),
getRedisClient: vi.fn(() => null),
closeRedisClient: vi.fn(),
}));
import { HealthController } from "../../src/shared/health/health.controller.js";
import { safeRedis } from "../../src/shared/cache/redis.client.js";
const mockSafeRedis = vi.mocked(safeRedis);
class UpIamClient implements IamClient {
async getUserInfo(userId: string): Promise<UserInfoDto> {
return {
id: userId,
email: "",
name: "",
roles: [],
permissions: [],
};
}
async getChildrenByParent(_parentId: string) {
return [];
}
async getViewports(_userId: string) {
return [];
}
async getEffectivePermissions(_userId: string) {
return [];
}
}
class DownIamClient implements IamClient {
async getUserInfo(_userId: string): Promise<UserInfoDto> {
throw new Error("iam gRPC unavailable");
}
async getChildrenByParent(_parentId: string) {
return [];
}
async getViewports(_userId: string) {
return [];
}
async getEffectivePermissions(_userId: string) {
return [];
}
}
class UpCoreEduClient implements CoreEduClient {
async listGradesByStudent(_studentId: string): Promise<GradeDto[]> {
return [];
}
async listHomeworkByClass(_classId: string): Promise<HomeworkDto[]> {
return [];
}
async listExamsByClass(_classId: string): Promise<ExamDto[]> {
return [];
}
async getClass(classId: string): Promise<ClassInfoDto> {
return { id: classId, name: "", gradeId: "" };
}
}
class DownCoreEduClient implements CoreEduClient {
async listGradesByStudent(_studentId: string): Promise<GradeDto[]> {
throw new Error("core-edu gRPC unavailable");
}
async listHomeworkByClass(_classId: string): Promise<HomeworkDto[]> {
return [];
}
async listExamsByClass(_classId: string): Promise<ExamDto[]> {
return [];
}
async getClass(classId: string): Promise<ClassInfoDto> {
return { id: classId, name: "", gradeId: "" };
}
}
class UpDataAnaClient implements DataAnaClient {
async getStudentWeakness(
studentId: string,
_subjectId: string,
): Promise<StudentWeaknessDto> {
return { studentId, weakPoints: [] };
}
async getLearningTrend(
studentId: string,
_startDate: number,
_endDate: number,
): Promise<LearningTrendDto> {
return { studentId, points: [] };
}
async getClassPerformance(
classId: string,
_subjectId: string,
_startDate: number,
_endDate: number,
): Promise<ClassPerformanceDto> {
return { classId, averageScore: 0, passRate: 0, scores: [] };
}
}
class DownDataAnaClient implements DataAnaClient {
async getStudentWeakness(
_studentId: string,
_subjectId: string,
): Promise<StudentWeaknessDto> {
throw new Error("data-ana gRPC unavailable");
}
async getLearningTrend(
studentId: string,
_startDate: number,
_endDate: number,
): Promise<LearningTrendDto> {
return { studentId, points: [] };
}
async getClassPerformance(
classId: string,
_subjectId: string,
_startDate: number,
_endDate: number,
): Promise<ClassPerformanceDto> {
return { classId, averageScore: 0, passRate: 0, scores: [] };
}
}
describe("测试用例 9/readyz 下游探针Integration", () => {
beforeEach(() => {
vi.clearAllMocks();
mockSafeRedis.mockResolvedValue("PONG");
});
describe("/healthzliveness", () => {
it("返回 status=ok + service 名 + timestamp", () => {
const controller = new HealthController(
new UpIamClient(),
new UpCoreEduClient(),
new UpDataAnaClient(),
);
const result = controller.liveness();
expect(result.status).toBe("ok");
expect(result.service).toBe("parent-bff");
expect(result.timestamp).toBeTruthy();
// timestamp 是合法 ISO
const ts = new Date(result.timestamp);
expect(Number.isNaN(ts.getTime())).toBe(false);
});
});
describe("/readyzreadiness全探针正常", () => {
it("4 项探针全部 up 时返回 status=ready", async () => {
mockSafeRedis.mockResolvedValue("PONG");
const controller = new HealthController(
new UpIamClient(),
new UpCoreEduClient(),
new UpDataAnaClient(),
);
const result = await controller.readiness();
expect(result.status).toBe("ready");
expect(result.service).toBe("parent-bff");
expect(result.checks["iam"]?.status).toBe("up");
expect(result.checks["core-edu"]?.status).toBe("up");
expect(result.checks["data-ana"]?.status).toBe("up");
expect(result.checks["redis"]?.status).toBe("up");
});
});
describe("/readyz iam 故障", () => {
it("iam 探针失败时返回 status=degraded + iam.status=down", async () => {
mockSafeRedis.mockResolvedValue("PONG");
const controller = new HealthController(
new DownIamClient(),
new UpCoreEduClient(),
new UpDataAnaClient(),
);
const result = await controller.readiness();
expect(result.status).toBe("degraded");
expect(result.checks["iam"]?.status).toBe("down");
expect(result.checks["iam"]?.error).toContain("iam gRPC unavailable");
// 其他探针仍正常
expect(result.checks["core-edu"]?.status).toBe("up");
expect(result.checks["data-ana"]?.status).toBe("up");
expect(result.checks["redis"]?.status).toBe("up");
});
});
describe("/readyz core-edu 故障", () => {
it("core-edu 探针失败时返回 status=degraded", async () => {
mockSafeRedis.mockResolvedValue("PONG");
const controller = new HealthController(
new UpIamClient(),
new DownCoreEduClient(),
new UpDataAnaClient(),
);
const result = await controller.readiness();
expect(result.status).toBe("degraded");
expect(result.checks["core-edu"]?.status).toBe("down");
expect(result.checks["core-edu"]?.error).toContain(
"core-edu gRPC unavailable",
);
});
});
describe("/readyz data-ana 故障", () => {
it("data-ana 探针失败时返回 status=degraded", async () => {
mockSafeRedis.mockResolvedValue("PONG");
const controller = new HealthController(
new UpIamClient(),
new UpCoreEduClient(),
new DownDataAnaClient(),
);
const result = await controller.readiness();
expect(result.status).toBe("degraded");
expect(result.checks["data-ana"]?.status).toBe("down");
expect(result.checks["data-ana"]?.error).toContain(
"data-ana gRPC unavailable",
);
});
});
describe("/readyz Redis 故障", () => {
it("Redis 探针失败时返回 status=degraded + redis.status=down", async () => {
mockSafeRedis.mockRejectedValue(new Error("redis connection refused"));
const controller = new HealthController(
new UpIamClient(),
new UpCoreEduClient(),
new UpDataAnaClient(),
);
const result = await controller.readiness();
expect(result.status).toBe("degraded");
expect(result.checks["redis"]?.status).toBe("down");
expect(result.checks["redis"]?.error).toContain(
"redis connection refused",
);
// 其他探针仍正常
expect(result.checks["iam"]?.status).toBe("up");
});
it("Redis 返回非 PONG 时 status=down", async () => {
mockSafeRedis.mockResolvedValue("unexpected");
const controller = new HealthController(
new UpIamClient(),
new UpCoreEduClient(),
new UpDataAnaClient(),
);
const result = await controller.readiness();
expect(result.status).toBe("degraded");
expect(result.checks["redis"]?.status).toBe("down");
expect(result.checks["redis"]?.error).toContain(
"unexpected ping response",
);
});
});
describe("/readyz 全部故障", () => {
it("所有探针都失败时 status=degraded", async () => {
mockSafeRedis.mockRejectedValue(new Error("redis down"));
const controller = new HealthController(
new DownIamClient(),
new DownCoreEduClient(),
new DownDataAnaClient(),
);
const result = await controller.readiness();
expect(result.status).toBe("degraded");
expect(result.checks["iam"]?.status).toBe("down");
expect(result.checks["core-edu"]?.status).toBe("down");
expect(result.checks["data-ana"]?.status).toBe("down");
expect(result.checks["redis"]?.status).toBe("down");
});
});
describe("探针延迟记录", () => {
it("每个探针返回 latency_ms", async () => {
mockSafeRedis.mockResolvedValue("PONG");
const controller = new HealthController(
new UpIamClient(),
new UpCoreEduClient(),
new UpDataAnaClient(),
);
const result = await controller.readiness();
expect(typeof result.checks["iam"]?.latency_ms).toBe("number");
expect(typeof result.checks["core-edu"]?.latency_ms).toBe("number");
expect(typeof result.checks["data-ana"]?.latency_ms).toBe("number");
expect(typeof result.checks["redis"]?.latency_ms).toBe("number");
});
});
});