Files
Edu/services/parent-bff/test/integration/health.controller.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

322 lines
9.5 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, 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");
});
});
});