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
This commit is contained in:
1 parent
a7f4aebaaa
commit
99f0cf6189
2 files changed
+391
-6
No files matched your search
@@ -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<typeof vi.fn>;
|
||||
};
|
||||
};
|
||||
const mockGetStripeAppKeys = vi.mocked(getStripeAppKeys);
|
||||
|
||||
function createMockContext(overrides: Partial<GetServerSidePropsContext> = {}): 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;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user