Skip to content
Closed
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
67 changes: 67 additions & 0 deletions apps/dokploy/__test__/billing/stripe-initialization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it, vi } from "vitest";

describe("Stripe Initialization in Self-Hosted vs Cloud (Fixes Issue #5344)", () => {
const createStripeLoader = (loadStripeMock: (key: string) => Promise<any>) => {
return (publishableKey?: string) => {
return publishableKey ? loadStripeMock(publishableKey) : null;
};
};
Comment on lines +4 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Tests duplicate production logic

These tests recreate the Stripe initialization and checkout guards locally instead of importing or exercising either changed production module. Reverting or breaking the guards in PlanStep or ShowBilling would therefore leave all three tests passing, so CI does not protect against this dashboard crash returning. Extract the initialization behavior into an imported helper or test the production modules with loadStripe mocked. The checkout handler at lines 53–60 has the same problem.


it("safely evaluates to null and avoids throwing IntegrationError when publishable key is not set", () => {
const loadStripeMock = vi.fn((key: string) => {
if (!key || typeof key !== "string") {
throw new Error("Missing value for Stripe(): apiKey should be a string.");
}
return Promise.resolve({ redirectToCheckout: vi.fn() });
});

const initStripe = createStripeLoader(loadStripeMock);

// Self-hosted environment: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is undefined
const stripePromise = initStripe(undefined);

expect(stripePromise).toBeNull();
expect(loadStripeMock).not.toHaveBeenCalled();
});

it("initializes Stripe normally when publishable key is present in Cloud environment", async () => {
const mockStripeInstance = { redirectToCheckout: vi.fn() };
const loadStripeMock = vi.fn(async (key: string) => mockStripeInstance);

const initStripe = createStripeLoader(loadStripeMock);

// Cloud environment: key is configured
const stripePromise = initStripe("pk_live_12345");

expect(stripePromise).not.toBeNull();
expect(loadStripeMock).toHaveBeenCalledWith("pk_live_12345");

const stripe = await stripePromise;
expect(stripe).toBe(mockStripeInstance);
});

it("handles checkout attempt safely when Stripe is not configured without crashing", async () => {
let toastErrorCalledWith: string | null = null;
const toastMock = {
error: (msg: string) => {
toastErrorCalledWith = msg;
},
};

const stripePromise: Promise<any> | null = null;

const handleCheckout = async () => {
const stripe = stripePromise ? await stripePromise : null;
if (!stripe) {
toastMock.error("Stripe is not configured");
return false;
}
return true;
};

const result = await handleCheckout();

expect(result).toBe(false);
expect(toastErrorCalledWith).toBe("Stripe is not configured");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { displayFont } from "../font";

const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
);
const stripePromise = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
? loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY)
: null;

interface Props {
onNext: () => void;
Expand All @@ -37,7 +37,12 @@ export const PlanStep = ({ onNext }: Props) => {
if (!productId) return;
setLoadingTier(tier);
try {
const stripe = await stripePromise;
const stripe = stripePromise ? await stripePromise : null;
if (!stripe) {
toast.error("Stripe is not configured");
setLoadingTier(null);
return;
}
const session = await createCheckoutSession({
tier,
productId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";

const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
);
const stripePromise = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
? loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY)
: null;

/** Precio legacy / Hobby: $4.50/mo primer servidor, $3.50 siguientes; anual $45.90 primero, $35.70 siguientes. */
export const calculatePrice = (count: number, isAnnual = false) => {
Expand Down Expand Up @@ -147,7 +147,11 @@ export const ShowBilling = () => {
tier: "legacy" | "hobby" | "startup",
productId: string,
) => {
const stripe = await stripePromise;
const stripe = stripePromise ? await stripePromise : null;
if (!stripe) {
toast.error("Stripe is not configured");
return;
}
const serverQuantity =
tier === "startup"
? startupServerQuantity
Expand Down