265 lines
11 KiB
Plaintext
265 lines
11 KiB
Plaintext
---
|
|
alwaysApply: true
|
|
---
|
|
|
|
## Test Writing Rules — NEVER Get These Wrong
|
|
|
|
### 1. NEVER Call `initScenario` Twice
|
|
|
|
```typescript
|
|
// WRONG — calling initScenario twice for multiple customers
|
|
const { autumnV1: a } = await initScenario({ customerId: "cus-a", ... });
|
|
const { autumnV1: b } = await initScenario({ customerId: "cus-b", ... }); // BREAKS
|
|
|
|
// RIGHT — single initScenario, create additional customers manually
|
|
const { autumnV1, ctx } = await initScenario({
|
|
customerId: "cus-a",
|
|
setup: [s.customer({ paymentMethod: "success" }), s.products({ list: [pro] })],
|
|
actions: [s.billing.attach({ productId: pro.id })],
|
|
});
|
|
await autumnV1.customers.create("cus-b", { name: "Customer B" });
|
|
await autumnV1.billing.attach({ customer_id: "cus-b", product_id: pro.id });
|
|
|
|
// ALSO RIGHT — use s.otherCustomers for additional customers sharing the same test clock
|
|
const { autumnV1, otherCustomers } = await initScenario({
|
|
customerId: "cus-a",
|
|
setup: [
|
|
s.customer({ paymentMethod: "success" }),
|
|
s.products({ list: [pro] }),
|
|
s.otherCustomers([{ id: "cus-b", paymentMethod: "success" }]),
|
|
],
|
|
actions: [s.billing.attach({ productId: pro.id })],
|
|
});
|
|
```
|
|
|
|
### 1.1. If You Are Testing Customer Creation, Do NOT Pass `customerId` to `initScenario`
|
|
|
|
`initScenario({ customerId, ... })` creates or registers that customer as part of setup. If the test itself is meant to call `autumn.customers.create(...)`, leave `customerId` out of `initScenario` or you will be testing re-create behavior instead of create behavior.
|
|
|
|
### 2. `s.billing.attach` and `s.attach` Already Have Timeouts
|
|
|
|
Both `s.billing.attach` (5-8s) and `s.attach` (4-5s) sleep after the API call. Do NOT add extra `await timeout()` after `initScenario` that already uses these in `actions`. Only add manual timeouts when calling `autumnV1.billing.attach()` directly in the test body.
|
|
|
|
### 3. `s.track()` Has NO Built-In Timeout
|
|
|
|
Unlike attach, `s.track()` fires and moves on immediately. If you need side effects to settle (e.g., before an attach that rebuilds from Postgres), pass `timeout` explicitly:
|
|
```typescript
|
|
s.track({ featureId: TestFeature.Messages, value: 50, timeout: 2000 })
|
|
```
|
|
|
|
### 4. `s.billing.attach` Is NOT the Same as `s.attach`
|
|
|
|
| | `s.attach` | `s.billing.attach` |
|
|
|---|---|---|
|
|
| Endpoint | V1 `/attach` | V2 `/billing.attach` |
|
|
| Extra params | none | `planSchedule`, `items` |
|
|
| Use for | Legacy tests, simple setup | Scenario setup for billing tests |
|
|
|
|
For the action under test, prefer direct client calls like `autumnV2_2.billing.previewAttach<AttachParamsV1Input>(...)`, `autumnV2_2.billing.attach<AttachParamsV1Input>(...)`, and `autumnV2_2.subscriptions.update<UpdateSubscriptionV1ParamsInput>(...)`.
|
|
|
|
### 5. Prepaid Quantity: Inclusive vs Exclusive of `includedUsage`
|
|
|
|
- **`s.billing.attach` (new V2)**: `quantity` is **inclusive** of `includedUsage`
|
|
- **`s.attach` (legacy V1)**: `quantity` is **exclusive** of `includedUsage`
|
|
|
|
### 6. `includedUsage` Must Be a Multiple of `billingUnits`
|
|
|
|
Stripe requires integer tier values. `includedUsage: 50` with `billingUnits: 100` = 0.5, which Stripe rejects.
|
|
|
|
### 7. `products.pro()` Already Includes $20/mo Base Price
|
|
|
|
Don't also add `items.monthlyPrice()` — you'll get double pricing. Same for `products.premium()` ($50/mo), `products.growth()` ($100/mo), `products.ultra()` ($200/mo).
|
|
|
|
### 8. Always Use `product.id`, Never String Literals
|
|
|
|
```typescript
|
|
// WRONG
|
|
s.attach({ productId: "pro" })
|
|
// RIGHT
|
|
s.attach({ productId: pro.id })
|
|
```
|
|
|
|
`initScenario` auto-prefixes product IDs with `customerId`. The product object's `.id` is mutated to include the prefix.
|
|
|
|
### 9. Always Use `test.concurrent()`, Never Plain `test()`
|
|
|
|
### 10. `expectCustomerFeatureCorrect` Requires `customer` Object
|
|
|
|
It does NOT fetch from the API. Passing only `customerId` without `customer` silently returns undefined features. Always pass the fetched customer object.
|
|
|
|
### 11. Lifetime/One-Off Breakdowns Use `ResetInterval.OneOff`, Not `null`
|
|
|
|
```typescript
|
|
// WRONG
|
|
breakdowns.find(b => b.reset?.interval === null)
|
|
// RIGHT
|
|
breakdowns.find(b => b.reset?.interval === ResetInterval.OneOff)
|
|
```
|
|
|
|
### 12. Assertion Style
|
|
|
|
- `toMatchObject` for comparing multiple properties at once
|
|
- `toEqual` for single-value comparisons (not `toMatchObject({ prop: value })`)
|
|
- `items.free()` for free metered features, not `items.consumable()` with `price: 0`
|
|
|
|
### 13. `Date.now()` Doesn't Change With Test Clocks
|
|
|
|
Always use `advancedTo` from `initScenario`:
|
|
```typescript
|
|
// WRONG
|
|
expect(trialEndsAt).toBeCloseTo(Date.now() + ms.days(14));
|
|
// RIGHT
|
|
expect(trialEndsAt).toBeCloseTo(advancedTo + ms.days(14));
|
|
```
|
|
|
|
### 14. Consumable + Prepaid on Same Feature: `includedUsage` Is the SUM
|
|
|
|
If a product has both consumable (includedUsage: 50) and prepaid (quantity: 100) for the same feature, total `included_usage` = 150, not 100.
|
|
|
|
### 15. "Canceling" Is NOT a Status Value
|
|
|
|
A canceling product has `status: "active"` with `canceled_at` set. Use `expectProductCanceling`, not `expect(status).toBe("canceling")`.
|
|
|
|
### 16. Always Call `expectStripeSubscriptionCorrect` After Billing Actions
|
|
|
|
After any `billing.attach()` or `subscriptions.update()` in new tests:
|
|
```typescript
|
|
await expectStripeSubscriptionCorrect({ ctx, customerId });
|
|
```
|
|
|
|
### 17. Tiered Pricing Must End With `"inf"`
|
|
|
|
```typescript
|
|
// WRONG — Stripe rejects without catch-all
|
|
tiers: [{ to: 500, amount: 10 }]
|
|
// RIGHT
|
|
tiers: [{ to: 500, amount: 10 }, { to: "inf", amount: 5 }]
|
|
```
|
|
|
|
### 18. Setup Actions Go in `initScenario`, Test Body Has Only the Action Under Test
|
|
|
|
```typescript
|
|
// WRONG — multiple attaches in test body
|
|
const { autumnV2_2 } = await initScenario({ actions: [] });
|
|
await autumnV2_2.billing.attach<AttachParamsV1Input>({ plan_id: pro.id }); // should be setup
|
|
await autumnV2_2.billing.attach<AttachParamsV1Input>({ plan_id: addon.id }); // the actual test
|
|
|
|
// RIGHT — prerequisite in initScenario, only tested action in body
|
|
const { autumnV2_2 } = await initScenario({
|
|
actions: [s.billing.attach({ productId: pro.id })],
|
|
});
|
|
await autumnV2_2.billing.attach<AttachParamsV1Input>({
|
|
customer_id: customerId,
|
|
plan_id: addon.id,
|
|
});
|
|
```
|
|
|
|
### 19. Type Check After Writing Tests
|
|
|
|
Run `bun ts` in the `server/` directory after writing or editing test files. This runs `bunx tsgo --build --noEmit`. Fix all type errors before considering the task done.
|
|
|
|
### 20. Use Generic Types With AutumnInt, Not `as unknown as`
|
|
|
|
```typescript
|
|
// WRONG
|
|
const customer = await autumnV1.customers.get(customerId) as unknown as ApiCustomerV3;
|
|
// RIGHT
|
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
|
```
|
|
|
|
| Client | customers.get | entities.get | check |
|
|
|--------|---------------|--------------|-------|
|
|
| `autumnV1` | `ApiCustomerV3` | `ApiEntityV0` | `CheckResponseV1` |
|
|
| `autumnV2` | `ApiCustomer` | `ApiEntityV1` | `CheckResponseV2` |
|
|
|
|
### 21. NEVER Run Tests Without Asking
|
|
|
|
Always ask the user for permission before running any test command. The user likely has a dev server running and needs to coordinate. Present the exact command you plan to run and wait for approval.
|
|
|
|
### 22. Use the Correct Param Types Per API Client Version
|
|
|
|
Each client version (`autumnV1`, `autumnV2`, `autumnV2_1`, `autumnV2_2`) expects different input/output types. Always pass the right types as generics and for local variables.
|
|
|
|
Billing endpoints are not strongly typed enough on their own. ALWAYS supply explicit generics on `billing.previewAttach(...)`, `billing.attach(...)`, and `subscriptions.update(...)`.
|
|
|
|
| Client | API version | Attach input | Attach output | Update subscription input |
|
|
|--------|------------|--------------|---------------|--------------------------|
|
|
| `autumnV1` | V1 (`/attach`, `/billing.attach`) | `AttachParamsV0Input` | `ApiCustomerV3` | `UpdateSubscriptionV0Params` |
|
|
| `autumnV2` | V2 (`/billing.attach`) | `AttachParamsV1Input` | `ApiCustomer` | `UpdateSubscriptionV1Params` |
|
|
| `autumnV2_1` | V2.1 (`/attach`, `/update_subscription`) | `AttachParamsV1Input` | `ApiCustomerV5` | `UpdateSubscriptionV1ParamsInput` |
|
|
| `autumnV2_2` | V2.2 (`/attach`, `/update_subscription`) | `AttachParamsV1Input` | `ApiCustomerV5` | `UpdateSubscriptionV1ParamsInput` |
|
|
|
|
Key differences between `AttachParamsV0Input` and `AttachParamsV1Input`:
|
|
- V0 (`autumnV1`): uses `product_id` + `options: [{ feature_id, quantity }]`
|
|
- V1 (`autumnV2`): uses `plan_id` + `feature_quantities: [{ feature_id, quantity }]`
|
|
|
|
```typescript
|
|
// ✅ CORRECT — autumnV1 uses AttachParamsV0Input
|
|
const params: AttachParamsV0Input = {
|
|
customer_id: customerId,
|
|
product_id: pro.id, // NOT plan_id
|
|
options: [{ feature_id: "messages", quantity: 200 }], // NOT feature_quantities
|
|
};
|
|
await autumnV1.billing.attach<AttachParamsV0Input>(params);
|
|
|
|
// ✅ CORRECT — autumnV2 uses AttachParamsV1Input
|
|
const params: AttachParamsV1Input = {
|
|
customer_id: customerId,
|
|
plan_id: pro.id, // NOT product_id
|
|
feature_quantities: [{ feature_id: "messages", quantity: 200 }], // NOT options
|
|
};
|
|
await autumnV2.billing.attach<AttachParamsV1Input>(params);
|
|
|
|
// ✅ CORRECT — autumnV2_2 uses V1-style attach/update-subscription params
|
|
await autumnV2_2.billing.previewAttach<AttachParamsV1Input>({
|
|
customer_id: customerId,
|
|
plan_id: pro.id,
|
|
feature_quantities: [{ feature_id: "messages", quantity: 200 }],
|
|
});
|
|
|
|
await autumnV2_2.billing.attach<AttachParamsV1Input>({
|
|
customer_id: customerId,
|
|
plan_id: pro.id,
|
|
feature_quantities: [{ feature_id: "messages", quantity: 200 }],
|
|
});
|
|
|
|
await autumnV2_2.subscriptions.update<UpdateSubscriptionV1ParamsInput>({
|
|
customer_id: customerId,
|
|
plan_id: pro.id,
|
|
redirect_mode: "always",
|
|
});
|
|
|
|
// ❌ WRONG — mixing V1 param names with autumnV1 client
|
|
await autumnV1.billing.attach({ customer_id, plan_id: pro.id, feature_quantities: [...] });
|
|
```
|
|
|
|
Gotcha: if a test uses V1-style `attach` / `update_subscription` params like `plan_id`, `feature_quantities`, or `redirect_mode`, prefer `autumnV2_2` and keep the explicit generic on the billing call.
|
|
|
|
### 23. Prefer `autumnV2_2` + `expectBalanceCorrect` for Balance Assertions
|
|
|
|
When a test is validating balances and the latest API version can be used, prefer `autumnV2_2` and assert with `@server/tests/integration/utils/expectBalanceCorrect.ts`.
|
|
|
|
- Use `autumnV2_2.customers.get<ApiCustomerV5>(customerId)` where possible for latest-version balance assertions
|
|
- Prefer `expectBalanceCorrect(...)` over older balance helpers when validating balance buckets, breakdowns, or rollovers
|
|
- Keep using older clients only when the endpoint/param shape under test specifically requires them
|
|
|
|
### 24. `autumnV2_2` Does NOT Return Negative `remaining`
|
|
|
|
For `ApiCustomerV5` / `autumnV2_2` balance responses:
|
|
|
|
- `remaining` does not go below `0`
|
|
- To assert overage, inspect `usage` (and breakdown buckets) instead of expecting negative `remaining`
|
|
|
|
```typescript
|
|
// ❌ WRONG - latest API does not expose negative remaining
|
|
expect(customer.balances.messages.remaining).toBe(-50)
|
|
|
|
// ✅ RIGHT - assert remaining floor + overage usage
|
|
expectBalanceCorrect({
|
|
customer,
|
|
featureId: TestFeature.Messages,
|
|
remaining: 0,
|
|
usage: 150,
|
|
})
|
|
```
|