From 0d8458bd94f461f1c32e6ce7cbfb5afeee152a70 Mon Sep 17 00:00:00 2001 From: Himanshu Pandey Date: Wed, 12 Feb 2025 20:09:43 +0530 Subject: [PATCH] Add unauthorization check for delete function and test --- apps/web/test/lib/deleteBooking.test.ts | 118 ++++++++++++++++++ .../routers/viewer/bookings/delete.handler.ts | 4 + .../bookings/deletePastBookings.handler.ts | 11 ++ 3 files changed, 133 insertions(+) create mode 100644 apps/web/test/lib/deleteBooking.test.ts diff --git a/apps/web/test/lib/deleteBooking.test.ts b/apps/web/test/lib/deleteBooking.test.ts new file mode 100644 index 00000000000000..2a11934e4ebe9c --- /dev/null +++ b/apps/web/test/lib/deleteBooking.test.ts @@ -0,0 +1,118 @@ +import prismaMock from "../../../../tests/libs/__mocks__/prismaMock"; + +import { describe, expect, it, beforeEach, vi } from "vitest"; + +import { deleteHandler } from "@calcom/trpc/server/routers/viewer/bookings/delete.handler"; +import { deletePastBookingsHandler } from "@calcom/trpc/server/routers/viewer/bookings/deletePastBookings.handler"; + +describe("Booking deletion", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should delete a single booking successfully", async () => { + const mockBooking = { + id: 1, + userId: 123, + }; + + prismaMock.booking.findFirst.mockResolvedValue(mockBooking); + prismaMock.booking.delete.mockResolvedValue(mockBooking); + + const ctx = { + user: { + id: 123, + }, + }; + + await deleteHandler({ + ctx, + input: { id: 1 }, + }); + + expect(prismaMock.booking.delete).toHaveBeenCalledTimes(1); + expect(prismaMock.booking.delete).toHaveBeenCalledWith({ + where: { id: 1 }, + }); + }); + + it("should delete multiple past bookings correctly", async () => { + const mockBookings = [ + { id: 1, userId: 123 }, + { id: 2, userId: 123 }, + ]; + + prismaMock.booking.findMany.mockResolvedValue(mockBookings); + prismaMock.booking.deleteMany.mockResolvedValue({ count: 2 }); + + const ctx = { + user: { + id: 123, + }, + }; + + await deletePastBookingsHandler({ + ctx, + input: { ids: [1, 2] }, + }); + + expect(prismaMock.booking.deleteMany).toHaveBeenCalledTimes(1); + expect(prismaMock.booking.deleteMany).toHaveBeenCalledWith({ + where: { + OR: [{ userId: 123 }, { attendees: { some: { email: undefined } } }], + endTime: { lt: expect.any(Date) }, + id: { in: undefined }, + status: { notIn: ["CANCELLED", "REJECTED"] }, + }, + }); + }); + + it("should prevent deletion of unauthorized bookings", async () => { + const mockBooking = { + id: 1, + userId: 456, + }; + + prismaMock.booking.findFirst.mockResolvedValue(mockBooking); + + const ctx = { + user: { + id: 123, + }, + }; + + await expect( + deleteHandler({ + ctx, + input: { id: 1 }, + }) + ).rejects.toThrow(/unauthorized/i); + + expect(prismaMock.booking.delete).not.toHaveBeenCalled(); + }); + + it("should prevent deletion of unauthorized multiple bookings", async () => { + const mockBooking = [ + { id: 1, userId: 456 }, + { id: 2, userId: 456 }, + ]; + + prismaMock.booking.findMany.mockResolvedValue(mockBooking); + prismaMock.booking.deleteMany.mockResolvedValue({ count: 0 }); + + const ctx = { + user: { + id: 123, + }, + }; + + await expect( + deletePastBookingsHandler({ + ctx, + input: { id: [1, 2] }, + }) + ).rejects.toThrow(/unauthorized/i); + + expect(prismaMock.booking.deleteMany).not.toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/trpc/server/routers/viewer/bookings/delete.handler.ts b/packages/trpc/server/routers/viewer/bookings/delete.handler.ts index 20866e90aa064e..698ca9f44843de 100644 --- a/packages/trpc/server/routers/viewer/bookings/delete.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/delete.handler.ts @@ -42,6 +42,10 @@ export const deleteHandler = async ({ ctx, input }: DeleteOptions) => { throw new Error("Booking not found"); } + if (booking.userId !== user.id) { + throw new Error("Unauthorized: You don't have permission to delete this booking"); + } + await prisma.booking.delete({ where: { id: booking.id, diff --git a/packages/trpc/server/routers/viewer/bookings/deletePastBookings.handler.ts b/packages/trpc/server/routers/viewer/bookings/deletePastBookings.handler.ts index fbb9137e254558..0f1de5c5f2e184 100644 --- a/packages/trpc/server/routers/viewer/bookings/deletePastBookings.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/deletePastBookings.handler.ts @@ -14,6 +14,17 @@ export const deletePastBookingsHandler = async ({ ctx, input }: DeletePastBookin const { user } = ctx; const { bookingIds } = input; + const bookings = await prisma.booking.findMany({ + where: { + id: { in: bookingIds }, + }, + }); + + const unauthorized = bookings.some((booking) => booking.userId !== user.id); + if (unauthorized) { + throw new Error("Unauthorized: Cannot delete bookings that don't belong to you"); + } + const result = await prisma.booking.deleteMany({ where: { id: { in: bookingIds },