From d66095bf9c1558e9c56f7e7c571b1d64243e8139 Mon Sep 17 00:00:00 2001 From: Wesley <100464352+ologbonowiwi@users.noreply.github.com> Date: Mon, 21 Aug 2023 06:12:39 -0300 Subject: [PATCH] test: adding confirmed payment test cases on Stripe (#10243) Co-authored-by: Joe Au-Yeung Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> --- .../components/booking/BookingListItem.tsx | 2 +- apps/web/playwright/fixtures/users.ts | 38 +++++++++++- .../web/playwright/integrations-stripe.e2e.ts | 62 +++++++++++++++++-- packages/features/ee/payments/api/webhook.ts | 4 +- 4 files changed, 95 insertions(+), 11 deletions(-) diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 4fd4681c22..518a0c21d8 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -368,7 +368,7 @@ function BookingListItem(booking: BookingItemProps) { {t("error_collecting_card")} ) : booking.paid ? ( - + {booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")} ) : null} diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index 1b855055e9..5e21eaa3a9 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -5,6 +5,7 @@ import { hashSync as hash } from "bcryptjs"; import type { API } from "mailhog"; import dayjs from "@calcom/dayjs"; +import stripe from "@calcom/features/ee/payments/server/stripe"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; import { prisma } from "@calcom/prisma"; import { MembershipRole, SchedulingType } from "@calcom/prisma/enums"; @@ -416,8 +417,8 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => { getPaymentCredential: async () => getPaymentCredential(store.page), setupEventWithPrice: async (eventType: Pick) => setupEventWithPrice(eventType, store.page), - bookAndPaidEvent: async (eventType: Pick) => - bookAndPaidEvent(user, eventType, store.page), + bookAndPayEvent: async (eventType: Pick) => + bookAndPayEvent(user, eventType, store.page), makePaymentUsingStripe: async () => makePaymentUsingStripe(store.page), // ths is for developemnt only aimed to inject debugging messages in the metadata field of the user debug: async (message: string | Record) => { @@ -427,6 +428,7 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => { }); }, delete: async () => await prisma.user.delete({ where: { id: store.user.id } }), + confirmPendingPayment: async () => confirmPendingPayment(store.page), }; }; @@ -468,6 +470,36 @@ const createUser = (workerInfo: WorkerInfo, opts?: CustomUserOpts | null): Prism }; }; +async function confirmPendingPayment(page: Page) { + await page.waitForURL(new RegExp("/booking/*")); + + const url = page.url(); + + const params = new URLSearchParams(url.split("?")[1]); + + const id = params.get("payment_intent"); + + if (!id) throw new Error(`Payment intent not found in url ${url}`); + + const payload = JSON.stringify( + { type: "payment_intent.succeeded", data: { object: { id } }, account: "e2e_test" }, + null, + 2 + ); + + const signature = stripe.webhooks.generateTestHeaderString({ + payload, + secret: process.env.STRIPE_WEBHOOK_SECRET as string, + }); + + const response = await page.request.post("/api/integrations/stripepayment/webhook", { + data: payload, + headers: { "stripe-signature": signature }, + }); + + if (response.status() !== 200) throw new Error(`Failed to confirm payment. Response: ${response.text()}`); +} + // login using a replay of an E2E routine. export async function login( user: Pick & Partial>, @@ -519,7 +551,7 @@ export async function setupEventWithPrice(eventType: Pick, eventType: Pick, page: Page diff --git a/apps/web/playwright/integrations-stripe.e2e.ts b/apps/web/playwright/integrations-stripe.e2e.ts index 52a4e9e81e..65dfc17950 100644 --- a/apps/web/playwright/integrations-stripe.e2e.ts +++ b/apps/web/playwright/integrations-stripe.e2e.ts @@ -1,6 +1,7 @@ import { expect } from "@playwright/test"; import type Prisma from "@prisma/client"; +import type { Fixtures } from "./lib/fixtures"; import { test } from "./lib/fixtures"; import { todo, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils"; @@ -41,7 +42,7 @@ test.describe("Stripe integration", () => { await user.getPaymentCredential(); await user.setupEventWithPrice(eventType); - await user.bookAndPaidEvent(eventType); + await user.bookAndPayEvent(eventType); // success await expect(page.locator("[data-testid=success-page]")).toBeVisible(); }); @@ -78,7 +79,7 @@ test.describe("Stripe integration", () => { await user.getPaymentCredential(); await user.setupEventWithPrice(eventType); - await user.bookAndPaidEvent(eventType); + await user.bookAndPayEvent(eventType); // Rescheduling the event await Promise.all([page.waitForURL("/booking/*"), page.click('[data-testid="reschedule-link"]')]); @@ -92,8 +93,57 @@ test.describe("Stripe integration", () => { await user.makePaymentUsingStripe(); }); - todo("Payment should confirm pending payment booking"); - todo("Payment should trigger a BOOKING_PAID webhook"); - todo("Paid booking should be able to be cancelled"); - todo("Cancelled paid booking should be refunded"); + + test("Paid booking should be able to be cancelled", async ({ page, users }) => { + const user = await users.create(); + const eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType; + await user.apiLogin(); + await page.goto("/apps/installed"); + + await user.getPaymentCredential(); + await user.setupEventWithPrice(eventType); + await user.bookAndPayEvent(eventType); + + await page.click('[data-testid="cancel"]'); + await page.click('[data-testid="confirm_cancel"]'); + + await expect(await page.locator('[data-testid="cancelled-headline"]').first()).toBeVisible(); + }); + + test.describe("When event is paid and confirmed", () => { + let user: Awaited>; + let eventType: Prisma.EventType; + + test.beforeEach(async ({ page, users }) => { + user = await users.create(); + eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType; + await user.apiLogin(); + await page.goto("/apps/installed"); + + await user.getPaymentCredential(); + await user.setupEventWithPrice(eventType); + await user.bookAndPayEvent(eventType); + await user.confirmPendingPayment(); + }); + + test("Cancelled paid booking should be refunded", async ({ page, users, request }) => { + await page.click('[data-testid="cancel"]'); + await page.click('[data-testid="confirm_cancel"]'); + + await expect(await page.locator('[data-testid="cancelled-headline"]').first()).toBeVisible(); + await expect(page.getByText("This booking payment has been refunded")).toBeVisible(); + }); + + test("Payment should confirm pending payment booking", async ({ page, users }) => { + await page.goto("/bookings/upcoming"); + + const paidBadge = page.locator('[data-testid="paid_badge"]').first(); + + await expect(paidBadge).toBeVisible(); + expect(await paidBadge.innerText()).toBe("Paid"); + }); + + todo("Payment should trigger a BOOKING_PAID webhook"); + todo("Paid and confirmed booking should be able to be rescheduled"); + }); }); diff --git a/packages/features/ee/payments/api/webhook.ts b/packages/features/ee/payments/api/webhook.ts index 5962868854..deb8f6e993 100644 --- a/packages/features/ee/payments/api/webhook.ts +++ b/packages/features/ee/payments/api/webhook.ts @@ -398,7 +398,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const event = stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET); - if (!event.account) { + // bypassing this validation for e2e tests + // in order to successfully confirm the payment + if (!event.account && !process.env.NEXT_PUBLIC_IS_E2E) { throw new HttpCode({ statusCode: 202, message: "Incoming connected account" }); }