fix: unit tests

This commit is contained in:
John Yeo
2026-05-14 13:10:10 +08:00
parent d28ef4ffe7
commit 40d09da77b
8 changed files with 87 additions and 46 deletions

View File

@@ -14,9 +14,10 @@ import {
} from "./initUtils/redisV2Config.js";
const redisV2Config = getRedisV2ConnectionConfig({
cacheV2Url: process.env.CACHE_V2_UPSTASH_URL,
cacheV2Url: process.env.CACHE_V2_DRAGONFLY_URL,
primaryCacheUrl: process.env.CACHE_URL,
currentRegion,
instanceName: "dragonfly",
});
export const hasRedisV2Config = Boolean(redisV2Config);
@@ -26,6 +27,7 @@ export const redisV2: Redis = redisV2Config
: redis;
const alternateInstanceUrls: Partial<Record<RedisV2InstanceName, string>> = {
upstash: process.env.CACHE_V2_UPSTASH_URL?.trim() || undefined,
redis: process.env.CACHE_V2_REDIS_URL?.trim() || undefined,
dragonfly: process.env.CACHE_V2_DRAGONFLY_URL?.trim() || undefined,
};

View File

@@ -6,16 +6,18 @@ export const getRedisV2ConnectionConfig = ({
cacheV2Url,
primaryCacheUrl,
currentRegion,
instanceName,
}: {
cacheV2Url?: string;
primaryCacheUrl?: string;
currentRegion: string;
instanceName: RedisV2InstanceName;
}) =>
cacheV2Url?.trim() && cacheV2Url.trim() !== primaryCacheUrl?.trim()
? {
cacheUrl: cacheV2Url.trim(),
region: `${currentRegion}:v2`,
supportsUpstashShebang: supportsUpstashShebangForRedisV2("upstash"),
supportsUpstashShebang: supportsUpstashShebangForRedisV2(instanceName),
commandTimeout: REDIS_V2_COMMAND_TIMEOUT_MS,
}
: null;

View File

@@ -22,7 +22,7 @@ export const resolveRedisV2 = (): Redis => {
lastLoggedInstance = activeInstance;
}
if (activeInstance === "upstash") return redisV2Primary;
if (activeInstance === "dragonfly") return redisV2Primary;
const alternate = getAlternateRedisV2Instance(activeInstance);
return alternate ?? redisV2Primary;

View File

@@ -4,7 +4,7 @@ export const RedisV2InstanceName = z.enum(["upstash", "redis", "dragonfly"]);
export type RedisV2InstanceName = z.infer<typeof RedisV2InstanceName>;
export const RedisV2CacheConfigSchema = z.object({
activeInstance: RedisV2InstanceName.default("upstash"),
activeInstance: RedisV2InstanceName.default("dragonfly"),
});
export type RedisV2CacheConfig = z.infer<typeof RedisV2CacheConfigSchema>;

View File

@@ -11,7 +11,7 @@ import {
const store = createEdgeConfigStore<RedisV2CacheConfig>({
s3Key: ADMIN_REDIS_V2_CACHE_CONFIG_KEY,
schema: RedisV2CacheConfigSchema,
defaultValue: () => ({ activeInstance: "upstash" }),
defaultValue: () => ({ activeInstance: "dragonfly" }),
pollIntervalMs: ms.seconds(10),
});

View File

@@ -11,12 +11,13 @@ import {
buildFullSubjectKey,
buildFullSubjectViewEpochKey,
getCachedFullSubject,
getOrSetCachedFullSubject,
invalidateCachedFullSubject,
} from "@/internal/customers/cache/fullSubject/index.js";
import { cleanupFullSubjectScenario } from "../../integration/db/full-subject/utils/cleanupFullSubjectScenario.js";
import { buildEntitySubjectScenario } from "../../integration/db/full-subject/utils/fullSubjectScenarioBuilders.js";
import { insertFullSubjectScenario } from "../../integration/db/full-subject/utils/insertFullSubjectScenario.js";
import { normalizedToCachedFullSubject } from "@/internal/customers/cache/fullSubject/fullSubjectCacheModel.js";
import { getFullSubjectNormalized } from "@/internal/customers/repos/getFullSubject/index.js";
import { cleanupFullSubjectScenario } from "../../db/full-subject/utils/cleanupFullSubjectScenario.js";
import { buildEntitySubjectScenario } from "../../db/full-subject/utils/fullSubjectScenarioBuilders.js";
import { insertFullSubjectScenario } from "../../db/full-subject/utils/insertFullSubjectScenario.js";
const describeDb = process.env.TESTS_ORG ? describe : describe.skip;
@@ -24,6 +25,47 @@ describeDb("invalidateCachedFullSubject", () => {
let ctx: TestContext;
let scenario: ReturnType<typeof buildEntitySubjectScenario>;
const cleanupScenarioState = async () => {
const customerKeys = await ctx.redisV2.keys(`{${scenario.ids.customerId}}:*`);
if (customerKeys.length > 0) await ctx.redisV2.unlink(...customerKeys);
await cleanupFullSubjectScenario({ ctx, scenario });
};
const getSubjectViewEpoch = async () => {
const epoch = await ctx.redisV2.get(
buildFullSubjectViewEpochKey({
orgId: ctx.org.id,
env: ctx.env,
customerId: scenario.ids.customerId,
}),
);
return epoch ? Number.parseInt(epoch, 10) : 0;
};
const seedCachedFullSubject = async ({ entityId }: { entityId?: string } = {}) => {
const result = await getFullSubjectNormalized({
ctx,
customerId: scenario.ids.customerId,
entityId,
});
if (!result) throw new Error("Failed to build full subject cache fixture");
const cached = normalizedToCachedFullSubject({
normalized: result.normalized,
subjectViewEpoch: await getSubjectViewEpoch(),
});
const subjectKey = buildFullSubjectKey({
orgId: ctx.org.id,
env: ctx.env,
customerId: scenario.ids.customerId,
entityId,
});
await ctx.redisV2.set(subjectKey, JSON.stringify(cached));
};
beforeAll(async () => {
const { createTestContext } = await import(
"@tests/utils/testInitUtils/createTestContext.js"
@@ -36,36 +78,19 @@ describeDb("invalidateCachedFullSubject", () => {
});
beforeEach(async () => {
await cleanupScenarioState();
await insertFullSubjectScenario({ ctx, scenario });
await getOrSetCachedFullSubject({
ctx,
customerId: scenario.ids.customerId,
source: "invalidateCachedFullSubjectTest",
});
await getOrSetCachedFullSubject({
ctx,
customerId: scenario.ids.customerId,
entityId: scenario.ids.entityIds[0],
source: "invalidateCachedFullSubjectTest",
});
await getOrSetCachedFullSubject({
ctx,
customerId: scenario.ids.customerId,
entityId: scenario.ids.entityIds[1],
source: "invalidateCachedFullSubjectTest",
});
await seedCachedFullSubject();
await seedCachedFullSubject({ entityId: scenario.ids.entityIds[0] });
await seedCachedFullSubject({ entityId: scenario.ids.entityIds[1] });
});
afterEach(async () => {
const customerKeys = await ctx.redisV2.keys(`{${scenario.ids.customerId}}:*`);
if (customerKeys.length > 0) {
await ctx.redisV2.unlink(...customerKeys);
}
await cleanupFullSubjectScenario({ ctx, scenario });
await cleanupScenarioState();
});
test("invalidates direct entity cache and increments subject view epoch", async () => {
const initialSubjectViewEpoch = await getSubjectViewEpoch();
const entityAKey = buildFullSubjectKey({
orgId: ctx.org.id,
env: ctx.env,
@@ -93,18 +118,11 @@ describeDb("invalidateCachedFullSubject", () => {
expect(await ctx.redisV2.exists(customerKey)).toBe(0);
expect(await ctx.redisV2.exists(entityAKey)).toBe(0);
expect(await ctx.redisV2.exists(entityBKey)).toBe(1);
expect(
await ctx.redisV2.get(
buildFullSubjectViewEpochKey({
orgId: ctx.org.id,
env: ctx.env,
customerId: scenario.ids.customerId,
}),
),
).toBe("1");
expect(await getSubjectViewEpoch()).toBe(initialSubjectViewEpoch + 1);
});
test("increments subject view epoch for customer invalidation", async () => {
const initialSubjectViewEpoch = await getSubjectViewEpoch();
const entityAKey = buildFullSubjectKey({
orgId: ctx.org.id,
env: ctx.env,
@@ -131,7 +149,9 @@ describeDb("invalidateCachedFullSubject", () => {
expect(await ctx.redisV2.exists(entityAKey)).toBe(1);
expect(await ctx.redisV2.exists(entityBKey)).toBe(1);
expect(await ctx.redisV2.get(epochKey)).toBe("1");
expect(Number.parseInt((await ctx.redisV2.get(epochKey)) ?? "0", 10)).toBe(
initialSubjectViewEpoch + 1,
);
});
test("sibling entity cache becomes stale after direct entity invalidation", async () => {

View File

@@ -17,7 +17,8 @@ import chalk from "chalk";
import { setupUpdateSubscriptionIntent } from "@/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionIntent";
const customerProduct = {
prices: [],
customer_prices: [],
customer_entitlements: [],
} as unknown as FullCusProduct;
const baseParams: UpdateSubscriptionV1Params = {

View File

@@ -6,27 +6,42 @@ import {
} from "@/external/redis/initUtils/redisV2Config.js";
describe("redis V2 connection config", () => {
test("uses a distinct CACHE_V2_UPSTASH_URL with the Upstash shebang", () => {
test("uses a distinct CACHE_V2_DRAGONFLY_URL without the Upstash shebang", () => {
expect(
getRedisV2ConnectionConfig({
cacheV2Url: " redis://v2 ",
primaryCacheUrl: "redis://primary",
currentRegion: "us-west-2",
instanceName: "dragonfly",
}),
).toEqual({
cacheUrl: "redis://v2",
region: "us-west-2:v2",
supportsUpstashShebang: true,
supportsUpstashShebang: false,
commandTimeout: REDIS_V2_COMMAND_TIMEOUT_MS,
});
});
test("falls back to primary Redis when CACHE_V2_UPSTASH_URL is absent or matches primary", () => {
test("uses the Upstash shebang when the upstash instance is selected", () => {
expect(
getRedisV2ConnectionConfig({
cacheV2Url: " redis://v2 ",
primaryCacheUrl: "redis://primary",
currentRegion: "us-west-2",
instanceName: "upstash",
}),
).toMatchObject({
supportsUpstashShebang: true,
});
});
test("falls back to primary Redis when the V2 URL is absent or matches primary", () => {
expect(
getRedisV2ConnectionConfig({
cacheV2Url: undefined,
primaryCacheUrl: "redis://primary",
currentRegion: "us-west-2",
instanceName: "dragonfly",
}),
).toBeNull();
expect(
@@ -34,6 +49,7 @@ describe("redis V2 connection config", () => {
cacheV2Url: " redis://primary ",
primaryCacheUrl: "redis://primary",
currentRegion: "us-west-2",
instanceName: "dragonfly",
}),
).toBeNull();
});