* refactor: apply biome formatting to packages/features (batch 1 - small subdirs) Format small subdirectories in packages/features: di, flags, holidays, oauth, settings, users, assignment-reason, selectedCalendar, hashedLink, host, form, form-builder, availability, data-table, pbac, schedules, troubleshooter, eventtypes, calendar-subscription, and root-level files. Also includes straggler apps/web BookEventForm.tsx. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: apply biome formatting to packages/features (batch 2 - medium subdirs) Format medium subdirectories in packages/features: auth, credentials, calendars, routing-forms, routing-trace, attributes, watchlist, calAIPhone, tasker, and webhooks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: apply biome formatting to packages/features (batch 3 - bookings + insights) Format bookings and insights subdirectories in packages/features. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: apply biome formatting to packages/features (batch 4 - ee) Format packages/features/ee subdirectory covering billing, workflows, organizations, teams, managed-event-types, round-robin, dsync, integration-attribute-sync, and payments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: apply biome formatting to packages/features (batch 5 - booking-audit part 1) Format booking-audit di, actions, common, dto, repository, and types subdirectories in packages/features/booking-audit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: apply biome formatting to packages/features (batch 6 - booking-audit part 2) Format booking-audit service subdirectory in packages/features/booking-audit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
315 lines
9.8 KiB
TypeScript
315 lines
9.8 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
|
|
|
import type { BookingWebhookEventDTO, WebhookEventDTO, WebhookSubscriber } from "../dto/types";
|
|
import { WebhookVersion } from "../interface/IWebhookRepository";
|
|
import type { PayloadBuilderFactory } from "../factory/versioned/PayloadBuilderFactory";
|
|
import type { ILogger } from "../interface/infrastructure";
|
|
import type { IWebhookService } from "../interface/services";
|
|
import { WebhookNotificationHandler } from "./WebhookNotificationHandler";
|
|
|
|
describe("WebhookNotificationHandler", () => {
|
|
let handler: WebhookNotificationHandler;
|
|
let mockWebhookService: IWebhookService;
|
|
let mockFactory: PayloadBuilderFactory;
|
|
let mockLogger: ILogger;
|
|
|
|
const mockSubscribers: WebhookSubscriber[] = [
|
|
{
|
|
id: "webhook-1",
|
|
subscriberUrl: "https://example.com/webhook",
|
|
payloadTemplate: null,
|
|
appId: null,
|
|
secret: "secret123",
|
|
eventTriggers: [WebhookTriggerEvents.BOOKING_CREATED],
|
|
time: null,
|
|
timeUnit: null,
|
|
version: WebhookVersion.V_2021_10_20,
|
|
},
|
|
{
|
|
id: "webhook-2",
|
|
subscriberUrl: "https://example2.com/webhook",
|
|
payloadTemplate: null,
|
|
appId: null,
|
|
secret: "secret456",
|
|
eventTriggers: [WebhookTriggerEvents.BOOKING_CREATED],
|
|
time: null,
|
|
timeUnit: null,
|
|
version: WebhookVersion.V_2021_10_20,
|
|
},
|
|
];
|
|
|
|
beforeEach(() => {
|
|
// Mock webhook service
|
|
mockWebhookService = {
|
|
getSubscribers: vi.fn().mockResolvedValue(mockSubscribers),
|
|
processWebhooks: vi.fn().mockResolvedValue(undefined),
|
|
sendWebhook: vi.fn(),
|
|
scheduleWebhook: vi.fn(),
|
|
sendWebhookDirectly: vi.fn(),
|
|
};
|
|
|
|
// Mock factory - returns appropriate payload based on trigger event
|
|
mockFactory = {
|
|
getBuilder: vi.fn().mockImplementation((_version: string, triggerEvent: WebhookTriggerEvents) => ({
|
|
build: vi.fn().mockImplementation((dto: WebhookEventDTO) => ({
|
|
triggerEvent,
|
|
createdAt: dto.createdAt,
|
|
payload: dto,
|
|
})),
|
|
})),
|
|
registerVersion: vi.fn(),
|
|
getRegisteredVersions: vi.fn().mockReturnValue([WebhookVersion.V_2021_10_20]),
|
|
};
|
|
|
|
// Mock logger
|
|
mockLogger = {
|
|
getSubLogger: vi.fn().mockReturnThis(),
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
};
|
|
|
|
handler = new WebhookNotificationHandler(mockWebhookService, mockFactory, mockLogger);
|
|
});
|
|
|
|
describe("Constructor", () => {
|
|
it("should initialize with dependencies", () => {
|
|
expect(handler).toBeInstanceOf(WebhookNotificationHandler);
|
|
});
|
|
|
|
it("should create sublogger", () => {
|
|
expect(mockLogger.getSubLogger).toHaveBeenCalledWith({
|
|
prefix: ["[WebhookNotificationHandler]"],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("handleNotification", () => {
|
|
const mockDTO: BookingWebhookEventDTO = {
|
|
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
|
|
createdAt: "2024-01-15T10:00:00Z",
|
|
bookingId: 1,
|
|
eventTypeId: 1,
|
|
userId: 1,
|
|
teamId: null,
|
|
orgId: null,
|
|
platformClientId: null,
|
|
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: {},
|
|
},
|
|
};
|
|
|
|
it("should query for subscribers with correct params", async () => {
|
|
await handler.handleNotification(mockDTO);
|
|
|
|
expect(mockWebhookService.getSubscribers).toHaveBeenCalledWith({
|
|
userId: 1,
|
|
eventTypeId: 1,
|
|
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
|
|
teamId: null,
|
|
orgId: null,
|
|
oAuthClientId: null,
|
|
});
|
|
});
|
|
|
|
it("should use factory to build payload", async () => {
|
|
await handler.handleNotification(mockDTO);
|
|
|
|
expect(mockFactory.getBuilder).toHaveBeenCalledWith(
|
|
WebhookVersion.V_2021_10_20,
|
|
WebhookTriggerEvents.BOOKING_CREATED
|
|
);
|
|
});
|
|
|
|
it("should process webhooks with built payload", async () => {
|
|
await handler.handleNotification(mockDTO);
|
|
|
|
expect(mockWebhookService.processWebhooks).toHaveBeenCalledWith(
|
|
WebhookTriggerEvents.BOOKING_CREATED,
|
|
expect.objectContaining({
|
|
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
|
|
payload: expect.any(Object),
|
|
}),
|
|
mockSubscribers
|
|
);
|
|
});
|
|
|
|
it("should log debug messages", async () => {
|
|
await handler.handleNotification(mockDTO);
|
|
|
|
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
"Querying for webhook subscribers with params:",
|
|
expect.any(Object)
|
|
);
|
|
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
expect.stringContaining("Successfully processed webhook notification"),
|
|
expect.any(Object)
|
|
);
|
|
});
|
|
|
|
it("should return early when no subscribers found", async () => {
|
|
mockWebhookService.getSubscribers.mockResolvedValue([]);
|
|
|
|
await handler.handleNotification(mockDTO);
|
|
|
|
expect(mockWebhookService.processWebhooks).not.toHaveBeenCalled();
|
|
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
expect.stringContaining("No subscribers found"),
|
|
expect.any(Object)
|
|
);
|
|
});
|
|
|
|
it("should handle errors gracefully", async () => {
|
|
const error = new Error("Test error");
|
|
mockWebhookService.getSubscribers.mockRejectedValue(error);
|
|
|
|
await expect(handler.handleNotification(mockDTO)).rejects.toThrow("Test error");
|
|
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
expect.stringContaining("Error handling webhook notification"),
|
|
expect.objectContaining({
|
|
error: "Test error",
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should support dry run mode", async () => {
|
|
await handler.handleNotification(mockDTO, true);
|
|
|
|
expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining("Dry run mode"));
|
|
expect(mockWebhookService.getSubscribers).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("Special Event Handling", () => {
|
|
it("should handle AFTER_HOSTS_CAL_VIDEO_NO_SHOW through factory", async () => {
|
|
const noShowDTO: WebhookEventDTO = {
|
|
triggerEvent: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
|
createdAt: "2024-01-15T10:00:00Z",
|
|
bookingId: 1,
|
|
webhook: { id: "webhook-1" },
|
|
};
|
|
|
|
mockWebhookService.getSubscribers.mockResolvedValue(mockSubscribers);
|
|
|
|
await handler.handleNotification(noShowDTO);
|
|
|
|
// All events now go through the factory for consistent versioning
|
|
expect(mockFactory.getBuilder).toHaveBeenCalledWith(
|
|
WebhookVersion.V_2021_10_20,
|
|
WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW
|
|
);
|
|
expect(mockWebhookService.processWebhooks).toHaveBeenCalledWith(
|
|
WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
|
{
|
|
triggerEvent: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
|
createdAt: noShowDTO.createdAt,
|
|
payload: noShowDTO,
|
|
},
|
|
mockSubscribers
|
|
);
|
|
});
|
|
|
|
it("should handle DELEGATION_CREDENTIAL_ERROR through factory", async () => {
|
|
const errorDTO: WebhookEventDTO = {
|
|
triggerEvent: WebhookTriggerEvents.DELEGATION_CREDENTIAL_ERROR,
|
|
createdAt: "2024-01-15T10:00:00Z",
|
|
error: "Credential error",
|
|
credential: { id: 1, type: "test" },
|
|
user: { id: 1, email: "user@test.com" },
|
|
};
|
|
|
|
mockWebhookService.getSubscribers.mockResolvedValue(mockSubscribers);
|
|
|
|
await handler.handleNotification(errorDTO);
|
|
|
|
// All events now go through the factory for consistent versioning
|
|
expect(mockFactory.getBuilder).toHaveBeenCalledWith(
|
|
WebhookVersion.V_2021_10_20,
|
|
WebhookTriggerEvents.DELEGATION_CREDENTIAL_ERROR
|
|
);
|
|
expect(mockWebhookService.processWebhooks).toHaveBeenCalledWith(
|
|
WebhookTriggerEvents.DELEGATION_CREDENTIAL_ERROR,
|
|
{
|
|
triggerEvent: WebhookTriggerEvents.DELEGATION_CREDENTIAL_ERROR,
|
|
createdAt: errorDTO.createdAt,
|
|
payload: errorDTO,
|
|
},
|
|
mockSubscribers
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Version Handling", () => {
|
|
it("should use default version (v2021-10-20) for all events currently", async () => {
|
|
const dto: BookingWebhookEventDTO = {
|
|
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
|
|
createdAt: "2024-01-15T10:00:00Z",
|
|
booking: {
|
|
id: 1,
|
|
eventTypeId: 1,
|
|
userId: 1,
|
|
smsReminderNumber: null,
|
|
},
|
|
eventType: {
|
|
eventTitle: "Test",
|
|
eventDescription: "",
|
|
requiresConfirmation: false,
|
|
price: 0,
|
|
currency: "USD",
|
|
length: 30,
|
|
},
|
|
evt: {},
|
|
};
|
|
|
|
mockWebhookService.getSubscribers.mockResolvedValue(mockSubscribers);
|
|
|
|
await handler.handleNotification(dto);
|
|
|
|
expect(mockFactory.getBuilder).toHaveBeenCalledWith(
|
|
WebhookVersion.V_2021_10_20,
|
|
WebhookTriggerEvents.BOOKING_CREATED
|
|
);
|
|
});
|
|
});
|
|
});
|