feat: send new booking uid on reschedule webhook (#10654)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
@@ -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<Fixtures, "users" | "bookings">,
|
||||
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" });
|
||||
|
||||
@@ -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<Fixtures, "users" | "bookings">,
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1756,6 +1756,7 @@ async function handler(
|
||||
const webhookData = {
|
||||
...evt,
|
||||
...eventTypeInfo,
|
||||
uid: resultBooking?.uid || uid,
|
||||
bookingId: booking?.id,
|
||||
rescheduleUid,
|
||||
rescheduleStartTime: originalRescheduledBooking?.startTime
|
||||
|
||||
Reference in New Issue
Block a user