import { describe, expect, it, vi } from "vitest"; import { createRequire } from "module"; const require = createRequire(import.meta.url); const { applyRedisHelpers } = require("./redisHelpers"); /** * Creates an in-memory Redis-like test double for feature flag cache helper tests. */ const createRedis = () => { const values = new Map(); const expirations = new Map(); return { del: vi.fn(async (...keys) => { let deleted = 0; for (const key of keys) { if (values.delete(key)) deleted += 1; } return deleted; }), expire: vi.fn(async (key, ttl) => { expirations.set(key, ttl); return 1; }), get: vi.fn(async (key) => values.get(key) ?? null), incr: vi.fn(async (key) => { const nextValue = Number(values.get(key) || 0) + 1; values.set(key, String(nextValue)); return nextValue; }), set: vi.fn(async (key, value) => { values.set(key, String(value)); return "OK"; }), setnx: vi.fn(async (key, value) => { if (values.has(key)) return 0; values.set(key, String(value)); return 1; }), values, expirations }; }; /** * Applies Redis helpers to the in-memory test double and returns the mounted API. */ const createHelpers = () => { const pubClient = createRedis(); const app = { use: vi.fn() }; const logger = { log: vi.fn() }; const helpers = applyRedisHelpers({ pubClient, app, logger }); return { app, helpers, logger, pubClient }; }; describe("feature flag Redis cache helpers", () => { it("stores and reads bodyshop feature flags under the current cache version", async () => { const { helpers, pubClient } = createHelpers(); await helpers.setBodyshopFeatureFlagsInRedis("shop-1", { flags: { Demo: { treatment: "on" } } }); expect(await helpers.getBodyshopFeatureFlagsCacheVersion()).toBe("1"); expect(await helpers.getBodyshopFeatureFlagsFromRedis("shop-1")).toEqual({ flags: { Demo: { treatment: "on" } } }); expect(pubClient.values.has("bodyshop-feature-flags:v1:shop-1")).toBe(true); expect(pubClient.expirations.get("bodyshop-feature-flags:v1:shop-1")).toBe(3600); }); it("global invalidation bumps the cache version instead of deleting old versioned keys", async () => { const { helpers, pubClient } = createHelpers(); await helpers.setBodyshopFeatureFlagsInRedis("shop-1", { flags: { Demo: { treatment: "on" } } }); const nextVersion = await helpers.invalidateAllBodyshopFeatureFlagsInRedis(); expect(nextVersion).toBe(2); expect(pubClient.del).not.toHaveBeenCalledWith("bodyshop-feature-flags:v1:shop-1"); expect(await helpers.getBodyshopFeatureFlagsFromRedis("shop-1")).toBeNull(); expect(pubClient.values.has("bodyshop-feature-flags:v1:shop-1")).toBe(true); }); it("bodyshop invalidation deletes only the current version cache key for that shop", async () => { const { helpers, pubClient } = createHelpers(); await helpers.setBodyshopFeatureFlagsInRedis("shop-1", { flags: { Demo: { treatment: "on" } } }); await helpers.setBodyshopFeatureFlagsInRedis("shop-2", { flags: { Demo: { treatment: "on" } } }); await helpers.invalidateAllBodyshopFeatureFlagsInRedis(); await helpers.setBodyshopFeatureFlagsInRedis("shop-1", { flags: { Demo: { treatment: "off" } } }); await helpers.setBodyshopFeatureFlagsInRedis("shop-2", { flags: { Demo: { treatment: "on" } } }); await helpers.invalidateBodyshopFeatureFlagsInRedis("shop-1"); expect(pubClient.values.has("bodyshop-feature-flags:v1:shop-1")).toBe(true); expect(pubClient.values.has("bodyshop-feature-flags:v2:shop-1")).toBe(false); expect(pubClient.values.has("bodyshop-feature-flags:v2:shop-2")).toBe(true); }); });