Files
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ab21c7f805 refactor: Cal.diy (#28903)
* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com

This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main:

- Rebrand Cal.com to Cal.diy across the entire codebase
- Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions
- Switch license from AGPL-3.0 to MIT
- Remove docs/ directory (migrated to Nextra at cal.diy)
- Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc.
- Clean up .env.example for self-hosted Cal.diy
- Update Docker image references to calcom/cal.diy
- Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork
- Add PR welcome bot for Cal.diy contributors
- Fix API v2 breaking changes oasdiff ignore entries
- Replace Blacksmith CI runners with default GitHub Actions

3893 files changed, 20789 insertions(+), 411020 deletions(-)

Co-Authored-By: [email protected] <[email protected]>

* refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701)

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

* fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702)

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

* rip out org related comments in api v2

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-04-15 09:52:36 -03:00

347 lines
11 KiB
TypeScript

import { PrismaBookingReportRepository } from "@calcom/features/bookingReport/repositories/PrismaBookingReportRepository";
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
import { BookingAccessService } from "@calcom/features/bookings/services/BookingAccessService";
import { BookingReportReason, BookingStatus } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { reportBookingHandler } from "./reportBooking.handler";
vi.mock("@calcom/features/bookingReport/repositories/PrismaBookingReportRepository");
vi.mock("@calcom/features/bookings/lib/handleCancelBooking");
vi.mock("@calcom/features/bookings/repositories/BookingRepository");
vi.mock("@calcom/features/bookings/services/BookingAccessService");
vi.mock("@calcom/lib/logger", () => ({
default: {
getSubLogger: () => ({
warn: vi.fn(),
error: vi.fn(),
}),
},
}));
vi.mock("@calcom/prisma", () => ({
default: {},
prisma: {},
}));
describe("reportBookingHandler", () => {
const mockUser = {
id: 1,
email: "[email protected]",
name: "Test User",
};
const mockBooking = {
id: 100,
uid: "test-booking-uid",
startTime: new Date("2025-12-01T10:00:00Z"),
status: BookingStatus.ACCEPTED,
recurringEventId: null,
attendees: [{ email: "[email protected]" }],
seatsReferences: [],
report: null,
};
const mockBookingRepo = {
findByUidIncludeReport: vi.fn(),
getActiveRecurringBookingsFromDate: vi.fn(),
};
const mockReportRepo = {
createReport: vi.fn(),
findAllReportedBookings: vi.fn(),
};
const mockBookingAccessService = {
doesUserIdHaveAccessToBooking: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(BookingRepository).mockImplementation(function() { return mockBookingRepo; });
vi.mocked(PrismaBookingReportRepository).mockImplementation(function() { return mockReportRepo; });
vi.mocked(BookingAccessService).mockImplementation(function() { return mockBookingAccessService; });
mockReportRepo.createReport.mockResolvedValue({ id: "new-report" });
});
describe("access control", () => {
it("should throw FORBIDDEN when user doesn't have access to booking", async () => {
mockBookingAccessService.doesUserIdHaveAccessToBooking.mockResolvedValue(false);
await expect(
reportBookingHandler({
ctx: { user: mockUser },
input: {
bookingUid: "test-booking-uid",
reason: BookingReportReason.SPAM,
},
})
).rejects.toThrow(TRPCError);
await expect(
reportBookingHandler({
ctx: { user: mockUser },
input: {
bookingUid: "test-booking-uid",
reason: BookingReportReason.SPAM,
},
})
).rejects.toMatchObject({
code: "FORBIDDEN",
message: "You don't have access to this booking",
});
});
it("should throw NOT_FOUND when booking doesn't exist", async () => {
mockBookingAccessService.doesUserIdHaveAccessToBooking.mockResolvedValue(true);
mockBookingRepo.findByUidIncludeReport.mockResolvedValue(null);
await expect(
reportBookingHandler({
ctx: { user: mockUser },
input: {
bookingUid: "test-booking-uid",
reason: BookingReportReason.SPAM,
},
})
).rejects.toMatchObject({
code: "NOT_FOUND",
message: "Booking not found",
});
});
});
describe("duplicate report prevention", () => {
it("should throw BAD_REQUEST when booking already has a report", async () => {
mockBookingAccessService.doesUserIdHaveAccessToBooking.mockResolvedValue(true);
mockBookingRepo.findByUidIncludeReport.mockResolvedValue({
...mockBooking,
report: { id: "existing-report" },
});
await expect(
reportBookingHandler({
ctx: { user: mockUser },
input: {
bookingUid: "test-booking-uid",
reason: BookingReportReason.SPAM,
},
})
).rejects.toMatchObject({
code: "BAD_REQUEST",
message: "This booking has already been reported",
});
});
});
describe("successful report creation", () => {
it("should successfully create a single booking report and auto-cancel upcoming booking", async () => {
mockBookingAccessService.doesUserIdHaveAccessToBooking.mockResolvedValue(true);
mockBookingRepo.findByUidIncludeReport.mockResolvedValue({
...mockBooking,
startTime: new Date(Date.now() + 86400000), // Tomorrow
});
vi.mocked(handleCancelBooking).mockResolvedValue({
success: true,
message: "Cancelled",
onlyRemovedAttendee: false,
bookingUid: "test-booking-uid",
});
const result = await reportBookingHandler({
ctx: { user: mockUser },
input: {
bookingUid: "test-booking-uid",
reason: BookingReportReason.SPAM,
description: "This is spam",
},
});
expect(result.success).toBe(true);
expect(result.message).toBe("Booking reported and cancelled successfully");
expect(result.reportedCount).toBe(1);
expect(mockReportRepo.createReport).toHaveBeenCalledWith({
bookingUid: "test-booking-uid",
bookerEmail: "[email protected]",
reportedById: mockUser.id,
reason: BookingReportReason.SPAM,
description: "This is spam",
cancelled: true,
organizationId: undefined,
});
});
it("should create report without description for past booking", async () => {
mockBookingAccessService.doesUserIdHaveAccessToBooking.mockResolvedValue(true);
mockBookingRepo.findByUidIncludeReport.mockResolvedValue({
...mockBooking,
startTime: new Date(Date.now() - 86400000), // Yesterday
});
const result = await reportBookingHandler({
ctx: { user: mockUser },
input: {
bookingUid: "test-booking-uid",
reason: BookingReportReason.DONT_KNOW_PERSON,
},
});
expect(result.message).toBe("Booking reported successfully");
expect(mockReportRepo.createReport).toHaveBeenCalledWith({
bookingUid: "test-booking-uid",
bookerEmail: "[email protected]",
reportedById: mockUser.id,
reason: BookingReportReason.DONT_KNOW_PERSON,
description: undefined,
cancelled: false,
organizationId: undefined,
});
});
});
describe("cancellation integration", () => {
it("should cancel booking when cancelBooking is true", async () => {
mockBookingAccessService.doesUserIdHaveAccessToBooking.mockResolvedValue(true);
mockBookingRepo.findByUidIncludeReport.mockResolvedValue({
...mockBooking,
startTime: new Date(Date.now() + 86400000), // Tomorrow
});
vi.mocked(handleCancelBooking).mockResolvedValue({
success: true,
message: "Cancelled",
onlyRemovedAttendee: false,
bookingUid: "test-booking-uid",
});
const result = await reportBookingHandler({
ctx: { user: mockUser },
input: {
bookingUid: "test-booking-uid",
reason: BookingReportReason.SPAM,
description: "Spam booking",
},
});
expect(result.message).toBe("Booking reported and cancelled successfully");
expect(handleCancelBooking).toHaveBeenCalledWith({
bookingData: {
uid: "test-booking-uid",
cancelledBy: mockUser.email,
skipCancellationReasonValidation: true,
},
userId: mockUser.id,
});
});
it("should not cancel booking if it's in the past", async () => {
mockBookingAccessService.doesUserIdHaveAccessToBooking.mockResolvedValue(true);
mockBookingRepo.findByUidIncludeReport.mockResolvedValue({
...mockBooking,
startTime: new Date(Date.now() - 86400000),
});
const result = await reportBookingHandler({
ctx: { user: mockUser },
input: {
bookingUid: "test-booking-uid",
reason: BookingReportReason.SPAM,
},
});
expect(handleCancelBooking).not.toHaveBeenCalled();
expect(result.message).toBe("Booking reported successfully");
});
it("should handle cancellation failure gracefully", async () => {
mockBookingAccessService.doesUserIdHaveAccessToBooking.mockResolvedValue(true);
mockBookingRepo.findByUidIncludeReport.mockResolvedValue({
...mockBooking,
startTime: new Date(Date.now() + 86400000),
});
vi.mocked(handleCancelBooking).mockRejectedValue(new Error("Cancellation failed"));
const result = await reportBookingHandler({
ctx: { user: mockUser },
input: {
bookingUid: "test-booking-uid",
reason: BookingReportReason.SPAM,
},
});
expect(result.success).toBe(true);
expect(result.message).toBe("Booking reported successfully, but cancellation failed");
expect(result).not.toHaveProperty("cancellationError");
});
it("should use cancelSubsequentBookings for recurring events", async () => {
mockBookingAccessService.doesUserIdHaveAccessToBooking.mockResolvedValue(true);
mockBookingRepo.findByUidIncludeReport.mockResolvedValue({
...mockBooking,
recurringEventId: "recurring-123",
startTime: new Date(Date.now() + 86400000),
});
mockBookingRepo.getActiveRecurringBookingsFromDate.mockResolvedValue([
{ uid: "100" },
{ uid: "101" },
{ uid: "102" },
]);
vi.mocked(handleCancelBooking).mockResolvedValue({
success: true,
message: "Cancelled",
onlyRemovedAttendee: false,
bookingUid: "test-booking-uid",
});
await reportBookingHandler({
ctx: { user: mockUser },
input: {
bookingUid: "test-booking-uid",
reason: BookingReportReason.SPAM,
},
});
expect(handleCancelBooking).toHaveBeenCalledWith({
bookingData: expect.objectContaining({
cancelSubsequentBookings: true,
}),
userId: mockUser.id,
});
});
it("should pass seatReferenceUid for seated events", async () => {
mockBookingAccessService.doesUserIdHaveAccessToBooking.mockResolvedValue(true);
mockBookingRepo.findByUidIncludeReport.mockResolvedValue({
...mockBooking,
startTime: new Date(Date.now() + 86400000),
seatsReferences: [
{ referenceUid: "seat-123", attendee: { email: mockUser.email } },
{ referenceUid: "seat-456", attendee: { email: "[email protected]" } },
],
});
vi.mocked(handleCancelBooking).mockResolvedValue({
success: true,
message: "Cancelled",
onlyRemovedAttendee: false,
bookingUid: "test-booking-uid",
});
await reportBookingHandler({
ctx: { user: mockUser },
input: {
bookingUid: "test-booking-uid",
reason: BookingReportReason.SPAM,
},
});
expect(handleCancelBooking).toHaveBeenCalledWith({
bookingData: expect.objectContaining({
seatReferenceUid: "seat-123",
}),
userId: mockUser.id,
});
});
});
});