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, fallback: unknown, ): Promise => { 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("测试用例 10:Kafka 缓存失效(Integration)", () => { let handler: EventHandler; beforeEach(() => { vi.clearAllMocks(); mockDelCalls.length = 0; handler = new CacheInvalidationHandler(); }); describe("edu.teaching.grade.recorded", () => { it("失效 grades:{childId}:* + dashboard:{parentId}", async () => { const ctx: EventContext = { 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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"); }); }); });