Files
calendar/packages/features/busyTimes/services/getBusyTimes.test.ts
T
RomitGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
4291a59b2c fix: add missing vi.mock() calls to prevent vitest worker shutdown flakiness (#28459)
* fix: add missing vi.mock() calls to prevent vitest worker shutdown flakiness

Add vi.mock() calls for modules that trigger background network requests
or database connections during import. These transitive imports can cause
the vitest worker RPC to shut down while pending fetch/network operations
are still in flight, resulting in flaky test failures with:
  Error: [vitest-worker]: Closing rpc while "fetch" was pending

The primary modules mocked are:
- @calcom/app-store/delegationCredential (triggers credential lookups)
- @calcom/prisma (triggers database initialization)
- @calcom/features/calendars/lib/CalendarManager (triggers calendar API calls)
- @calcom/features/auth/lib/verifyEmail (triggers email service)
- @calcom/lib/domainManager/organization (triggers domain lookups)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: remove conflicting empty prisma mocks from files with prismock/prismaMock setups

- Remove vi.mock('@calcom/prisma', () => ({ default: {}, prisma: {} })) from 28 files
  that already have prismock/prismaMock test doubles. Vitest hoists all vi.mock() calls
  and the last one wins, so these empty mocks were overriding the functional test doubles.
- Fix CalendarSubscriptionService.test.ts to reuse the shared mock from
  __mocks__/delegationCredential instead of creating a new unconfigured vi.fn()
- Remove DelegationCredentialRepository.test.ts empty prisma mock (different pattern)
- Remove vi.mock from inside beforeEach in intentToCreateOrg.handler.test.ts

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: add comprehensive delegationCredential mock exports to prevent CI test failures

The vi.mock blocks for @calcom/app-store/delegationCredential were missing
exports that the code under test transitively imports (e.g.
enrichUsersWithDelegationCredentials, enrichUserWithDelegationCredentialsIncludeServiceAccountKey,
buildAllCredentials, getFirstDelegationConferencingCredentialAppLocation).

Added all exports with passthrough implementations so the booking flow
works correctly without triggering real network requests.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: correct credential mock return shapes to match real module API

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: revert unintended yarn.lock changes

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-03-17 09:55:37 +05:30

401 lines
13 KiB
TypeScript

import { prisma } from "@calcom/prisma/__mocks__/prisma";
import dayjs from "@calcom/dayjs";
import { getBusyTimesService } from "@calcom/features/di/containers/BusyTimes";
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@calcom/prisma", () => ({
default: prisma,
prisma,
}));
vi.mock("@calcom/features/calendars/lib/CalendarManager", () => ({
getBusyCalendarTimes: vi.fn().mockResolvedValue([]),
createEvent: vi.fn().mockResolvedValue({}),
updateEvent: vi.fn().mockResolvedValue({}),
deleteEvent: vi.fn().mockResolvedValue({}),
}));
vi.mock("@calcom/app-store/delegationCredential", () => ({
enrichHostsWithDelegationCredentials: vi.fn(),
getUsersCredentialsIncludeServiceAccountKey: vi.fn(),
getCredentialForSelectedCalendar: vi.fn(),
findUniqueDelegationCalendarCredential: vi.fn(),
getAllDelegationCredentialsForUserIncludeServiceAccountKey: vi.fn(),
getDelegationCredentialOrFindRegularCredential: vi.fn(),
}));
const startOfTomorrow = dayjs().add(1, "day").startOf("day");
const tomorrowDate = startOfTomorrow.format("YYYY-MM-DD");
const mockBookings = ({
beforeEventBuffer = 0,
afterEventBuffer = 0,
seatsPerTimeSlot,
}: {
beforeEventBuffer?: number;
afterEventBuffer?: number;
seatsPerTimeSlot?: number;
}) => [
{
id: 1,
startTime: startOfTomorrow.set("hour", 10).toDate(),
endTime: startOfTomorrow.set("hour", 11).toDate(),
title: "Booking Between X and Y",
userId: 1,
uid: "xxxx1",
eventType: {
id: 1,
beforeEventBuffer,
afterEventBuffer,
seatsPerTimeSlot: seatsPerTimeSlot ?? null,
},
_count: {
seatsReferences: 1,
},
},
{
id: 2,
startTime: startOfTomorrow.set("hour", 14).toDate(),
endTime: startOfTomorrow.set("hour", 15).toDate(),
title: "Booking Between X and Y",
userId: 1,
uid: "xxxx2",
eventType: {
id: 1,
beforeEventBuffer,
afterEventBuffer,
seatsPerTimeSlot: seatsPerTimeSlot ?? null,
},
...(seatsPerTimeSlot
? {
_count: {
seatsReferences: 1,
},
}
: {}),
},
];
describe("getBusyTimes", () => {
it("blocks a regular time slot", async () => {
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimes({
credentials: [],
userId: 1,
userEmail: "exampleuser1@example.com",
username: "exampleuser1",
bypassBusyCalendarTimes: false,
selectedCalendars: [],
startTime: startOfTomorrow.format(),
endTime: startOfTomorrow.endOf("day").format(),
currentBookings: mockBookings({}),
});
expect(busyTimes).toEqual([
expect.objectContaining({
start: dayjs(`${tomorrowDate}`).startOf("day").set("hour", 10).toDate(),
end: dayjs(`${tomorrowDate}`).startOf("day").set("hour", 11).toDate(),
source: "eventType-1-booking-1",
title: "Booking Between X and Y",
}),
expect.objectContaining({
start: dayjs(`${tomorrowDate}`).startOf("day").set("hour", 14).toDate(),
end: dayjs(`${tomorrowDate}`).startOf("day").set("hour", 15).toDate(),
source: "eventType-1-booking-2",
title: "Booking Between X and Y",
}),
]);
});
it("should block before and after buffer times", async () => {
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimes({
credentials: [],
userId: 1,
userEmail: "exampleuser1@example.com",
username: "exampleuser1",
bypassBusyCalendarTimes: false,
selectedCalendars: [],
startTime: startOfTomorrow.format(),
endTime: startOfTomorrow.endOf("day").format(),
currentBookings: [mockBookings({ beforeEventBuffer: 10 })[0]],
});
expect(busyTimes).toEqual([
expect.objectContaining({
start: dayjs(`${tomorrowDate}`).startOf("day").set("hour", 9).set("minute", 50).toDate(),
end: dayjs(`${tomorrowDate}`).startOf("day").set("hour", 11).toDate(),
source: "eventType-1-booking-1",
title: "Booking Between X and Y",
}),
]);
});
it("should have busy times only if seated with remaining seats when buffers exist", async () => {
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimes({
credentials: [],
userId: 1,
eventTypeId: 1,
userEmail: "exampleuser1@example.com",
username: "exampleuser1",
bypassBusyCalendarTimes: false,
selectedCalendars: [],
startTime: startOfTomorrow.format(),
endTime: startOfTomorrow.endOf("day").format(),
currentBookings: [mockBookings({ beforeEventBuffer: 10, seatsPerTimeSlot: 10 })[0]],
});
expect(busyTimes).toEqual([
expect.objectContaining({
start: dayjs(`${tomorrowDate}`).startOf("day").set("hour", 9).set("minute", 50).toDate(),
end: dayjs(`${tomorrowDate}`).startOf("day").set("hour", 10).toDate(),
}),
]);
});
});
describe("getBusyTimesForLimitChecks", () => {
beforeEach(() => {
vi.clearAllMocks();
});
const createMockBookingResult = (
overrides: Partial<{
id: number;
startTime: Date;
endTime: Date;
eventTypeId: number | null;
title: string;
userId: number | null;
}> = {}
) => ({
id: 1,
startTime: startOfTomorrow.set("hour", 10).toDate(),
endTime: startOfTomorrow.set("hour", 11).toDate(),
eventTypeId: 1,
title: "Test Booking",
userId: 1,
...overrides,
});
it("should return empty array when no booking or duration limits are provided", async () => {
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
userIds: [1, 2, 3],
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: null,
durationLimits: null,
});
expect(busyTimes).toEqual([]);
expect(prisma.booking.findMany).not.toHaveBeenCalled();
});
it("should return empty array when userIds is empty", async () => {
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
userIds: [],
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_DAY: 5 },
});
expect(busyTimes).toEqual([]);
});
it("should fetch bookings for a single user with booking limits", async () => {
const mockBooking = createMockBookingResult();
vi.mocked(prisma.booking.findMany).mockResolvedValue([mockBooking]);
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
userIds: [1],
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_DAY: 5 },
});
expect(busyTimes).toHaveLength(1);
expect(busyTimes[0]).toEqual(
expect.objectContaining({
start: mockBooking.startTime,
end: mockBooking.endTime,
title: mockBooking.title,
source: `eventType-${mockBooking.eventTypeId}-booking-${mockBooking.id}`,
userId: mockBooking.userId,
})
);
expect(prisma.booking.findMany).toHaveBeenCalledTimes(1);
});
it("should fetch bookings for multiple users with duration limits", async () => {
const mockBookings = [
createMockBookingResult({ id: 1, userId: 1 }),
createMockBookingResult({
id: 2,
userId: 2,
startTime: startOfTomorrow.set("hour", 14).toDate(),
endTime: startOfTomorrow.set("hour", 15).toDate(),
}),
];
vi.mocked(prisma.booking.findMany).mockResolvedValue(mockBookings);
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
userIds: [1, 2],
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
durationLimits: { PER_DAY: 120 },
});
expect(busyTimes).toHaveLength(2);
expect(busyTimes[0].userId).toBe(1);
expect(busyTimes[1].userId).toBe(2);
});
it("should batch queries when userIds exceeds batch size (50)", async () => {
const userIds = Array.from({ length: 75 }, (_, i) => i + 1);
const mockBookings = [createMockBookingResult()];
vi.mocked(prisma.booking.findMany).mockResolvedValue(mockBookings);
const busyTimesService = getBusyTimesService();
await busyTimesService.getBusyTimesForLimitChecks({
userIds,
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_DAY: 5 },
});
// Should be called twice: once for first 50 users, once for remaining 25
expect(prisma.booking.findMany).toHaveBeenCalledTimes(2);
});
it("should batch queries correctly for exactly 100 users (2 full batches)", async () => {
const userIds = Array.from({ length: 100 }, (_, i) => i + 1);
vi.mocked(prisma.booking.findMany).mockResolvedValue([]);
const busyTimesService = getBusyTimesService();
await busyTimesService.getBusyTimesForLimitChecks({
userIds,
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_WEEK: 10 },
});
expect(prisma.booking.findMany).toHaveBeenCalledTimes(2);
});
it("should batch queries correctly for 150 users (3 batches)", async () => {
const userIds = Array.from({ length: 150 }, (_, i) => i + 1);
vi.mocked(prisma.booking.findMany).mockResolvedValue([]);
const busyTimesService = getBusyTimesService();
await busyTimesService.getBusyTimesForLimitChecks({
userIds,
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_MONTH: 20 },
});
expect(prisma.booking.findMany).toHaveBeenCalledTimes(3);
});
it("should merge results from multiple batches correctly", async () => {
const userIds = Array.from({ length: 75 }, (_, i) => i + 1);
// First batch returns 2 bookings, second batch returns 1 booking
vi.mocked(prisma.booking.findMany)
.mockResolvedValueOnce([
createMockBookingResult({ id: 1, userId: 1 }),
createMockBookingResult({ id: 2, userId: 25 }),
])
.mockResolvedValueOnce([createMockBookingResult({ id: 3, userId: 60 })]);
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
userIds,
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_DAY: 5 },
});
expect(busyTimes).toHaveLength(3);
expect(busyTimes.map((b) => b.userId)).toEqual([1, 25, 60]);
});
it("should exclude rescheduleUid from results", async () => {
const mockBooking = createMockBookingResult();
vi.mocked(prisma.booking.findMany).mockResolvedValue([mockBooking]);
const busyTimesService = getBusyTimesService();
await busyTimesService.getBusyTimesForLimitChecks({
userIds: [1],
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
rescheduleUid: "existing-booking-uid",
bookingLimits: { PER_DAY: 5 },
});
// Verify the query was called (the actual exclusion happens in the Prisma query)
expect(prisma.booking.findMany).toHaveBeenCalledTimes(1);
});
it("should handle both booking and duration limits together", async () => {
const mockBooking = createMockBookingResult();
vi.mocked(prisma.booking.findMany).mockResolvedValue([mockBooking]);
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
userIds: [1],
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_DAY: 5 },
durationLimits: { PER_DAY: 120 },
});
expect(busyTimes).toHaveLength(1);
expect(prisma.booking.findMany).toHaveBeenCalledTimes(1);
});
it("should handle null eventTypeId in booking results", async () => {
const mockBooking = createMockBookingResult({ eventTypeId: null });
vi.mocked(prisma.booking.findMany).mockResolvedValue([mockBooking]);
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
userIds: [1],
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_DAY: 5 },
});
expect(busyTimes).toHaveLength(1);
expect(busyTimes[0].source).toBe(`eventType-null-booking-${mockBooking.id}`);
});
it("should handle null userId in booking results", async () => {
const mockBooking = createMockBookingResult({ userId: null });
vi.mocked(prisma.booking.findMany).mockResolvedValue([mockBooking]);
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
userIds: [1],
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_DAY: 5 },
});
expect(busyTimes).toHaveLength(1);
expect(busyTimes[0].userId).toBeNull();
});
});