Files
Edu/services/parent-bff/test/unit/fallback-strategy.test.ts
SpecialX 8f21ca6a68 chore(parent-bff): merge parent-bff module into main
Merge feat/parent-bff-ai05 into main, conflicts resolved in favor of feature branch
2026-07-10 19:00:29 +08:00

164 lines
4.9 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";
// Mock safeRedis 以控制 Redis 行为(单元测试不依赖真实 Redis
vi.mock("../../src/shared/cache/redis.client.js", () => ({
safeRedis: vi.fn().mockResolvedValue(null),
getRedisClient: vi.fn(() => null),
closeRedisClient: vi.fn(),
}));
import {
withCacheFallback,
invalidateCache,
} from "../../src/aggregation/fallback-strategy.js";
import { safeRedis } from "../../src/shared/cache/redis.client.js";
const mockSafeRedis = vi.mocked(safeRedis);
describe("FallbackStrategy三级降级", () => {
beforeEach(() => {
vi.clearAllMocks();
mockSafeRedis.mockResolvedValue(null);
});
describe("Redis 缓存命中(第一级)", () => {
it("Redis 命中时返回缓存数据fromCache=true", async () => {
const cachedData = { id: "test-1", name: "测试" };
mockSafeRedis.mockResolvedValueOnce(JSON.stringify(cachedData));
const result = await withCacheFallback(
"test-redis-hit",
() => Promise.resolve({ id: "test-1", name: "不应该被调用" }),
30,
"test",
);
expect(result.data).toEqual(cachedData);
expect(result.fromCache).toBe(true);
expect(result.stale).toBe(false);
});
});
describe("Redis 未命中 → fn 成功(第三级直连)", () => {
it("Redis miss 后调 fn 获取数据并写入缓存", async () => {
const fnData = { id: "test-2", value: 42 };
mockSafeRedis
.mockResolvedValueOnce(null) // get → miss
.mockResolvedValueOnce(undefined); // set → ok
const result = await withCacheFallback(
"test-redis-miss-fn-success",
() => Promise.resolve(fnData),
30,
"test",
);
expect(result.data).toEqual(fnData);
expect(result.fromCache).toBe(false);
expect(result.stale).toBe(false);
// safeRedis 被调用 2 次get + set
expect(mockSafeRedis).toHaveBeenCalledTimes(2);
});
});
describe("LRU 缓存命中(第二级降级)", () => {
it("Redis 不可用时从 LRU 缓存返回数据", async () => {
// 第一次调用miss Redis + miss LRU → 调 fn → 写入 LRU
const fnData = { id: "test-3", label: "LRU测试" };
mockSafeRedis
.mockResolvedValueOnce(null) // get → miss
.mockResolvedValueOnce(undefined); // set → ok
await withCacheFallback(
"test-lru-hit-key",
() => Promise.resolve(fnData),
60,
"test",
);
// 第二次调用miss Redis → hit LRU
mockSafeRedis.mockResolvedValueOnce(null); // get → miss
const result = await withCacheFallback(
"test-lru-hit-key",
() => Promise.resolve({ id: "test-3", label: "不应该被调用" }),
60,
"test",
);
expect(result.data).toEqual(fnData);
expect(result.fromCache).toBe(true);
expect(result.stale).toBe(false);
});
});
describe("fn 失败降级", () => {
it("fn 抛错时返回 null", async () => {
mockSafeRedis.mockResolvedValueOnce(null); // get → miss
const result = await withCacheFallback(
"test-fn-fail",
() => Promise.reject(new Error("downstream error")),
30,
"test",
);
expect(result.data).toBeNull();
expect(result.fromCache).toBe(false);
expect(result.stale).toBe(false);
});
});
describe("Redis JSON 解析失败", () => {
it("Redis 返回非法 JSON 时降级到 fn", async () => {
mockSafeRedis
.mockResolvedValueOnce("not-valid-json") // get → 返回非法 JSON
.mockResolvedValueOnce(null) // LRU miss
.mockResolvedValueOnce(undefined); // set → ok
const result = await withCacheFallback(
"test-bad-json",
() => Promise.resolve({ data: "from-fn" }),
30,
"test",
);
expect(result.data).toEqual({ data: "from-fn" });
expect(result.fromCache).toBe(false);
});
});
describe("invalidateCache", () => {
it("调用 safeRedis del + LRU delete", async () => {
// 先写入 LRU
mockSafeRedis
.mockResolvedValueOnce(null) // get → miss
.mockResolvedValueOnce(undefined); // set → ok
await withCacheFallback(
"test-invalidate",
() => Promise.resolve({ val: 1 }),
30,
"test",
);
// 失效缓存
mockSafeRedis.mockResolvedValueOnce(undefined); // del → ok
await invalidateCache("test-invalidate");
// 再次查询,应该 miss LRU
mockSafeRedis
.mockResolvedValueOnce(null) // get → miss
.mockResolvedValueOnce(undefined); // set → ok
const result = await withCacheFallback(
"test-invalidate",
() => Promise.resolve({ val: 2 }),
30,
"test",
);
expect(result.data).toEqual({ val: 2 });
expect(result.fromCache).toBe(false);
});
});
});