Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions apps/web/__tests__/unit/checkout-plans.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { STRIPE_PLAN_IDS } from "@cap/utils";
import { describe, expect, it } from "vitest";
import {
checkoutPath,
parseCheckoutSeats,
pickCheckoutCurrency,
planForPriceId,
planPriceId,
sessionIdFromClientSecret,
} from "@/lib/checkout/plans";

describe("checkout plans", () => {
it("maps plans to the deployment's price ids and back", () => {
expect(planPriceId("yearly", "production")).toBe(
STRIPE_PLAN_IDS.production.yearly,
);
expect(
planForPriceId(STRIPE_PLAN_IDS.development.monthly, "development"),
).toBe("monthly");
expect(
planForPriceId(STRIPE_PLAN_IDS.development.monthly, "production"),
).toBe(null);
expect(planForPriceId("price_unknown", "production")).toBe(null);
expect(planForPriceId(undefined, "production")).toBe(null);
});

it("falls back to one seat for anything that is not a sane integer", () => {
expect(parseCheckoutSeats("4")).toBe(4);
expect(parseCheckoutSeats(7)).toBe(7);
for (const value of [
"0",
"-2",
"1.5",
"abc",
"",
"10001",
undefined,
null,
]) {
expect(parseCheckoutSeats(value)).toBe(1);
}
});

it("builds checkout links that keep onboarding only when asked", () => {
expect(checkoutPath({ plan: "monthly", seats: 2 })).toBe(
"/checkout?plan=monthly&seats=2",
);
expect(checkoutPath({ plan: "yearly", seats: 1, onboarding: true })).toBe(
"/checkout?plan=yearly&seats=1&onboarding=1",
);
});

it("only presents a local currency every plan price supports", () => {
const both = new Set(["usd", "gbp", "eur"]);
const usdOnly = new Set(["usd"]);
expect(pickCheckoutCurrency("gbp", [both, both])).toBe("gbp");
expect(pickCheckoutCurrency("eur", [both, usdOnly])).toBe("usd");
expect(pickCheckoutCurrency("usd", [usdOnly, usdOnly])).toBe("usd");
});

it("extracts the session id from a client secret and rejects anything else", () => {
expect(sessionIdFromClientSecret("cs_test_a1B2c3_secret_xyz")).toBe(
"cs_test_a1B2c3",
);
expect(sessionIdFromClientSecret("cs_test_a1B2c3")).toBe(null);
expect(sessionIdFromClientSecret("pi_123_secret_abc")).toBe(null);
expect(sessionIdFromClientSecret("_secret_abc")).toBe(null);
});
});
36 changes: 34 additions & 2 deletions apps/web/__tests__/unit/mobile-checkout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import {
const checkoutMocks = vi.hoisted(() => ({
create: vi.fn(),
track: vi.fn(() => Promise.resolve()),
env: { WEB_URL: "https://cap.so" } as Record<string, string | undefined>,
}));

vi.mock("@cap/env", () => ({
buildEnv: {},
serverEnv: () => ({ WEB_URL: "https://cap.so" }),
buildEnv: { NEXT_PUBLIC_IS_CAP: "true" },
serverEnv: () => checkoutMocks.env,
}));

vi.mock("@cap/utils", () => ({
Expand All @@ -23,6 +24,10 @@ vi.mock("@cap/utils", () => ({
},
}),
isValidStripePlanPriceId: (id: string) => id === "price_pro",
STRIPE_PLAN_IDS: {
development: { monthly: "price_pro", yearly: "price_pro_yearly" },
production: { monthly: "price_pro_live", yearly: "price_pro_live_yr" },
},
}));

vi.mock("@/lib/server-analytics", () => ({
Expand All @@ -39,6 +44,7 @@ const makeGuestCheckoutRequest = (body: Record<string, unknown>) =>
describe("checkout redirects", () => {
beforeEach(() => {
vi.clearAllMocks();
checkoutMocks.env = { WEB_URL: "https://cap.so" };
checkoutMocks.create.mockResolvedValue({
id: "cs_test",
url: "https://pay.cap.so/session",
Expand Down Expand Up @@ -89,6 +95,32 @@ describe("checkout redirects", () => {
});
});

it("routes web guests to the in-app checkout once it is configured", async () => {
checkoutMocks.env = {
WEB_URL: "https://cap.so",
STRIPE_SECRET_KEY: "sk_test_x",
STRIPE_PUBLISHABLE_KEY: "pk_test_x",
};

const web = await startGuestCheckout(
makeGuestCheckoutRequest({ priceId: "price_pro", quantity: 4 }),
);
expect(await web.json()).toEqual({
url: "/checkout?plan=monthly&seats=4",
});
expect(checkoutMocks.create).not.toHaveBeenCalled();

const mobile = await startGuestCheckout(
makeGuestCheckoutRequest({
priceId: "price_pro",
quantity: 1,
platform: "mobile",
}),
);
expect(await mobile.json()).toEqual({ url: "https://pay.cap.so/session" });
expect(checkoutMocks.create).toHaveBeenCalledTimes(1);
});

it("rejects non-allowlisted arbitrary priceId with 400", async () => {
const response = await startGuestCheckout(
makeGuestCheckoutRequest({
Expand Down
Loading
Loading