* add webhook version schema * version the code * update version from numeric to date val * migration * update schema and build factory * update string * move version picker * tooltip instead of infobadge * -- * fix type * -- * fix type * fix type * -- * fix messed up merge * improvements to payloadfactory * extract version off of DB and instead keep it in IWebhookRepository * fix webhookform * fix type safety and routing ambiguity * scalable with easier factory extensions and base definition * fix types * -- * -- * clean up prisma/client type imports * fix * type fix * type fix * cleanup * add tests and registry changes * unintended file inclusion * type-fix * select in repo * -- * explicit return type * -- * fix type * fixes * feedback 1 * feedback 2 * use enum instead of string * fixes
257 lines
9.9 KiB
TypeScript
257 lines
9.9 KiB
TypeScript
import { describe, it, expect, beforeEach } from "vitest";
|
|
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
|
|
|
import type { BookingWebhookEventDTO } from "../../dto/types";
|
|
import { PayloadBuilderFactory, type PayloadBuilderSet } from "./PayloadBuilderFactory";
|
|
import * as V2021_10_20 from "./v2021-10-20";
|
|
import { WebhookVersion as WebhookVersionEnum } from "../../interface/IWebhookRepository";
|
|
|
|
describe("PayloadBuilderFactory", () => {
|
|
let factory: PayloadBuilderFactory;
|
|
let defaultBuilders: PayloadBuilderSet;
|
|
|
|
beforeEach(() => {
|
|
defaultBuilders = {
|
|
booking: new V2021_10_20.BookingPayloadBuilder(),
|
|
form: new V2021_10_20.FormPayloadBuilder(),
|
|
ooo: new V2021_10_20.OOOPayloadBuilder(),
|
|
recording: new V2021_10_20.RecordingPayloadBuilder(),
|
|
meeting: new V2021_10_20.MeetingPayloadBuilder(),
|
|
instantMeeting: new V2021_10_20.InstantMeetingBuilder(),
|
|
};
|
|
|
|
factory = new PayloadBuilderFactory(WebhookVersionEnum.V_2021_10_20, defaultBuilders);
|
|
});
|
|
|
|
describe("Constructor", () => {
|
|
it("should initialize with default version and builders", () => {
|
|
expect(factory).toBeInstanceOf(PayloadBuilderFactory);
|
|
expect(factory.getRegisteredVersions()).toContain(WebhookVersionEnum.V_2021_10_20);
|
|
});
|
|
|
|
it("should enforce required parameters at compile-time", () => {
|
|
// TypeScript ensures parameters are provided - no runtime check needed
|
|
expect(factory).toBeDefined();
|
|
expect(factory.getRegisteredVersions()).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe("Version Registration", () => {
|
|
it("should register new version with complete builder set", () => {
|
|
const newVersionBuilders: PayloadBuilderSet = {
|
|
booking: new V2021_10_20.BookingPayloadBuilder(),
|
|
form: new V2021_10_20.FormPayloadBuilder(),
|
|
ooo: new V2021_10_20.OOOPayloadBuilder(),
|
|
recording: new V2021_10_20.RecordingPayloadBuilder(),
|
|
meeting: new V2021_10_20.MeetingPayloadBuilder(),
|
|
instantMeeting: new V2021_10_20.InstantMeetingBuilder(),
|
|
};
|
|
|
|
factory.registerVersion("2024-12-01", newVersionBuilders);
|
|
|
|
expect(factory.getRegisteredVersions()).toContain("2024-12-01");
|
|
expect(factory.getRegisteredVersions()).toHaveLength(2);
|
|
});
|
|
|
|
it("should allow overwriting existing version", () => {
|
|
const newBuilders: PayloadBuilderSet = {
|
|
booking: new V2021_10_20.BookingPayloadBuilder(),
|
|
form: new V2021_10_20.FormPayloadBuilder(),
|
|
ooo: new V2021_10_20.OOOPayloadBuilder(),
|
|
recording: new V2021_10_20.RecordingPayloadBuilder(),
|
|
meeting: new V2021_10_20.MeetingPayloadBuilder(),
|
|
instantMeeting: new V2021_10_20.InstantMeetingBuilder(),
|
|
};
|
|
|
|
factory.registerVersion(WebhookVersionEnum.V_2021_10_20, newBuilders);
|
|
|
|
expect(factory.getRegisteredVersions()).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe("Builder Routing", () => {
|
|
it("should route booking events to booking builder", () => {
|
|
const bookingTriggers = [
|
|
WebhookTriggerEvents.BOOKING_CREATED,
|
|
WebhookTriggerEvents.BOOKING_CANCELLED,
|
|
WebhookTriggerEvents.BOOKING_REQUESTED,
|
|
WebhookTriggerEvents.BOOKING_RESCHEDULED,
|
|
WebhookTriggerEvents.BOOKING_REJECTED,
|
|
WebhookTriggerEvents.BOOKING_PAID,
|
|
WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED,
|
|
WebhookTriggerEvents.BOOKING_NO_SHOW_UPDATED,
|
|
];
|
|
|
|
bookingTriggers.forEach((trigger) => {
|
|
const builder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, trigger);
|
|
expect(builder).toBe(defaultBuilders.booking);
|
|
});
|
|
});
|
|
|
|
it("should route form events to form builder", () => {
|
|
const formTriggers = [
|
|
WebhookTriggerEvents.FORM_SUBMITTED,
|
|
WebhookTriggerEvents.FORM_SUBMITTED_NO_EVENT,
|
|
];
|
|
|
|
formTriggers.forEach((trigger) => {
|
|
const builder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, trigger);
|
|
expect(builder).toBe(defaultBuilders.form);
|
|
});
|
|
});
|
|
|
|
it("should route OOO events to OOO builder", () => {
|
|
const builder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.OOO_CREATED);
|
|
expect(builder).toBe(defaultBuilders.ooo);
|
|
});
|
|
|
|
it("should route recording events to recording builder", () => {
|
|
const recordingTriggers = [
|
|
WebhookTriggerEvents.RECORDING_READY,
|
|
WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED,
|
|
];
|
|
|
|
recordingTriggers.forEach((trigger) => {
|
|
const builder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, trigger);
|
|
expect(builder).toBe(defaultBuilders.recording);
|
|
});
|
|
});
|
|
|
|
it("should route meeting events to meeting builder", () => {
|
|
const meetingTriggers = [WebhookTriggerEvents.MEETING_STARTED, WebhookTriggerEvents.MEETING_ENDED];
|
|
|
|
meetingTriggers.forEach((trigger) => {
|
|
const builder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, trigger);
|
|
expect(builder).toBe(defaultBuilders.meeting);
|
|
});
|
|
});
|
|
|
|
it("should route instant meeting events to instant meeting builder", () => {
|
|
const builder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.INSTANT_MEETING);
|
|
expect(builder).toBe(defaultBuilders.instantMeeting);
|
|
});
|
|
});
|
|
|
|
describe("Fallback Behavior", () => {
|
|
it("should fallback to default version when requested version not found", () => {
|
|
const builder = factory.getBuilder(WebhookVersionEnum.V_2099_99_99, WebhookTriggerEvents.BOOKING_CREATED);
|
|
|
|
// Should get default builder, not throw
|
|
expect(builder).toBe(defaultBuilders.booking);
|
|
});
|
|
|
|
it("should log warning when falling back to default", () => {
|
|
// Just verify it doesn't throw - logging is tested elsewhere
|
|
expect(() => {
|
|
factory.getBuilder("invalid-version", WebhookTriggerEvents.BOOKING_CREATED);
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it("should never return undefined builder", () => {
|
|
const unknownVersion = WebhookVersionEnum.V_2099_99_99;
|
|
const builder = factory.getBuilder(unknownVersion, WebhookTriggerEvents.BOOKING_CREATED);
|
|
|
|
expect(builder).toBeDefined();
|
|
expect(builder).toBe(defaultBuilders.booking);
|
|
});
|
|
});
|
|
|
|
describe("Type Safety", () => {
|
|
it("should return correctly typed builder for each trigger", () => {
|
|
// These type assertions verify compile-time type safety
|
|
const bookingBuilder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.BOOKING_CREATED);
|
|
const formBuilder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.FORM_SUBMITTED);
|
|
const oooBuilder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.OOO_CREATED);
|
|
const recordingBuilder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.RECORDING_READY);
|
|
const meetingBuilder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.MEETING_STARTED);
|
|
const instantBuilder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.INSTANT_MEETING);
|
|
|
|
// Runtime verification
|
|
expect(bookingBuilder).toBeDefined();
|
|
expect(formBuilder).toBeDefined();
|
|
expect(oooBuilder).toBeDefined();
|
|
expect(recordingBuilder).toBeDefined();
|
|
expect(meetingBuilder).toBeDefined();
|
|
expect(instantBuilder).toBeDefined();
|
|
});
|
|
|
|
it("should build valid payload with correctly typed DTO", () => {
|
|
const mockDTO: BookingWebhookEventDTO = {
|
|
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
|
|
createdAt: "2024-01-15T10:00:00Z",
|
|
booking: {
|
|
id: 1,
|
|
eventTypeId: 1,
|
|
userId: 1,
|
|
smsReminderNumber: null,
|
|
},
|
|
eventType: {
|
|
eventTitle: "Test Event",
|
|
eventDescription: "Test Description",
|
|
requiresConfirmation: false,
|
|
price: 0,
|
|
currency: "USD",
|
|
length: 30,
|
|
},
|
|
evt: {
|
|
type: "test-event",
|
|
title: "Test Meeting",
|
|
description: "Meeting description",
|
|
startTime: "2024-01-15T10:00:00Z",
|
|
endTime: "2024-01-15T10:30:00Z",
|
|
organizer: {
|
|
id: 1,
|
|
email: "organizer@test.com",
|
|
name: "Test Organizer",
|
|
timeZone: "UTC",
|
|
language: { locale: "en" },
|
|
},
|
|
attendees: [
|
|
{
|
|
email: "attendee@test.com",
|
|
name: "Test Attendee",
|
|
timeZone: "UTC",
|
|
language: { locale: "en" },
|
|
},
|
|
],
|
|
uid: "booking-uid-123",
|
|
customInputs: {},
|
|
responses: {},
|
|
userFieldsResponses: {},
|
|
},
|
|
};
|
|
|
|
const builder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.BOOKING_CREATED);
|
|
const payload = builder.build(mockDTO);
|
|
|
|
expect(payload).toBeDefined();
|
|
expect(payload.triggerEvent).toBe(WebhookTriggerEvents.BOOKING_CREATED);
|
|
expect(payload.payload).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("Multiple Versions", () => {
|
|
it("should support multiple versions simultaneously", () => {
|
|
const v2Builders: PayloadBuilderSet = {
|
|
booking: new V2021_10_20.BookingPayloadBuilder(),
|
|
form: new V2021_10_20.FormPayloadBuilder(),
|
|
ooo: new V2021_10_20.OOOPayloadBuilder(),
|
|
recording: new V2021_10_20.RecordingPayloadBuilder(),
|
|
meeting: new V2021_10_20.MeetingPayloadBuilder(),
|
|
instantMeeting: new V2021_10_20.InstantMeetingBuilder(),
|
|
};
|
|
|
|
factory.registerVersion(WebhookVersionEnum.V_2024_12_01, v2Builders);
|
|
|
|
// Both versions should work
|
|
const v1Builder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.BOOKING_CREATED);
|
|
const v2Builder = factory.getBuilder(WebhookVersionEnum.V_2024_12_01, WebhookTriggerEvents.BOOKING_CREATED);
|
|
|
|
expect(v1Builder).toBe(defaultBuilders.booking);
|
|
expect(v2Builder).toBe(v2Builders.booking);
|
|
expect(v1Builder).not.toBe(v2Builder); // Different instances
|
|
});
|
|
});
|
|
});
|
|
|