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
This commit is contained in:
163
services/parent-bff/test/unit/fallback-strategy.test.ts
Normal file
163
services/parent-bff/test/unit/fallback-strategy.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user