Files
cal.diy/packages/features/bookings/lib/service/InstantBookingCreateService.test.ts
T
Sahitya Chandra dcbb417aec fix: use Proxy-based CalendarServiceMap mock to prevent test flakes (#28787)
* fix: add vi.mock calls to prevent InstantBookingCreateService test flake

Add two vi.mock() calls ported from cal repo to prevent
'Closing rpc while fetch was pending' errors:

1. Mock @calcom/app-store/calendar.services.generated with a Proxy
   to prevent real calendar modules (feishu, lark) from triggering
   async getAppAccessToken fetch calls during test worker shutdown.

2. Mock @calcom/features/ee/organizations/di/OrganizationRepository.container
   to prevent deep transitive import chain from triggering a Vitest
   module-resolution RPC still in flight at shutdown.

Also removes the narrow feishucalendar mock which is now covered by
the broader calendar.services.generated Proxy mock.

* fix: update bookingScenario to use Proxy-based CalendarServiceMap

Replace the plain-object CalendarServiceMap mock with a Proxy-based
implementation that catches ALL calendar service accesses. This prevents
real calendar modules (feishu, lark, etc.) from being imported during
tests, which was causing 'Closing rpc while fetch was pending' errors
when the Vitest worker shut down.

Also update mockCalendar to use a shared mock map directly instead of
dynamically importing calendar.services.generated, avoiding loading
real calendar service modules during test execution.
2026-04-08 13:17:30 +05:30

375 lines
12 KiB
TypeScript

import prismock from "@calcom/testing/lib/__mocks__/prisma";
import {
createBookingScenario,
getGoogleCalendarCredential,
getOrganizer,
getScenarioData,
mockCalendarToHaveNoBusySlots,
mockNoTranslations,
mockSuccessfulVideoMeetingCreation,
TestData,
} from "@calcom/testing/lib/bookingScenario/bookingScenario";
import { BookingStatus } from "@calcom/prisma/enums";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getInstantBookingCreateService } from "../../di/InstantBookingCreateService.container";
import type { CreateInstantBookingData } from "../dto/types";
// Mock calendar services map to prevent real calendar service modules (feishu, lark, etc.) from being
// imported. Their top-level imports trigger async fetch calls (getAppAccessToken) that cause
// "Closing rpc while fetch was pending" errors when the test worker shuts down.
// This vi.mock must be in the test file itself (not just in bookingScenario.ts) to guarantee
// Vitest hoists it before any transitive imports resolve the real module.
vi.mock("@calcom/app-store/calendar.services.generated", () => ({
CalendarServiceMap: new Proxy(
{},
{
get(_target: Record<string, unknown>, prop: string) {
if (typeof prop === "symbol") return undefined;
return Promise.resolve({ default: vi.fn() });
},
}
),
}));
// Mock OrganizationRepository container to prevent a deep transitive import chain
// (InstantBookingCreateService → getBookingFields → workflows/types → routing-forms →
// webhooks DI → eventTypes → OrganizationRepository.container) from triggering a Vitest
// module-resolution RPC that is still in flight when the worker shuts down, causing
// "Closing rpc while fetch was pending" errors.
vi.mock("@calcom/features/ee/organizations/di/OrganizationRepository.container", () => ({
getOrganizationRepository: vi.fn().mockReturnValue({}),
}));
vi.mock("@calcom/features/notifications/sendNotification", () => ({
sendNotification: vi.fn(),
}));
vi.mock("@calcom/features/conferencing/lib/videoClient", () => ({
createInstantMeetingWithCalVideo: vi.fn().mockResolvedValue({
type: "daily_video",
id: "MOCK_INSTANT_MEETING_ID",
password: "MOCK_INSTANT_PASS",
url: "http://mock-dailyvideo.example.com/instant-meeting-url",
}),
}));
describe("handleInstantMeeting", () => {
beforeEach(() => {
mockNoTranslations();
});
describe("team event instant meeting", () => {
it("should successfully create instant meeting for team event", async () => {
const instantBookingCreateService = getInstantBookingCreateService();
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
users: [
{
id: 101,
},
],
team: {
id: 1,
},
instantMeetingExpiryTimeOffsetInSeconds: 90,
},
],
organizer,
apps: [TestData.apps["daily-video"], TestData.apps["google-calendar"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-1`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
},
});
const mockBookingData: CreateInstantBookingData = {
eventTypeId: 1,
timeZone: "UTC",
language: "en",
start: `${plus1DateString}T04:00:00.000Z`,
end: `${plus1DateString}T04:45:00.000Z`,
responses: {
name: "Test User",
email: "test@example.com",
attendeePhoneNumber: "+918888888888",
},
metadata: {},
instant: true,
};
const result = await instantBookingCreateService.createBooking({
bookingData: mockBookingData,
});
expect(result.message).toBe("Success");
expect(result.bookingId).toBeDefined();
expect(result.bookingUid).toBeDefined();
expect(result.meetingTokenId).toBeDefined();
expect(result.expires).toBeInstanceOf(Date);
const booking = await prismock.booking.findUnique({
where: { id: result.bookingId },
select: { status: true, attendees: true, references: true },
});
expect(booking).toBeDefined();
expect(booking?.status).toBe(BookingStatus.AWAITING_HOST);
expect(booking?.attendees).toHaveLength(1);
expect(booking?.attendees[0].email).toBe("test@example.com");
expect(booking?.attendees[0].phoneNumber).toBe("+918888888888");
expect(booking?.references).toHaveLength(1);
expect(booking?.references[0].type).toBe("daily_video");
});
it("should throw error for non-team event types", async () => {
const instantBookingCreateService = getInstantBookingCreateService();
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
users: [
{
id: 101,
},
],
},
],
organizer,
apps: [TestData.apps["daily-video"], TestData.apps["google-calendar"]],
})
);
const mockBookingData: CreateInstantBookingData = {
eventTypeId: 1,
timeZone: "UTC",
language: "en",
start: `${plus1DateString}T04:00:00.000Z`,
end: `${plus1DateString}T04:45:00.000Z`,
responses: {
name: "Test User",
email: "test@example.com",
attendeePhoneNumber: "+918888888888",
},
metadata: {},
instant: true,
};
await expect(
instantBookingCreateService.createBooking({
bookingData: mockBookingData,
})
).rejects.toThrow("Only Team Event Types are supported for Instant Meeting");
});
it("should fire booking audit event with correct data when org has booking-audit feature", async () => {
const { BookingEventHandlerService } = await import("../onBookingEvents/BookingEventHandlerService");
const onBookingCreatedSpy = vi
.spyOn(BookingEventHandlerService.prototype, "onBookingCreated")
.mockResolvedValue(undefined);
const instantBookingCreateService = getInstantBookingCreateService();
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
users: [{ id: 101 }],
team: { id: 1 },
instantMeetingExpiryTimeOffsetInSeconds: 90,
},
],
organizer,
apps: [TestData.apps["daily-video"], TestData.apps["google-calendar"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-1`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: { uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID" },
});
const mockBookingData: CreateInstantBookingData = {
eventTypeId: 1,
timeZone: "UTC",
language: "en",
start: `${plus1DateString}T04:00:00.000Z`,
end: `${plus1DateString}T04:45:00.000Z`,
responses: {
name: "Test User",
email: "test@example.com",
attendeePhoneNumber: "+918888888888",
},
metadata: {},
instant: true,
};
const result = await instantBookingCreateService.createBooking({
bookingData: mockBookingData,
});
expect(result.message).toBe("Success");
expect(result.bookingId).toBeDefined();
expect(onBookingCreatedSpy).toHaveBeenCalledTimes(1);
const callArgs = onBookingCreatedSpy.mock.calls[0][0];
expect(callArgs.payload.booking.uid).toBe(result.bookingUid);
expect(callArgs.payload.config.isDryRun).toBe(false);
expect(callArgs.actor).toEqual(expect.objectContaining({ identifiedBy: expect.any(String) }));
expect(callArgs.auditData).toEqual(
expect.objectContaining({
startTime: expect.any(Number),
endTime: expect.any(Number),
status: expect.any(String),
})
);
expect(callArgs.source).toEqual(expect.any(String));
onBookingCreatedSpy.mockRestore();
});
it("should not throw when booking audit event fails", async () => {
const { BookingEventHandlerService } = await import("../onBookingEvents/BookingEventHandlerService");
const onBookingCreatedSpy = vi
.spyOn(BookingEventHandlerService.prototype, "onBookingCreated")
.mockRejectedValue(new Error("Audit event handler failure"));
const instantBookingCreateService = getInstantBookingCreateService();
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
users: [{ id: 101 }],
team: { id: 1 },
instantMeetingExpiryTimeOffsetInSeconds: 90,
},
],
organizer,
apps: [TestData.apps["daily-video"], TestData.apps["google-calendar"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-1`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: { uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID" },
});
const mockBookingData: CreateInstantBookingData = {
eventTypeId: 1,
timeZone: "UTC",
language: "en",
start: `${plus1DateString}T04:00:00.000Z`,
end: `${plus1DateString}T04:45:00.000Z`,
responses: {
name: "Test User",
email: "test@example.com",
attendeePhoneNumber: "+918888888888",
},
metadata: {},
instant: true,
};
const result = await instantBookingCreateService.createBooking({
bookingData: mockBookingData,
});
expect(result.message).toBe("Success");
expect(result.bookingId).toBeDefined();
expect(onBookingCreatedSpy).toHaveBeenCalled();
onBookingCreatedSpy.mockRestore();
});
});
});
function getDate(param: { dateIncrement?: number } = {}) {
const { dateIncrement = 0 } = param;
const date = new Date();
date.setDate(date.getDate() + dateIncrement);
return {
date,
dateString: date.toISOString().split("T")[0],
};
}