From 99f0cf61896d3d2eba3d59b3d6e0682baf811510 Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:15:40 +0530 Subject: [PATCH] fix: initiate OAuth flow in setup page instead of redirecting to dashboard for stripe (#27608) * fix stripe setup * fix type error * address cubic * Add tests: packages/app-store/stripepayment/pages/setup/__tests__/_getServerSideProps.test.ts Generated by Paragon from proposal for PR #27608 --- .../__tests__/_getServerSideProps.test.ts | 310 ++++++++++++++++++ .../pages/setup/_getServerSideProps.ts | 87 ++++- 2 files changed, 391 insertions(+), 6 deletions(-) create mode 100644 packages/app-store/stripepayment/pages/setup/__tests__/_getServerSideProps.test.ts diff --git a/packages/app-store/stripepayment/pages/setup/__tests__/_getServerSideProps.test.ts b/packages/app-store/stripepayment/pages/setup/__tests__/_getServerSideProps.test.ts new file mode 100644 index 0000000000..5ef22bcd45 --- /dev/null +++ b/packages/app-store/stripepayment/pages/setup/__tests__/_getServerSideProps.test.ts @@ -0,0 +1,310 @@ +import type { GetServerSidePropsContext } from "next"; +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// Mock dependencies before imports +vi.mock("@calcom/features/auth/lib/getServerSession", () => ({ + getServerSession: vi.fn(), +})); + +vi.mock("@calcom/prisma", () => ({ + default: { + user: { + findUnique: vi.fn(), + }, + }, +})); + +vi.mock("../../../lib/getStripeAppKeys", () => ({ + getStripeAppKeys: vi.fn(), +})); + +vi.mock("@calcom/lib/constants", () => ({ + WEBAPP_URL: "https://app.cal.com", +})); + +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import prisma from "@calcom/prisma"; + +import { getStripeAppKeys } from "../../../lib/getStripeAppKeys"; +import { getServerSideProps } from "../_getServerSideProps"; + +const mockGetServerSession = vi.mocked(getServerSession); +const mockPrisma = prisma as unknown as { + user: { + findUnique: ReturnType; + }; +}; +const mockGetStripeAppKeys = vi.mocked(getStripeAppKeys); + +function createMockContext(overrides: Partial = {}): GetServerSidePropsContext { + return { + req: { headers: {} } as GetServerSidePropsContext["req"], + res: {} as GetServerSidePropsContext["res"], + params: { slug: "stripe" }, + query: {}, + resolvedUrl: "/apps/stripe/setup", + ...overrides, + }; +} + +describe("Stripe Setup Page getServerSideProps", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockGetStripeAppKeys.mockResolvedValue({ + client_id: "ca_test_client_id", + payment_fee_fixed: 0, + payment_fee_percentage: 0, + public_key: "pk_test", + webhook_secret: "whsec_test", + }); + }); + + describe("Slug validation", () => { + it("should return notFound when slug is not a string", async () => { + const ctx = createMockContext({ params: {} }); + + const result = await getServerSideProps(ctx); + + expect(result).toEqual({ notFound: true }); + }); + + it("should return notFound when params is undefined", async () => { + const ctx = createMockContext({ params: undefined }); + + const result = await getServerSideProps(ctx); + + expect(result).toEqual({ notFound: true }); + }); + }); + + describe("Authentication validation", () => { + it("should redirect to login when user is not authenticated", async () => { + const ctx = createMockContext(); + mockGetServerSession.mockResolvedValue(null); + + const result = await getServerSideProps(ctx); + + expect(result).toEqual({ + redirect: { + destination: "/auth/login", + permanent: false, + }, + }); + }); + + it("should redirect to login when session has no user uuid", async () => { + const ctx = createMockContext(); + mockGetServerSession.mockResolvedValue({ + user: {}, + hasValidLicense: true, + upId: "test", + expires: "2024-12-31", + } as any); + + const result = await getServerSideProps(ctx); + + expect(result).toEqual({ + redirect: { + destination: "/auth/login", + permanent: false, + }, + }); + }); + }); + + describe("User lookup", () => { + it("should return notFound when user does not exist in database", async () => { + const ctx = createMockContext(); + const testUuid = "550e8400-e29b-41d4-a716-446655440000"; + mockGetServerSession.mockResolvedValue({ + user: { uuid: testUuid }, + hasValidLicense: true, + upId: "test", + expires: "2024-12-31", + } as any); + mockPrisma.user.findUnique.mockResolvedValue(null); + + const result = await getServerSideProps(ctx); + + expect(result).toEqual({ notFound: true }); + expect(mockPrisma.user.findUnique).toHaveBeenCalledWith({ + where: { uuid: testUuid }, + select: { email: true, name: true }, + }); + }); + }); + + describe("OAuth redirect construction", () => { + beforeEach(() => { + mockGetServerSession.mockResolvedValue({ + user: { uuid: "550e8400-e29b-41d4-a716-446655440000" }, + hasValidLicense: true, + upId: "test", + expires: "2024-12-31", + } as any); + mockPrisma.user.findUnique.mockResolvedValue({ + email: "user@example.com", + name: "Test User", + }); + }); + + it("should redirect to Stripe OAuth with correct parameters", async () => { + const ctx = createMockContext(); + + const result = await getServerSideProps(ctx); + + expect(result).toHaveProperty("redirect"); + const redirect = (result as { redirect: { destination: string; permanent: boolean } }).redirect; + expect(redirect.permanent).toBe(false); + expect(redirect.destination).toContain("https://connect.stripe.com/oauth/authorize"); + expect(redirect.destination).toContain("client_id=ca_test_client_id"); + expect(redirect.destination).toContain("scope=read_write"); + expect(redirect.destination).toContain("response_type=code"); + }); + + it("should include user email in stripe_user params", async () => { + const ctx = createMockContext(); + + const result = await getServerSideProps(ctx); + + const redirect = (result as { redirect: { destination: string; permanent: boolean } }).redirect; + expect(redirect.destination).toContain(encodeURIComponent("user@example.com")); + }); + + it("should include user first name in stripe_user params", async () => { + const ctx = createMockContext(); + + const result = await getServerSideProps(ctx); + + const redirect = (result as { redirect: { destination: string; permanent: boolean } }).redirect; + expect(redirect.destination).toContain("first_name"); + }); + + it("should include correct redirect_uri", async () => { + const ctx = createMockContext(); + + const result = await getServerSideProps(ctx); + + const redirect = (result as { redirect: { destination: string; permanent: boolean } }).redirect; + expect(redirect.destination).toContain( + encodeURIComponent("https://app.cal.com/api/integrations/stripepayment/callback") + ); + }); + + it("should include state with default onErrorReturnTo when not provided", async () => { + const ctx = createMockContext(); + + const result = await getServerSideProps(ctx); + + const redirect = (result as { redirect: { destination: string; permanent: boolean } }).redirect; + // State should contain fromApp: true and default onErrorReturnTo + const stateMatch = redirect.destination.match(/state=([^&]+)/); + expect(stateMatch).toBeTruthy(); + const stateStr = decodeURIComponent(stateMatch![1]); + const state = JSON.parse(stateStr); + expect(state.fromApp).toBe(true); + expect(state.onErrorReturnTo).toBe("https://app.cal.com/apps/installed/payment"); + expect(state.returnTo).toBeUndefined(); + }); + + it("should include returnTo in state when provided", async () => { + const ctx = createMockContext({ + query: { returnTo: "/event-types/123" }, + }); + + const result = await getServerSideProps(ctx); + + const redirect = (result as { redirect: { destination: string; permanent: boolean } }).redirect; + const stateMatch = redirect.destination.match(/state=([^&]+)/); + const stateStr = decodeURIComponent(stateMatch![1]); + const state = JSON.parse(stateStr); + expect(state.returnTo).toBe("/event-types/123"); + }); + + it("should use custom onErrorReturnTo when provided", async () => { + const ctx = createMockContext({ + query: { onErrorReturnTo: "/settings/integrations" }, + }); + + const result = await getServerSideProps(ctx); + + const redirect = (result as { redirect: { destination: string; permanent: boolean } }).redirect; + const stateMatch = redirect.destination.match(/state=([^&]+)/); + const stateStr = decodeURIComponent(stateMatch![1]); + const state = JSON.parse(stateStr); + expect(state.onErrorReturnTo).toBe("/settings/integrations"); + }); + + it("should handle user with null name", async () => { + mockPrisma.user.findUnique.mockResolvedValue({ + email: "user@example.com", + name: null, + }); + const ctx = createMockContext(); + + const result = await getServerSideProps(ctx); + + expect(result).toHaveProperty("redirect"); + const redirect = (result as { redirect: { destination: string; permanent: boolean } }).redirect; + expect(redirect.destination).toContain("https://connect.stripe.com/oauth/authorize"); + }); + }); + + describe("Error handling", () => { + beforeEach(() => { + mockGetServerSession.mockResolvedValue({ + user: { uuid: "550e8400-e29b-41d4-a716-446655440000" }, + hasValidLicense: true, + upId: "test", + expires: "2024-12-31", + } as any); + mockPrisma.user.findUnique.mockResolvedValue({ + email: "user@example.com", + name: "Test User", + }); + }); + + it("should redirect to error page when getStripeAppKeys fails", async () => { + mockGetStripeAppKeys.mockRejectedValue(new Error("Failed to get Stripe keys")); + const ctx = createMockContext(); + + const result = await getServerSideProps(ctx); + + expect(result).toEqual({ + redirect: { + destination: "https://app.cal.com/apps/installed/payment?error=stripe_oauth_failed", + permanent: false, + }, + }); + }); + }); + + describe("E2E mode", () => { + beforeEach(() => { + mockGetServerSession.mockResolvedValue({ + user: { uuid: "550e8400-e29b-41d4-a716-446655440000" }, + hasValidLicense: true, + upId: "test", + expires: "2024-12-31", + } as any); + mockPrisma.user.findUnique.mockResolvedValue({ + email: "user@example.com", + name: "Test User", + }); + }); + + it("should include country=US when NEXT_PUBLIC_IS_E2E is set", async () => { + const originalEnv = process.env.NEXT_PUBLIC_IS_E2E; + process.env.NEXT_PUBLIC_IS_E2E = "true"; + + const ctx = createMockContext(); + + const result = await getServerSideProps(ctx); + + const redirect = (result as { redirect: { destination: string; permanent: boolean } }).redirect; + expect(redirect.destination).toContain("country"); + + process.env.NEXT_PUBLIC_IS_E2E = originalEnv; + }); + }); +}); diff --git a/packages/app-store/stripepayment/pages/setup/_getServerSideProps.ts b/packages/app-store/stripepayment/pages/setup/_getServerSideProps.ts index 242439236a..df8f6e6e40 100644 --- a/packages/app-store/stripepayment/pages/setup/_getServerSideProps.ts +++ b/packages/app-store/stripepayment/pages/setup/_getServerSideProps.ts @@ -1,13 +1,88 @@ import type { GetServerSidePropsContext } from "next"; +import stringify from "qs-stringify"; +import type Stripe from "stripe"; +import { z } from "zod"; + +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { WEBAPP_URL } from "@calcom/lib/constants"; +import prisma from "@calcom/prisma"; + +import type { IntegrationOAuthCallbackState } from "@calcom/app-store/types"; +import { getStripeAppKeys } from "../../lib/getStripeAppKeys"; export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { const notFound = { notFound: true } as const; if (typeof ctx.params?.slug !== "string") return notFound; - const targetUrl = "https://dashboard.stripe.com/settings/connect"; - return { - redirect: { - destination: targetUrl, - permanent: false, + + const session = await getServerSession({ req: ctx.req }); + + if (!session?.user?.uuid) { + return { + redirect: { + destination: "/auth/login", + permanent: false, + }, + }; + } + + const user = await prisma.user.findUnique({ + where: { + uuid: session.user.uuid, }, - }; + select: { + email: true, + name: true, + }, + }); + + if (!user) { + return notFound; + } + + try { + const { client_id } = await getStripeAppKeys(); + const returnTo = ctx.query.returnTo ? String(ctx.query.returnTo) : undefined; + const onErrorReturnTo = ctx.query.onErrorReturnTo + ? String(ctx.query.onErrorReturnTo) + : `${WEBAPP_URL}/apps/installed/payment`; + + const state: IntegrationOAuthCallbackState = { + returnTo, + onErrorReturnTo, + fromApp: true, + }; + + const redirect_uri = encodeURI(`${WEBAPP_URL}/api/integrations/stripepayment/callback`); + const stripeConnectParams: Stripe.OAuthAuthorizeUrlParams = { + client_id, + scope: "read_write", + response_type: "code", + stripe_user: { + email: user.email, + first_name: user.name || undefined, + country: process.env.NEXT_PUBLIC_IS_E2E ? "US" : undefined, + }, + redirect_uri, + state: JSON.stringify(state), + }; + + const params = z.record(z.any()).parse(stripeConnectParams); + const query = stringify(params); + + const url = `https://connect.stripe.com/oauth/authorize?${query}`; + + return { + redirect: { + destination: url, + permanent: false, + }, + }; + } catch (error) { + return { + redirect: { + destination: `${WEBAPP_URL}/apps/installed/payment?error=stripe_oauth_failed`, + permanent: false, + }, + }; + } };