From 0e022b013c67b256aaeaf61f174cfd4a0fa78ae8 Mon Sep 17 00:00:00 2001 From: Andres Vargas Date: Sat, 1 Jul 2023 13:18:27 -0400 Subject: [PATCH] fix: Stripe integration (#9462) * Add Stripe integration tests * Add Stripe payment feature * Update Stripe payment form with valid card info * Fill postal code if visible in Stripe test * Implementing new e2e test * lint fix --------- Co-authored-by: Peer Richelsen Co-authored-by: alannnc --- apps/web/playwright/fixtures/users.ts | 38 ++++++++++++ .../web/playwright/integrations-stripe.e2e.ts | 62 +++++++++---------- 2 files changed, 68 insertions(+), 32 deletions(-) diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index 20bed52198..8047305f42 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -8,6 +8,7 @@ import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/avail import { prisma } from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/enums"; +import { selectFirstAvailableTimeSlotNextMonth } from "../lib/testUtils"; import type { TimeZoneEnum } from "./types"; // Don't import hashPassword from app as that ends up importing next-auth and initializing it before NEXTAUTH_URL can be updated during tests. @@ -279,6 +280,10 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => { await page.goto("/auth/logout"); }, getPaymentCredential: async () => getPaymentCredential(store.page), + setupEventWithPrice: async (eventType: Pick) => + setupEventWithPrice(eventType, store.page), + bookAndPaidEvent: async (eventType: Pick) => + bookAndPaidEvent(user, eventType, store.page), // ths is for developemnt only aimed to inject debugging messages in the metadata field of the user debug: async (message: string | Record) => { await prisma.user.update({ @@ -372,6 +377,39 @@ export async function apiLogin( }); } +export async function setupEventWithPrice(eventType: Pick, page: Page) { + await page.goto(`/event-types/${eventType?.id}?tabName=apps`); + await page.locator("div > .ml-auto").first().click(); + await page.getByPlaceholder("Price").fill("100"); + await page.getByTestId("update-eventtype").click(); +} + +export async function bookAndPaidEvent( + user: Pick, + eventType: Pick, + page: Page +) { + // booking process with stripe integration + await page.goto(`${user.username}/${eventType?.slug}`); + await selectFirstAvailableTimeSlotNextMonth(page); + // --- fill form + await page.fill('[name="name"]', "Stripe Stripeson"); + await page.fill('[name="email"]', "test@example.com"); + + await Promise.all([page.waitForURL("/payment/*"), page.press('[name="email"]', "Enter")]); + + const stripeFrame = page.frameLocator("iframe").first(); + await stripeFrame.locator('[name="number"]').fill("4242 4242 4242 4242"); + const now = new Date(); + await stripeFrame.locator('[name="expiry"]').fill(`${now.getMonth()} / ${now.getFullYear() + 1}`); + await stripeFrame.locator('[name="cvc"]').fill("111"); + const postcalCodeIsVisible = await stripeFrame.locator('[name="postalCode"]').isVisible(); + if (postcalCodeIsVisible) { + await stripeFrame.locator('[name="postalCode"]').fill("111111"); + } + await page.click('button:has-text("Pay now")'); +} + export async function getPaymentCredential(page: Page) { await page.goto("/apps/stripe"); diff --git a/apps/web/playwright/integrations-stripe.e2e.ts b/apps/web/playwright/integrations-stripe.e2e.ts index 6f4310dd91..e1ebbac30b 100644 --- a/apps/web/playwright/integrations-stripe.e2e.ts +++ b/apps/web/playwright/integrations-stripe.e2e.ts @@ -1,22 +1,20 @@ import { expect } from "@playwright/test"; +import type Prisma from "@prisma/client"; import { test } from "./lib/fixtures"; -import { selectFirstAvailableTimeSlotNextMonth, todo } from "./lib/testUtils"; +import { todo, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils"; test.describe.configure({ mode: "parallel" }); test.afterEach(({ users }) => users.deleteAll()); const IS_STRIPE_ENABLED = !!( - process.env.STRIPE_CLIENT_ID && process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY && - process.env.STRIPE_PRIVATE_KEY + process.env.STRIPE_CLIENT_ID && + process.env.STRIPE_PRIVATE_KEY && + process.env.PAYMENT_FEE_FIXED && + process.env.PAYMENT_FEE_PERCENTAGE ); -// TODO: No longer up to date, rewrite needed. - -// eslint-disable-next-line playwright/no-skipped-test -test.skip(); - test.describe("Stripe integration", () => { // eslint-disable-next-line playwright/no-skipped-test test.skip(!IS_STRIPE_ENABLED, "It should only run if Stripe is installed"); @@ -29,20 +27,35 @@ test.describe("Stripe integration", () => { await user.getPaymentCredential(); - /** If Stripe is added correctly we should see the "Disconnect" button */ - await expect( - page.locator(`li:has-text("Stripe") >> [data-testid="stripe_payment-integration-disconnect-button"]`) - ).toContainText(""); + await expect(page.locator(`h3:has-text("Stripe")`)).toBeVisible(); + await page.getByRole("list").getByRole("button").click(); + await expect(page.getByRole("button", { name: "Remove App" })).toBeVisible(); }); }); test("Can book a paid booking", async ({ page, users }) => { const user = await users.create(); - const eventType = user.eventTypes.find((e) => e.slug === "paid"); + 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.getPaymentCredential(); + await user.setupEventWithPrice(eventType); + await user.bookAndPaidEvent(eventType); + // success + await expect(page.locator("[data-testid=success-page]")).toBeVisible(); + }); + + test("Pending payment booking should not be confirmed by default", 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); + + // booking process without payment await page.goto(`${user.username}/${eventType?.slug}`); await selectFirstAvailableTimeSlotNextMonth(page); // --- fill form @@ -51,26 +64,11 @@ test.describe("Stripe integration", () => { await Promise.all([page.waitForURL("/payment/*"), page.press('[name="email"]', "Enter")]); - const stripeFrame = page - .frameLocator('iframe[src^="https://js.stripe.com/v3/elements-inner-card-"]') - .first(); + await page.goto(`/bookings/upcoming`); - // Fill [placeholder="Card number"] - await stripeFrame.locator('[placeholder="Card number"]').fill("4242 4242 4242 4242"); - // Fill [placeholder="MM / YY"] - await stripeFrame.locator('[placeholder="MM / YY"]').fill("12 / 24"); - // Fill [placeholder="CVC"] - await stripeFrame.locator('[placeholder="CVC"]').fill("111"); - // Fill [placeholder="ZIP"] - await stripeFrame.locator('[placeholder="ZIP"]').fill("11111"); - // Click button:has-text("Pay now") - await page.click('button:has-text("Pay now")'); - - // Make sure we're navigated to the success page - await expect(page.locator("[data-testid=success-page]")).toBeVisible(); + await expect(page.getByText("Unconfirmed")).toBeVisible(); + await expect(page.getByText("Pending payment").last()).toBeVisible(); }); - - todo("Pending payment booking should not be confirmed by default"); todo("Payment should confirm pending payment booking"); todo("Payment should trigger a BOOKING_PAID webhook"); todo("Paid booking should be able to be rescheduled");