Files
webstudio/packages/plans/src/plan-features.test.ts
T
Oleg Isonen 618f901c5c chore: add PlanConfig type with name and prices to parsePlansEnv (#5702)
## Description

1. What is this PR about (link the issue and add a short description)

## Steps for reproduction

1. click button
2. expect xyz

## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
0000)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env` file
2026-04-15 23:18:38 +02:00

176 lines
5.4 KiB
TypeScript

import { describe, test, expect } from "vitest";
import {
type PlanFeatures,
type PlanConfig,
defaultPlanFeatures,
parsePlansEnv,
} from "./plan-features";
/** Full-featured plan for use in tests — all booleans true, numeric limits at max */
const fullFeatures: PlanFeatures = {
...defaultPlanFeatures,
canDownloadAssets: true,
canRestoreBackups: true,
allowAdditionalPermissions: true,
allowDynamicData: true,
allowContentMode: true,
allowStagingPublish: true,
maxContactEmailsPerProject: 5,
maxDomainsAllowedPerUser: 100,
maxDailyPublishesPerUser: 100,
maxWorkspaces: 1,
maxProjectsAllowedPerUser: 100,
};
/** Helper to build expected PlanConfig objects in tests */
const planConfig = (
name: string,
features: PlanFeatures,
prices: Record<string, string> = {}
): PlanConfig => ({ name, features, prices });
describe("parsePlansEnv", () => {
const validEntry = JSON.stringify({ name: "Pro", features: fullFeatures });
const twoEntries = JSON.stringify([
{ name: "Pro", features: fullFeatures },
{ name: "Workspaces", features: { ...fullFeatures, maxWorkspaces: 10 } },
]);
test("returns empty map for empty JSON array", () => {
expect(parsePlansEnv("[]").size).toBe(0);
});
test("returns empty map for invalid JSON", () => {
expect(parsePlansEnv("not json").size).toBe(0);
});
test("returns empty map for non-array JSON", () => {
expect(parsePlansEnv(validEntry).size).toBe(0);
});
test("extends-only entry (no features key) inherits parent features", () => {
const result = parsePlansEnv(
JSON.stringify([
{ name: "Pro", features: fullFeatures },
{ name: "LTD T2", extends: "Pro" },
])
);
expect(result.size).toBe(2);
expect(result.get("LTD T2")).toEqual(planConfig("LTD T2", fullFeatures));
expect(result.get("Pro")).toEqual(planConfig("Pro", fullFeatures));
});
test("entry with no features key and no extends resolves to defaultPlanFeatures", () => {
const result = parsePlansEnv(JSON.stringify([{ name: "Free" }]));
expect(result.size).toBe(1);
expect(result.get("Free")).toEqual(planConfig("Free", defaultPlanFeatures));
});
test("extends: child inherits parent features and overrides specific fields", () => {
const result = parsePlansEnv(
JSON.stringify([
{ name: "Pro", features: fullFeatures },
{ name: "Team", extends: "Pro", features: { maxWorkspaces: 50 } },
])
);
expect(result.size).toBe(2);
expect(result.get("Team")!.features.canDownloadAssets).toBe(true);
expect(result.get("Team")!.features.maxWorkspaces).toBe(50);
expect(result.get("Pro")!.features.maxWorkspaces).toBe(
fullFeatures.maxWorkspaces
);
});
test("extends: throws when extending an unknown plan", () => {
expect(() =>
parsePlansEnv(
JSON.stringify([
{
name: "Team",
extends: "NonExistent",
features: { maxWorkspaces: 5 },
},
])
)
).toThrow('PLANS entry "Team" extends unknown plan "NonExistent"');
});
test("parses a single valid entry", () => {
const result = parsePlansEnv(`[${validEntry}]`);
expect(result.size).toBe(1);
expect(result.get("Pro")).toEqual(planConfig("Pro", fullFeatures));
});
test("parses prices from entry", () => {
const result = parsePlansEnv(
JSON.stringify([
{
name: "Pro",
features: fullFeatures,
prices: { monthly: "price_abc", yearly: "price_def" },
},
])
);
expect(result.size).toBe(1);
expect(result.get("Pro")!.prices).toEqual({
monthly: "price_abc",
yearly: "price_def",
});
});
test("prices default to empty object when not provided", () => {
const result = parsePlansEnv(`[${validEntry}]`);
expect(result.get("Pro")!.prices).toEqual({});
});
test("skips entry with invalid prices (non-string value)", () => {
const result = parsePlansEnv(
JSON.stringify([
{ name: "Bad", features: fullFeatures, prices: { monthly: 123 } },
{ name: "Pro", features: fullFeatures },
])
);
expect(result.size).toBe(1);
expect(result.get("Pro")).toBeDefined();
});
test("parses multiple valid entries", () => {
const result = parsePlansEnv(twoEntries);
expect(result.size).toBe(2);
expect(result.get("Workspaces")!.features.maxWorkspaces).toBe(10);
});
test("skips entries with invalid features (wrong type), keeps valid ones", () => {
const result = parsePlansEnv(
JSON.stringify([
{ name: "Bad", features: { canDownloadAssets: "not-a-bool" } },
{ name: "Pro", features: fullFeatures },
])
);
expect(result.size).toBe(1);
expect(result.get("Pro")).toEqual(planConfig("Pro", fullFeatures));
});
test("partial features without extends fills in from defaultPlanFeatures", () => {
const result = parsePlansEnv(
JSON.stringify([{ name: "Pro", features: { canDownloadAssets: true } }])
);
expect(result.size).toBe(1);
expect(result.get("Pro")).toEqual(
planConfig("Pro", { ...defaultPlanFeatures, canDownloadAssets: true })
);
});
test("strips unknown keys from features", () => {
const result = parsePlansEnv(
JSON.stringify([
{ name: "Pro", features: { ...fullFeatures, admin: true } },
])
);
expect(result.size).toBe(1);
expect(
(result.get("Pro")!.features as Record<string, unknown>)["admin"]
).toBeUndefined();
});
});