288 lines
9.4 KiB
TypeScript
288 lines
9.4 KiB
TypeScript
/**
|
|
* Migration Error Tests
|
|
*
|
|
* Tests for migration error cases and validation.
|
|
* Ensures migrations fail gracefully with proper error messages.
|
|
*
|
|
* Key behaviors:
|
|
* - Free → Paid migration should error
|
|
* - One-off products cannot be migrated
|
|
* - Prepaid feature mismatches should error
|
|
* - Concurrent migrations for same product should error
|
|
*/
|
|
|
|
import { expect, test } from "bun:test";
|
|
import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils";
|
|
import { items } from "@tests/utils/fixtures/items";
|
|
import { products } from "@tests/utils/fixtures/products";
|
|
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario";
|
|
import chalk from "chalk";
|
|
|
|
const _waitForMigration = (ms = 5000) =>
|
|
new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// TEST 1: Free to Paid Migration (Error)
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Scenario:
|
|
* - Customer has free product
|
|
* - Try to migrate to paid product
|
|
*
|
|
* Expected Result:
|
|
* - Error: Cannot migrate from free to paid
|
|
*/
|
|
test.concurrent(`${chalk.yellowBright("migrate-errors-1: free to paid migration should error")}`, async () => {
|
|
const customerId = "migrate-err-free-to-paid";
|
|
|
|
const free = products.base({
|
|
id: "free",
|
|
items: [items.monthlyMessages({ includedUsage: 500 })],
|
|
});
|
|
|
|
const pro = products.pro({
|
|
id: "pro",
|
|
items: [items.monthlyMessages({ includedUsage: 1000 })],
|
|
});
|
|
|
|
const { autumnV1 } = await initScenario({
|
|
customerId,
|
|
setup: [
|
|
s.customer({ paymentMethod: "success" }),
|
|
s.products({ list: [free, pro] }),
|
|
],
|
|
actions: [s.billing.attach({ productId: "free" })],
|
|
});
|
|
|
|
// Try to migrate from free to paid - should error
|
|
|
|
await expectAutumnError({
|
|
func: async () => {
|
|
await autumnV1.migrate({
|
|
from_product_id: free.id,
|
|
to_product_id: pro.id,
|
|
from_version: 1,
|
|
to_version: 1,
|
|
});
|
|
},
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// TEST 2: Migrate FROM One-Off Product (Error)
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Scenario:
|
|
* - Customer has one-off product
|
|
* - Try to migrate to another version
|
|
*
|
|
* Expected Result:
|
|
* - Error: Cannot migrate one-off products
|
|
*/
|
|
test.concurrent(`${chalk.yellowBright("migrate-errors-2: migrate from one-off product should error")}`, async () => {
|
|
const customerId = "migrate-err-from-oneoff";
|
|
|
|
const oneOff = products.oneOff({
|
|
id: "one-off",
|
|
items: [items.oneOffMessages({ includedUsage: 100 })],
|
|
});
|
|
|
|
const { autumnV1 } = await initScenario({
|
|
customerId,
|
|
setup: [
|
|
s.customer({ paymentMethod: "success" }),
|
|
s.products({ list: [oneOff] }),
|
|
],
|
|
actions: [s.billing.attach({ productId: "one-off" })],
|
|
});
|
|
|
|
// Update to v2
|
|
await autumnV1.products.update(oneOff.id, {
|
|
items: [items.oneOffMessages({ includedUsage: 200 })],
|
|
});
|
|
|
|
// Try to migrate one-off product - should error
|
|
await expectAutumnError({
|
|
func: async () => {
|
|
await autumnV1.migrate({
|
|
from_product_id: oneOff.id,
|
|
to_product_id: oneOff.id,
|
|
from_version: 1,
|
|
to_version: 2,
|
|
});
|
|
},
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// TEST 3: Migrate TO One-Off Product (Error)
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Scenario:
|
|
* - Customer has pro product
|
|
* - Try to migrate to one-off product
|
|
*
|
|
* Expected Result:
|
|
* - Error: Cannot migrate to one-off products
|
|
*/
|
|
test.concurrent(`${chalk.yellowBright("migrate-errors-3: migrate to one-off product should error")}`, async () => {
|
|
const customerId = "migrate-err-to-oneoff";
|
|
|
|
const pro = products.pro({
|
|
id: "pro",
|
|
items: [items.monthlyMessages({ includedUsage: 500 })],
|
|
});
|
|
|
|
const oneOff = products.oneOff({
|
|
id: "one-off",
|
|
items: [items.oneOffMessages({ includedUsage: 100 })],
|
|
});
|
|
|
|
const { autumnV1 } = await initScenario({
|
|
customerId,
|
|
setup: [
|
|
s.customer({ paymentMethod: "success" }),
|
|
s.products({ list: [pro, oneOff] }),
|
|
],
|
|
actions: [s.billing.attach({ productId: "pro" })],
|
|
});
|
|
|
|
// Try to migrate to one-off product - should error
|
|
await expectAutumnError({
|
|
func: async () => {
|
|
await autumnV1.migrate({
|
|
from_product_id: pro.id,
|
|
to_product_id: oneOff.id,
|
|
from_version: 1,
|
|
to_version: 1,
|
|
});
|
|
},
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// TEST 4: New Product Has Prepaid Feature Old Doesn't (Error)
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Scenario:
|
|
* - Customer has pro product with monthly messages (no prepaid)
|
|
* - Update product to v2 with prepaid messages
|
|
* - Try to migrate
|
|
*
|
|
* Expected Result:
|
|
* - Error: Can't perform migration (prepaid feature mismatch)
|
|
*/
|
|
test.concurrent(`${chalk.yellowBright("migrate-errors-4: new product has prepaid feature old doesn't")}`, async () => {
|
|
const customerId = "migrate-err-prepaid-mismatch";
|
|
|
|
const pro = products.pro({
|
|
id: "pro",
|
|
items: [items.monthlyMessages({ includedUsage: 500 })],
|
|
});
|
|
|
|
const { autumnV1 } = await initScenario({
|
|
customerId,
|
|
setup: [
|
|
s.customer({ paymentMethod: "success" }),
|
|
s.products({ list: [pro] }),
|
|
],
|
|
actions: [s.billing.attach({ productId: "pro" })],
|
|
});
|
|
|
|
// Update to v2 with prepaid messages instead of monthly
|
|
await autumnV1.products.update(pro.id, {
|
|
items: [items.prepaidMessages({ includedUsage: 0, billingUnits: 100 })],
|
|
});
|
|
|
|
// Try to migrate - should error due to prepaid mismatch
|
|
await expectAutumnError({
|
|
func: async () => {
|
|
await autumnV1.migrate({
|
|
from_product_id: pro.id,
|
|
to_product_id: pro.id,
|
|
from_version: 1,
|
|
to_version: 2,
|
|
});
|
|
},
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// TEST 5: Concurrent Migrations for Same Product (Error)
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Scenario:
|
|
* - Customer has pro v1
|
|
* - Update product to v2 and v3
|
|
* - Try to run two migrations concurrently (v1→v2 and v1→v3)
|
|
*
|
|
* Expected Result:
|
|
* - One should succeed, one should error with "migration is ongoing"
|
|
*/
|
|
test.concurrent(`${chalk.yellowBright("migrate-errors-5: concurrent migrations for same product should error")}`, async () => {
|
|
const customerId = "migrate-err-concurrent";
|
|
|
|
const pro = products.pro({
|
|
id: "pro",
|
|
items: [items.monthlyMessages({ includedUsage: 500 })],
|
|
});
|
|
|
|
const { autumnV1 } = await initScenario({
|
|
customerId,
|
|
setup: [
|
|
s.customer({ paymentMethod: "success" }),
|
|
s.products({ list: [pro] }),
|
|
],
|
|
actions: [s.billing.attach({ productId: "pro" })],
|
|
});
|
|
|
|
// Update to v2
|
|
await autumnV1.products.update(pro.id, {
|
|
items: [
|
|
items.monthlyPrice({ price: 20 }),
|
|
items.monthlyMessages({ includedUsage: 600 }),
|
|
],
|
|
});
|
|
|
|
// Update to v3
|
|
await autumnV1.products.update(pro.id, {
|
|
items: [
|
|
items.monthlyPrice({ price: 25 }),
|
|
items.monthlyMessages({ includedUsage: 700 }),
|
|
],
|
|
});
|
|
|
|
// Try to run two migrations concurrently - one should fail
|
|
const migration1 = autumnV1.migrate({
|
|
from_product_id: pro.id,
|
|
to_product_id: pro.id,
|
|
from_version: 1,
|
|
to_version: 2,
|
|
});
|
|
|
|
const migration2 = autumnV1.migrate({
|
|
from_product_id: pro.id,
|
|
to_product_id: pro.id,
|
|
from_version: 1,
|
|
to_version: 2,
|
|
});
|
|
|
|
const results = await Promise.allSettled([migration1, migration2]);
|
|
|
|
// One should succeed, one should fail with the "migration is ongoing" error
|
|
const successes = results.filter((r) => r.status === "fulfilled");
|
|
const failures = results.filter((r) => r.status === "rejected");
|
|
|
|
expect(successes.length).toBe(1);
|
|
expect(failures.length).toBe(1);
|
|
|
|
// The failure should have the "migration is ongoing" message
|
|
const failedResult = failures[0] as PromiseRejectedResult;
|
|
expect(failedResult.reason.message).toInclude("Another migration is ongoing");
|
|
});
|