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 { 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 { 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 { return []; } async listHomeworkByClass(_classId: string): Promise { return []; } async listExamsByClass(_classId: string): Promise { return []; } async getClass(classId: string): Promise { return { id: classId, name: "", gradeId: "" }; } } class DownCoreEduClient implements CoreEduClient { async listGradesByStudent(_studentId: string): Promise { throw new Error("core-edu gRPC unavailable"); } async listHomeworkByClass(_classId: string): Promise { return []; } async listExamsByClass(_classId: string): Promise { return []; } async getClass(classId: string): Promise { return { id: classId, name: "", gradeId: "" }; } } class UpDataAnaClient implements DataAnaClient { async getStudentWeakness( studentId: string, _subjectId: string, ): Promise { return { studentId, weakPoints: [] }; } async getLearningTrend( studentId: string, _startDate: number, _endDate: number, ): Promise { return { studentId, points: [] }; } async getClassPerformance( classId: string, _subjectId: string, _startDate: number, _endDate: number, ): Promise { return { classId, averageScore: 0, passRate: 0, scores: [] }; } } class DownDataAnaClient implements DataAnaClient { async getStudentWeakness( _studentId: string, _subjectId: string, ): Promise { throw new Error("data-ana gRPC unavailable"); } async getLearningTrend( studentId: string, _startDate: number, _endDate: number, ): Promise { return { studentId, points: [] }; } async getClassPerformance( classId: string, _subjectId: string, _startDate: number, _endDate: number, ): Promise { return { classId, averageScore: 0, passRate: 0, scores: [] }; } } describe("测试用例 9:/readyz 下游探针(Integration)", () => { beforeEach(() => { vi.clearAllMocks(); mockSafeRedis.mockResolvedValue("PONG"); }); describe("/healthz(liveness)", () => { 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("/readyz(readiness)全探针正常", () => { 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"); }); }); });