mirror of
https://github.com/hanzoai/calendar.git
synced 2026-09-24 22:42:24 +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>
346 lines
11 KiB
TypeScript
346 lines
11 KiB
TypeScript
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
|
|
import { PrismaBookingAttendeeRepository } from "@calcom/features/bookings/repositories/PrismaBookingAttendeeRepository";
|
|
import { prisma } from "@calcom/prisma";
|
|
import { BookingStatus, CreationSource } from "@calcom/prisma/enums";
|
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mockUpdateCalendarAttendees = vi.fn().mockResolvedValue({});
|
|
vi.mock("@calcom/features/bookings/lib/EventManager", () => ({
|
|
default: class MockEventManager {
|
|
updateCalendarAttendees = mockUpdateCalendarAttendees;
|
|
},
|
|
}));
|
|
|
|
const mockSendEmail = vi.fn().mockResolvedValue(undefined);
|
|
vi.mock("@calcom/emails/templates/attendee-cancelled-email", () => ({
|
|
default: class MockAttendeeCancelledEmail {
|
|
constructor() {}
|
|
sendEmail = mockSendEmail;
|
|
},
|
|
}));
|
|
|
|
vi.mock("@calcom/trpc/server/routers/viewer/bookings/addGuests.handler", async (importOriginal) => {
|
|
const actual = (await importOriginal()) as Record<string, unknown>;
|
|
return {
|
|
...actual,
|
|
validateUserPermissions: vi.fn().mockResolvedValue(undefined),
|
|
updateCalendarEvent: vi.fn().mockResolvedValue(undefined),
|
|
};
|
|
});
|
|
|
|
import type { BookingAttendeesRemoveServiceDeps } from "./BookingAttendeesRemoveService";
|
|
import { BookingAttendeesRemoveService } from "./BookingAttendeesRemoveService";
|
|
|
|
const createdBookingIds: number[] = [];
|
|
const createdUserIds: number[] = [];
|
|
const createdEventTypeIds: number[] = [];
|
|
|
|
describe("BookingAttendeesRemoveService.removeAttendee (Integration Tests)", () => {
|
|
const timestamp = Date.now();
|
|
const bookingRepo = new BookingRepository(prisma);
|
|
const service = new BookingAttendeesRemoveService({
|
|
bookingAttendeeRepository: new PrismaBookingAttendeeRepository(prisma),
|
|
});
|
|
|
|
let organizerId: number;
|
|
let bookingId: number;
|
|
let primaryAttendeeId: number;
|
|
let secondaryAttendeeId: number;
|
|
|
|
const testUser = {
|
|
id: 0,
|
|
email: "",
|
|
organizationId: null as number | null,
|
|
uuid: `test-uuid-${timestamp}`,
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
const organizer = await prisma.user.create({
|
|
data: {
|
|
email: `attendee-svc-organizer-${timestamp}@test.com`,
|
|
username: `attendee-svc-organizer-${timestamp}`,
|
|
name: "Test Organizer",
|
|
timeZone: "UTC",
|
|
locale: "en",
|
|
creationSource: CreationSource.WEBAPP,
|
|
schedules: {
|
|
create: {
|
|
name: "Default Schedule",
|
|
timeZone: "UTC",
|
|
availability: {
|
|
create: [
|
|
{
|
|
days: [0, 1, 2, 3, 4, 5, 6],
|
|
startTime: new Date("1970-01-01T09:00:00.000Z"),
|
|
endTime: new Date("1970-01-01T17:00:00.000Z"),
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
select: { id: true, email: true },
|
|
});
|
|
organizerId = organizer.id;
|
|
createdUserIds.push(organizer.id);
|
|
|
|
testUser.id = organizer.id;
|
|
testUser.email = organizer.email;
|
|
|
|
const eventType = await prisma.eventType.create({
|
|
data: {
|
|
title: `attendee-svc-event-${timestamp}`,
|
|
slug: `attendee-svc-event-${timestamp}`,
|
|
length: 30,
|
|
userId: organizerId,
|
|
},
|
|
select: { id: true },
|
|
});
|
|
createdEventTypeIds.push(eventType.id);
|
|
|
|
const booking = await bookingRepo.createBookingForManagedEventReassignment({
|
|
uid: `attendee-svc-booking-${timestamp}`,
|
|
userId: organizerId,
|
|
userPrimaryEmail: organizer.email,
|
|
eventTypeId: eventType.id,
|
|
title: `Attendee Service Test Booking ${timestamp}`,
|
|
description: null,
|
|
startTime: new Date("2030-06-01T10:00:00.000Z"),
|
|
endTime: new Date("2030-06-01T10:30:00.000Z"),
|
|
status: BookingStatus.ACCEPTED,
|
|
location: null,
|
|
smsReminderNumber: null,
|
|
idempotencyKey: `attendee-svc-idempotency-${timestamp}`,
|
|
iCalUID: `attendee-svc-ical-${timestamp}@test.com`,
|
|
iCalSequence: 0,
|
|
attendees: [
|
|
{ email: "[email protected]", name: "Primary Attendee", timeZone: "UTC", locale: "en" },
|
|
{
|
|
email: "[email protected]",
|
|
name: "Secondary Attendee",
|
|
timeZone: "America/New_York",
|
|
locale: "en",
|
|
},
|
|
],
|
|
});
|
|
bookingId = booking.id;
|
|
createdBookingIds.push(booking.id);
|
|
|
|
const attendees = await prisma.attendee.findMany({
|
|
where: { bookingId: booking.id },
|
|
orderBy: { id: "asc" },
|
|
select: { id: true, email: true },
|
|
});
|
|
primaryAttendeeId = attendees[0].id;
|
|
secondaryAttendeeId = attendees[1].id;
|
|
|
|
await prisma.booking.update({
|
|
where: { id: booking.id },
|
|
data: {
|
|
responses: {
|
|
name: "Primary Attendee",
|
|
email: "[email protected]",
|
|
guests: ["[email protected]"],
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (createdBookingIds.length > 0) {
|
|
await prisma.attendee.deleteMany({ where: { bookingId: { in: createdBookingIds } } });
|
|
await prisma.booking.deleteMany({ where: { id: { in: createdBookingIds } } });
|
|
}
|
|
if (createdEventTypeIds.length > 0) {
|
|
await prisma.eventType.deleteMany({ where: { id: { in: createdEventTypeIds } } });
|
|
}
|
|
if (createdUserIds.length > 0) {
|
|
await prisma.availability.deleteMany({ where: { Schedule: { userId: { in: createdUserIds } } } });
|
|
await prisma.schedule.deleteMany({ where: { userId: { in: createdUserIds } } });
|
|
await prisma.user.deleteMany({ where: { id: { in: createdUserIds } } });
|
|
}
|
|
});
|
|
|
|
it("should remove a secondary attendee, update DB, and emit audit event", async () => {
|
|
const result = await service.removeAttendee({
|
|
bookingId,
|
|
attendeeId: secondaryAttendeeId,
|
|
user: testUser,
|
|
emailsEnabled: true,
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
id: secondaryAttendeeId,
|
|
bookingId,
|
|
name: "Secondary Attendee",
|
|
email: "[email protected]",
|
|
timeZone: "America/New_York",
|
|
});
|
|
|
|
const attendee = await prisma.attendee.findUnique({
|
|
where: { id: secondaryAttendeeId },
|
|
});
|
|
expect(attendee).toBeNull();
|
|
|
|
const updatedBooking = await prisma.booking.findUnique({
|
|
where: { id: bookingId },
|
|
select: { responses: true },
|
|
});
|
|
const responses = updatedBooking?.responses as { guests?: string[] } | null;
|
|
expect(responses?.guests ?? []).not.toContain("[email protected]");
|
|
});
|
|
|
|
describe("primary attendee guard", () => {
|
|
it("should throw when trying to remove the primary attendee", async () => {
|
|
await expect(
|
|
service.removeAttendee({
|
|
bookingId,
|
|
attendeeId: primaryAttendeeId,
|
|
user: testUser,
|
|
emailsEnabled: true,
|
|
})
|
|
).rejects.toThrow("cannot_remove_primary_attendee");
|
|
});
|
|
});
|
|
|
|
describe("non-existent attendee", () => {
|
|
it("should throw when attendee does not exist", async () => {
|
|
await expect(
|
|
service.removeAttendee({
|
|
bookingId,
|
|
attendeeId: 999999,
|
|
user: testUser,
|
|
emailsEnabled: true,
|
|
})
|
|
).rejects.toThrow("attendee_not_found");
|
|
});
|
|
});
|
|
|
|
describe("emails disabled", () => {
|
|
let emailsDisabledBookingId: number;
|
|
let emailsDisabledAttendeeId: number;
|
|
|
|
beforeAll(async () => {
|
|
const booking = await bookingRepo.createBookingForManagedEventReassignment({
|
|
uid: `attendee-svc-no-email-${timestamp}`,
|
|
userId: organizerId,
|
|
userPrimaryEmail: testUser.email,
|
|
eventTypeId: createdEventTypeIds[0],
|
|
title: `No Email Test Booking ${timestamp}`,
|
|
description: null,
|
|
startTime: new Date("2030-06-02T10:00:00.000Z"),
|
|
endTime: new Date("2030-06-02T10:30:00.000Z"),
|
|
status: BookingStatus.ACCEPTED,
|
|
location: null,
|
|
smsReminderNumber: null,
|
|
idempotencyKey: `attendee-svc-no-email-idempotency-${timestamp}`,
|
|
iCalUID: `attendee-svc-no-email-ical-${timestamp}@test.com`,
|
|
iCalSequence: 0,
|
|
attendees: [
|
|
{ email: "[email protected]", name: "Primary", timeZone: "UTC", locale: "en" },
|
|
{ email: "[email protected]", name: "Secondary", timeZone: "UTC", locale: "en" },
|
|
],
|
|
});
|
|
emailsDisabledBookingId = booking.id;
|
|
createdBookingIds.push(booking.id);
|
|
|
|
await prisma.booking.update({
|
|
where: { id: booking.id },
|
|
data: {
|
|
responses: {
|
|
name: "Primary",
|
|
email: "[email protected]",
|
|
guests: ["[email protected]"],
|
|
},
|
|
},
|
|
});
|
|
|
|
const attendees = await prisma.attendee.findMany({
|
|
where: { bookingId: booking.id },
|
|
orderBy: { id: "asc" },
|
|
select: { id: true },
|
|
});
|
|
emailsDisabledAttendeeId = attendees[1].id;
|
|
});
|
|
|
|
it("should not send cancellation email when emailsEnabled is false", async () => {
|
|
await service.removeAttendee({
|
|
bookingId: emailsDisabledBookingId,
|
|
attendeeId: emailsDisabledAttendeeId,
|
|
user: testUser,
|
|
emailsEnabled: false,
|
|
});
|
|
|
|
expect(mockSendEmail).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("responses.guests cleanup with plus-addressed email", () => {
|
|
let plusAddressBookingId: number;
|
|
let plusAddressAttendeeId: number;
|
|
|
|
beforeAll(async () => {
|
|
const booking = await bookingRepo.createBookingForManagedEventReassignment({
|
|
uid: `attendee-svc-plus-addr-${timestamp}`,
|
|
userId: organizerId,
|
|
userPrimaryEmail: testUser.email,
|
|
eventTypeId: createdEventTypeIds[0],
|
|
title: `Plus Address Test Booking ${timestamp}`,
|
|
description: null,
|
|
startTime: new Date("2030-06-03T10:00:00.000Z"),
|
|
endTime: new Date("2030-06-03T10:30:00.000Z"),
|
|
status: BookingStatus.ACCEPTED,
|
|
location: null,
|
|
smsReminderNumber: null,
|
|
idempotencyKey: `attendee-svc-plus-addr-idempotency-${timestamp}`,
|
|
iCalUID: `attendee-svc-plus-addr-ical-${timestamp}@test.com`,
|
|
iCalSequence: 0,
|
|
attendees: [
|
|
{ email: "[email protected]", name: "Primary", timeZone: "UTC", locale: "en" },
|
|
{ email: "[email protected]", name: "Plus Guest", timeZone: "UTC", locale: "en" },
|
|
],
|
|
});
|
|
plusAddressBookingId = booking.id;
|
|
createdBookingIds.push(booking.id);
|
|
|
|
await prisma.booking.update({
|
|
where: { id: booking.id },
|
|
data: {
|
|
responses: {
|
|
name: "Primary",
|
|
email: "[email protected]",
|
|
guests: ["[email protected]"],
|
|
},
|
|
},
|
|
});
|
|
|
|
const attendees = await prisma.attendee.findMany({
|
|
where: { bookingId: booking.id },
|
|
orderBy: { id: "asc" },
|
|
select: { id: true },
|
|
});
|
|
plusAddressAttendeeId = attendees[1].id;
|
|
});
|
|
|
|
it("should remove plus-addressed email from responses.guests using extractBaseEmail", async () => {
|
|
await service.removeAttendee({
|
|
bookingId: plusAddressBookingId,
|
|
attendeeId: plusAddressAttendeeId,
|
|
user: testUser,
|
|
emailsEnabled: false,
|
|
});
|
|
|
|
const updatedBooking = await prisma.booking.findUnique({
|
|
where: { id: plusAddressBookingId },
|
|
select: { responses: true },
|
|
});
|
|
const responses = updatedBooking?.responses as { guests?: string[] } | null;
|
|
expect(responses?.guests ?? []).toEqual([]);
|
|
});
|
|
});
|
|
});
|