Files
cfw-autumn/server/tests/unit/balances/track/handle-track-queue-fallback.test.ts
2026-04-24 16:40:36 +01:00

133 lines
3.2 KiB
TypeScript

import { beforeEach, describe, expect, mock, test } from "bun:test";
import {
ApiVersion,
ApiVersionClass,
AppEnv,
type TrackResponseV3,
} from "@autumn/shared";
import { RedisUnavailableError } from "@/external/redis/utils/errors.js";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
const mockState = {
queueCalls: [] as Record<string, unknown>[],
runTrackV2Calls: [] as Record<string, unknown>[],
runTrackV3Calls: [] as Record<string, unknown>[],
v3Error: null as unknown,
queuedResponse: {
customer_id: "cus_123",
value: 2,
balance: null,
} as TrackResponseV3 | null,
};
mock.module("@/internal/balances/track/runTrackV2.js", () => ({
runTrackV2: async (args: Record<string, unknown>) => {
mockState.runTrackV2Calls.push(args);
return { ok: true };
},
}));
mock.module("@/internal/balances/track/v3/runTrackV3.js", () => ({
runTrackV3: async (args: Record<string, unknown>) => {
mockState.runTrackV3Calls.push(args);
if (mockState.v3Error) throw mockState.v3Error;
return { ok: true };
},
}));
mock.module("@/internal/balances/track/utils/queueTrack.js", () => ({
queueTrack: async (args: Record<string, unknown>) => {
mockState.queueCalls.push(args);
return mockState.queuedResponse;
},
}));
mock.module("@/external/redis/initUtils/redisV2Availability.js", () => ({
shouldUseRedisV2: () => true,
}));
import { runTrackWithRollout } from "@/internal/balances/track/runTrackWithRollout.js";
const ctx = {
org: { id: "org_123" },
env: AppEnv.Sandbox,
apiVersion: new ApiVersionClass(ApiVersion.V2_1),
logger: {
warn: () => undefined,
},
rolloutSnapshot: {
rolloutId: "v2-cache",
enabled: true,
percent: 100,
previousPercent: 0,
changedAt: 1,
customerBucket: 10,
},
} as unknown as AutumnContext;
const body = {
customer_id: "cus_123",
feature_id: "messages",
value: 2,
idempotency_key: "idem_123",
};
describe("track queue fallback", () => {
beforeEach(() => {
mockState.queueCalls = [];
mockState.runTrackV2Calls = [];
mockState.runTrackV3Calls = [];
mockState.v3Error = null;
mockState.queuedResponse = {
customer_id: "cus_123",
value: 2,
balance: null,
};
});
test("queues track when rollout path hits a retryable Redis failure", async () => {
mockState.v3Error = new RedisUnavailableError({
source: "runTrackV3",
reason: "not_ready",
});
const response = await runTrackWithRollout({
ctx,
body,
featureDeductions: [],
});
expect(mockState.runTrackV3Calls).toHaveLength(1);
expect(mockState.runTrackV2Calls).toHaveLength(0);
expect(mockState.queueCalls).toHaveLength(1);
expect(mockState.queueCalls[0]).toMatchObject({
body: {
customer_id: "cus_123",
feature_id: "messages",
},
});
if (!mockState.queuedResponse) throw new Error("expected queued response");
expect(response).toEqual(mockState.queuedResponse);
});
test("throws retryable Redis failure when queue fallback is unavailable", async () => {
const error = new RedisUnavailableError({
source: "runTrackV3",
reason: "timeout",
});
mockState.v3Error = error;
mockState.queuedResponse = null;
await expect(
runTrackWithRollout({
ctx,
body,
featureDeductions: [],
}),
).rejects.toBe(error);
expect(mockState.queueCalls).toHaveLength(1);
});
});