fix: unit tests
This commit is contained in:
4
server/src/external/redis/initRedisV2.ts
vendored
4
server/src/external/redis/initRedisV2.ts
vendored
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
2
server/src/external/redis/resolveRedisV2.ts
vendored
2
server/src/external/redis/resolveRedisV2.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user