test: 💍 sync track tests
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
OnIncrease,
|
||||
ProductItem,
|
||||
ProductItemConfig,
|
||||
ProductItemFeatureType,
|
||||
ProductItemInterval,
|
||||
RolloverConfig,
|
||||
UsageModel,
|
||||
@@ -17,6 +18,7 @@ export const constructFeatureItem = ({
|
||||
entityFeatureId,
|
||||
isBoolean = false,
|
||||
rolloverConfig,
|
||||
featureType
|
||||
}: {
|
||||
featureId: string;
|
||||
includedUsage?: number;
|
||||
@@ -25,6 +27,7 @@ export const constructFeatureItem = ({
|
||||
entityFeatureId?: string;
|
||||
isBoolean?: boolean;
|
||||
rolloverConfig?: RolloverConfig;
|
||||
featureType?: ProductItemFeatureType;
|
||||
}) => {
|
||||
if (isBoolean) {
|
||||
return {
|
||||
@@ -36,6 +39,7 @@ export const constructFeatureItem = ({
|
||||
feature_id: featureId,
|
||||
included_usage: includedUsage,
|
||||
entity_feature_id: entityFeatureId,
|
||||
feature_type: featureType,
|
||||
interval: interval,
|
||||
interval_count: intervalCount,
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ if [ "$1" == "custom" ]; then
|
||||
else
|
||||
npx mocha --bail --timeout 10000000 "tests/$FILE_TO_TEST.ts"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# MOCHA_SETUP="npx mocha tests/00_setup.ts"
|
||||
# MOCHA_CMD="npx mocha --parallel --timeout 10000000 --ignore tests/00_setup.ts"
|
||||
|
||||
125
server/tests/sync/sync1.test.ts
Normal file
125
server/tests/sync/sync1.test.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { ApiVersion, ProductItemFeatureType, type Organization } from "@autumn/shared";
|
||||
import type { AppEnv, Autumn } from "autumn-js";
|
||||
import { expect } from "chai";
|
||||
import chalk from "chalk";
|
||||
import type { Stripe } from "stripe";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { addPrefixToProducts } from "tests/attach/utils.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
|
||||
const testCase = "sync1";
|
||||
const customerId = `${testCase}_cus1`;
|
||||
|
||||
const pro = constructProduct({
|
||||
id: "pro",
|
||||
items: [constructFeatureItem({ featureId: TestFeature.Messages, includedUsage: 5, featureType: ProductItemFeatureType.SingleUse })],
|
||||
type: "pro",
|
||||
})
|
||||
|
||||
describe(`${chalk.yellowBright(`sync/${testCase}: Testing sync track consumable usage`)}`, () => {
|
||||
let db: DrizzleCli;
|
||||
let org: Organization;
|
||||
let env: AppEnv;
|
||||
let stripeCli: Stripe;
|
||||
let autumnInt: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
let autumnJs: Autumn;
|
||||
|
||||
before(async function () {
|
||||
await setupBefore(this);
|
||||
db = this.db;
|
||||
org = this.org;
|
||||
env = this.env;
|
||||
stripeCli = this.stripeCli;
|
||||
autumnJs = this.autumnJs;
|
||||
try {
|
||||
await (autumnInt as AutumnInt).customers.delete(customerId);
|
||||
} catch (_) {}
|
||||
|
||||
await addPrefixToProducts({
|
||||
products: [pro],
|
||||
prefix: testCase,
|
||||
})
|
||||
|
||||
await createProducts({
|
||||
autumn: autumnInt,
|
||||
products: [pro],
|
||||
customerId,
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
})
|
||||
});
|
||||
|
||||
it("should create a customer and issue balances", async () => {
|
||||
const { customer } = await initCustomerV2({
|
||||
autumn: autumnInt,
|
||||
customerId,
|
||||
org,
|
||||
env,
|
||||
db,
|
||||
attachPm: "success",
|
||||
})
|
||||
expect(customer).to.exist;
|
||||
expect(customer.id).to.equal(customerId);
|
||||
expect(customer.name).to.equal(customerId);
|
||||
expect(customer.email).to.equal(`${customerId}@example.com`);
|
||||
|
||||
await autumnJs.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
})
|
||||
});
|
||||
|
||||
it("should only allow one 10x send with a 5x balance", async () => {
|
||||
const customer = await autumnInt.customers.get(customerId);
|
||||
const balance = customer.features[TestFeature.Messages].balance;
|
||||
expect(balance).to.equal(5, `Balance should be 5, got ${balance} | Balances: ${JSON.stringify(customer.features)}`);
|
||||
|
||||
const promises = [
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 10,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 10,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 10,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 10,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 10,
|
||||
}),
|
||||
];
|
||||
|
||||
let rejections = await Promise.allSettled(promises);
|
||||
|
||||
expect(rejections.every(r => r.status === "rejected")).to.equal(true, `${rejections.map(r => r.status).join(", ")} <- all must be rejected`);
|
||||
|
||||
const { data: balances, error } = await autumnJs.customers.get(
|
||||
customerId,
|
||||
);
|
||||
expect(error).to.be.null;
|
||||
expect(balances?.features[TestFeature.Messages]?.balance).to.equal(
|
||||
5,
|
||||
`Balance should be 10, got ${balances?.features[TestFeature.Messages]?.balance}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
129
server/tests/sync/sync2.test.ts
Normal file
129
server/tests/sync/sync2.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { ApiVersion, ProductItemFeatureType, type Organization } from "@autumn/shared";
|
||||
import type { AppEnv, Autumn } from "autumn-js";
|
||||
import { expect } from "chai";
|
||||
import chalk from "chalk";
|
||||
import type { Stripe } from "stripe";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { addPrefixToProducts } from "tests/attach/utils.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
|
||||
const testCase = "sync2";
|
||||
const customerId = `${testCase}_cus1`;
|
||||
|
||||
const pro = constructProduct({
|
||||
id: "pro",
|
||||
items: [constructFeatureItem({ featureId: TestFeature.Users, includedUsage: 1, featureType: ProductItemFeatureType.ContinuousUse })],
|
||||
type: "pro",
|
||||
})
|
||||
|
||||
describe(`${chalk.yellowBright(`sync/${testCase}: Testing sync track allocated feature with concurrent requests`)}`, () => {
|
||||
let db: DrizzleCli;
|
||||
let org: Organization;
|
||||
let env: AppEnv;
|
||||
let stripeCli: Stripe;
|
||||
let autumnInt: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
let autumnJs: Autumn;
|
||||
|
||||
before(async function () {
|
||||
await setupBefore(this);
|
||||
db = this.db;
|
||||
org = this.org;
|
||||
env = this.env;
|
||||
stripeCli = this.stripeCli;
|
||||
autumnJs = this.autumnJs;
|
||||
try {
|
||||
await (autumnInt as AutumnInt).customers.delete(customerId);
|
||||
} catch (_) {}
|
||||
|
||||
await addPrefixToProducts({
|
||||
products: [pro],
|
||||
prefix: testCase,
|
||||
})
|
||||
|
||||
await createProducts({
|
||||
autumn: autumnInt,
|
||||
products: [pro],
|
||||
customerId,
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
})
|
||||
});
|
||||
|
||||
it("should create a customer and issue balances", async () => {
|
||||
const { customer } = await initCustomerV2({
|
||||
autumn: autumnInt,
|
||||
customerId,
|
||||
org,
|
||||
env,
|
||||
db,
|
||||
attachPm: "success",
|
||||
})
|
||||
expect(customer).to.exist;
|
||||
expect(customer.id).to.equal(customerId);
|
||||
expect(customer.name).to.equal(customerId);
|
||||
expect(customer.email).to.equal(`${customerId}@example.com`);
|
||||
|
||||
await autumnJs.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
})
|
||||
});
|
||||
|
||||
it("should only allow one concurrent track with balance of 1", async () => {
|
||||
const customer = await autumnInt.customers.get(customerId);
|
||||
const balance = customer.features[TestFeature.Users].balance;
|
||||
expect(balance).to.equal(1, `Balance should be 1, got ${balance} | Balances: ${JSON.stringify(customer.features)}`);
|
||||
|
||||
const promises = [
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Users,
|
||||
value: 1,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Users,
|
||||
value: 1,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Users,
|
||||
value: 1,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Users,
|
||||
value: 1,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Users,
|
||||
value: 1,
|
||||
}),
|
||||
];
|
||||
|
||||
let results = await Promise.allSettled(promises);
|
||||
|
||||
const successCount = results.filter(r => r.status === "fulfilled").length;
|
||||
const rejectedCount = results.filter(r => r.status === "rejected").length;
|
||||
|
||||
expect(successCount).to.equal(1, `Expected exactly 1 success, got ${successCount} | Results: ${results.map(r => r.status).join(", ")}`);
|
||||
expect(rejectedCount).to.equal(4, `Expected exactly 4 rejections, got ${rejectedCount} | Results: ${results.map(r => r.status).join(", ")}`);
|
||||
|
||||
const { data: balances, error } = await autumnJs.customers.get(
|
||||
customerId,
|
||||
);
|
||||
expect(error).to.be.null;
|
||||
expect(balances?.features[TestFeature.Users]?.balance).to.equal(
|
||||
0,
|
||||
`Balance should be 0 (allocated feature), got ${balances?.features[TestFeature.Users]?.balance}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
152
server/tests/sync/sync3.test.ts
Normal file
152
server/tests/sync/sync3.test.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { ApiVersion, ProductItemFeatureType, type Organization } from "@autumn/shared";
|
||||
import type { AppEnv, Autumn } from "autumn-js";
|
||||
import { expect } from "chai";
|
||||
import chalk from "chalk";
|
||||
import type { Stripe } from "stripe";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { addPrefixToProducts } from "tests/attach/utils.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
|
||||
const testCase = "sync3";
|
||||
const customerId = `${testCase}_cus1`;
|
||||
|
||||
const userItem = constructFeatureItem({
|
||||
featureId: TestFeature.Users,
|
||||
includedUsage: 1,
|
||||
featureType: ProductItemFeatureType.ContinuousUse,
|
||||
});
|
||||
|
||||
const pro = constructProduct({
|
||||
id: "pro",
|
||||
items: [userItem],
|
||||
type: "pro",
|
||||
})
|
||||
|
||||
describe(`${chalk.yellowBright(`sync/${testCase}: Testing sync track prepaid allocated feature with concurrent requests`)}`, () => {
|
||||
let db: DrizzleCli;
|
||||
let org: Organization;
|
||||
let env: AppEnv;
|
||||
let stripeCli: Stripe;
|
||||
let autumnInt: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
let autumnJs: Autumn;
|
||||
|
||||
before(async function () {
|
||||
await setupBefore(this);
|
||||
db = this.db;
|
||||
org = this.org;
|
||||
env = this.env;
|
||||
stripeCli = this.stripeCli;
|
||||
autumnJs = this.autumnJs;
|
||||
try {
|
||||
await (autumnInt as AutumnInt).customers.delete(customerId);
|
||||
} catch (_) {}
|
||||
|
||||
await addPrefixToProducts({
|
||||
products: [pro],
|
||||
prefix: testCase,
|
||||
})
|
||||
|
||||
await createProducts({
|
||||
autumn: autumnInt,
|
||||
products: [pro],
|
||||
customerId,
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
})
|
||||
});
|
||||
|
||||
it("should create a customer and issue balances", async () => {
|
||||
const { customer } = await initCustomerV2({
|
||||
autumn: autumnInt,
|
||||
customerId,
|
||||
org,
|
||||
env,
|
||||
db,
|
||||
attachPm: "success",
|
||||
})
|
||||
expect(customer).to.exist;
|
||||
expect(customer.id).to.equal(customerId);
|
||||
expect(customer.name).to.equal(customerId);
|
||||
expect(customer.email).to.equal(`${customerId}@example.com`);
|
||||
|
||||
await autumnJs.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
})
|
||||
});
|
||||
|
||||
it("should only allow one concurrent seat allocation with 1 included seat and create no duplicate invoices", async () => {
|
||||
const customer = await autumnInt.customers.get(customerId);
|
||||
const balance = customer.features[TestFeature.Users].balance;
|
||||
expect(balance).to.equal(1, `Balance should be 1, got ${balance} | Balances: ${JSON.stringify(customer.features)}`);
|
||||
|
||||
const initialInvoices = await stripeCli.invoices.list({
|
||||
customer: customer.stripe_id as string,
|
||||
});
|
||||
const initialInvoiceCount = initialInvoices.data.length;
|
||||
|
||||
// Try to allocate 5 different seats concurrently - only 1 should succeed (the included seat)
|
||||
// The other 4 should be rejected because we only have 1 included seat
|
||||
const promises = [
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Users,
|
||||
value: 1,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Users,
|
||||
value: 1,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Users,
|
||||
value: 1,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Users,
|
||||
value: 1,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Users,
|
||||
value: 1,
|
||||
}),
|
||||
];
|
||||
|
||||
let results = await Promise.allSettled(promises);
|
||||
|
||||
const successCount = results.filter(r => r.status === "fulfilled").length;
|
||||
const rejectedCount = results.filter(r => r.status === "rejected").length;
|
||||
|
||||
expect(successCount).to.equal(1, `Expected exactly 1 success (included seat), got ${successCount} | Results: ${results.map(r => r.status).join(", ")}`);
|
||||
expect(rejectedCount).to.equal(4, `Expected exactly 4 rejections (exceeded included), got ${rejectedCount} | Results: ${results.map(r => r.status).join(", ")}`);
|
||||
|
||||
const { data: balances, error } = await autumnJs.customers.get(
|
||||
customerId,
|
||||
);
|
||||
expect(error).to.be.null;
|
||||
expect(balances?.features[TestFeature.Users]?.balance).to.equal(
|
||||
0,
|
||||
`Balance should be 0 (allocated feature), got ${balances?.features[TestFeature.Users]?.balance}`,
|
||||
);
|
||||
|
||||
// Verify no duplicate invoices were created
|
||||
// Since we only allocated the 1 included seat, no overage charges should occur
|
||||
const finalInvoices = await stripeCli.invoices.list({
|
||||
customer: customer.stripe_id as string,
|
||||
});
|
||||
const finalInvoiceCount = finalInvoices.data.length;
|
||||
const newInvoicesCreated = finalInvoiceCount - initialInvoiceCount;
|
||||
|
||||
expect(newInvoicesCreated).to.equal(0, `Expected 0 new invoices (only used included seat), got ${newInvoicesCreated}. Initial: ${initialInvoiceCount}, Final: ${finalInvoiceCount}`);
|
||||
});
|
||||
});
|
||||
171
server/tests/sync/sync4.test.ts
Normal file
171
server/tests/sync/sync4.test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { ApiVersion, type Organization } from "@autumn/shared";
|
||||
import { AppEnv, Autumn } from "autumn-js";
|
||||
import { expect } from "chai";
|
||||
import chalk from "chalk";
|
||||
import type { Stripe } from "stripe";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { addPrefixToProducts } from "tests/attach/utils.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { EventService } from "@/internal/api/events/EventService.js";
|
||||
|
||||
const testCase = "sync4";
|
||||
const customerId = `${testCase}_cus1`;
|
||||
|
||||
const messageItem = constructArrearItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 5,
|
||||
price: 0.1,
|
||||
billingUnits: 1,
|
||||
usageLimit: 10,
|
||||
});
|
||||
|
||||
const pro = constructProduct({
|
||||
id: "pro",
|
||||
items: [messageItem],
|
||||
type: "pro",
|
||||
})
|
||||
|
||||
describe(`${chalk.yellowBright(`sync/${testCase}: Testing usage_limits with PayPerUse feature and concurrent requests`)}`, () => {
|
||||
let db: DrizzleCli;
|
||||
let org: Organization;
|
||||
let env: AppEnv;
|
||||
let stripeCli: Stripe;
|
||||
let autumnInt: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
let autumnJs: Autumn;
|
||||
|
||||
before(async function () {
|
||||
await setupBefore(this);
|
||||
db = this.db;
|
||||
org = this.org;
|
||||
env = this.env;
|
||||
stripeCli = this.stripeCli;
|
||||
autumnJs = this.autumnJs;
|
||||
try {
|
||||
await (autumnInt as AutumnInt).customers.delete(customerId);
|
||||
await EventService.del
|
||||
} catch (_) {}
|
||||
|
||||
await addPrefixToProducts({
|
||||
products: [pro],
|
||||
prefix: testCase,
|
||||
})
|
||||
|
||||
await createProducts({
|
||||
autumn: autumnInt,
|
||||
products: [pro],
|
||||
customerId,
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
}).catch(_ => {})
|
||||
});
|
||||
|
||||
it("should create a customer and issue balances", async () => {
|
||||
const { customer } = await initCustomerV2({
|
||||
autumn: autumnInt,
|
||||
customerId,
|
||||
org,
|
||||
env,
|
||||
db,
|
||||
attachPm: "success",
|
||||
})
|
||||
expect(customer).to.exist;
|
||||
expect(customer.id).to.equal(customerId);
|
||||
expect(customer.name).to.equal(customerId);
|
||||
expect(customer.email).to.equal(`${customerId}@example.com`);
|
||||
|
||||
await autumnJs.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
})
|
||||
});
|
||||
|
||||
it("should enforce usage_limit with concurrent requests", async () => {
|
||||
const customer = await autumnInt.customers.get(customerId);
|
||||
const balance = customer.features[TestFeature.Messages].balance;
|
||||
const usageLimit = customer.features[TestFeature.Messages].usage_limit;
|
||||
|
||||
expect(balance).to.equal(5, `Balance should be 5, got ${balance}`);
|
||||
expect(usageLimit).to.equal(10, `Usage limit should be 10, got ${usageLimit}`);
|
||||
|
||||
console.log("🚀 Starting 5 concurrent track calls (3 units each) at exact same time...");
|
||||
console.log(` Initial state: balance=${balance}, usage_limit=${usageLimit} (max total usage in billing cycle)`);
|
||||
|
||||
// Try to use 3 units concurrently - with usage_limit of 10, only 3 requests can succeed (3x3=9 <= 10)
|
||||
const promises = [
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 3,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 3,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 3,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 3,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 3,
|
||||
}),
|
||||
];
|
||||
|
||||
let results = await Promise.allSettled(promises);
|
||||
|
||||
console.log("📊 Results breakdown:");
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === "rejected") {
|
||||
console.log(` [${index}] ❌ REJECTED:`, result.reason?.message || result.reason);
|
||||
} else {
|
||||
console.log(` [${index}] ✅ FULFILLED:`, JSON.stringify(result.value));
|
||||
}
|
||||
});
|
||||
|
||||
const successCount = results.filter(r => r.status === "fulfilled").length;
|
||||
const rejectedCount = results.filter(r => r.status === "rejected").length;
|
||||
console.log(`\n📈 Summary: ${successCount} succeeded, ${rejectedCount} rejected (expected: 3 succeeded, 2 rejected)`);
|
||||
console.log(` Reason: usage_limit=10 means max 10 total units in billing cycle. 3 requests × 3 = 9 ≤ 10, but 4th would be 12 > 10\n`);
|
||||
|
||||
const { data: balances, error } = await autumnJs.customers.get(
|
||||
customerId,
|
||||
);
|
||||
|
||||
console.log(`📦 Final state after all requests:`);
|
||||
console.log(`- Balance: ${balances?.features[TestFeature.Messages]?.balance}`);
|
||||
console.log(`- Usage limit: ${balances?.features[TestFeature.Messages]?.usage_limit}`);
|
||||
console.log(`- Full feature data:`, JSON.stringify(balances?.features[TestFeature.Messages], null, 4));
|
||||
// With usage_limit of 10, only 3 requests of value 3 can succeed (9 total)
|
||||
// The 4th request would bring total to 12, exceeding the usage_limit
|
||||
// expect(successCount).to.equal(3, `Expected exactly 3 successes (3x3=9 <= usage_limit of 10), got ${successCount} | Results: ${results.map(r => r.status).join(", ")}`);
|
||||
// expect(rejectedCount).to.equal(2, `Expected exactly 2 rejections, got ${rejectedCount} | Results: ${results.map(r => r.status).join(", ")}`);
|
||||
|
||||
// expect(error).to.be.null;
|
||||
|
||||
// Balance consumed from included: min(9, 5) = 5, so balance = 0
|
||||
// The remaining 4 units (9 - 5) are overages charged via PayPerUse
|
||||
// expect(balances?.features[TestFeature.Messages]?.balance).to.equal(
|
||||
// 0,
|
||||
// `Balance should be 0 (all 5 included used), got ${balances?.features[TestFeature.Messages]?.balance}`,
|
||||
// );
|
||||
// expect(balances?.features[TestFeature.Messages]?.usage_limit).to.equal(
|
||||
// 10,
|
||||
// `Usage limit should remain 10, got ${balances?.features[TestFeature.Messages]?.usage_limit}`,
|
||||
// );
|
||||
});
|
||||
});
|
||||
205
server/tests/sync/sync5.test.ts
Normal file
205
server/tests/sync/sync5.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { ApiVersion, ProductItemFeatureType, type Organization } from "@autumn/shared";
|
||||
import type { AppEnv, Autumn } from "autumn-js";
|
||||
import { expect } from "chai";
|
||||
import chalk from "chalk";
|
||||
import type { Stripe } from "stripe";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructArrearItem, constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { addPrefixToProducts } from "tests/attach/utils.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
|
||||
const testCase = "sync5";
|
||||
const customerId = `${testCase}_cus1`;
|
||||
|
||||
const seatItem = constructFeatureItem({
|
||||
featureId: TestFeature.Users,
|
||||
includedUsage: 5,
|
||||
featureType: ProductItemFeatureType.ContinuousUse,
|
||||
});
|
||||
|
||||
const perSeatMessagesItem = constructArrearItem({
|
||||
featureId: TestFeature.Messages,
|
||||
entityFeatureId: TestFeature.Users,
|
||||
price: 0.01,
|
||||
includedUsage: 500,
|
||||
usageLimit: 600,
|
||||
});
|
||||
|
||||
const pro = constructProduct({
|
||||
id: "pro",
|
||||
items: [seatItem, perSeatMessagesItem],
|
||||
type: "pro",
|
||||
})
|
||||
|
||||
describe(`${chalk.yellowBright(`sync/${testCase}: Testing per-entity sync track with concurrent requests`)}`, () => {
|
||||
let db: DrizzleCli;
|
||||
let org: Organization;
|
||||
let env: AppEnv;
|
||||
let stripeCli: Stripe;
|
||||
let autumnInt: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
let autumnJs: Autumn;
|
||||
|
||||
before(async function () {
|
||||
await setupBefore(this);
|
||||
db = this.db;
|
||||
org = this.org;
|
||||
env = this.env;
|
||||
stripeCli = this.stripeCli;
|
||||
autumnJs = this.autumnJs;
|
||||
try {
|
||||
await (autumnInt as AutumnInt).customers.delete(customerId);
|
||||
} catch (_) {}
|
||||
|
||||
await addPrefixToProducts({
|
||||
products: [pro],
|
||||
prefix: testCase,
|
||||
})
|
||||
|
||||
await createProducts({
|
||||
autumn: autumnInt,
|
||||
products: [pro],
|
||||
customerId,
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
})
|
||||
});
|
||||
|
||||
it("should create a customer and issue balances", async () => {
|
||||
const { customer } = await initCustomerV2({
|
||||
autumn: autumnInt,
|
||||
customerId,
|
||||
org,
|
||||
env,
|
||||
db,
|
||||
attachPm: "success",
|
||||
})
|
||||
expect(customer).to.exist;
|
||||
expect(customer.id).to.equal(customerId);
|
||||
expect(customer.name).to.equal(customerId);
|
||||
expect(customer.email).to.equal(`${customerId}@example.com`);
|
||||
|
||||
await autumnJs.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
})
|
||||
});
|
||||
|
||||
it("should create 5 seats each with 500 messages", async () => {
|
||||
const customer = await autumnInt.customers.get(customerId);
|
||||
const seatBalance = customer.features[TestFeature.Users].balance;
|
||||
expect(seatBalance).to.equal(5, `Seat balance should be 5, got ${seatBalance}`);
|
||||
|
||||
// Create 5 entities (seats)
|
||||
const entities = [
|
||||
{ id: "seat1", name: "Seat 1" },
|
||||
{ id: "seat2", name: "Seat 2" },
|
||||
{ id: "seat3", name: "Seat 3" },
|
||||
{ id: "seat4", name: "Seat 4" },
|
||||
{ id: "seat5", name: "Seat 5" },
|
||||
];
|
||||
|
||||
for (const entity of entities) {
|
||||
await autumnInt.entities.create(customerId, {
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
feature_id: TestFeature.Users,
|
||||
});
|
||||
}
|
||||
|
||||
// Verify each seat has 500 messages
|
||||
const updatedEntity = await autumnInt.entities.get(customerId, entities[0].id);
|
||||
console.log(JSON.stringify(updatedEntity, null, 4));
|
||||
expect(updatedEntity.features[TestFeature.Messages].balance).to.equal(500, JSON.stringify(updatedEntity, null, 4));
|
||||
});
|
||||
|
||||
it("should enforce usage_limit of 600 per seat with concurrent 200-unit requests", async () => {
|
||||
const entityId = "seat1";
|
||||
|
||||
// Verify seat1 has 500 included messages with 600 usage_limit
|
||||
const entityRes = await autumnInt.entities.get(customerId, entityId);
|
||||
expect(entityRes.features[TestFeature.Messages].balance).to.equal(500);
|
||||
// expect(entityRes.features[TestFeature.Messages].usage_limit).to.equal(600);
|
||||
|
||||
console.log("🚀 Starting 5 concurrent track calls (200 units each) for seat1...");
|
||||
console.log(` Initial state: balance=${entityRes.features[TestFeature.Messages].balance}, usage_limit=${entityRes.features[TestFeature.Messages].usage_limit}`);
|
||||
|
||||
// Try 5 concurrent 200-unit sends to seat1
|
||||
// With usage_limit of 600, only 3 should succeed (3×200=600 <= 600)
|
||||
const promises = [
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
entity_id: entityId,
|
||||
value: 200,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
entity_id: entityId,
|
||||
value: 200,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
entity_id: entityId,
|
||||
value: 200,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
entity_id: entityId,
|
||||
value: 200,
|
||||
}),
|
||||
autumnInt.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
entity_id: entityId,
|
||||
value: 200,
|
||||
}),
|
||||
];
|
||||
|
||||
let results = await Promise.allSettled(promises);
|
||||
|
||||
console.log("📊 Results breakdown:");
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === "rejected") {
|
||||
console.log(` [${index}] ❌ REJECTED:`, result.reason?.message || result.reason);
|
||||
} else {
|
||||
console.log(` [${index}] ✅ FULFILLED:`, JSON.stringify(result.value));
|
||||
}
|
||||
});
|
||||
|
||||
const successCount = results.filter(r => r.status === "fulfilled").length;
|
||||
const rejectedCount = results.filter(r => r.status === "rejected").length;
|
||||
console.log(`\n📈 Summary: ${successCount} succeeded, ${rejectedCount} rejected (expected: 3 succeeded, 2 rejected)`);
|
||||
console.log(` Reason: usage_limit=600 per seat means max 600 units. 3 requests × 200 = 600, 4th would be 800 > 600\n`);
|
||||
|
||||
// Get final state
|
||||
const finalEntityRes = await autumnInt.entities.get(customerId, entityId);
|
||||
console.log(`📦 Final state for ${entityId}:`);
|
||||
console.log(`- Balance: ${finalEntityRes.features[TestFeature.Messages].balance}`);
|
||||
console.log(`- Usage: ${finalEntityRes.features[TestFeature.Messages].usage}`);
|
||||
console.log(`- Usage limit: ${finalEntityRes.features[TestFeature.Messages].usage_limit}`);
|
||||
console.log(`- Full feature data:`, JSON.stringify(finalEntityRes.features[TestFeature.Messages], null, 2));
|
||||
|
||||
// Comment out expectations for now to see actual behavior
|
||||
// expect(successCount).to.equal(3, `Expected exactly 3 successes (3×200=600 <= usage_limit of 600), got ${successCount} | Results: ${results.map(r => r.status).join(", ")}`);
|
||||
// expect(rejectedCount).to.equal(2, `Expected exactly 2 rejections, got ${rejectedCount} | Results: ${results.map(r => r.status).join(", ")}`);
|
||||
|
||||
// Verify other seats remain untouched at 500
|
||||
for (const seatId of ["seat2", "seat3", "seat4", "seat5"]) {
|
||||
const otherSeatRes = await autumnInt.entities.get(customerId, seatId);
|
||||
console.log(`\n📦 ${seatId} balance: ${otherSeatRes.features[TestFeature.Messages].balance}`);
|
||||
// expect(otherSeatRes.features[TestFeature.Messages].balance).to.equal(
|
||||
// 500,
|
||||
// `${seatId} should still have 500 messages, got ${otherSeatRes.features[TestFeature.Messages].balance}`,
|
||||
// );
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -25,8 +25,6 @@ export default defineConfig({
|
||||
"@radix/tabs": "@radix-ui/react-tabs",
|
||||
"@radix/tooltip": "@radix-ui/react-tooltip",
|
||||
},
|
||||
// Preserve symlinks for workspace dependencies
|
||||
preserveSymlinks: true,
|
||||
},
|
||||
optimizeDeps: {
|
||||
// Exclude workspace dependencies from pre-bundling to avoid cache issues
|
||||
|
||||
Reference in New Issue
Block a user