import { describe, expect, it, vi } from "vitest"; import { createRequire } from "module"; const require = createRequire(import.meta.url); const { getBodyshopFeatureFlags, invalidateBodyshopFeatureFlags } = require("./feature-flags"); /** * Creates the minimal Express response mock needed by feature flag route tests. */ const createResponse = () => { const res = { body: null, code: 200, json: vi.fn((body) => { res.body = body; return res; }), status: vi.fn((code) => { res.code = code; return res; }) }; return res; }; describe("feature flag runtime route", () => { it("returns cached flags without re-querying runtime assignments", async () => { const req = { params: { bodyshopId: "shop-1" }, sessionUtils: { getBodyshopFeatureFlagsCacheVersion: vi.fn(async () => "7"), getBodyshopFeatureFlagsFromRedis: vi.fn(async () => ({ bodyshopId: "shop-1", flags: { Demo: { treatment: "on", config: null } } })) }, user: { email: "tester@example.com" }, userGraphQLClient: { request: vi.fn(async () => ({ bodyshops_by_pk: { id: "shop-1" } })) } }; const res = createResponse(); await getBodyshopFeatureFlags(req, res); expect(req.sessionUtils.getBodyshopFeatureFlagsFromRedis).toHaveBeenCalledWith("shop-1", "7"); expect(req.userGraphQLClient.request).toHaveBeenCalledTimes(1); expect(res.body).toEqual({ bodyshopId: "shop-1", flags: { Demo: { treatment: "on", config: null } }, source: "redis" }); }); it("merges active definitions with bodyshop assignments on cache miss", async () => { const req = { params: { bodyshopId: "shop-1" }, sessionUtils: { getBodyshopFeatureFlagsCacheVersion: vi.fn(async () => "3"), getBodyshopFeatureFlagsFromRedis: vi.fn(async () => null), setBodyshopFeatureFlagsInRedis: vi.fn() }, user: { email: "tester@example.com" }, userGraphQLClient: { request: vi .fn() .mockResolvedValueOnce({ bodyshops_by_pk: { id: "shop-1" } }) .mockResolvedValueOnce({ feature_flags: [ { name: "Default_Off", default_treatment: "off" }, { name: "Default_Custom", default_treatment: "variant-a" } ], bodyshop_feature_flags: [ { name: "Default_Off", treatment: "on", config: { limit: 10 }, activeDate: "2026-05-19T15:00:00.000Z", deactiveDate: null } ] }) } }; const res = createResponse(); await getBodyshopFeatureFlags(req, res); expect(req.sessionUtils.setBodyshopFeatureFlagsInRedis).toHaveBeenCalledWith( "shop-1", expect.objectContaining({ bodyshopId: "shop-1", flags: { Default_Off: { treatment: "on", config: { limit: 10 }, activeDate: "2026-05-19T15:00:00.000Z", deactiveDate: null }, Default_Custom: { treatment: "variant-a", config: null } } }), "3" ); expect(res.body).toEqual( expect.objectContaining({ bodyshopId: "shop-1", flags: { Default_Off: { treatment: "on", config: { limit: 10 }, activeDate: "2026-05-19T15:00:00.000Z", deactiveDate: null }, Default_Custom: { treatment: "variant-a", config: null } }, source: "database" }) ); }); }); describe("feature flag cache invalidation route", () => { it("invalidates one bodyshop when a bodyshop assignment event is received", async () => { const emit = vi.fn(); const req = { body: { event: { table: { name: "bodyshop_feature_flags" }, data: { new: { bodyshopid: "shop-1" } } } }, ioHelpers: { getBodyshopRoom: vi.fn(() => "bodyshop-room-shop-1") }, ioRedis: { to: vi.fn(() => ({ emit })) }, sessionUtils: { invalidateBodyshopFeatureFlagsInRedis: vi.fn() } }; const res = createResponse(); await invalidateBodyshopFeatureFlags(req, res); expect(req.sessionUtils.invalidateBodyshopFeatureFlagsInRedis).toHaveBeenCalledWith("shop-1"); expect(req.ioRedis.to).toHaveBeenCalledWith("bodyshop-room-shop-1"); expect(emit).toHaveBeenCalledWith( "feature-flags-changed", expect.objectContaining({ bodyshopId: "shop-1", scope: "bodyshop", source: "hasura", table: "bodyshop_feature_flags" }) ); expect(res.body).toEqual({ ok: true, bodyshopId: "shop-1" }); }); it("bumps global cache version when a feature definition event is received", async () => { const req = { body: { event: { table: { name: "feature_flags" }, data: { new: { name: "Demo" } } } }, ioRedis: { emit: vi.fn() }, sessionUtils: { invalidateAllBodyshopFeatureFlagsInRedis: vi.fn(async () => 12) } }; const res = createResponse(); await invalidateBodyshopFeatureFlags(req, res); expect(req.sessionUtils.invalidateAllBodyshopFeatureFlagsInRedis).toHaveBeenCalled(); expect(req.ioRedis.emit).toHaveBeenCalledWith( "feature-flags-changed", expect.objectContaining({ name: "Demo", scope: "global", source: "hasura", table: "feature_flags" }) ); expect(res.body).toEqual({ ok: true, table: "feature_flags", cacheVersion: 12 }); }); });