diff --git a/packages/trpc/server/routers/loggedInViewer/__tests__/outOfOffice.handler.test.ts b/packages/trpc/server/routers/loggedInViewer/__tests__/outOfOffice.handler.test.ts new file mode 100644 index 0000000000..03535529a7 --- /dev/null +++ b/packages/trpc/server/routers/loggedInViewer/__tests__/outOfOffice.handler.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, it, beforeEach, afterEach } from "vitest"; +import { vi } from "vitest"; + +import { prisma } from "@calcom/prisma"; + +import { outOfOfficeCreateOrUpdate } from "../outOfOffice.handler"; + +const prismaMock = { + outOfOfficeEntry: { + findFirst: vi.fn().mockResolvedValue(undefined), + findUnique: vi.fn().mockResolvedValue(undefined), + upsert: vi.fn().mockResolvedValue(undefined), + }, +}; +vi.spyOn(prisma.outOfOfficeEntry, "findFirst").mockImplementation(prismaMock.outOfOfficeEntry.findFirst); +vi.spyOn(prisma.outOfOfficeEntry, "findUnique").mockImplementation(prismaMock.outOfOfficeEntry.findUnique); +vi.spyOn(prisma.outOfOfficeEntry, "upsert").mockImplementation(prismaMock.outOfOfficeEntry.upsert); + +afterEach(() => { + prismaMock.outOfOfficeEntry.findFirst.mockClear(); + prismaMock.outOfOfficeEntry.findUnique.mockClear(); + prismaMock.outOfOfficeEntry.upsert.mockClear(); +}); + +const mockUser = { + id: 4, + username: "pro", + email: "pro@example.com", + password: { + hash: "", + userId: 0, + }, + completedOnboarding: true, + identityProvider: "CAL", + profiles: [], + avatar: "", + organization: null, + organizationId: null, + locale: "en", + timeZone: "UTC", + defaultScheduleId: null, + email_verified: true, + name: null, + theme: null, +} as const; + +describe("outOfOfficeCreateOrUpdate", () => { + beforeEach(() => { + vi.resetAllMocks(); + vi.setSystemTime(new Date("2024-11-22T03:23:45Z")); + }); + + it("should throw error if start date is after end date", async () => { + const input = { + dateRange: { + startDate: new Date("2024-11-23T23:00:00.000Z"), + endDate: new Date("2024-11-22T23:00:00.000Z"), + }, + offset: 60, + reasonId: 1, + notes: "", + toTeamUserId: null, + }; + + await expect(outOfOfficeCreateOrUpdate({ ctx: { user: mockUser }, input })).rejects.toThrow( + "start_date_must_be_before_end_date" + ); + }); + + it("should handle timezone offset correctly", async () => { + const input = { + dateRange: { + startDate: new Date("2024-11-23T23:00:00.000Z"), + endDate: new Date("2024-11-23T23:00:00.000Z"), + }, + offset: 60, // Paris timezone offset in minutes + reasonId: 1, + notes: "", + toTeamUserId: null, + }; + const startTimeUtc = "2024-11-24T00:00:00.000Z"; + const endTimeUtc = "2024-11-24T23:59:59.999Z"; + + await outOfOfficeCreateOrUpdate({ + ctx: { user: mockUser }, + input, + }); + + expect(prisma.outOfOfficeEntry.findFirst).toHaveBeenNthCalledWith(1, { + select: { + toUserId: true, + userId: true, + }, + where: { + OR: [ + { + AND: [ + { + start: { + lte: endTimeUtc, + }, + }, + { + end: { + gte: startTimeUtc, + }, + }, + ], + }, + { + AND: [ + { + start: { + gte: startTimeUtc, + }, + }, + { + end: { + lte: endTimeUtc, + }, + }, + ], + }, + ], + toUserId: 4, + }, + }); + }); +}); diff --git a/packages/trpc/server/routers/loggedInViewer/outOfOffice.handler.ts b/packages/trpc/server/routers/loggedInViewer/outOfOffice.handler.ts index 683f27ee89..c24f9a1dbd 100644 --- a/packages/trpc/server/routers/loggedInViewer/outOfOffice.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/outOfOffice.handler.ts @@ -10,12 +10,12 @@ import type { OOOEntryPayloadType } from "@calcom/features/webhooks/lib/sendPayl import sendPayload from "@calcom/features/webhooks/lib/sendPayload"; import { getTranslation } from "@calcom/lib/server"; import prisma from "@calcom/prisma"; +import { WebhookTriggerEvents } from "@calcom/prisma/enums"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; import { TRPCError } from "@trpc/server"; import type { TOutOfOfficeDelete, TOutOfOfficeInputSchema } from "./outOfOffice.schema"; -import { WebhookTriggerEvents } from ".prisma/client"; type TBookingRedirect = { ctx: { @@ -30,11 +30,11 @@ export const outOfOfficeCreateOrUpdate = async ({ ctx, input }: TBookingRedirect throw new TRPCError({ code: "BAD_REQUEST", message: "start_date_and_end_date_required" }); } - const inputStartTime = dayjs(startDate).startOf("day"); - const inputEndTime = dayjs(endDate).endOf("day"); + const startTimeUtc = dayjs.utc(startDate).add(input.offset, "minute").startOf("day"); + const endTimeUtc = dayjs.utc(endDate).add(input.offset, "minute").endOf("day"); // If start date is after end date throw error - if (inputStartTime.isAfter(inputEndTime)) { + if (startTimeUtc.isAfter(endTimeUtc)) { throw new TRPCError({ code: "BAD_REQUEST", message: "start_date_must_be_before_end_date" }); } @@ -85,17 +85,11 @@ export const outOfOfficeCreateOrUpdate = async ({ ctx, input }: TBookingRedirect OR: [ // Outside of range { - AND: [ - { start: { lte: inputEndTime.toISOString() } }, - { end: { gte: inputStartTime.toISOString() } }, - ], + AND: [{ start: { lte: endTimeUtc.toISOString() } }, { end: { gte: startTimeUtc.toISOString() } }], }, // Inside of range { - AND: [ - { start: { gte: inputStartTime.toISOString() } }, - { end: { lte: inputEndTime.toISOString() } }, - ], + AND: [{ start: { gte: startTimeUtc.toISOString() } }, { end: { lte: endTimeUtc.toISOString() } }], }, ], }, @@ -106,14 +100,11 @@ export const outOfOfficeCreateOrUpdate = async ({ ctx, input }: TBookingRedirect throw new TRPCError({ code: "BAD_REQUEST", message: "booking_redirect_infinite_not_allowed" }); } - const startTimeUtc = dayjs.utc(startDate).add(input.offset, "minute").startOf("day").toISOString(); - const endTimeUtc = dayjs.utc(endDate).add(input.offset, "minute").endOf("day").toISOString(); - const isDuplicateOutOfOfficeEntry = await prisma.outOfOfficeEntry.findFirst({ where: { userId: ctx.user.id, - start: startTimeUtc, - end: endTimeUtc, + start: startTimeUtc.toISOString(), + end: endTimeUtc.toISOString(), }, }); @@ -144,8 +135,8 @@ export const outOfOfficeCreateOrUpdate = async ({ ctx, input }: TBookingRedirect }, create: { uuid: uuidv4(), - start: startTimeUtc, - end: endTimeUtc, + start: startTimeUtc.toISOString(), + end: endTimeUtc.toISOString(), notes: input.notes, userId: ctx.user.id, reasonId: input.reasonId, @@ -154,8 +145,8 @@ export const outOfOfficeCreateOrUpdate = async ({ ctx, input }: TBookingRedirect updatedAt: new Date(), }, update: { - start: startTimeUtc, - end: endTimeUtc, + start: startTimeUtc.toISOString(), + end: endTimeUtc.toISOString(), notes: input.notes, userId: ctx.user.id, reasonId: input.reasonId,