Files
cfw-autumn/server/tests/unit/customers/full-customer-cache-redis-gating.test.ts
2026-05-28 11:53:49 +01:00

185 lines
4.7 KiB
TypeScript

import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
import { CustomerNotFoundError, type FullCustomer } from "@autumn/shared";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
const mockState = {
cached: undefined as FullCustomer | undefined,
dbResult: undefined as FullCustomer | undefined,
hydrated: undefined as FullCustomer | undefined,
created: undefined as FullCustomer | undefined,
cacheReads: 0,
cacheWrites: 0,
dbCalls: 0,
createCalls: 0,
updateDetailsCalls: 0,
};
mock.module("@/internal/customers/CusService.js", () => ({
CusService: {
getFull: async () => {
mockState.dbCalls++;
return mockState.dbResult;
},
},
}));
mock.module(
"@/internal/customers/cusUtils/fullCustomerCacheUtils/getCachedFullCustomer.js",
() => ({
getCachedFullCustomer: async () => {
mockState.cacheReads++;
return mockState.cached;
},
}),
);
mock.module(
"@/internal/customers/cusUtils/fullCustomerCacheUtils/setCachedFullCustomer.js",
() => ({
setCachedFullCustomer: async () => {
mockState.cacheWrites++;
return "OK";
},
}),
);
mock.module("@/internal/customers/cusUtils/getFullCustomerSchedule.js", () => ({
hydrateFullCustomerSchedule: async ({
fullCustomer,
}: {
fullCustomer: unknown;
}) => mockState.hydrated ?? fullCustomer,
}));
mock.module("@/internal/customers/actions/index.js", () => ({
customerActions: {
createWithDefaults: async () => {
mockState.createCalls++;
return mockState.created;
},
},
}));
mock.module(
"@/internal/entities/handlers/handleCreateEntity/autoCreateEntity.js",
() => ({
autoCreateEntity: async () => null,
}),
);
mock.module("@/internal/customers/cusUtils/cusUtils.js", () => ({
updateCustomerDetails: async () => {
mockState.updateDetailsCalls++;
},
}));
import { getOrCreateCachedFullCustomer } from "@/internal/customers/cusUtils/fullCustomerCacheUtils/getOrCreateCachedFullCustomer.js";
import { getOrSetCachedFullCustomer } from "@/internal/customers/cusUtils/fullCustomerCacheUtils/getOrSetCachedFullCustomer.js";
const makeCtx = ({ skipCache = false }: { skipCache?: boolean } = {}) =>
({
skipCache,
logger: {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
},
}) as AutumnContext;
const makeFullCustomer = (id: string) =>
({
id,
internal_id: `int_${id}`,
org_id: "test_org",
created_at: Date.now(),
env: "sandbox",
name: null,
email: null,
metadata: {},
processor: null,
send_email_receipts: false,
customer_products: [],
extra_customer_entitlements: [],
entities: [],
}) as FullCustomer;
describe("full customer cache Redis gating", () => {
beforeEach(() => {
mockState.cached = undefined;
mockState.dbResult = undefined;
mockState.hydrated = undefined;
mockState.created = undefined;
mockState.cacheReads = 0;
mockState.cacheWrites = 0;
mockState.dbCalls = 0;
mockState.createCalls = 0;
mockState.updateDetailsCalls = 0;
});
test("getOrSetCachedFullCustomer skips Redis read/write when skipCache is true", async () => {
const dbCustomer = makeFullCustomer("db-customer");
mockState.cached = makeFullCustomer("cached-customer");
mockState.dbResult = dbCustomer;
const result = await getOrSetCachedFullCustomer({
ctx: makeCtx({ skipCache: true }),
customerId: "db-customer",
source: "unit-test",
});
expect(result).toBe(dbCustomer);
expect(mockState.cacheReads).toBe(0);
expect(mockState.cacheWrites).toBe(0);
expect(mockState.dbCalls).toBe(1);
});
test("getOrCreateCachedFullCustomer still creates when skipCache is true and DB misses", async () => {
const createdCustomer = makeFullCustomer("created-customer");
mockState.dbResult = undefined;
mockState.created = createdCustomer;
const result = await getOrCreateCachedFullCustomer({
ctx: makeCtx({ skipCache: true }),
params: {
customer_id: "created-customer",
feature_id: "messages",
},
source: "unit-test",
});
expect(result).toBe(createdCustomer);
expect(mockState.cacheReads).toBe(0);
expect(mockState.cacheWrites).toBe(0);
expect(mockState.dbCalls).toBe(1);
expect(mockState.createCalls).toBe(1);
});
test("getOrCreateCachedFullCustomer honors skipCreate when skipCache is true and DB misses", async () => {
mockState.dbResult = undefined;
await expect(
getOrCreateCachedFullCustomer({
ctx: makeCtx({ skipCache: true }),
params: {
customer_id: "missing-customer",
feature_id: "messages",
},
source: "unit-test",
skipCreate: true,
}),
).rejects.toBeInstanceOf(CustomerNotFoundError);
expect(mockState.cacheReads).toBe(0);
expect(mockState.cacheWrites).toBe(0);
expect(mockState.dbCalls).toBe(1);
expect(mockState.createCalls).toBe(0);
});
});
afterAll(() => {
mock.restore();
});