Files
calendar/packages/features/webhooks/lib/service/__tests__/WebhookTaskerProducerService.test.ts
T
Syed Ali ShahbazGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2ec9e903ce feat: implement webhook tasker with async/sync fallback (#27378)
* feat: implement webhook tasker with async/sync fallback

This PR implements a webhook tasker with async/sync fallback architecture
to fix failing E2E tests. The solution follows the existing proration
tasker pattern and uses Dependency Injection with @evyweb/ioctopus.

Key changes:
- Create IWebhookTasker interface for webhook delivery
- Implement WebhookSyncTasker for immediate execution (E2E tests)
- Implement WebhookAsyncTasker for queued execution (production)
- Create main WebhookTasker class extending Tasker<IWebhookTasker>
- Add DI modules and tokens for all tasker components
- Update WebhookTaskerProducerService to use new WebhookTasker
- Add unit tests for sync and async taskers

The ENABLE_ASYNC_TASKER flag automatically selects the appropriate mode:
- Production: Uses WebhookAsyncTasker to queue tasks
- E2E Tests: Uses WebhookSyncTasker for immediate execution

This ensures webhooks are delivered immediately in E2E tests without
requiring the cron job that processes queued tasks.

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

* fix: update WebhookTaskerProducerService tests for new interface

Update tests to use the new deps-based constructor and
mockWebhookTasker.deliverWebhook instead of mockTasker.create

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

* fix: use moduleLoader pattern for WebhookProducerService in container

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

* refactor: replace InternalTasker with Trigger.dev for webhook delivery

This commit refactors the WebhookTasker to use Trigger.dev instead of
InternalTasker, following the pattern established in BookingEmailAndSmsTasker
and PlatformOrganizationBillingTasker (PR #26803).

Changes:
- Replace WebhookAsyncTasker with WebhookTriggerTasker that uses trigger.dev
- Create trigger.dev task files (deliver-webhook.ts, config.ts, schema.ts)
- Update DI modules to use WebhookTriggerTasker
- Remove old InternalTasker-based implementation
- Update unit tests for new implementation

The WebhookSyncTasker continues to execute webhooks immediately for E2E tests
where ENABLE_ASYNC_TASKER is automatically false.

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

* fix: update comments to reflect Trigger.dev usage instead of InternalTasker

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-29 15:30:07 +04:00

274 lines
8.9 KiB
TypeScript

import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ILogger } from "../../interface/infrastructure";
import type { WebhookTasker } from "../../tasker/WebhookTasker";
import { WebhookTaskerProducerService } from "../WebhookTaskerProducerService";
/**
* Unit Tests for WebhookTaskerProducerService
*
* Tests the lightweight Producer service for queueing webhook delivery tasks.
*/
describe("WebhookTaskerProducerService", () => {
let producer: WebhookTaskerProducerService;
let mockWebhookTasker: WebhookTasker;
let mockLogger: ILogger;
beforeEach(() => {
// Mock WebhookTasker
mockWebhookTasker = {
deliverWebhook: vi.fn().mockResolvedValue({ taskId: "task-id-123" }),
} as unknown as WebhookTasker;
// Mock Logger
mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
getSubLogger: vi.fn().mockReturnThis(),
} as unknown as ILogger;
producer = new WebhookTaskerProducerService({
webhookTasker: mockWebhookTasker,
logger: mockLogger,
});
});
describe("Constructor & Dependencies", () => {
it("should be instantiable with Tasker and Logger", () => {
expect(producer).toBeInstanceOf(WebhookTaskerProducerService);
});
it("should create sub-logger with prefix", () => {
expect(mockLogger.getSubLogger).toHaveBeenCalledWith({
prefix: ["[WebhookTaskerProducerService]"],
});
});
});
describe("queueBookingCreatedWebhook", () => {
it("should queue a BOOKING_CREATED webhook task", async () => {
await producer.queueBookingCreatedWebhook({
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
bookingUid: "booking-123",
eventTypeId: 456,
userId: 789,
});
expect(mockWebhookTasker.deliverWebhook).toHaveBeenCalledWith(
expect.objectContaining({
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
bookingUid: "booking-123",
eventTypeId: 456,
userId: 789,
operationId: expect.any(String),
timestamp: expect.any(String),
})
);
});
it("should generate operationId if not provided", async () => {
await producer.queueBookingCreatedWebhook({
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
bookingUid: "booking-123",
});
const callArgs = vi.mocked(mockWebhookTasker.deliverWebhook).mock.calls[0];
const payload = callArgs[0];
expect(payload).toHaveProperty("operationId");
expect(payload.operationId).toMatch(/^[0-9a-f-]{36}$/); // UUID format
});
it("should use provided operationId", async () => {
await producer.queueBookingCreatedWebhook({
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
bookingUid: "booking-123",
operationId: "custom-op-id",
});
const callArgs = vi.mocked(mockWebhookTasker.deliverWebhook).mock.calls[0];
const payload = callArgs[0];
expect(payload.operationId).toBe("custom-op-id");
});
it("should log debug messages", async () => {
await producer.queueBookingCreatedWebhook({
bookingUid: "booking-123",
});
expect(mockLogger.debug).toHaveBeenCalledWith(
"Queueing booking webhook task",
expect.objectContaining({
operationId: expect.any(String),
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
bookingUid: "booking-123",
})
);
expect(mockLogger.debug).toHaveBeenCalledWith(
"Webhook delivery task queued",
expect.objectContaining({
operationId: expect.any(String),
taskId: "task-id-123",
})
);
});
});
describe("queueBookingCancelledWebhook", () => {
it("should queue a BOOKING_CANCELLED webhook task", async () => {
await producer.queueBookingCancelledWebhook({
bookingUid: "booking-456",
});
expect(mockWebhookTasker.deliverWebhook).toHaveBeenCalledWith(
expect.objectContaining({
triggerEvent: WebhookTriggerEvents.BOOKING_CANCELLED,
bookingUid: "booking-456",
})
);
});
});
describe("Metadata Support", () => {
it("should include metadata if provided", async () => {
await producer.queueBookingCreatedWebhook({
bookingUid: "booking-123",
metadata: { customField: "value" },
});
const callArgs = vi.mocked(mockWebhookTasker.deliverWebhook).mock.calls[0];
const payload = callArgs[0];
expect(payload.metadata).toEqual({ customField: "value" });
});
});
describe("Error Handling", () => {
it("should log and rethrow error if WebhookTasker fails", async () => {
const error = new Error("WebhookTasker failed");
vi.mocked(mockWebhookTasker.deliverWebhook).mockRejectedValueOnce(error);
await expect(
producer.queueBookingCreatedWebhook({
bookingUid: "booking-123",
})
).rejects.toThrow("WebhookTasker failed");
expect(mockLogger.error).toHaveBeenCalledWith(
"Failed to queue webhook delivery task",
expect.objectContaining({
error: "WebhookTasker failed",
})
);
});
});
describe("All Event-Specific Methods", () => {
it("should have queueBookingCreatedWebhook", async () => {
await producer.queueBookingCreatedWebhook({
bookingUid: "test-123",
});
expect(mockWebhookTasker.deliverWebhook).toHaveBeenCalledWith(
expect.objectContaining({ triggerEvent: WebhookTriggerEvents.BOOKING_CREATED })
);
});
it("should have queueBookingCancelledWebhook", async () => {
await producer.queueBookingCancelledWebhook({
bookingUid: "test-123",
});
expect(mockWebhookTasker.deliverWebhook).toHaveBeenCalledWith(
expect.objectContaining({ triggerEvent: WebhookTriggerEvents.BOOKING_CANCELLED })
);
});
it("should have queueBookingRescheduledWebhook", async () => {
await producer.queueBookingRescheduledWebhook({
bookingUid: "test-123",
});
expect(mockWebhookTasker.deliverWebhook).toHaveBeenCalledWith(
expect.objectContaining({ triggerEvent: WebhookTriggerEvents.BOOKING_RESCHEDULED })
);
});
it("should have queueBookingRequestedWebhook", async () => {
await producer.queueBookingRequestedWebhook({
bookingUid: "test-123",
});
expect(mockWebhookTasker.deliverWebhook).toHaveBeenCalledWith(
expect.objectContaining({ triggerEvent: WebhookTriggerEvents.BOOKING_REQUESTED })
);
});
it("should have queueBookingRejectedWebhook", async () => {
await producer.queueBookingRejectedWebhook({
bookingUid: "test-123",
});
expect(mockWebhookTasker.deliverWebhook).toHaveBeenCalledWith(
expect.objectContaining({ triggerEvent: WebhookTriggerEvents.BOOKING_REJECTED })
);
});
it("should have queueBookingPaymentInitiatedWebhook", async () => {
await producer.queueBookingPaymentInitiatedWebhook({
bookingUid: "test-123",
});
expect(mockWebhookTasker.deliverWebhook).toHaveBeenCalledWith(
expect.objectContaining({ triggerEvent: WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED })
);
});
it("should have queueBookingPaidWebhook", async () => {
await producer.queueBookingPaidWebhook({
bookingUid: "test-123",
});
expect(mockWebhookTasker.deliverWebhook).toHaveBeenCalledWith(
expect.objectContaining({ triggerEvent: WebhookTriggerEvents.BOOKING_PAID })
);
});
it("should have queueBookingNoShowUpdatedWebhook", async () => {
await producer.queueBookingNoShowUpdatedWebhook({
bookingUid: "test-123",
});
expect(mockWebhookTasker.deliverWebhook).toHaveBeenCalledWith(
expect.objectContaining({ triggerEvent: WebhookTriggerEvents.BOOKING_NO_SHOW_UPDATED })
);
});
it("should have queueFormSubmittedWebhook", async () => {
await producer.queueFormSubmittedWebhook({
formId: "form-123",
});
expect(mockWebhookTasker.deliverWebhook).toHaveBeenCalledWith(
expect.objectContaining({ triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED })
);
});
it("should have queueRecordingReadyWebhook", async () => {
await producer.queueRecordingReadyWebhook({
recordingId: "rec-123",
bookingUid: "booking-123",
});
expect(mockWebhookTasker.deliverWebhook).toHaveBeenCalledWith(
expect.objectContaining({ triggerEvent: WebhookTriggerEvents.RECORDING_READY })
);
});
it("should have queueOOOCreatedWebhook", async () => {
await producer.queueOOOCreatedWebhook({
oooEntryId: 123,
userId: 456,
});
expect(mockWebhookTasker.deliverWebhook).toHaveBeenCalledWith(
expect.objectContaining({ triggerEvent: WebhookTriggerEvents.OOO_CREATED })
);
});
});
});