Files
calendar/apps/web/playwright/webhook.e2e.ts
T
Syed Ali ShahbazGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Hariom Balhara
5d65df9c05 chore: migrate booking requested webhook trigger (#27546)
* init

* wiring up

* fix type

* feat: implement DI pattern for webhook producer in API v2

- Export IWebhookProducerService and getWebhookProducer from platform-libraries
- Add WEBHOOK_PRODUCER token and useFactory provider in RegularBookingModule
- Inject webhookProducer in RegularBookingService and pass to base class

This follows the composition root pattern where only the NestJS module
knows about getWebhookProducer(), and all consumers depend only on the
IWebhookProducerService interface via constructor injection.

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* test: migrate BOOKING_REQUESTED tests to new webhook architecture

- Remove failing BOOKING_REQUESTED tests from fresh-booking.test.ts (4 tests)
- Remove failing BOOKING_REQUESTED tests from reschedule.test.ts (2 tests)
- Remove failing BOOKING_REQUESTED test from collective-scheduling.test.ts (1 test)
- Replace WebhookTaskConsumer.test.ts with placeholder (constructor changed)
- Create new webhook architecture test suite:
  - producer/WebhookTaskerProducerService.test.ts (14 tests)
  - consumer/WebhookTaskConsumer.test.ts (8 tests)
  - consumer/triggers/booking-requested.test.ts (8 tests)

The new test suite is organized by trigger type for extensibility as more
triggers are migrated to the producer/consumer pattern.

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* test: remove paid events BOOKING_REQUESTED test (moved to new architecture)

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* wrap webhook in own try-catch

* wire datafetcher

* fix

* fix v2

* fix circular dependency

* --

* merge-conflict-resolve

* mreg-conflict-resolve

* remove early return

* test: add integration tests for BOOKING_REQUESTED webhook producer invocation

Cover all 8 scenarios verifying the booking flow correctly invokes
the webhook producer for BOOKING_REQUESTED:

1. Basic confirmation → queueBookingRequestedWebhook called
2. Booker-is-organizer + confirmation → still called
3. Confirmation threshold NOT met → not called (BOOKING_CREATED instead)
4. Confirmation threshold IS met → called
5. Paid event + confirmation → called after payment succeeds
6. Reschedule + confirmation (non-organizer) → called (not BOOKING_RESCHEDULED)
7. Reschedule + confirmation (organizer) → not called (BOOKING_RESCHEDULED instead)
8. Collective scheduling + confirmation → called

Adds reusable MockWebhookProducer helper in @calcom/testing for
extendable use as more webhook triggers migrate to the new architecture.

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* fix bug

* fix conditional check

* remove unnecessary comment

* add missing expect

* remove empty test

* clean up

* tasker config

* --

* fix missing metadata

* remove faulty if else

* test: add payload content verification tests for BOOKING_REQUESTED webhook

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* remove unnecessary tests

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Hariom Balhara <1780212+hariombalhara@users.noreply.github.com>
2026-02-19 20:21:26 +04:00

911 lines
27 KiB
TypeScript

import dayjs from "@calcom/dayjs";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { expect } from "@playwright/test";
import { v4 as uuidv4 } from "uuid";
import { test } from "./lib/fixtures";
import {
bookOptinEvent,
bookTimeSlot,
confirmReschedule,
createUserWithSeatedEventAndAttendees,
gotoRoutingLink,
selectFirstAvailableTimeSlotNextMonth,
submitAndWaitForResponse,
} from "./lib/testUtils";
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
test.afterEach(async ({ users }) => {
// This also delete forms on cascade
await users.deleteAll();
});
test.describe("BOOKING_CREATED", async () => {
test("add webhook & test that creating an event triggers a webhook call", async ({
page,
users,
webhooks,
}, _testInfo) => {
const user = await users.create();
const [eventType] = user.eventTypes;
await user.apiLogin();
const webhookReceiver = await webhooks.createReceiver();
// --- Book the first available day next month in the pro user's "30min"-event
await page.goto(`/${user.username}/${eventType.slug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await webhookReceiver.waitForRequestCount(1);
const [request] = webhookReceiver.requestList;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body: any = request.body;
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
body.payload.location = dynamic;
for (const attendee of body.payload.attendees) {
attendee.timeZone = dynamic;
attendee.language = dynamic;
}
body.payload.organizer.id = dynamic;
body.payload.organizer.email = dynamic;
body.payload.organizer.timeZone = dynamic;
body.payload.organizer.language = dynamic;
body.payload.uid = dynamic;
body.payload.bookingId = dynamic;
body.payload.additionalInformation = dynamic;
body.payload.requiresConfirmation = dynamic;
body.payload.eventTypeId = dynamic;
body.payload.videoCallData = dynamic;
body.payload.appsStatus = dynamic;
body.payload.metadata.videoCallUrl = dynamic;
expect(body).toMatchObject({
triggerEvent: "BOOKING_CREATED",
createdAt: "[redacted/dynamic]",
payload: {
type: "30-min",
title: "30 min between Nameless and Test Testson",
description: "",
additionalNotes: "",
customInputs: {},
startTime: "[redacted/dynamic]",
endTime: "[redacted/dynamic]",
organizer: {
id: "[redacted/dynamic]",
name: "Nameless",
email: "[redacted/dynamic]",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
responses: {
email: {
value: "test@example.com",
label: "email_address",
},
name: {
value: "Test Testson",
label: "your_name",
},
},
userFieldsResponses: {},
attendees: [
{
email: "test@example.com",
name: "Test Testson",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
],
location: "[redacted/dynamic]",
destinationCalendar: null,
hideCalendarNotes: false,
hideCalendarEventDetails: false,
requiresConfirmation: "[redacted/dynamic]",
eventTypeId: "[redacted/dynamic]",
seatsShowAttendees: true,
seatsPerTimeSlot: null,
uid: "[redacted/dynamic]",
eventTitle: "30 min",
eventDescription: null,
price: 0,
currency: "usd",
length: 30,
bookingId: "[redacted/dynamic]",
metadata: { videoCallUrl: "[redacted/dynamic]" },
status: "ACCEPTED",
additionalInformation: "[redacted/dynamic]",
},
});
webhookReceiver.close();
});
});
test.describe("BOOKING_REJECTED", async () => {
test("can book an event that requires confirmation and then that booking can be rejected by organizer", async ({
page,
users,
webhooks,
}) => {
// --- create a user
const user = await users.create();
// --- visit user page
await page.goto(`/${user.username}`);
// --- book the user's event
await bookOptinEvent(page);
// --- login as that user
await user.apiLogin();
const webhookReceiver = await webhooks.createReceiver();
await page.goto("/bookings/unconfirmed");
await page.click('[data-testid="reject"]');
await submitAndWaitForResponse(page, "/api/trpc/bookings/confirm?batch=1", {
action: () => page.click('[data-testid="rejection-confirm"]'),
});
await webhookReceiver.waitForRequestCount(1);
const [request] = webhookReceiver.requestList;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
body.payload.location = dynamic;
for (const attendee of body.payload.attendees) {
attendee.timeZone = dynamic;
attendee.language = dynamic;
}
body.payload.organizer.id = dynamic;
body.payload.organizer.email = dynamic;
body.payload.organizer.timeZone = dynamic;
body.payload.organizer.language = dynamic;
body.payload.uid = dynamic;
body.payload.bookingId = dynamic;
body.payload.additionalInformation = dynamic;
body.payload.requiresConfirmation = dynamic;
body.payload.eventTypeId = dynamic;
body.payload.videoCallData = dynamic;
body.payload.appsStatus = dynamic;
// body.payload.metadata.videoCallUrl = dynamic;
expect(body).toMatchObject({
triggerEvent: "BOOKING_REJECTED",
createdAt: "[redacted/dynamic]",
payload: {
type: "opt-in",
title: "Opt in between Nameless and Test Testson",
customInputs: {},
startTime: "[redacted/dynamic]",
endTime: "[redacted/dynamic]",
organizer: {
id: "[redacted/dynamic]",
name: "Nameless",
email: "[redacted/dynamic]",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
responses: {
email: {
value: "test@example.com",
label: "email",
},
name: {
value: "Test Testson",
label: "name",
},
},
userFieldsResponses: {},
attendees: [
{
email: "test@example.com",
name: "Test Testson",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
],
location: "[redacted/dynamic]",
destinationCalendar: [],
// hideCalendarNotes: false,
requiresConfirmation: "[redacted/dynamic]",
eventTypeId: "[redacted/dynamic]",
uid: "[redacted/dynamic]",
eventTitle: "Opt in",
eventDescription: null,
price: 0,
currency: "usd",
length: 30,
bookingId: "[redacted/dynamic]",
// metadata: { videoCallUrl: "[redacted/dynamic]" },
status: "REJECTED",
additionalInformation: "[redacted/dynamic]",
},
});
webhookReceiver.close();
});
});
test.describe("BOOKING_REQUESTED", async () => {
test("can book an event that requires confirmation and get a booking requested event", async ({
page,
users,
webhooks,
}) => {
// --- create a user
const user = await users.create();
// --- login as that user
await user.apiLogin();
const webhookReceiver = await webhooks.createReceiver();
// --- visit user page
await page.goto(`/${user.username}`);
// --- book the user's opt in
await bookOptinEvent(page);
// --- check that webhook was called
await webhookReceiver.waitForRequestCount(1);
const [request] = webhookReceiver.requestList;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
body.payload.location = dynamic;
for (const attendee of body.payload.attendees) {
attendee.timeZone = dynamic;
attendee.language = dynamic;
}
body.payload.organizer.id = dynamic;
body.payload.organizer.email = dynamic;
body.payload.organizer.timeZone = dynamic;
body.payload.organizer.language = dynamic;
body.payload.uid = dynamic;
body.payload.bookingId = dynamic;
body.payload.additionalInformation = dynamic;
body.payload.requiresConfirmation = dynamic;
body.payload.eventTypeId = dynamic;
body.payload.videoCallData = dynamic;
body.payload.appsStatus = dynamic;
body.payload.metadata.videoCallUrl = dynamic;
expect(body).toMatchObject({
triggerEvent: "BOOKING_REQUESTED",
createdAt: "[redacted/dynamic]",
payload: {
type: "opt-in",
title: "Opt in between Nameless and Test Testson",
customInputs: {},
startTime: "[redacted/dynamic]",
endTime: "[redacted/dynamic]",
organizer: {
id: "[redacted/dynamic]",
name: "Nameless",
email: "[redacted/dynamic]",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
responses: {
email: {
value: "test@example.com",
label: "email_address",
},
name: {
value: "Test Testson",
label: "your_name",
},
},
userFieldsResponses: {},
attendees: [
{
email: "test@example.com",
name: "Test Testson",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
],
location: "[redacted/dynamic]",
destinationCalendar: null,
requiresConfirmation: "[redacted/dynamic]",
eventTypeId: "[redacted/dynamic]",
uid: "[redacted/dynamic]",
eventTitle: "Opt in",
eventDescription: null,
price: 0,
currency: "usd",
length: 30,
bookingId: "[redacted/dynamic]",
status: "PENDING",
additionalInformation: "[redacted/dynamic]",
metadata: { videoCallUrl: "[redacted/dynamic]" },
},
});
webhookReceiver.close();
});
});
test.describe("BOOKING_RESCHEDULED", async () => {
test("can reschedule a booking and get a booking rescheduled event", async ({
page,
users,
bookings,
webhooks,
}) => {
const user = await users.create();
const [eventType] = user.eventTypes;
await user.apiLogin();
const webhookReceiver = await webhooks.createReceiver();
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 confirmReschedule(page);
await expect(page.getByTestId("success-page")).toBeVisible();
// 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 webhookReceiver.waitForRequestCount(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 existent booking uid", async ({
page,
users,
bookings,
webhooks,
}) => {
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 webhooks.createReceiver();
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 confirmReschedule(page);
await expect(page.getByTestId("success-page")).toBeVisible();
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 webhookReceiver.waitForRequestCount(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 confirmReschedule(page);
await expect(page).toHaveURL(/.*booking/);
await webhookReceiver.waitForRequestCount(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("MEETING_ENDED, MEETING_STARTED", async () => {
test("should create/remove scheduledWebhookTriggers for existing bookings", async ({
page,
users,
bookings,
}, _testInfo) => {
const user = await users.create();
await user.apiLogin();
const [eventType] = user.eventTypes;
bookings.create(user.id, user.name, eventType.id);
bookings.create(user.id, user.name, eventType.id, { startTime: dayjs().add(2, "day").toDate() });
//create a new webhook with meeting ended trigger here
await page.goto("/settings/developer/webhooks");
// --- add webhook
await page.click('[data-testid="new_webhook"]');
await page.fill('[name="subscriberUrl"]', "https://www.example.com");
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
const scheduledTriggers = await prisma.webhookScheduledTriggers.findMany({
where: {
webhook: {
userId: user.id,
},
},
select: {
payload: true,
webhook: {
select: {
userId: true,
id: true,
subscriberUrl: true,
},
},
startAfter: true,
},
});
const existingUserBookings = await prisma.booking.findMany({
where: {
userId: user.id,
startTime: {
gt: new Date(),
},
},
});
const meetingStartedTriggers = scheduledTriggers.filter((trigger) =>
trigger.payload.includes("MEETING_STARTED")
);
const meetingEndedTriggers = scheduledTriggers.filter((trigger) =>
trigger.payload.includes("MEETING_ENDED")
);
expect(meetingStartedTriggers.length).toBe(existingUserBookings.length);
expect(meetingEndedTriggers.length).toBe(existingUserBookings.length);
expect(meetingStartedTriggers.map((trigger) => trigger.startAfter)).toEqual(
expect.arrayContaining(existingUserBookings.map((booking) => booking.startTime))
);
expect(meetingEndedTriggers.map((trigger) => trigger.startAfter)).toEqual(
expect.arrayContaining(existingUserBookings.map((booking) => booking.endTime))
);
page.reload();
// edit webhook and remove trigger meeting ended trigger
await page.click('[data-testid="webhook-edit-button"]');
await page.getByRole("button", { name: "Remove Meeting Ended" }).click();
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
const scheduledTriggersAfterRemovingTrigger = await prisma.webhookScheduledTriggers.findMany({
where: {
webhook: {
userId: user.id,
},
},
});
const newMeetingStartedTriggers = scheduledTriggersAfterRemovingTrigger.filter((trigger) =>
trigger.payload.includes("MEETING_STARTED")
);
const newMeetingEndedTriggers = scheduledTriggersAfterRemovingTrigger.filter((trigger) =>
trigger.payload.includes("MEETING_ENDED")
);
expect(newMeetingStartedTriggers.length).toBe(existingUserBookings.length);
expect(newMeetingEndedTriggers.length).toBe(0);
// disable webhook
await submitAndWaitForResponse(page, "/api/trpc/webhook/edit?batch=1", {
action: () => page.getByTestId("webhook-switch").click(),
});
const scheduledTriggersAfterDisabling = await prisma.webhookScheduledTriggers.findMany({
where: {
webhook: {
userId: user.id,
},
},
select: {
payload: true,
webhook: {
select: {
userId: true,
},
},
startAfter: true,
},
});
expect(scheduledTriggersAfterDisabling.length).toBe(0);
});
});
test.describe("FORM_SUBMITTED", async () => {
test("on submitting user form, triggers user webhook", async ({ page, users, routingForms, webhooks }) => {
const user = await users.create();
await user.apiLogin();
const webhookReceiver = await webhooks.createReceiver();
const form = await routingForms.create({
name: "Test Form",
userId: user.id,
teamId: null,
fields: [
{
type: "text",
label: "Name",
identifier: "name",
required: true,
},
],
});
await gotoRoutingLink({ page, formId: form.id });
const fieldName = "name";
await page.fill(`[data-testid="form-field-${fieldName}"]`, "John Doe");
page.click('button[type="submit"]');
await webhookReceiver.waitForRequestCount(1);
const [request] = webhookReceiver.requestList;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
body.createdAt = dynamic;
expect(body).toEqual({
triggerEvent: "FORM_SUBMITTED",
createdAt: dynamic,
payload: {
formId: form.id,
formName: form.name,
teamId: null,
responses: {
name: {
value: "John Doe",
response: "John Doe",
},
},
},
name: "John Doe",
});
webhookReceiver.close();
});
test("on submitting team form, triggers team webhook", async ({ page, users, routingForms, webhooks }) => {
const user = await users.create(null, {
hasTeam: true,
});
await user.apiLogin();
const { webhookReceiver, teamId } = await webhooks.createTeamReceiver();
const form = await routingForms.create({
name: "Test Form",
userId: user.id,
teamId: teamId,
fields: [
{
type: "text",
label: "Name",
identifier: "name",
required: true,
},
{
type: "multiselect",
label: "Multi Select",
identifier: "multi",
required: true,
options: [
{
label: "Option-1",
id: "1",
},
{
label: "Option-2",
id: "2",
},
],
},
],
});
await gotoRoutingLink({ page, formId: form.id });
const textFieldIdentifier = "name";
const multiSelectFieldIdentifier = "multi";
await page.fill(`[data-testid="form-field-${textFieldIdentifier}"]`, "John Doe");
await page.click(`[data-testid="form-field-${multiSelectFieldIdentifier}"]`); // Open dropdown
await page.click("text=Option-2"); // Select option
page.click('button[type="submit"]');
await webhookReceiver.waitForRequestCount(1);
const [request] = webhookReceiver.requestList;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
body.createdAt = dynamic;
expect(body).toEqual({
triggerEvent: "FORM_SUBMITTED",
createdAt: dynamic,
payload: {
formId: form.id,
formName: form.name,
teamId,
responses: {
name: {
value: "John Doe",
response: "John Doe",
},
multi: {
value: ["Option-2"],
response: [
{
id: "2",
label: "Option-2",
},
],
},
},
},
/* Legacy fields Start */
name: "John Doe",
multi: ["Option-2"],
/* Legacy fields End */
});
webhookReceiver.close();
});
});
test.describe("OOO_CREATED", async () => {
test("on creating an OOO, triggers OOO webhook", async ({ page, users, webhooks }) => {
const user = await users.create();
await user.apiLogin();
const webhookReceiver = await webhooks.createReceiver();
await page.goto("/settings/my-account/out-of-office");
await page.getByTestId("add_entry_ooo").click();
await page.getByTestId("reason_select").click();
await page.getByTestId("select-option-4").click();
await page.getByTestId("notes_input").click();
await page.getByTestId("notes_input").fill("Demo notes");
await page.getByTestId("create-or-edit-entry-ooo-redirect").click();
await expect(page.locator(`data-testid=table-redirect-n-a`)).toBeVisible();
await webhookReceiver.waitForRequestCount(1);
const [request] = webhookReceiver.requestList;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
body.createdAt = dynamic;
body.payload.oooEntry.createdAt = dynamic;
body.payload.oooEntry.updatedAt = dynamic;
body.payload.oooEntry.end = dynamic;
body.payload.oooEntry.id = dynamic;
body.payload.oooEntry.start = dynamic;
body.payload.oooEntry.updatedAt = dynamic;
body.payload.oooEntry.user.id = dynamic;
body.payload.oooEntry.user.email = dynamic;
body.payload.oooEntry.user.username = dynamic;
body.payload.oooEntry.uuid = dynamic;
expect(body).toEqual({
createdAt: "[redacted/dynamic]",
payload: {
oooEntry: {
createdAt: "[redacted/dynamic]",
end: "[redacted/dynamic]",
id: "[redacted/dynamic]",
notes: "Demo notes",
reason: {
emoji: "🤒",
reason: "ooo_reasons_sick_leave",
},
reasonId: 4,
start: "[redacted/dynamic]",
toUser: null,
updatedAt: "[redacted/dynamic]",
user: {
id: "[redacted/dynamic]",
email: "[redacted/dynamic]",
name: null,
timeZone: "Europe/London",
username: "[redacted/dynamic]",
},
uuid: "[redacted/dynamic]",
},
},
triggerEvent: "OOO_CREATED",
});
webhookReceiver.close();
});
test("on creating an OOO inside a team, triggers OOO webhook", async ({ page, users, webhooks }) => {
const user = await users.create(null, {
hasTeam: true,
});
await user.apiLogin();
const { webhookReceiver } = await webhooks.createTeamReceiver();
await page.goto("/settings/my-account/out-of-office");
await page.getByTestId("add_entry_ooo").click();
await page.getByTestId("reason_select").click();
await page.getByTestId("select-option-4").click();
await page.getByTestId("notes_input").click();
await page.getByTestId("notes_input").fill("Demo notes");
await page.getByTestId("create-or-edit-entry-ooo-redirect").click();
await expect(page.locator(`data-testid=table-redirect-n-a`)).toBeVisible();
await webhookReceiver.waitForRequestCount(1);
const [request] = webhookReceiver.requestList;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
body.createdAt = dynamic;
body.payload.oooEntry.createdAt = dynamic;
body.payload.oooEntry.updatedAt = dynamic;
body.payload.oooEntry.end = dynamic;
body.payload.oooEntry.id = dynamic;
body.payload.oooEntry.start = dynamic;
body.payload.oooEntry.updatedAt = dynamic;
body.payload.oooEntry.user.id = dynamic;
body.payload.oooEntry.user.email = dynamic;
body.payload.oooEntry.user.username = dynamic;
body.payload.oooEntry.uuid = dynamic;
expect(body).toEqual({
triggerEvent: "OOO_CREATED",
createdAt: "[redacted/dynamic]",
payload: {
oooEntry: {
createdAt: "[redacted/dynamic]",
end: "[redacted/dynamic]",
id: "[redacted/dynamic]",
notes: "Demo notes",
reason: {
emoji: "🤒",
reason: "ooo_reasons_sick_leave",
},
reasonId: 4,
start: "[redacted/dynamic]",
toUser: null,
updatedAt: "[redacted/dynamic]",
user: {
id: "[redacted/dynamic]",
email: "[redacted/dynamic]",
name: null,
timeZone: "Europe/London",
username: "[redacted/dynamic]",
},
uuid: "[redacted/dynamic]",
},
},
});
webhookReceiver.close();
});
});
test.describe("Webhook deletion", async () => {
test("shows confirmation dialog and deletes webhook on confirm", async ({ page, users }) => {
const user = await users.create();
await user.apiLogin();
await page.goto("/settings/developer/webhooks");
await page.click('[data-testid="new_webhook"]');
await page.fill('[name="subscriberUrl"]', "https://example.com/test-webhook");
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
const webhookListItem = page.getByTestId("webhook-list-item");
await expect(webhookListItem).toBeVisible();
const deleteButton = page.getByTestId("delete-webhook");
await expect(deleteButton).toBeVisible();
await deleteButton.click();
await expect(page.getByTestId("dialog-confirmation")).toBeVisible();
const deleteResponsePromise = page.waitForResponse((res) =>
res.url().includes("/api/trpc/webhook/delete")
);
await page.getByTestId("dialog-confirmation").click();
const deleteResponse = await deleteResponsePromise;
expect(deleteResponse.ok()).toBe(true);
await expect(webhookListItem).not.toBeVisible();
});
});