Files
cfw-autumn/server/tests/_temp/temp3.test.ts
2025-11-18 10:39:06 +00:00

278 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { beforeAll, describe, expect, test } from "bun:test";
import { ApiVersion, type LimitedItem } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { timeout } from "@tests/utils/genUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { Decimal } from "decimal.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
const testCase = "concurrent-track7";
// Product with both lifetime and monthly Messages features
const lifetimeMessagesItem = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 20000,
interval: null, // Lifetime
}) as LimitedItem;
const monthlyMessagesItem = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 10000,
interval: "month" as any,
intervalCount: 1,
}) as LimitedItem;
const pro = constructProduct({
type: "free",
isDefault: false,
items: [lifetimeMessagesItem, monthlyMessagesItem],
});
const NUM_REQUESTS = 500; // Reduced from 10000 to avoid DB parameter limits
const NUM_CUSTOMERS = 3;
// Calculate total included usage dynamically
const TOTAL_INCLUDED_USAGE =
(lifetimeMessagesItem.included_usage ?? 0) +
(monthlyMessagesItem.included_usage ?? 0);
// Helper to generate random decimal between min and max using Decimal.js
const randomDecimal = (min: number, max: number): Decimal => {
const value = Math.random() * (max - min) + min;
return new Decimal(value).toDecimalPlaces(2);
};
describe(`${chalk.yellowBright(`${testCase}: Stress test with 10k concurrent requests per customer through check (send_event)`)}`, () => {
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
const customerIds = Array.from(
{ length: NUM_CUSTOMERS },
(_, i) => `${testCase}_customer${i + 1}`,
);
// Store expected total usage per customer using Decimal for precision
const customerExpectedUsage: Record<string, Decimal> = {};
beforeAll(async () => {
// Initialize all customers
for (const customerId of customerIds) {
await initCustomerV3({
ctx,
customerId,
withTestClock: false,
});
}
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
});
for (const customerId of customerIds) {
await autumnV1.attach({
customer_id: customerId,
product_id: pro.id,
});
// Initialize expected usage to 0
customerExpectedUsage[customerId] = new Decimal(0);
}
});
test("should have initial balances for all customers", async () => {
for (const customerId of customerIds) {
const customer = await autumnV1.customers.get(customerId);
console.log(`\n🔍 Initial state for ${customerId}:`);
console.log(
` Balance: ${customer.features[TestFeature.Messages].balance}`,
);
console.log(` Usage: ${customer.features[TestFeature.Messages].usage}`);
// Total balance should be lifetime + monthly
expect(customer.features[TestFeature.Messages].balance).toBe(
TOTAL_INCLUDED_USAGE,
);
expect(customer.features[TestFeature.Messages].usage).toBe(0);
expect(customer.features[TestFeature.Messages].breakdown?.length).toBe(2);
}
});
test(`should handle ${NUM_REQUESTS * NUM_CUSTOMERS} concurrent requests across ${NUM_CUSTOMERS} customers`, async () => {
console.log(
`\n🚀 Starting ${NUM_REQUESTS * NUM_CUSTOMERS} concurrent track requests...`,
);
console.log(
` ${NUM_REQUESTS} requests per customer × ${NUM_CUSTOMERS} customers`,
);
const allPromises: Promise<number>[] = [];
// Generate requests for each customer
for (const customerId of customerIds) {
const customerPromises: Promise<number>[] = [];
for (let i = 0; i < NUM_REQUESTS; i++) {
// Generate random value between 0.01 and 2.00 using Decimal
const decimalValue = randomDecimal(0.01, 2.0);
const value = decimalValue.toDecimalPlaces(5).toNumber();
// Accumulate expected usage using Decimal for precision
customerExpectedUsage[customerId] =
customerExpectedUsage[customerId].plus(decimalValue);
// Create track request for Messages feature with timing
const requestStart = Date.now();
const promise = autumnV1
.check({
customer_id: customerId,
feature_id: TestFeature.Messages,
send_event: true,
required_balance: value,
skip_event: true, // Skip event insertion for stress test
})
.then(() => Date.now() - requestStart);
customerPromises.push(promise);
}
allPromises.push(...customerPromises);
}
// Execute all requests concurrently
const startTime = Date.now();
const durations = await Promise.all(allPromises);
const endTime = Date.now();
// Calculate P99
const sortedDurations = durations.sort((a, b) => a - b);
const p99Index = Math.floor(sortedDurations.length * 0.99);
const p99 = sortedDurations[p99Index];
console.log(
`\n✅ Completed ${NUM_REQUESTS * NUM_CUSTOMERS} requests in ${endTime - startTime}ms`,
);
console.log(
` Average: ${((endTime - startTime) / (NUM_REQUESTS * NUM_CUSTOMERS)).toFixed(2)}ms per request`,
);
console.log(` P99: ${p99.toFixed(2)}ms`);
// Log expected totals per customer
for (const customerId of customerIds) {
console.log(`\n📊 ${customerId}:`);
console.log(
` Total usage: ${customerExpectedUsage[customerId].toFixed(2)} units`,
);
}
});
test("should have correct cached balances for all customers", async () => {
for (const customerId of customerIds) {
const customer = await autumnV1.customers.get(customerId);
const totalUsage = customerExpectedUsage[customerId];
// Balance should be capped at 0 (no negative balances without overage_allowed)
const expectedBalance = Decimal.max(
0,
new Decimal(TOTAL_INCLUDED_USAGE).minus(totalUsage),
)
.toDP(5)
.toNumber();
const actualBalance = new Decimal(
customer.features[TestFeature.Messages].balance ?? 0,
)
.toDP(5)
.toNumber();
// Usage should be capped at included_usage without overage_allowed
const expectedUsage = Decimal.min(totalUsage, TOTAL_INCLUDED_USAGE)
.toDP(5)
.toNumber();
const actualUsage = new Decimal(
customer.features[TestFeature.Messages].usage ?? 0,
)
.toDP(5)
.toNumber();
// Verify balance and usage match expectations
expect(actualBalance).toEqual(expectedBalance);
expect(actualUsage).toEqual(expectedUsage);
// Verify breakdown balances sum to top-level balance
const breakdown = customer.features[TestFeature.Messages].breakdown;
if (breakdown && breakdown.length > 0) {
const breakdownBalance = breakdown.reduce(
(sum, b) => new Decimal(sum).plus(b.balance || 0).toNumber(),
0,
);
expect(new Decimal(breakdownBalance).toDP(5).toNumber()).toEqual(
actualBalance!,
);
}
}
});
test("should have correct non-cached balances for all customers after 2s", async () => {
console.log("\n⏳ Waiting 2s for DB sync...");
await timeout(5000);
for (const customerId of customerIds) {
const customer = await autumnV1.customers.get(customerId, {
skip_cache: "true",
});
const totalUsage = customerExpectedUsage[customerId];
// Balance should be capped at 0 (no negative balances without overage_allowed)
const expectedBalance = Decimal.max(
0,
new Decimal(TOTAL_INCLUDED_USAGE).minus(totalUsage),
)
.toDP(5)
.toNumber();
const actualBalance = new Decimal(
customer.features[TestFeature.Messages].balance ?? 0,
)
.toDP(5)
.toNumber();
// Usage should be capped at included_usage without overage_allowed
const expectedUsage = Decimal.min(totalUsage, TOTAL_INCLUDED_USAGE)
.toDP(5)
.toNumber();
const actualUsage = new Decimal(
customer.features[TestFeature.Messages].usage ?? 0,
)
.toDP(5)
.toNumber();
// Use Decimal for precise comparisons - expect exact match
expect(actualBalance).toEqual(expectedBalance);
// Verify usage matches - expect exact match
expect(actualUsage).toEqual(expectedUsage);
// Verify breakdown balances match top-level (lifetime + monthly)
const breakdown = customer.features[TestFeature.Messages].breakdown;
if (breakdown && breakdown.length > 0) {
const breakdownBalance = breakdown.reduce(
(sum, b) => new Decimal(sum).plus(b.balance || 0).toNumber(),
0,
);
expect(new Decimal(breakdownBalance).toDP(5).toNumber()).toEqual(
actualBalance!,
);
}
}
console.log("\n✅ All balances verified successfully!");
});
});