From db29cc1251173d953b4f02498d3f59eb94f24fed Mon Sep 17 00:00:00 2001 From: Wesley <100464352+ologbonowiwi@users.noreply.github.com> Date: Thu, 7 Sep 2023 13:27:46 -0300 Subject: [PATCH] feat: send new booking uid on reschedule webhook (#10654) Co-authored-by: CarinaWolli --- apps/web/playwright/booking-seats.e2e.ts | 41 +---- apps/web/playwright/lib/testUtils.ts | 46 +++++ apps/web/playwright/webhook.e2e.ts | 169 ++++++++++++++++++ .../features/bookings/lib/handleNewBooking.ts | 1 + 4 files changed, 217 insertions(+), 40 deletions(-) diff --git a/apps/web/playwright/booking-seats.e2e.ts b/apps/web/playwright/booking-seats.e2e.ts index d173992d00..c46e84d4d2 100644 --- a/apps/web/playwright/booking-seats.e2e.ts +++ b/apps/web/playwright/booking-seats.e2e.ts @@ -1,60 +1,21 @@ import { expect } from "@playwright/test"; -import type { Prisma } from "@prisma/client"; import { uuid } from "short-uuid"; import { v4 as uuidv4 } from "uuid"; import prisma from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/enums"; -import type { Fixtures } from "./lib/fixtures"; import { test } from "./lib/fixtures"; import { bookTimeSlot, createNewSeatedEventType, selectFirstAvailableTimeSlotNextMonth, + createUserWithSeatedEventAndAttendees, } from "./lib/testUtils"; test.describe.configure({ mode: "parallel" }); test.afterEach(({ users }) => users.deleteAll()); -async function createUserWithSeatedEvent(users: Fixtures["users"]) { - const slug = "seats"; - const user = await users.create({ - eventTypes: [ - { - title: "Seated event", - slug, - seatsPerTimeSlot: 10, - requiresConfirmation: true, - length: 30, - disableGuests: true, // should always be true for seated events - }, - ], - }); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const eventType = user.eventTypes.find((e) => e.slug === slug)!; - return { user, eventType }; -} - -async function createUserWithSeatedEventAndAttendees( - fixtures: Pick, - attendees: Prisma.AttendeeCreateManyBookingInput[] -) { - const { user, eventType } = await createUserWithSeatedEvent(fixtures.users); - const booking = await fixtures.bookings.create(user.id, user.username, eventType.id, { - status: BookingStatus.ACCEPTED, - // startTime with 1 day from now and endTime half hour after - startTime: new Date(Date.now() + 24 * 60 * 60 * 1000), - endTime: new Date(Date.now() + 24 * 60 * 60 * 1000 + 30 * 60 * 1000), - attendees: { - createMany: { - data: attendees, - }, - }, - }); - return { user, eventType, booking }; -} - test.describe("Booking with Seats", () => { test("User can create a seated event (2 seats as example)", async ({ users, page }) => { const user = await users.create({ name: "Seated event" }); diff --git a/apps/web/playwright/lib/testUtils.ts b/apps/web/playwright/lib/testUtils.ts index d23493e659..7279d39d8f 100644 --- a/apps/web/playwright/lib/testUtils.ts +++ b/apps/web/playwright/lib/testUtils.ts @@ -6,6 +6,10 @@ import { createServer } from "http"; import { noop } from "lodash"; import type { API, Messages } from "mailhog"; +import type { Prisma } from "@calcom/prisma/client"; +import { BookingStatus } from "@calcom/prisma/enums"; + +import type { Fixtures } from "./fixtures"; import { test } from "./fixtures"; export function todo(title: string) { @@ -192,6 +196,7 @@ export async function installAppleCalendar(page: Page) { await page.waitForURL("/apps/apple-calendar"); await page.click('[data-testid="install-app-button"]'); } + export async function getEmailsReceivedByUser({ emails, userEmail, @@ -228,3 +233,44 @@ export async function expectEmailsToHaveSubject({ expect(organizerFirstEmail.subject).toBe(emailSubject); expect(bookerFirstEmail.subject).toBe(emailSubject); } + +// this method is not used anywhere else +// but I'm keeping it here in case we need in the future +async function createUserWithSeatedEvent(users: Fixtures["users"]) { + const slug = "seats"; + const user = await users.create({ + eventTypes: [ + { + title: "Seated event", + slug, + seatsPerTimeSlot: 10, + requiresConfirmation: true, + length: 30, + disableGuests: true, // should always be true for seated events + }, + ], + }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const eventType = user.eventTypes.find((e) => e.slug === slug)!; + return { user, eventType }; +} + +export async function createUserWithSeatedEventAndAttendees( + fixtures: Pick, + attendees: Prisma.AttendeeCreateManyBookingInput[] +) { + const { user, eventType } = await createUserWithSeatedEvent(fixtures.users); + + const booking = await fixtures.bookings.create(user.id, user.username, eventType.id, { + status: BookingStatus.ACCEPTED, + // startTime with 1 day from now and endTime half hour after + startTime: new Date(Date.now() + 24 * 60 * 60 * 1000), + endTime: new Date(Date.now() + 24 * 60 * 60 * 1000 + 30 * 60 * 1000), + attendees: { + createMany: { + data: attendees, + }, + }, + }); + return { user, eventType, booking }; +} diff --git a/apps/web/playwright/webhook.e2e.ts b/apps/web/playwright/webhook.e2e.ts index af0be0f580..d5e1d5b512 100644 --- a/apps/web/playwright/webhook.e2e.ts +++ b/apps/web/playwright/webhook.e2e.ts @@ -1,5 +1,9 @@ import type { Page } from "@playwright/test"; import { expect } from "@playwright/test"; +import { v4 as uuidv4 } from "uuid"; + +import prisma from "@calcom/prisma"; +import { BookingStatus } from "@calcom/prisma/client"; import { test } from "./lib/fixtures"; import { @@ -8,6 +12,7 @@ import { selectFirstAvailableTimeSlotNextMonth, waitFor, gotoRoutingLink, + createUserWithSeatedEventAndAttendees, } from "./lib/testUtils"; // remove dynamic properties that differs depending on where you run the tests @@ -15,6 +20,29 @@ const dynamic = "[redacted/dynamic]"; test.afterEach(({ users }) => users.deleteAll()); +async function createWebhookReceiver(page: Page) { + const webhookReceiver = createHttpServer(); + + await page.goto(`/settings/developer/webhooks`); + + // --- add webhook + await page.click('[data-testid="new_webhook"]'); + + await page.fill('[name="subscriberUrl"]', webhookReceiver.url); + + await page.fill('[name="secret"]', "secret"); + + await Promise.all([ + page.click("[type=submit]"), + page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")), + ]); + + // page contains the url + expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined(); + + return webhookReceiver; +} + test.describe("BOOKING_CREATED", async () => { test("add webhook & test that creating an event triggers a webhook call", async ({ page, @@ -388,6 +416,147 @@ test.describe("BOOKING_REQUESTED", async () => { }); }); +test.describe("BOOKING_RESCHEDULED", async () => { + test("can reschedule a booking and get a booking rescheduled event", async ({ page, users, bookings }) => { + const user = await users.create(); + const [eventType] = user.eventTypes; + + await user.apiLogin(); + + const webhookReceiver = await createWebhookReceiver(page); + + const booking = await bookings.create(user.id, user.username, eventType.id, { + status: BookingStatus.ACCEPTED, + }); + + await page.goto(`/${user.username}/${eventType.slug}?rescheduleUid=${booking.uid}`); + + await selectFirstAvailableTimeSlotNextMonth(page); + + await page.locator('[data-testid="confirm-reschedule-button"]').click(); + + await expect(page).toHaveURL(/.*booking/); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } })!; + expect(newBooking).not.toBeNull(); + + // --- check that webhook was called + await waitFor(() => { + expect(webhookReceiver.requestList.length).toBe(1); + }); + + const [request] = webhookReceiver.requestList; + + expect(request.body).toMatchObject({ + triggerEvent: "BOOKING_RESCHEDULED", + payload: { + uid: newBooking?.uid, + }, + }); + }); + + test("when rescheduling to a booking that already exists, should send a booking rescheduled event with the existant booking uid", async ({ + page, + users, + bookings, + }) => { + const { user, eventType, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [ + { name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" }, + { name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" }, + ]); + + await prisma.eventType.update({ + where: { id: eventType.id }, + data: { requiresConfirmation: false }, + }); + + await user.apiLogin(); + + const webhookReceiver = await createWebhookReceiver(page); + + const bookingAttendees = await prisma.attendee.findMany({ + where: { bookingId: booking.id }, + select: { + id: true, + email: true, + }, + }); + + const bookingSeats = bookingAttendees.map((attendee) => ({ + bookingId: booking.id, + attendeeId: attendee.id, + referenceUid: uuidv4(), + })); + + await prisma.bookingSeat.createMany({ + data: bookingSeats, + }); + + const references = await prisma.bookingSeat.findMany({ + where: { bookingId: booking.id }, + include: { attendee: true }, + }); + + await page.goto(`/reschedule/${references[0].referenceUid}`); + + await selectFirstAvailableTimeSlotNextMonth(page); + + await page.locator('[data-testid="confirm-reschedule-button"]').click(); + + await expect(page).toHaveURL(/.*booking/); + + const newBooking = await prisma.booking.findFirst({ + where: { + attendees: { + some: { + email: bookingAttendees[0].email, + }, + }, + }, + }); + + // --- ensuring that new booking was created + expect(newBooking).not.toBeNull(); + + // --- check that webhook was called + await waitFor(() => { + expect(webhookReceiver.requestList.length).toBe(1); + }); + + const [firstRequest] = webhookReceiver.requestList; + + expect(firstRequest?.body).toMatchObject({ + triggerEvent: "BOOKING_RESCHEDULED", + payload: { + uid: newBooking?.uid, + }, + }); + + await page.goto(`/reschedule/${references[1].referenceUid}`); + + await selectFirstAvailableTimeSlotNextMonth(page); + + await page.locator('[data-testid="confirm-reschedule-button"]').click(); + + await expect(page).toHaveURL(/.*booking/); + + await waitFor(() => { + expect(webhookReceiver.requestList.length).toBe(2); + }); + + const [_, secondRequest] = webhookReceiver.requestList; + + expect(secondRequest?.body).toMatchObject({ + triggerEvent: "BOOKING_RESCHEDULED", + payload: { + // in the current implementation, it is the same as the first booking + uid: newBooking?.uid, + }, + }); + }); +}); + test.describe("FORM_SUBMITTED", async () => { test("on submitting user form, triggers user webhook", async ({ page, users, routingForms }) => { const webhookReceiver = createHttpServer(); diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 0dbd8f4021..fc344fa536 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1756,6 +1756,7 @@ async function handler( const webhookData = { ...evt, ...eventTypeInfo, + uid: resultBooking?.uid || uid, bookingId: booking?.id, rescheduleUid, rescheduleStartTime: originalRescheduledBooking?.startTime