mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
* 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>
347 lines
11 KiB
TypeScript
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,
|
|
});
|
|
});
|
|
});
|
|
});
|