Files
calendar/packages/features/webhooks/lib/tasker/WebhookTasker.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

100 lines
3.1 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from "vitest";
import type { WebhookTaskConsumer } from "../service/WebhookTaskConsumer";
import type { WebhookTaskPayload } from "../types/webhookTask";
import { WebhookSyncTasker } from "./WebhookSyncTasker";
import { WebhookTriggerTasker } from "./WebhookTriggerTasker";
vi.mock("nanoid", () => ({
nanoid: vi.fn().mockReturnValue("test123456"),
}));
vi.mock("./trigger/deliver-webhook", () => ({
deliverWebhook: {
trigger: vi.fn().mockResolvedValue({ id: "trigger-task-id-123" }),
},
}));
const createMockWebhookTaskPayload = (): WebhookTaskPayload => ({
operationId: "test-operation-id",
triggerEvent: "BOOKING_CREATED",
bookingUid: "test-booking-uid",
eventTypeId: 1,
teamId: null,
userId: 1,
timestamp: new Date().toISOString(),
});
describe("WebhookSyncTasker", () => {
let mockWebhookTaskConsumer: WebhookTaskConsumer;
let syncTasker: WebhookSyncTasker;
beforeEach(() => {
vi.clearAllMocks();
mockWebhookTaskConsumer = {
processWebhookTask: vi.fn().mockResolvedValue(undefined),
} as unknown as WebhookTaskConsumer;
syncTasker = new WebhookSyncTasker({
webhookTaskConsumer: mockWebhookTaskConsumer,
});
});
it("should execute webhook delivery immediately via consumer", async () => {
const payload = createMockWebhookTaskPayload();
const result = await syncTasker.deliverWebhook(payload);
expect(mockWebhookTaskConsumer.processWebhookTask).toHaveBeenCalledTimes(1);
expect(mockWebhookTaskConsumer.processWebhookTask).toHaveBeenCalledWith(
payload,
expect.stringMatching(/^sync_/)
);
expect(result.taskId).toMatch(/^sync_/);
});
it("should generate unique task IDs for each delivery", async () => {
const payload = createMockWebhookTaskPayload();
const result1 = await syncTasker.deliverWebhook(payload);
const result2 = await syncTasker.deliverWebhook(payload);
expect(result1.taskId).toBe("sync_test123456");
expect(result2.taskId).toBe("sync_test123456");
expect(mockWebhookTaskConsumer.processWebhookTask).toHaveBeenCalledTimes(2);
});
it("should propagate errors from consumer", async () => {
const payload = createMockWebhookTaskPayload();
const error = new Error("Consumer processing failed");
vi.mocked(mockWebhookTaskConsumer.processWebhookTask).mockRejectedValueOnce(error);
await expect(syncTasker.deliverWebhook(payload)).rejects.toThrow("Consumer processing failed");
});
});
describe("WebhookTriggerTasker", () => {
let triggerTasker: WebhookTriggerTasker;
let mockLogger: { info: ReturnType<typeof vi.fn>; error: ReturnType<typeof vi.fn> };
beforeEach(() => {
vi.clearAllMocks();
mockLogger = {
info: vi.fn(),
error: vi.fn(),
};
triggerTasker = new WebhookTriggerTasker({
logger: mockLogger as never,
});
});
it("should trigger webhook delivery via trigger.dev", async () => {
const payload = createMockWebhookTaskPayload();
const result = await triggerTasker.deliverWebhook(payload);
expect(result.taskId).toBe("trigger-task-id-123");
});
});