9.4 KiB
Guide: Writing /check Endpoint Tests
What is /check?
The /check endpoint validates whether a customer has access to a feature and returns their usage balance.
Parameters:
customer_id(required) - The customer to checkfeature_id(required) - The feature to check access forrequired_balance(optional) - How much balance/usage is needed (defaults to 1)
Returns: Whether the customer is allowed to use the feature, along with balance information.
Step-by-Step: Writing a /check Test
Step 1: Define What You're Testing
Identify the specific scenario:
- Feature not attached to customer
- Boolean feature (on/off access)
- Metered feature with usage limits
- Unlimited feature
- Credit system (actions that consume from a credit pool)
- Overage behavior
Step 2: Construct Features & Products
Feature Types
Boolean Features - Simple on/off access:
const dashboardFeature = constructFeatureItem({
featureId: TestFeature.Dashboard,
isBoolean: true,
});
Metered Features - Usage-based with limits:
// Basic metered (resets monthly)
const messagesFeature = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 1000,
});
// Unlimited
const storageFeature = constructFeatureItem({
featureId: TestFeature.Storage,
unlimited: true,
});
Pay-per-use (Arrear) - Overage pricing:
const apiCallsFeature = constructArrearItem({
featureId: TestFeature.ApiCalls,
includedUsage: 10000,
price: 0.1, // Price per billing_units
billingUnits: 1000, // Charged per 1000 calls
usageLimit: 50000, // Hard cap (optional)
});
Prepaid (Allocated) - Pre-purchased units (seats, licenses):
const seatsFeature = constructPrepaidItem({
featureId: TestFeature.Seats,
price: 10,
billingUnits: 1,
includedUsage: 5,
});
Credit Systems - A credit pool that multiple features consume from:
// The credit pool
const creditsFeature = constructFeatureItem({
featureId: TestFeature.Credits,
includedUsage: 100,
});
// When testing, check Action1 or Action2 features
// These will consume from the Credits pool
IMPORTANT for Credit Systems:
- Attach the
Creditsfeature to the product - Call
/checkonAction1orAction2(NOT on Credits directly) - The response will show the Credits balance in the
balancesarray - When testing v0 responses, use
getCreditCost({ featureId, creditSystem, amount })from@/internal/features/creditSystemUtils.jsto calculate the expectedrequiredfield in balances - Example: Customer has 100 credits, checking Action1 for 50 units → allowed, shows 100 credit balance
Combine into Products
const proProd = constructProduct({
type: "free", // IMPORTANT: Set type to "free" for immediate attachment
isDefault: false,
items: [messagesFeature, dashboardFeature],
});
IMPORTANT: Product Type
type: "free"- Feature is attached to customer immediately afterattach()calltype: "pro"or other paid types - Feature requires payment/subscription flow and may not be immediately available for testing- Rule of thumb: For track/check tests, always use
type: "free"unless specifically testing paid subscription flows
Step 3: Initialize Test Environment
Always use this exact order in beforeAll:
const testCase = "your-test-name";
const customerId = "your-test-name";
beforeAll(async () => {
// 1. Create customer
await initCustomerV3({
ctx,
customerId,
attachPm: "success", // Include if testing paid features
withTestClock: false,
});
// 2. Create products
await initProductsV0({
ctx,
products: [proProd],
prefix: testCase,
});
// 3. Attach product to customer (if testing attached features)
await autumnV1.attach({
customer_id: customerId,
product_id: proProd.id,
});
});
Step 4: Write Test Cases
Test both v0 and v1 APIs:
test("v0 response", async () => {
const res = (await autumnV0.check({
customer_id: customerId,
feature_id: TestFeature.Messages,
required_balance: 100,
})) as unknown as CheckResponseV0;
expect(res.allowed).toBe(true);
expect(res.balances).toHaveLength(1);
expect(res.balances[0]).toMatchObject({
feature_id: TestFeature.Messages,
balance: 1000,
required: 100,
});
});
test("v1 response", async () => {
const res = (await autumnV1.check({
customer_id: customerId,
feature_id: TestFeature.Messages,
required_balance: 100,
})) as unknown as CheckResponse;
expect(res).toMatchObject({
allowed: true,
customer_id: customerId,
feature_id: TestFeature.Messages,
balance: 1000,
required_balance: 100,
code: SuccessCode.FeatureFound,
usage: 0,
included_usage: 1000,
overage_allowed: false,
});
expect(res.next_reset_at).toBeDefined();
});
Common Scenarios
Feature Not Attached
// Don't call autumnV1.attach() in beforeAll
const res = await autumnV1.check({
customer_id: customerId,
feature_id: TestFeature.Messages,
});
expect(res.allowed).toBe(false);
Exceeds Limit
const res = await autumnV0.check({
customer_id: customerId,
feature_id: TestFeature.Messages,
required_balance: 9999, // More than available
});
expect(res.allowed).toBe(false);
Boolean Feature
const res = await autumnV1.check({
customer_id: customerId,
feature_id: TestFeature.Dashboard,
});
expect(res.allowed).toBe(true);
// No balance field for boolean features
Credit System
// Product has Credits feature attached
const res = await autumnV1.check({
customer_id: customerId,
feature_id: TestFeature.Action1, // Check the action, not Credits
required_balance: 50,
});
expect(res.allowed).toBe(true);
expect(res.balance).toBe(100); // Shows Credits balance
Required Imports
import { beforeAll, describe, expect, test } from "bun:test";
import {
ApiVersion,
type CheckResponse,
type CheckResponseV0,
SuccessCode,
} from "@autumn/shared";
import chalk from "chalk";
import { TestFeature } from "tests/setup/v2Features.js";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem, constructArrearItem, constructPrepaidItem } 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";
Test File Template
const testCase = "check-X";
const customerId = "check-X";
describe(`${chalk.yellowBright("check-X: description")}`, () => {
const autumnV0: AutumnInt = new AutumnInt({ version: ApiVersion.V0_2 });
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
beforeAll(async () => {
// Initialize customer, products, attach
});
test("v0 response", async () => {
// Test v0
});
test("v1 response", async () => {
// Test v1
});
});
Common Pitfalls
Multiple Products Need Unique IDs
When creating multiple products with the same type: "free", you MUST specify unique id values or they will conflict:
// ❌ BAD - Both products will have the same ID
const prod1 = constructProduct({
type: "free",
isDefault: false,
items: [feature1],
});
const prod2 = constructProduct({
type: "free",
isDefault: false,
items: [feature2],
});
// ✅ GOOD - Unique IDs for each product
const prod1 = constructProduct({
type: "free",
id: "monthly-prod",
isDefault: false,
items: [feature1],
});
const prod2 = constructProduct({
type: "free",
id: "lifetime-prod",
isDefault: false,
items: [feature2],
});
Second Product Needs isAddOn: true
When attaching multiple products to a customer, the second product MUST have isAddOn: true or it will replace the first product:
// ❌ BAD - Second attach will replace first product
const prod1 = constructProduct({ type: "free", id: "prod1", ... });
const prod2 = constructProduct({ type: "free", id: "prod2", ... });
// ✅ GOOD - Second product is an add-on
const prod1 = constructProduct({ type: "free", id: "prod1", ... });
const prod2 = constructProduct({ type: "free", id: "prod2", isAddOn: true, ... });
Lifetime/One-off Reset Format
For consumable features with lifetime (no reset interval), the reset object is NOT null. It has this format:
// ❌ BAD - Incorrect expectation
expect(breakdown).toMatchObject({
reset: null,
});
// ✅ GOOD - Correct format for lifetime/one-off features
expect(breakdown).toMatchObject({
reset: {
interval: "one_off",
resets_at: null,
},
});
Checklist
- Unique test case name (e.g., "credit-systems1")
- Use chalk for describe block
- Test both v0 and v1 APIs
- Initialize in correct order: customer → products → attach
- For credit systems: attach Credits, check Action1/Action2
- Verify
next_reset_atis defined (v1 time-based features) - Use
.toMatchObject()for partial matches,.toStrictEqual()for exact - Multiple products need unique
idvalues - Second product needs
isAddOn: truewhen attaching multiple - Lifetime features use
reset: { interval: "one_off", resets_at: null }, NOTnull