Files
calendar/packages/features/tasker/tasks/crm/__tests__/createCRMEvent.test.ts
T
98b6d63164 refactor: apply biome formatting to packages/features (#27844)
* 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>
2026-02-11 15:47:14 +01:00

488 lines
12 KiB
TypeScript

import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock";
import { describe, expect, it, beforeEach, vi } from "vitest";
import { RetryableError } from "@calcom/lib/crmManager/errors";
import { BookingStatus } from "@calcom/prisma/enums";
import { createCRMEvent } from "../createCRMEvent";
interface User {
id: number;
email: string;
name: string;
}
interface EventTypeMetadata {
apps: {
salesforce: {
enabled: boolean;
credentialId: number;
appCategories: string[];
};
};
}
interface Booking {
id: number;
uid: string;
status: BookingStatus;
title: string;
startTime: Date;
endTime: Date;
user: User;
eventType: {
metadata: EventTypeMetadata;
};
}
interface CRMCredential {
id: number;
type: string;
key: {
access_token?: string;
refresh_token?: string;
expiry_date?: number;
key1?: string;
};
userId: number;
}
interface CRMManagerInterface {
createEvent: (event: unknown) => Promise<{ id: string }>;
}
vi.mock("../lib/buildCalendarEvent", () => ({
default: vi.fn().mockResolvedValue({
title: "Test Event",
description: "Test Description",
startTime: new Date(),
endTime: new Date(),
}),
}));
const mockCreateEvent = vi.fn().mockResolvedValue({ id: "sf-event-123" });
vi.mock("@calcom/features/crmManager/crmManager", () => ({
default: class MockCrmManager {
private credential: CRMCredential;
constructor(credential: CRMCredential) {
this.credential = credential;
}
createEvent = mockCreateEvent;
getManager(): CRMManagerInterface {
return {
createEvent: this.createEvent,
};
}
},
}));
describe("createCRMEvent", () => {
beforeEach(() => {
vi.clearAllMocks();
mockCreateEvent.mockClear();
// Clear Prisma mocks
prismaMock.booking.findUnique.mockReset();
prismaMock.credential.findUnique.mockReset();
prismaMock.bookingReference.createMany.mockReset();
prismaMock.bookingReference.findMany.mockReset();
});
it("should successfully create a Salesforce CRM event", async () => {
const mockBooking: Booking = {
id: 1,
uid: "booking-123",
status: BookingStatus.ACCEPTED,
title: "Test Booking",
startTime: new Date(),
endTime: new Date(),
user: {
id: 1,
email: "test@example.com",
name: "Test User",
},
eventType: {
metadata: {
apps: {
salesforce: {
enabled: true,
credentialId: 1,
appCategories: ["crm"],
},
},
},
},
};
const mockCredential: CRMCredential = {
id: 1,
type: "salesforce_crm",
key: {
access_token: "mock_token",
refresh_token: "mock_refresh",
expiry_date: new Date().getTime() + 3600000,
},
userId: 1,
};
// Set up Prisma mocks with proper return values
prismaMock.booking.findUnique.mockResolvedValueOnce(mockBooking);
prismaMock.credential.findUnique.mockResolvedValueOnce(mockCredential);
prismaMock.credential.findMany.mockResolvedValueOnce([mockCredential]);
prismaMock.bookingReference.createMany.mockResolvedValueOnce({ count: 1 });
prismaMock.bookingReference.findMany.mockResolvedValueOnce([]);
const payload = JSON.stringify({
bookingUid: "booking-123",
});
await createCRMEvent(payload);
expect(mockCreateEvent).toHaveBeenCalled();
expect(prismaMock.bookingReference.createMany).toHaveBeenCalledWith({
data: [
{
type: "salesforce_crm",
uid: "sf-event-123",
meetingId: "sf-event-123",
credentialId: 1,
bookingId: 1,
},
],
});
});
it("should throw error for invalid payload", async () => {
const invalidPayload = JSON.stringify({
invalidField: "test",
});
await expect(createCRMEvent(invalidPayload)).rejects.toThrow("malformed payload in createCRMEvent");
});
it("should throw error when booking is not found", async () => {
prismaMock.booking.findUnique.mockResolvedValue(null);
const payload = JSON.stringify({
bookingUid: "non-existent-booking",
});
await expect(createCRMEvent(payload)).rejects.toThrow("booking not found");
});
it("should handle case when Salesforce CRM is not enabled", async () => {
const mockBooking: Booking = {
id: 1,
uid: "booking-123",
status: BookingStatus.ACCEPTED,
title: "Test Booking",
startTime: new Date(),
endTime: new Date(),
user: {
id: 1,
email: "test@example.com",
name: "Test User",
},
eventType: {
metadata: {
apps: {
salesforce: {
enabled: false,
credentialId: 1,
appCategories: ["crm"],
},
},
},
},
};
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
prismaMock.credential.findMany.mockResolvedValueOnce([]);
const payload = JSON.stringify({
bookingUid: "booking-123",
});
await createCRMEvent(payload);
expect(prismaMock.bookingReference.createMany).toHaveBeenCalledWith({
data: [],
});
});
it("should not throw if there is 'Credential not found' error", async () => {
const mockBooking: Booking = {
id: 1,
uid: "booking-123",
status: BookingStatus.ACCEPTED,
title: "Test Booking",
startTime: new Date(),
endTime: new Date(),
user: {
id: 1,
email: "test@example.com",
name: "Test User",
},
eventType: {
metadata: {
apps: {
salesforce: {
enabled: true,
credentialId: 1,
appCategories: ["crm"],
},
},
},
},
};
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
prismaMock.credential.findUnique.mockResolvedValue(null);
prismaMock.credential.findMany.mockResolvedValueOnce([]);
prismaMock.bookingReference.findMany.mockResolvedValueOnce([]);
mockCreateEvent.mockRejectedValue(new Error("Salesforce API error"));
const payload = JSON.stringify({
bookingUid: "booking-123",
});
await createCRMEvent(payload);
expect(prismaMock.bookingReference.createMany).toHaveBeenCalledWith({
data: [],
});
});
it("should throw(and thus retry) if there is RetryableError from `createEvent`", async () => {
const mockBooking: Booking = {
id: 1,
uid: "booking-123",
status: BookingStatus.ACCEPTED,
title: "Test Booking",
startTime: new Date(),
endTime: new Date(),
user: {
id: 1,
email: "test@example.com",
name: "Test User",
},
eventType: {
metadata: {
apps: {
salesforce: {
enabled: true,
credentialId: 1,
appCategories: ["crm"],
},
},
},
},
};
const mockCredential: CRMCredential = {
id: 1,
type: "salesforce_crm",
key: { key1: "value1" },
userId: 1,
};
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
prismaMock.credential.findUnique.mockResolvedValue(mockCredential);
prismaMock.credential.findMany.mockResolvedValueOnce([mockCredential]);
prismaMock.bookingReference.findMany.mockResolvedValueOnce([]);
mockCreateEvent.mockRejectedValue(new RetryableError("Salesforce API Retryable error"));
const payload = JSON.stringify({
bookingUid: "booking-123",
});
await expect(createCRMEvent(payload)).rejects.toThrow("Salesforce API Retryable error");
});
it("should not throw(and thus not retry) if there is non-RetryableError from `createEvent`", async () => {
const mockBooking: Booking = {
id: 1,
uid: "booking-123",
status: BookingStatus.ACCEPTED,
title: "Test Booking",
startTime: new Date(),
endTime: new Date(),
user: {
id: 1,
email: "test@example.com",
name: "Test User",
},
eventType: {
metadata: {
apps: {
salesforce: {
enabled: true,
credentialId: 1,
appCategories: ["crm"],
},
},
},
},
};
const mockCredential: CRMCredential = {
id: 1,
type: "salesforce_crm",
key: { key1: "value1" },
userId: 1,
};
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
prismaMock.credential.findUnique.mockResolvedValue(mockCredential);
prismaMock.credential.findMany.mockResolvedValueOnce([mockCredential]);
prismaMock.bookingReference.findMany.mockResolvedValueOnce([]);
mockCreateEvent.mockRejectedValue(new Error("Salesforce API error"));
const payload = JSON.stringify({
bookingUid: "booking-123",
});
await createCRMEvent(payload);
expect(prismaMock.bookingReference.createMany).toHaveBeenCalledWith({
data: [],
});
});
it("should process second app evenif first app throws error in processing", async () => {
const mockBooking: Booking = {
id: 1,
uid: "booking-123",
status: BookingStatus.ACCEPTED,
title: "Test Booking",
startTime: new Date(),
endTime: new Date(),
user: {
id: 1,
email: "test@example.com",
name: "Test User",
},
eventType: {
metadata: {
apps: {
salesforce: {
enabled: true,
credentialId: 1,
appCategories: ["crm"],
},
hubspot: {
enabled: true,
credentialId: 2,
appCategories: ["crm"],
},
},
},
},
};
const mockSalesforceCredential: CRMCredential = {
id: 1,
type: "salesforce_crm",
key: { key1: "value1" },
userId: 1,
};
const mockHubspotCredential: CRMCredential = {
id: 2,
type: "hubspot",
key: { key1: "value1" },
userId: 1,
};
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
// First call returns Salesforce credential and second call returns Hubspot credential
prismaMock.credential.findUnique
.mockResolvedValueOnce(mockSalesforceCredential)
.mockResolvedValueOnce(mockHubspotCredential);
prismaMock.credential.findMany.mockResolvedValueOnce([mockSalesforceCredential, mockHubspotCredential]);
prismaMock.bookingReference.findMany.mockResolvedValueOnce([]);
// Throw error for first app and resolve for second app
mockCreateEvent
.mockRejectedValueOnce(new Error("Salesforce API error"))
.mockResolvedValueOnce({ id: "hubspot-event-123" });
const payload = JSON.stringify({
bookingUid: "booking-123",
});
await createCRMEvent(payload);
expect(prismaMock.bookingReference.createMany).toHaveBeenCalledWith({
data: [
{
type: "hubspot",
uid: "hubspot-event-123",
meetingId: "hubspot-event-123",
credentialId: 2,
bookingId: 1,
},
],
});
});
it("should skip a credential from creatingEvent if bookingReference already exists(Helpful in case of retry)", async () => {
const mockBooking: Booking = {
id: 1,
uid: "booking-123",
status: BookingStatus.ACCEPTED,
title: "Test Booking",
startTime: new Date(),
endTime: new Date(),
user: {
id: 1,
email: "test@example.com",
name: "Test User",
},
eventType: {
metadata: {
apps: {
salesforce: {
enabled: true,
credentialId: 1,
appCategories: ["crm"],
},
},
},
},
};
const mockSalesforceCredential: CRMCredential = {
id: 1,
type: "salesforce_crm",
key: { key1: "value1" },
userId: 1,
};
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
prismaMock.credential.findUnique.mockResolvedValueOnce(mockSalesforceCredential);
prismaMock.credential.findMany.mockResolvedValueOnce([mockSalesforceCredential]);
prismaMock.bookingReference.findMany.mockResolvedValueOnce([
{ id: 1, type: "salesforce_crm", uid: "sf-event-123", credentialId: 1, bookingId: 1 },
]);
const payload = JSON.stringify({
bookingUid: "booking-123",
});
await createCRMEvent(payload);
expect(prismaMock.bookingReference.createMany).toHaveBeenCalledWith({
data: [],
});
});
});