Files
Edu/services/parent-bff/test/integration/cache-invalidation.handler.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

255 lines
7.4 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";
// 使用 vi.hoisted 确保 mockDelCalls 在 mock factory 之前定义
const mockDelCalls = vi.hoisted(() => [] as string[]);
// Mock safeRedis捕获 del 调用以验证失效的缓存 key
vi.mock("../../src/shared/cache/redis.client.js", () => ({
safeRedis: vi.fn(
async (
fn: (client: unknown) => Promise<unknown>,
fallback: unknown,
): Promise<unknown> => {
const mockClient = {
del: (key: string): number => {
mockDelCalls.push(key);
return 1;
},
get: (): null => null,
set: (): string => "OK",
ping: (): string => "PONG",
};
try {
return await fn(mockClient);
} catch {
return fallback;
}
},
),
getRedisClient: vi.fn(() => null),
closeRedisClient: vi.fn(),
}));
import { CacheInvalidationHandler } from "../../src/shared/kafka/handlers/cache-invalidation.handler.js";
import type {
EventHandler,
EventContext,
} from "../../src/shared/kafka/handlers/event-handler.js";
import { safeRedis } from "../../src/shared/cache/redis.client.js";
const mockSafeRedis = vi.mocked(safeRedis);
interface TeachingEventBody {
studentId?: string;
childId?: string;
classId?: string;
parentId?: string;
}
describe("测试用例 10Kafka 缓存失效Integration", () => {
let handler: EventHandler<TeachingEventBody>;
beforeEach(() => {
vi.clearAllMocks();
mockDelCalls.length = 0;
handler = new CacheInvalidationHandler();
});
describe("edu.teaching.grade.recorded", () => {
it("失效 grades:{childId}:* + dashboard:{parentId}", async () => {
const ctx: EventContext<TeachingEventBody> = {
topic: "edu.teaching.grade.recorded",
eventId: "evt-001",
key: "parent-001",
body: {
childId: "student-001",
parentId: "parent-001",
classId: "class-001",
},
};
await handler.handle(ctx);
// 失效 grades:student-001:1 + dashboard:parent-001
expect(mockDelCalls).toContain("grades:student-001:1");
expect(mockDelCalls).toContain("dashboard:parent-001");
expect(mockDelCalls).toHaveLength(2);
});
it("使用 studentId 作为 childId fallback", async () => {
const ctx: EventContext<TeachingEventBody> = {
topic: "edu.teaching.grade.recorded",
eventId: "evt-002",
key: "parent-001",
body: {
studentId: "student-002",
parentId: "parent-001",
},
};
await handler.handle(ctx);
// childId 从 studentId fallback
expect(mockDelCalls).toContain("grades:student-002:1");
expect(mockDelCalls).toContain("dashboard:parent-001");
});
it("无 parentId 时只失效 grades 缓存", async () => {
const ctx: EventContext<TeachingEventBody> = {
topic: "edu.teaching.grade.recorded",
eventId: "evt-003",
key: "student-001",
body: {
childId: "student-001",
},
};
await handler.handle(ctx);
expect(mockDelCalls).toContain("grades:student-001:1");
expect(mockDelCalls).not.toContain("dashboard:parent-001");
expect(mockDelCalls).toHaveLength(1);
});
});
describe("edu.teaching.homework.graded", () => {
it("失效 homework:{childId}:{classId} + dashboard:{parentId}", async () => {
const ctx: EventContext<TeachingEventBody> = {
topic: "edu.teaching.homework.graded",
eventId: "evt-004",
key: "parent-001",
body: {
childId: "student-001",
classId: "class-001",
parentId: "parent-001",
},
};
await handler.handle(ctx);
expect(mockDelCalls).toContain("homework:student-001:class-001");
expect(mockDelCalls).toContain("dashboard:parent-001");
expect(mockDelCalls).toHaveLength(2);
});
it("无 classId 时只失效 dashboard", async () => {
const ctx: EventContext<TeachingEventBody> = {
topic: "edu.teaching.homework.graded",
eventId: "evt-005",
key: "parent-001",
body: {
childId: "student-001",
parentId: "parent-001",
},
};
await handler.handle(ctx);
// 无 classId 时 homework 缓存不失效条件childId && classId
expect(mockDelCalls).not.toContain("homework:student-001:undefined");
expect(mockDelCalls).toContain("dashboard:parent-001");
expect(mockDelCalls).toHaveLength(1);
});
});
describe("edu.teaching.exam.published", () => {
it("失效 exams:{childId}:{classId} + dashboard:{parentId}", async () => {
const ctx: EventContext<TeachingEventBody> = {
topic: "edu.teaching.exam.published",
eventId: "evt-006",
key: "parent-001",
body: {
childId: "student-001",
classId: "class-001",
parentId: "parent-001",
},
};
await handler.handle(ctx);
expect(mockDelCalls).toContain("exams:student-001:class-001");
expect(mockDelCalls).toContain("dashboard:parent-001");
expect(mockDelCalls).toHaveLength(2);
});
});
describe("edu.notification.read", () => {
it("失效 notifications:{parentId}:1", async () => {
const ctx: EventContext<TeachingEventBody> = {
topic: "edu.notification.read",
eventId: "evt-007",
key: "parent-001",
body: {
parentId: "parent-001",
},
};
await handler.handle(ctx);
expect(mockDelCalls).toContain("notifications:parent-001:1");
expect(mockDelCalls).toHaveLength(1);
});
});
describe("edu.notification.recalled", () => {
it("失效 notifications:{parentId}:1", async () => {
const ctx: EventContext<TeachingEventBody> = {
topic: "edu.notification.recalled",
eventId: "evt-008",
key: "parent-001",
body: {
parentId: "parent-001",
},
};
await handler.handle(ctx);
expect(mockDelCalls).toContain("notifications:parent-001:1");
expect(mockDelCalls).toHaveLength(1);
});
});
describe("未知 topic", () => {
it("不失效任何缓存", async () => {
const ctx: EventContext<TeachingEventBody> = {
topic: "edu.unknown.event",
eventId: "evt-009",
key: "parent-001",
body: {
childId: "student-001",
parentId: "parent-001",
},
};
await handler.handle(ctx);
expect(mockDelCalls).toHaveLength(0);
});
});
describe("P5 场景:成绩录入后缓存失效全链路", () => {
it("成绩事件同时失效 grades + dashboard前端刷新后重新拉取", async () => {
// 模拟真实场景:教师录入成绩 → Kafka → 缓存失效 → 前端下次查询走下游
const ctx: EventContext<TeachingEventBody> = {
topic: "edu.teaching.grade.recorded",
eventId: "evt-full-chain",
key: "parent-001",
body: {
childId: "student-001",
parentId: "parent-001",
classId: "class-001",
},
};
await handler.handle(ctx);
// 验证 safeRedis 被调用了 2 次grades del + dashboard del
expect(mockSafeRedis).toHaveBeenCalledTimes(2);
// 验证失效的 key 正确
expect(mockDelCalls).toContain("grades:student-001:1");
expect(mockDelCalls).toContain("dashboard:parent-001");
});
});
});