cleanup(test): remove org booking integration coverage (#29019)
This commit is contained in:
-257
@@ -1,257 +0,0 @@
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { getBookingAuditTaskConsumer } from "../../../di/BookingAuditTaskConsumer.container";
|
||||
import { getBookingAuditViewerService } from "../../../di/BookingAuditViewerService.container";
|
||||
import { makeUserActor } from "../../makeActor";
|
||||
import type { BookingAuditTaskConsumer } from "../BookingAuditTaskConsumer";
|
||||
import type { BookingAuditViewerService } from "../BookingAuditViewerService";
|
||||
import {
|
||||
cleanupTestData,
|
||||
createTestBooking,
|
||||
createTestEventType,
|
||||
createTestMembership,
|
||||
createTestOrganization,
|
||||
createTestUser,
|
||||
enableFeatureForOrganization,
|
||||
} from "./integration-utils";
|
||||
|
||||
describe("Accepted Action Integration", () => {
|
||||
let bookingAuditTaskConsumer: BookingAuditTaskConsumer;
|
||||
let bookingAuditViewerService: BookingAuditViewerService;
|
||||
|
||||
let testData: {
|
||||
owner: { id: number; uuid: string; email: string };
|
||||
attendee: { id: number; email: string };
|
||||
organization: { id: number };
|
||||
eventType: { id: number };
|
||||
booking: { uid: string; startTime: Date; endTime: Date; status: BookingStatus };
|
||||
};
|
||||
const additionalBookingUids: string[] = [];
|
||||
|
||||
beforeEach(async () => {
|
||||
bookingAuditTaskConsumer = getBookingAuditTaskConsumer();
|
||||
bookingAuditViewerService = getBookingAuditViewerService();
|
||||
|
||||
const owner = await createTestUser({ name: "Test Audit User" });
|
||||
const organization = await createTestOrganization();
|
||||
await createTestMembership(owner.id, organization.id);
|
||||
await enableFeatureForOrganization(organization.id, "booking-audit");
|
||||
const eventType = await createTestEventType(owner.id);
|
||||
const attendee = await createTestUser({ name: "Test Attendee" });
|
||||
|
||||
const booking = await createTestBooking(owner.id, eventType.id, {
|
||||
status: BookingStatus.PENDING,
|
||||
attendees: [
|
||||
{
|
||||
email: attendee.email,
|
||||
name: attendee.name || "Test Attendee",
|
||||
timeZone: "UTC",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
testData = {
|
||||
owner: { id: owner.id, uuid: owner.uuid, email: owner.email },
|
||||
attendee: { id: attendee.id, email: attendee.email },
|
||||
organization: { id: organization.id },
|
||||
eventType: { id: eventType.id },
|
||||
booking: {
|
||||
uid: booking.uid,
|
||||
startTime: booking.startTime,
|
||||
endTime: booking.endTime,
|
||||
status: booking.status,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
for (const bookingUid of additionalBookingUids) {
|
||||
await cleanupTestData({
|
||||
bookingUid,
|
||||
});
|
||||
}
|
||||
additionalBookingUids.length = 0; // Clear the array for next test
|
||||
|
||||
if (!testData) return;
|
||||
|
||||
await cleanupTestData({
|
||||
bookingUid: testData.booking?.uid,
|
||||
userUuids: testData.owner?.uuid ? [testData.owner.uuid] : [],
|
||||
attendeeEmails: testData.attendee?.email ? [testData.attendee.email] : [],
|
||||
eventTypeId: testData.eventType?.id,
|
||||
organizationId: testData.organization?.id,
|
||||
userIds: [testData.owner?.id, testData.attendee?.id].filter((id): id is number => id !== undefined),
|
||||
featureSlug: "booking-audit",
|
||||
});
|
||||
});
|
||||
|
||||
describe("when single booking is accepted", () => {
|
||||
it("should create audit record and retrieve it with correct data formatting", async () => {
|
||||
const actor = makeUserActor(testData.owner.uuid);
|
||||
|
||||
await bookingAuditTaskConsumer.onBookingAction({
|
||||
bookingUid: testData.booking.uid,
|
||||
actor,
|
||||
action: "ACCEPTED",
|
||||
source: "WEBAPP",
|
||||
operationId: `op-${Date.now()}`,
|
||||
data: {
|
||||
status: { old: BookingStatus.PENDING, new: BookingStatus.ACCEPTED },
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const result = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
expect(result.bookingUid).toBe(testData.booking.uid);
|
||||
expect(result.auditLogs).toHaveLength(1);
|
||||
|
||||
const auditLog = result.auditLogs[0];
|
||||
expect(auditLog.bookingUid).toBe(testData.booking.uid);
|
||||
expect(auditLog.action).toBe("ACCEPTED");
|
||||
expect(auditLog.type).toBe("RECORD_UPDATED");
|
||||
|
||||
const displayData = auditLog.displayJson as Record<string, unknown>;
|
||||
expect(displayData).toBeDefined();
|
||||
expect(displayData.previousStatus).toBe(BookingStatus.PENDING);
|
||||
expect(displayData.newStatus).toBe(BookingStatus.ACCEPTED);
|
||||
});
|
||||
|
||||
it("should enrich actor information with user details from database", async () => {
|
||||
const actor = makeUserActor(testData.owner.uuid);
|
||||
|
||||
await bookingAuditTaskConsumer.onBookingAction({
|
||||
bookingUid: testData.booking.uid,
|
||||
actor,
|
||||
action: "ACCEPTED",
|
||||
source: "WEBAPP",
|
||||
operationId: `op-${Date.now()}`,
|
||||
data: {
|
||||
status: { old: BookingStatus.PENDING, new: BookingStatus.ACCEPTED },
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const result = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
const auditLog = result.auditLogs[0];
|
||||
expect(auditLog.actor.displayName).toBe("Test Audit User");
|
||||
expect(auditLog.actor.displayEmail).toBe(testData.owner.email);
|
||||
expect(auditLog.actor.userUuid).toBe(testData.owner.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when multiple bookings are accepted in bulk", () => {
|
||||
it("should create audit records for all bookings with same operation ID", async () => {
|
||||
const booking2 = await createTestBooking(testData.owner.id, testData.eventType.id, {
|
||||
status: BookingStatus.PENDING,
|
||||
attendees: [
|
||||
{
|
||||
email: testData.attendee.email,
|
||||
name: "Test Attendee",
|
||||
timeZone: "UTC",
|
||||
},
|
||||
],
|
||||
});
|
||||
additionalBookingUids.push(booking2.uid);
|
||||
|
||||
const booking3 = await createTestBooking(testData.owner.id, testData.eventType.id, {
|
||||
status: BookingStatus.PENDING,
|
||||
attendees: [
|
||||
{
|
||||
email: testData.attendee.email,
|
||||
name: "Test Attendee",
|
||||
timeZone: "UTC",
|
||||
},
|
||||
],
|
||||
});
|
||||
additionalBookingUids.push(booking3.uid);
|
||||
|
||||
const actor = makeUserActor(testData.owner.uuid);
|
||||
const operationId = `bulk-op-${Date.now()}`;
|
||||
const timestamp = Date.now();
|
||||
|
||||
await bookingAuditTaskConsumer.processBulkAuditTask(
|
||||
{
|
||||
isBulk: true,
|
||||
bookings: [
|
||||
{
|
||||
bookingUid: testData.booking.uid,
|
||||
data: {
|
||||
status: { old: BookingStatus.PENDING, new: BookingStatus.ACCEPTED },
|
||||
},
|
||||
},
|
||||
{
|
||||
bookingUid: booking2.uid,
|
||||
data: {
|
||||
status: { old: BookingStatus.PENDING, new: BookingStatus.ACCEPTED },
|
||||
},
|
||||
},
|
||||
{
|
||||
bookingUid: booking3.uid,
|
||||
data: {
|
||||
status: { old: BookingStatus.PENDING, new: BookingStatus.ACCEPTED },
|
||||
},
|
||||
},
|
||||
],
|
||||
actor,
|
||||
action: "ACCEPTED",
|
||||
source: "WEBAPP",
|
||||
operationId,
|
||||
timestamp,
|
||||
organizationId: testData.organization.id,
|
||||
},
|
||||
"test-task-id"
|
||||
);
|
||||
|
||||
const result1 = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
const result2 = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: booking2.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
const result3 = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: booking3.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
expect(result1.auditLogs).toHaveLength(1);
|
||||
expect(result2.auditLogs).toHaveLength(1);
|
||||
expect(result3.auditLogs).toHaveLength(1);
|
||||
|
||||
expect(result1.auditLogs[0].action).toBe("ACCEPTED");
|
||||
expect(result2.auditLogs[0].action).toBe("ACCEPTED");
|
||||
expect(result3.auditLogs[0].action).toBe("ACCEPTED");
|
||||
|
||||
// Verify all bookings share the same operationId
|
||||
expect(result1.auditLogs[0].operationId).toBe(operationId);
|
||||
expect(result2.auditLogs[0].operationId).toBe(operationId);
|
||||
expect(result3.auditLogs[0].operationId).toBe(operationId);
|
||||
});
|
||||
});
|
||||
});
|
||||
-122
@@ -1,122 +0,0 @@
|
||||
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
||||
|
||||
import type { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { BookingAuditTaskConsumer } from "../BookingAuditTaskConsumer";
|
||||
import type { BookingAuditViewerService } from "../BookingAuditViewerService";
|
||||
import { makeUserActor } from "../../makeActor";
|
||||
import { getBookingAuditTaskConsumer } from "../../../di/BookingAuditTaskConsumer.container";
|
||||
import { getBookingAuditViewerService } from "../../../di/BookingAuditViewerService.container";
|
||||
import {
|
||||
createTestUser,
|
||||
createTestOrganization,
|
||||
createTestMembership,
|
||||
createTestEventType,
|
||||
createTestBooking,
|
||||
enableFeatureForOrganization,
|
||||
cleanupTestData,
|
||||
} from "./integration-utils";
|
||||
|
||||
describe("Attendee Added Action Integration", () => {
|
||||
let bookingAuditTaskConsumer: BookingAuditTaskConsumer;
|
||||
let bookingAuditViewerService: BookingAuditViewerService;
|
||||
|
||||
let testData: {
|
||||
owner: { id: number; uuid: string; email: string };
|
||||
attendee: { id: number; email: string };
|
||||
organization: { id: number };
|
||||
eventType: { id: number };
|
||||
booking: { uid: string; startTime: Date; endTime: Date; status: BookingStatus };
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
bookingAuditTaskConsumer = getBookingAuditTaskConsumer();
|
||||
bookingAuditViewerService = getBookingAuditViewerService();
|
||||
|
||||
const owner = await createTestUser({ name: "Test Audit User" });
|
||||
const organization = await createTestOrganization();
|
||||
await createTestMembership(owner.id, organization.id);
|
||||
await enableFeatureForOrganization(organization.id, "booking-audit");
|
||||
const eventType = await createTestEventType(owner.id);
|
||||
const attendee = await createTestUser({ name: "Test Attendee" });
|
||||
|
||||
const booking = await createTestBooking(owner.id, eventType.id, {
|
||||
attendees: [
|
||||
{
|
||||
email: attendee.email,
|
||||
name: attendee.name || "Test Attendee",
|
||||
timeZone: "UTC",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
testData = {
|
||||
owner: { id: owner.id, uuid: owner.uuid, email: owner.email },
|
||||
attendee: { id: attendee.id, email: attendee.email },
|
||||
organization: { id: organization.id },
|
||||
eventType: { id: eventType.id },
|
||||
booking: {
|
||||
uid: booking.uid,
|
||||
startTime: booking.startTime,
|
||||
endTime: booking.endTime,
|
||||
status: booking.status,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (!testData) return;
|
||||
|
||||
await cleanupTestData({
|
||||
bookingUid: testData.booking?.uid,
|
||||
userUuids: testData.owner?.uuid ? [testData.owner.uuid] : [],
|
||||
attendeeEmails: testData.attendee?.email ? [testData.attendee.email] : [],
|
||||
eventTypeId: testData.eventType?.id,
|
||||
organizationId: testData.organization?.id,
|
||||
userIds: [testData.owner?.id, testData.attendee?.id].filter((id): id is number => id !== undefined),
|
||||
featureSlug: "booking-audit",
|
||||
});
|
||||
});
|
||||
|
||||
describe("when guests are added to a booking", () => {
|
||||
it("should create audit record with correct added attendees data", async () => {
|
||||
const actor = makeUserActor(testData.owner.uuid);
|
||||
const newGuestEmails = ["guest1@example.com", "guest2@example.com"];
|
||||
|
||||
await bookingAuditTaskConsumer.onBookingAction({
|
||||
bookingUid: testData.booking.uid,
|
||||
actor,
|
||||
action: "ATTENDEE_ADDED",
|
||||
source: "WEBAPP",
|
||||
operationId: `op-${Date.now()}`,
|
||||
data: {
|
||||
added: newGuestEmails,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const result = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
expect(result.bookingUid).toBe(testData.booking.uid);
|
||||
expect(result.auditLogs).toHaveLength(1);
|
||||
|
||||
const auditLog = result.auditLogs[0];
|
||||
expect(auditLog.bookingUid).toBe(testData.booking.uid);
|
||||
expect(auditLog.action).toBe("ATTENDEE_ADDED");
|
||||
expect(auditLog.type).toBe("RECORD_UPDATED");
|
||||
|
||||
const displayData = auditLog.displayJson as { addedAttendees: string[] };
|
||||
expect(displayData).toBeDefined();
|
||||
expect(displayData.addedAttendees).toEqual(newGuestEmails);
|
||||
|
||||
expect(auditLog.actor.displayName).toBe("Test Audit User");
|
||||
expect(auditLog.actor.displayEmail).toBe(testData.owner.email);
|
||||
expect(auditLog.actor.userUuid).toBe(testData.owner.uuid);
|
||||
});
|
||||
});
|
||||
});
|
||||
-125
@@ -1,125 +0,0 @@
|
||||
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
||||
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import type { BookingAuditTaskConsumer } from "../BookingAuditTaskConsumer";
|
||||
import type { BookingAuditViewerService } from "../BookingAuditViewerService";
|
||||
import { makeUserActor } from "../../makeActor";
|
||||
import { getBookingAuditTaskConsumer } from "../../../di/BookingAuditTaskConsumer.container";
|
||||
import { getBookingAuditViewerService } from "../../../di/BookingAuditViewerService.container";
|
||||
import {
|
||||
createTestUser,
|
||||
createTestOrganization,
|
||||
createTestMembership,
|
||||
createTestEventType,
|
||||
createTestBooking,
|
||||
enableFeatureForOrganization,
|
||||
cleanupTestData,
|
||||
} from "./integration-utils";
|
||||
|
||||
describe("Cancelled Action Integration", () => {
|
||||
let bookingAuditTaskConsumer: BookingAuditTaskConsumer;
|
||||
let bookingAuditViewerService: BookingAuditViewerService;
|
||||
|
||||
let testData: {
|
||||
owner: { id: number; uuid: string; email: string };
|
||||
attendee: { id: number; email: string };
|
||||
organization: { id: number };
|
||||
eventType: { id: number };
|
||||
booking: { uid: string; startTime: Date; endTime: Date; status: BookingStatus };
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
bookingAuditTaskConsumer = getBookingAuditTaskConsumer();
|
||||
bookingAuditViewerService = getBookingAuditViewerService();
|
||||
|
||||
const owner = await createTestUser({ name: "Test Audit User" });
|
||||
const organization = await createTestOrganization();
|
||||
await createTestMembership(owner.id, organization.id);
|
||||
await enableFeatureForOrganization(organization.id, "booking-audit");
|
||||
const eventType = await createTestEventType(owner.id);
|
||||
const attendee = await createTestUser({ name: "Test Attendee" });
|
||||
|
||||
const booking = await createTestBooking(owner.id, eventType.id, {
|
||||
attendees: [
|
||||
{
|
||||
email: attendee.email,
|
||||
name: attendee.name || "Test Attendee",
|
||||
timeZone: "UTC",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
testData = {
|
||||
owner: { id: owner.id, uuid: owner.uuid, email: owner.email },
|
||||
attendee: { id: attendee.id, email: attendee.email },
|
||||
organization: { id: organization.id },
|
||||
eventType: { id: eventType.id },
|
||||
booking: {
|
||||
uid: booking.uid,
|
||||
startTime: booking.startTime,
|
||||
endTime: booking.endTime,
|
||||
status: booking.status,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (!testData) return;
|
||||
|
||||
await cleanupTestData({
|
||||
bookingUid: testData.booking?.uid,
|
||||
userUuids: testData.owner?.uuid ? [testData.owner.uuid] : [],
|
||||
attendeeEmails: testData.attendee?.email ? [testData.attendee.email] : [],
|
||||
eventTypeId: testData.eventType?.id,
|
||||
organizationId: testData.organization?.id,
|
||||
userIds: [testData.owner?.id, testData.attendee?.id].filter((id): id is number => id !== undefined),
|
||||
featureSlug: "booking-audit",
|
||||
});
|
||||
});
|
||||
|
||||
describe("when booking is cancelled", () => {
|
||||
it("should create audit record with cancellation details and retrieve it correctly", async () => {
|
||||
const actor = makeUserActor(testData.owner.uuid);
|
||||
const cancellationReason = "Schedule conflict";
|
||||
const cancelledBy = "owner";
|
||||
|
||||
await bookingAuditTaskConsumer.onBookingAction({
|
||||
bookingUid: testData.booking.uid,
|
||||
actor,
|
||||
action: "CANCELLED",
|
||||
source: "WEBAPP",
|
||||
operationId: `op-${Date.now()}`,
|
||||
data: {
|
||||
cancellationReason,
|
||||
cancelledBy,
|
||||
status: {
|
||||
old: BookingStatus.ACCEPTED,
|
||||
new: BookingStatus.CANCELLED,
|
||||
},
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const result = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
expect(result.auditLogs).toHaveLength(1);
|
||||
const auditLog = result.auditLogs[0];
|
||||
|
||||
expect(auditLog.action).toBe("CANCELLED");
|
||||
expect(auditLog.type).toBe("RECORD_UPDATED");
|
||||
|
||||
const displayData = auditLog.displayJson as Record<string, unknown>;
|
||||
expect(displayData.cancellationReason).toBe(cancellationReason);
|
||||
expect(displayData.cancelledBy).toBe(cancelledBy);
|
||||
expect(displayData.previousStatus).toBe(BookingStatus.ACCEPTED);
|
||||
expect(displayData.newStatus).toBe(BookingStatus.CANCELLED);
|
||||
});
|
||||
});
|
||||
});
|
||||
-359
@@ -1,359 +0,0 @@
|
||||
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
||||
|
||||
import type { BookingStatus } from "@calcom/prisma/enums";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { BookingAuditTaskConsumer } from "../BookingAuditTaskConsumer";
|
||||
import type { BookingAuditViewerService } from "../BookingAuditViewerService";
|
||||
import { makeUserActor } from "../../makeActor";
|
||||
import { getBookingAuditTaskConsumer } from "../../../di/BookingAuditTaskConsumer.container";
|
||||
import { getBookingAuditViewerService } from "../../../di/BookingAuditViewerService.container";
|
||||
import {
|
||||
createTestUser,
|
||||
createTestOrganization,
|
||||
createTestMembership,
|
||||
createTestEventType,
|
||||
createTestBooking,
|
||||
enableFeatureForOrganization,
|
||||
cleanupTestData,
|
||||
} from "./integration-utils";
|
||||
|
||||
describe("Created Action Integration", () => {
|
||||
let bookingAuditTaskConsumer: BookingAuditTaskConsumer;
|
||||
let bookingAuditViewerService: BookingAuditViewerService;
|
||||
|
||||
let testData: {
|
||||
owner: { id: number; uuid: string; email: string };
|
||||
attendee: { id: number; email: string };
|
||||
organization: { id: number };
|
||||
eventType: { id: number };
|
||||
booking: { uid: string; startTime: Date; endTime: Date; status: BookingStatus };
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
bookingAuditTaskConsumer = getBookingAuditTaskConsumer();
|
||||
bookingAuditViewerService = getBookingAuditViewerService();
|
||||
|
||||
const owner = await createTestUser({ name: "Test Audit User" });
|
||||
const organization = await createTestOrganization();
|
||||
await createTestMembership(owner.id, organization.id);
|
||||
await enableFeatureForOrganization(organization.id, "booking-audit");
|
||||
const eventType = await createTestEventType(owner.id);
|
||||
const attendee = await createTestUser({ name: "Test Attendee" });
|
||||
|
||||
const booking = await createTestBooking(owner.id, eventType.id, {
|
||||
attendees: [
|
||||
{
|
||||
email: attendee.email,
|
||||
name: attendee.name || "Test Attendee",
|
||||
timeZone: "UTC",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
testData = {
|
||||
owner: { id: owner.id, uuid: owner.uuid, email: owner.email },
|
||||
attendee: { id: attendee.id, email: attendee.email },
|
||||
organization: { id: organization.id },
|
||||
eventType: { id: eventType.id },
|
||||
booking: {
|
||||
uid: booking.uid,
|
||||
startTime: booking.startTime,
|
||||
endTime: booking.endTime,
|
||||
status: booking.status,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (!testData) return;
|
||||
|
||||
await cleanupTestData({
|
||||
bookingUid: testData.booking?.uid,
|
||||
userUuids: testData.owner?.uuid ? [testData.owner.uuid] : [],
|
||||
attendeeEmails: testData.attendee?.email ? [testData.attendee.email] : [],
|
||||
eventTypeId: testData.eventType?.id,
|
||||
organizationId: testData.organization?.id,
|
||||
userIds: [testData.owner?.id, testData.attendee?.id].filter((id): id is number => id !== undefined),
|
||||
featureSlug: "booking-audit",
|
||||
});
|
||||
});
|
||||
|
||||
describe("when single booking is created", () => {
|
||||
it("should create audit record and retrieve it with correct data formatting", async () => {
|
||||
const actor = makeUserActor(testData.owner.uuid);
|
||||
|
||||
await bookingAuditTaskConsumer.onBookingAction({
|
||||
bookingUid: testData.booking.uid,
|
||||
actor,
|
||||
action: "CREATED",
|
||||
source: "WEBAPP",
|
||||
operationId: `op-${Date.now()}`,
|
||||
data: {
|
||||
startTime: testData.booking.startTime.getTime(),
|
||||
endTime: testData.booking.endTime.getTime(),
|
||||
status: testData.booking.status,
|
||||
hostUserUuid: testData.owner.uuid,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const result = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
expect(result.bookingUid).toBe(testData.booking.uid);
|
||||
expect(result.auditLogs).toHaveLength(1);
|
||||
|
||||
const auditLog = result.auditLogs[0];
|
||||
expect(auditLog.bookingUid).toBe(testData.booking.uid);
|
||||
expect(auditLog.action).toBe("CREATED");
|
||||
expect(auditLog.type).toBe("RECORD_CREATED");
|
||||
|
||||
const displayData = auditLog.displayJson as Record<string, unknown>;
|
||||
expect(displayData).toBeDefined();
|
||||
expect(displayData.startTime).toBeDefined();
|
||||
expect(typeof displayData.startTime).toBe("string");
|
||||
expect(displayData.startTime).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
|
||||
|
||||
expect(displayData.endTime).toBeDefined();
|
||||
expect(typeof displayData.endTime).toBe("string");
|
||||
expect(displayData.endTime).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
|
||||
|
||||
expect(displayData.status).toBe(testData.booking.status);
|
||||
});
|
||||
|
||||
it("should enrich actor information with user details from database", async () => {
|
||||
const actor = makeUserActor(testData.owner.uuid);
|
||||
|
||||
await bookingAuditTaskConsumer.onBookingAction({
|
||||
bookingUid: testData.booking.uid,
|
||||
actor,
|
||||
action: "CREATED",
|
||||
source: "WEBAPP",
|
||||
operationId: `op-${Date.now()}`,
|
||||
data: {
|
||||
startTime: testData.booking.startTime.getTime(),
|
||||
endTime: testData.booking.endTime.getTime(),
|
||||
status: testData.booking.status,
|
||||
hostUserUuid: testData.owner.uuid,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const result = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
const auditLog = result.auditLogs[0];
|
||||
expect(auditLog.actor.displayName).toBe("Test Audit User");
|
||||
expect(auditLog.actor.displayEmail).toBe(testData.owner.email);
|
||||
expect(auditLog.actor.userUuid).toBe(testData.owner.uuid);
|
||||
});
|
||||
|
||||
it("should include impersonator details when context has impersonatedBy", async () => {
|
||||
// Create a second user to act as impersonator
|
||||
const impersonator = await createTestUser({ name: "Admin Impersonator" });
|
||||
|
||||
const actor = makeUserActor(testData.owner.uuid);
|
||||
|
||||
await bookingAuditTaskConsumer.onBookingAction({
|
||||
bookingUid: testData.booking.uid,
|
||||
actor,
|
||||
action: "CREATED",
|
||||
source: "WEBAPP",
|
||||
operationId: `op-${Date.now()}`,
|
||||
data: {
|
||||
startTime: testData.booking.startTime.getTime(),
|
||||
endTime: testData.booking.endTime.getTime(),
|
||||
status: testData.booking.status,
|
||||
hostUserUuid: testData.owner.uuid,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
context: {
|
||||
impersonatedBy: impersonator.uuid,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
expect(result.auditLogs).toHaveLength(1);
|
||||
|
||||
const auditLog = result.auditLogs[0];
|
||||
expect(auditLog.impersonatedBy).toBeDefined();
|
||||
expect(auditLog.impersonatedBy?.displayName).toBe("Admin Impersonator");
|
||||
expect(auditLog.impersonatedBy?.displayEmail).toBe(impersonator.email);
|
||||
|
||||
// Cleanup impersonator user
|
||||
await prisma.user.delete({ where: { id: impersonator.id } });
|
||||
});
|
||||
|
||||
it.skip("should deny access to unauthorized users viewing audit logs", async () => {
|
||||
const actor = makeUserActor(testData.owner.uuid);
|
||||
|
||||
await bookingAuditTaskConsumer.onBookingAction({
|
||||
bookingUid: testData.booking.uid,
|
||||
actor,
|
||||
action: "CREATED",
|
||||
source: "WEBAPP",
|
||||
operationId: `op-${Date.now()}`,
|
||||
data: {
|
||||
startTime: testData.booking.startTime.getTime(),
|
||||
endTime: testData.booking.endTime.getTime(),
|
||||
status: testData.booking.status,
|
||||
hostUserUuid: testData.owner.uuid,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const ownerResult = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
expect(ownerResult.auditLogs).toHaveLength(1);
|
||||
|
||||
const unauthorizedUserId = 999999;
|
||||
const unauthorizedEmail = "unauthorized@example.com";
|
||||
|
||||
await expect(
|
||||
bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: unauthorizedUserId,
|
||||
userEmail: unauthorizedEmail,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when multiple bookings are created in bulk", () => {
|
||||
it("should create audit records for all bookings with same operation ID", async () => {
|
||||
const booking2 = await createTestBooking(testData.owner.id, testData.eventType.id, {
|
||||
attendees: [
|
||||
{
|
||||
email: testData.attendee.email,
|
||||
name: "Test Attendee",
|
||||
timeZone: "UTC",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const booking3 = await createTestBooking(testData.owner.id, testData.eventType.id, {
|
||||
attendees: [
|
||||
{
|
||||
email: testData.attendee.email,
|
||||
name: "Test Attendee",
|
||||
timeZone: "UTC",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const actor = makeUserActor(testData.owner.uuid);
|
||||
const operationId = `bulk-op-${Date.now()}`;
|
||||
const timestamp = Date.now();
|
||||
|
||||
await bookingAuditTaskConsumer.processBulkAuditTask(
|
||||
{
|
||||
isBulk: true,
|
||||
bookings: [
|
||||
{
|
||||
bookingUid: testData.booking.uid,
|
||||
data: {
|
||||
startTime: testData.booking.startTime.getTime(),
|
||||
endTime: testData.booking.endTime.getTime(),
|
||||
status: testData.booking.status,
|
||||
hostUserUuid: testData.owner.uuid,
|
||||
},
|
||||
},
|
||||
{
|
||||
bookingUid: booking2.uid,
|
||||
data: {
|
||||
startTime: booking2.startTime.getTime(),
|
||||
endTime: booking2.endTime.getTime(),
|
||||
status: booking2.status,
|
||||
hostUserUuid: testData.owner.uuid,
|
||||
},
|
||||
},
|
||||
{
|
||||
bookingUid: booking3.uid,
|
||||
data: {
|
||||
startTime: booking3.startTime.getTime(),
|
||||
endTime: booking3.endTime.getTime(),
|
||||
status: booking3.status,
|
||||
hostUserUuid: testData.owner.uuid,
|
||||
},
|
||||
},
|
||||
],
|
||||
actor,
|
||||
action: "CREATED",
|
||||
source: "WEBAPP",
|
||||
operationId,
|
||||
timestamp,
|
||||
organizationId: testData.organization.id,
|
||||
},
|
||||
"test-task-id"
|
||||
);
|
||||
|
||||
const result1 = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
const result2 = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: booking2.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
const result3 = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: booking3.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
expect(result1.auditLogs).toHaveLength(1);
|
||||
expect(result2.auditLogs).toHaveLength(1);
|
||||
expect(result3.auditLogs).toHaveLength(1);
|
||||
|
||||
expect(result1.auditLogs[0].action).toBe("CREATED");
|
||||
expect(result2.auditLogs[0].action).toBe("CREATED");
|
||||
expect(result3.auditLogs[0].action).toBe("CREATED");
|
||||
|
||||
// Verify all bookings share the same operationId
|
||||
expect(result1.auditLogs[0].operationId).toBe(operationId);
|
||||
expect(result2.auditLogs[0].operationId).toBe(operationId);
|
||||
expect(result3.auditLogs[0].operationId).toBe(operationId);
|
||||
|
||||
await cleanupTestData({
|
||||
bookingUid: booking2.uid,
|
||||
});
|
||||
await cleanupTestData({
|
||||
bookingUid: booking3.uid,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
-173
@@ -1,173 +0,0 @@
|
||||
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
||||
|
||||
import type { BookingAuditTaskConsumer } from "../BookingAuditTaskConsumer";
|
||||
import type { BookingAuditViewerService } from "../BookingAuditViewerService";
|
||||
import { makeUserActor } from "../../makeActor";
|
||||
import { getBookingAuditTaskConsumer } from "../../../di/BookingAuditTaskConsumer.container";
|
||||
import { getBookingAuditViewerService } from "../../../di/BookingAuditViewerService.container";
|
||||
import {
|
||||
createTestUser,
|
||||
createTestOrganization,
|
||||
createTestMembership,
|
||||
createTestEventType,
|
||||
createTestBooking,
|
||||
enableFeatureForOrganization,
|
||||
cleanupTestData,
|
||||
} from "./integration-utils";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
describe("Location Changed Action Integration", () => {
|
||||
let bookingAuditTaskConsumer: BookingAuditTaskConsumer;
|
||||
let bookingAuditViewerService: BookingAuditViewerService;
|
||||
|
||||
let testData: {
|
||||
owner: { id: number; uuid: string; email: string };
|
||||
attendee: { id: number; email: string };
|
||||
organization: { id: number };
|
||||
eventType: { id: number };
|
||||
booking: { uid: string; startTime: Date; endTime: Date; status: BookingStatus };
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
bookingAuditTaskConsumer = getBookingAuditTaskConsumer();
|
||||
bookingAuditViewerService = getBookingAuditViewerService();
|
||||
|
||||
// Setup basic test data using Test Data Builder pattern
|
||||
const owner = await createTestUser({ name: "Location Audit User" });
|
||||
const organization = await createTestOrganization();
|
||||
await createTestMembership(owner.id, organization.id);
|
||||
await enableFeatureForOrganization(organization.id, "booking-audit");
|
||||
const eventType = await createTestEventType(owner.id);
|
||||
const attendee = await createTestUser({ name: "Test Attendee" });
|
||||
|
||||
const booking = await createTestBooking(owner.id, eventType.id, {
|
||||
attendees: [
|
||||
{
|
||||
email: attendee.email,
|
||||
name: attendee.name || "Test Attendee",
|
||||
timeZone: "UTC",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
testData = {
|
||||
owner: { id: owner.id, uuid: owner.uuid, email: owner.email },
|
||||
attendee: { id: attendee.id, email: attendee.email },
|
||||
organization: { id: organization.id },
|
||||
eventType: { id: eventType.id },
|
||||
booking: {
|
||||
uid: booking.uid,
|
||||
startTime: booking.startTime,
|
||||
endTime: booking.endTime,
|
||||
status: booking.status,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (!testData) return;
|
||||
|
||||
await cleanupTestData({
|
||||
bookingUid: testData.booking?.uid,
|
||||
userUuids: testData.owner?.uuid ? [testData.owner.uuid] : [],
|
||||
attendeeEmails: testData.attendee?.email ? [testData.attendee.email] : [],
|
||||
eventTypeId: testData.eventType?.id,
|
||||
organizationId: testData.organization?.id,
|
||||
userIds: [testData.owner?.id, testData.attendee?.id].filter((id): id is number => id !== undefined),
|
||||
featureSlug: "booking-audit",
|
||||
});
|
||||
});
|
||||
|
||||
describe("when location is changed", () => {
|
||||
it("should create audit record and retrieve it with correct location data", async () => {
|
||||
const actor = makeUserActor(testData.owner.uuid);
|
||||
const operationId = `op-${Date.now()}`;
|
||||
|
||||
// Simulate LOCATION_CHANGED action
|
||||
await bookingAuditTaskConsumer.onBookingAction({
|
||||
bookingUid: testData.booking.uid,
|
||||
actor,
|
||||
action: "LOCATION_CHANGED",
|
||||
source: "WEBAPP",
|
||||
operationId,
|
||||
data: {
|
||||
location: {
|
||||
old: "Zoom",
|
||||
new: "Google Meet",
|
||||
},
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// Retrieve logs
|
||||
const result = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
expect(result.bookingUid).toBe(testData.booking.uid);
|
||||
|
||||
// Find the specific log we just created
|
||||
const auditLog = result.auditLogs.find((log) => log.operationId === operationId);
|
||||
expect(auditLog).toBeDefined();
|
||||
|
||||
if (!auditLog) throw new Error("Audit log not found");
|
||||
|
||||
expect(auditLog.action).toBe("LOCATION_CHANGED");
|
||||
// The type identifier from the service
|
||||
expect(auditLog.type).toBe("RECORD_UPDATED");
|
||||
|
||||
expect(auditLog.displayJson).toBeNull();
|
||||
|
||||
// Verify the title params are correct
|
||||
expect(auditLog.actionDisplayTitle).toBeDefined();
|
||||
expect(auditLog.actionDisplayTitle.key).toBe("booking_audit_action.location_changed_from_to");
|
||||
expect(auditLog.actionDisplayTitle.params).toEqual({
|
||||
fromLocation: "Zoom",
|
||||
toLocation: "Google Meet",
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle location change where old value was null", async () => {
|
||||
const actor = makeUserActor(testData.owner.uuid);
|
||||
const operationId = `op-initial-${Date.now()}`;
|
||||
|
||||
// Simulate setting location for the first time (old is null)
|
||||
await bookingAuditTaskConsumer.onBookingAction({
|
||||
bookingUid: testData.booking.uid,
|
||||
actor,
|
||||
action: "LOCATION_CHANGED",
|
||||
source: "WEBAPP",
|
||||
operationId,
|
||||
data: {
|
||||
location: {
|
||||
old: null,
|
||||
new: "In Person",
|
||||
},
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const result = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
const auditLog = result.auditLogs.find((log) => log.operationId === operationId);
|
||||
expect(auditLog).toBeDefined();
|
||||
|
||||
expect(auditLog!.displayJson).toBeNull();
|
||||
|
||||
expect(auditLog!.actionDisplayTitle.params).toEqual({
|
||||
fromLocation: "No location defined",
|
||||
toLocation: "In Person",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
-320
@@ -1,320 +0,0 @@
|
||||
import type { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { getBookingAuditTaskConsumer } from "../../../di/BookingAuditTaskConsumer.container";
|
||||
import { getBookingAuditViewerService } from "../../../di/BookingAuditViewerService.container";
|
||||
import { makeUserActor } from "../../makeActor";
|
||||
import type { BookingAuditTaskConsumer } from "../BookingAuditTaskConsumer";
|
||||
import type { BookingAuditViewerService } from "../BookingAuditViewerService";
|
||||
import {
|
||||
cleanupTestData,
|
||||
createTestBooking,
|
||||
createTestEventType,
|
||||
createTestMembership,
|
||||
createTestOrganization,
|
||||
createTestUser,
|
||||
enableFeatureForOrganization,
|
||||
} from "./integration-utils";
|
||||
|
||||
describe("No-Show Updated Action Integration", () => {
|
||||
let bookingAuditTaskConsumer: BookingAuditTaskConsumer;
|
||||
let bookingAuditViewerService: BookingAuditViewerService;
|
||||
|
||||
let testData: {
|
||||
owner: { id: number; uuid: string; email: string };
|
||||
attendee: { id: number; email: string };
|
||||
organization: { id: number };
|
||||
eventType: { id: number };
|
||||
booking: { id: number; uid: string; startTime: Date; endTime: Date; status: BookingStatus };
|
||||
};
|
||||
|
||||
const additionalAttendeeEmails: string[] = [];
|
||||
|
||||
beforeEach(async () => {
|
||||
bookingAuditTaskConsumer = getBookingAuditTaskConsumer();
|
||||
bookingAuditViewerService = getBookingAuditViewerService();
|
||||
|
||||
const owner = await createTestUser({ name: "Test Host User" });
|
||||
const organization = await createTestOrganization();
|
||||
await createTestMembership(owner.id, organization.id);
|
||||
await enableFeatureForOrganization(organization.id, "booking-audit");
|
||||
const eventType = await createTestEventType(owner.id);
|
||||
const attendee = await createTestUser({ name: "Test Attendee" });
|
||||
|
||||
const booking = await createTestBooking(owner.id, eventType.id, {
|
||||
attendees: [
|
||||
{
|
||||
email: attendee.email,
|
||||
name: attendee.name || "Test Attendee",
|
||||
timeZone: "UTC",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
testData = {
|
||||
owner: { id: owner.id, uuid: owner.uuid, email: owner.email },
|
||||
attendee: { id: attendee.id, email: attendee.email },
|
||||
organization: { id: organization.id },
|
||||
eventType: { id: eventType.id },
|
||||
booking: {
|
||||
id: booking.id,
|
||||
uid: booking.uid,
|
||||
startTime: booking.startTime,
|
||||
endTime: booking.endTime,
|
||||
status: booking.status,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (!testData) return;
|
||||
|
||||
await cleanupTestData({
|
||||
bookingUid: testData.booking?.uid,
|
||||
userUuids: testData.owner?.uuid ? [testData.owner.uuid] : [],
|
||||
attendeeEmails: [
|
||||
...(testData.attendee?.email ? [testData.attendee.email] : []),
|
||||
...additionalAttendeeEmails,
|
||||
],
|
||||
eventTypeId: testData.eventType?.id,
|
||||
organizationId: testData.organization?.id,
|
||||
userIds: [testData.owner?.id, testData.attendee?.id].filter((id): id is number => id !== undefined),
|
||||
featureSlug: "booking-audit",
|
||||
});
|
||||
additionalAttendeeEmails.length = 0;
|
||||
});
|
||||
|
||||
describe("when host is marked as no-show", () => {
|
||||
it("should create audit record with host field containing userUuid and noShow", async () => {
|
||||
const actor = makeUserActor(testData.owner.uuid);
|
||||
|
||||
await bookingAuditTaskConsumer.onBookingAction({
|
||||
bookingUid: testData.booking.uid,
|
||||
actor,
|
||||
action: "NO_SHOW_UPDATED",
|
||||
source: "WEBAPP",
|
||||
operationId: `op-${Date.now()}`,
|
||||
data: {
|
||||
// New schema: host contains userUuid and noShow change
|
||||
host: {
|
||||
userUuid: testData.owner.uuid,
|
||||
noShow: { old: null, new: true },
|
||||
},
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const result = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
expect(result.bookingUid).toBe(testData.booking.uid);
|
||||
expect(result.auditLogs).toHaveLength(1);
|
||||
|
||||
const auditLog = result.auditLogs[0];
|
||||
expect(auditLog.bookingUid).toBe(testData.booking.uid);
|
||||
expect(auditLog.action).toBe("NO_SHOW_UPDATED");
|
||||
expect(auditLog.type).toBe("RECORD_UPDATED");
|
||||
|
||||
const displayData = auditLog.displayJson as Record<string, unknown>;
|
||||
expect(displayData).toBeDefined();
|
||||
expect(displayData.hostNoShow).toBe(true);
|
||||
expect(displayData.previousHostNoShow).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when attendees are marked as no-show", () => {
|
||||
it("should create audit record with attendeesNoShow array", async () => {
|
||||
const actor = makeUserActor(testData.owner.uuid);
|
||||
|
||||
const attendeesNoShow = [{ attendeeEmail: testData.attendee.email, noShow: { old: null, new: true } }];
|
||||
|
||||
await bookingAuditTaskConsumer.onBookingAction({
|
||||
bookingUid: testData.booking.uid,
|
||||
actor,
|
||||
action: "NO_SHOW_UPDATED",
|
||||
source: "WEBAPP",
|
||||
operationId: `op-${Date.now()}`,
|
||||
data: {
|
||||
attendeesNoShow,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const result = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
expect(result.bookingUid).toBe(testData.booking.uid);
|
||||
expect(result.auditLogs).toHaveLength(1);
|
||||
|
||||
const auditLog = result.auditLogs[0];
|
||||
expect(auditLog.action).toBe("NO_SHOW_UPDATED");
|
||||
expect(auditLog.type).toBe("RECORD_UPDATED");
|
||||
|
||||
const displayData = auditLog.displayJson as Record<string, unknown>;
|
||||
expect(displayData).toBeDefined();
|
||||
expect(displayData.attendeesNoShow).toBeDefined();
|
||||
|
||||
const storedAttendeesNoShow = displayData.attendeesNoShow as Array<{
|
||||
attendeeEmail: string;
|
||||
noShow: { old: boolean | null; new: boolean };
|
||||
}>;
|
||||
expect(storedAttendeesNoShow).toHaveLength(1);
|
||||
expect(storedAttendeesNoShow[0].attendeeEmail).toBe(testData.attendee.email);
|
||||
expect(storedAttendeesNoShow[0].noShow.old).toBe(null);
|
||||
expect(storedAttendeesNoShow[0].noShow.new).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle multiple attendees marked as no-show", async () => {
|
||||
const { prisma } = await import("@calcom/prisma");
|
||||
const secondAttendeeEmail = `second-attendee-${Date.now()}@example.com`;
|
||||
additionalAttendeeEmails.push(secondAttendeeEmail);
|
||||
await prisma.attendee.create({
|
||||
data: {
|
||||
email: secondAttendeeEmail,
|
||||
name: "Second Attendee",
|
||||
timeZone: "UTC",
|
||||
bookingId: testData.booking.id,
|
||||
},
|
||||
});
|
||||
|
||||
const actor = makeUserActor(testData.owner.uuid);
|
||||
|
||||
const attendeesNoShow = [
|
||||
{ attendeeEmail: testData.attendee.email, noShow: { old: null, new: true } },
|
||||
{ attendeeEmail: secondAttendeeEmail, noShow: { old: false, new: true } },
|
||||
];
|
||||
|
||||
await bookingAuditTaskConsumer.onBookingAction({
|
||||
bookingUid: testData.booking.uid,
|
||||
actor,
|
||||
action: "NO_SHOW_UPDATED",
|
||||
source: "WEBAPP",
|
||||
operationId: `op-${Date.now()}`,
|
||||
data: {
|
||||
attendeesNoShow,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const result = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
expect(result.auditLogs).toHaveLength(1);
|
||||
|
||||
const displayData = result.auditLogs[0].displayJson as Record<string, unknown>;
|
||||
const storedAttendeesNoShow = displayData.attendeesNoShow as Array<{
|
||||
attendeeEmail: string;
|
||||
noShow: { old: boolean | null; new: boolean };
|
||||
}>;
|
||||
|
||||
expect(storedAttendeesNoShow).toHaveLength(2);
|
||||
const firstAttendee = storedAttendeesNoShow.find((a) => a.attendeeEmail === testData.attendee.email);
|
||||
const secondAttendeeData = storedAttendeesNoShow.find((a) => a.attendeeEmail === secondAttendeeEmail);
|
||||
expect(firstAttendee?.noShow).toEqual({ old: null, new: true });
|
||||
expect(secondAttendeeData?.noShow).toEqual({ old: false, new: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("when both host and attendees are marked as no-show", () => {
|
||||
it("should create single audit record with both host and attendeesNoShow fields", async () => {
|
||||
const actor = makeUserActor(testData.owner.uuid);
|
||||
|
||||
await bookingAuditTaskConsumer.onBookingAction({
|
||||
bookingUid: testData.booking.uid,
|
||||
actor,
|
||||
action: "NO_SHOW_UPDATED",
|
||||
source: "API_V2",
|
||||
operationId: `op-${Date.now()}`,
|
||||
data: {
|
||||
host: {
|
||||
userUuid: testData.owner.uuid,
|
||||
noShow: { old: null, new: true },
|
||||
},
|
||||
attendeesNoShow: [{ attendeeEmail: testData.attendee.email, noShow: { old: null, new: true } }],
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const result = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
expect(result.auditLogs).toHaveLength(1);
|
||||
|
||||
const auditLog = result.auditLogs[0];
|
||||
expect(auditLog.action).toBe("NO_SHOW_UPDATED");
|
||||
expect(auditLog.source).toBe("API_V2");
|
||||
|
||||
const displayData = auditLog.displayJson as Record<string, unknown>;
|
||||
|
||||
expect(displayData.hostNoShow).toBe(true);
|
||||
expect(displayData.previousHostNoShow).toBe(null);
|
||||
|
||||
const storedAttendeesNoShow = displayData.attendeesNoShow as Array<{
|
||||
attendeeEmail: string;
|
||||
noShow: { old: boolean | null; new: boolean };
|
||||
}>;
|
||||
expect(storedAttendeesNoShow).toHaveLength(1);
|
||||
expect(storedAttendeesNoShow[0].attendeeEmail).toBe(testData.attendee.email);
|
||||
expect(storedAttendeesNoShow[0].noShow).toEqual({ old: null, new: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("schema validation with array format", () => {
|
||||
it("should accept attendeesNoShow data with array format", async () => {
|
||||
const actor = makeUserActor(testData.owner.uuid);
|
||||
|
||||
const dataWithArrayFormat = {
|
||||
attendeesNoShow: [{ attendeeEmail: testData.attendee.email, noShow: { old: null, new: true } }],
|
||||
};
|
||||
|
||||
await bookingAuditTaskConsumer.onBookingAction({
|
||||
bookingUid: testData.booking.uid,
|
||||
actor,
|
||||
action: "NO_SHOW_UPDATED",
|
||||
source: "WEBAPP",
|
||||
operationId: `op-${Date.now()}`,
|
||||
data: dataWithArrayFormat,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const result = await bookingAuditViewerService.getAuditLogsForBooking({
|
||||
bookingUid: testData.booking.uid,
|
||||
userId: testData.owner.id,
|
||||
userEmail: testData.owner.email,
|
||||
userTimeZone: "UTC",
|
||||
organizationId: testData.organization.id,
|
||||
});
|
||||
|
||||
expect(result.auditLogs).toHaveLength(1);
|
||||
|
||||
const displayData = result.auditLogs[0].displayJson as Record<string, unknown>;
|
||||
expect(displayData.attendeesNoShow).toBeDefined();
|
||||
|
||||
const storedAttendeesNoShow = displayData.attendeesNoShow as Array<{
|
||||
attendeeEmail: string;
|
||||
noShow: { old: boolean | null; new: boolean };
|
||||
}>;
|
||||
expect(storedAttendeesNoShow).toHaveLength(1);
|
||||
expect(storedAttendeesNoShow[0].attendeeEmail).toBe(testData.attendee.email);
|
||||
});
|
||||
});
|
||||
});
|
||||
-485
@@ -8,7 +8,6 @@ import {
|
||||
mockCalendarToHaveNoBusySlots,
|
||||
mockCalendarToCrashOnGetAvailability,
|
||||
BookingLocations,
|
||||
createOrganization,
|
||||
} from "@calcom/testing/lib/bookingScenario/bookingScenario";
|
||||
import { getMockRequestDataForBooking } from "@calcom/testing/lib/bookingScenario/getMockRequestDataForBooking";
|
||||
import { setupAndTeardown } from "@calcom/testing/lib/bookingScenario/setupAndTeardown";
|
||||
@@ -54,22 +53,6 @@ const createGlobalWatchlistEntry = async (overrides: {
|
||||
});
|
||||
};
|
||||
|
||||
const createOrganizationWatchlistEntry = async (
|
||||
organizationId: number,
|
||||
overrides: {
|
||||
type: WatchlistType;
|
||||
value: string;
|
||||
action: "BLOCK" | "REPORT";
|
||||
}
|
||||
) => {
|
||||
return createTestWatchlistEntry({
|
||||
type: overrides.type,
|
||||
value: overrides.value,
|
||||
action: overrides.action,
|
||||
organizationId,
|
||||
});
|
||||
};
|
||||
|
||||
const expectDecoyBookingResponse = (booking: Record<string, unknown>) => {
|
||||
expect(booking).toHaveProperty("isShortCircuitedBooking", true);
|
||||
expect(booking).toHaveProperty("uid");
|
||||
@@ -402,472 +385,4 @@ describe("handleNewBooking - Spam Detection", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("Organization Watchlist Blocking:", () => {
|
||||
test(
|
||||
"should block booking when email is in organization watchlist and return decoy response",
|
||||
async () => {
|
||||
const handleNewBooking = getNewBookingHandler();
|
||||
const blockedEmail = "org-spammer@example.com";
|
||||
|
||||
// Create organization with a team
|
||||
const org = await createOrganization({
|
||||
name: "Test Org",
|
||||
slug: "test-org",
|
||||
withTeam: true,
|
||||
});
|
||||
|
||||
const booker = getBooker({
|
||||
email: blockedEmail,
|
||||
name: "Org Blocked Booker",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
organizationId: org.id,
|
||||
});
|
||||
|
||||
await createOrganizationWatchlistEntry(org.id, {
|
||||
type: WatchlistType.EMAIL,
|
||||
value: blockedEmail,
|
||||
action: "BLOCK",
|
||||
});
|
||||
|
||||
// Use the child team ID for the eventType
|
||||
const teamId = org.children && org.children.length > 0 ? org.children[0].id : null;
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData(
|
||||
{
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 30,
|
||||
length: 30,
|
||||
teamId,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
},
|
||||
{ id: org.id }
|
||||
)
|
||||
);
|
||||
|
||||
await mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
user: organizer.username,
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createdBooking = await handleNewBooking({
|
||||
bookingData: mockBookingData,
|
||||
});
|
||||
|
||||
expectDecoyBookingResponse(createdBooking);
|
||||
expect(createdBooking.attendees[0].email).toBe(blockedEmail);
|
||||
await expectNoBookingInDatabase(blockedEmail);
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
test(
|
||||
"should block booking when domain is in organization watchlist and return decoy response",
|
||||
async () => {
|
||||
const handleNewBooking = getNewBookingHandler();
|
||||
const blockedDomain = "spammydomain.com";
|
||||
const blockedEmail = `user@${blockedDomain}`;
|
||||
|
||||
// Create organization with a team
|
||||
const org = await createOrganization({
|
||||
name: "Test Org",
|
||||
slug: "test-org",
|
||||
withTeam: true,
|
||||
});
|
||||
|
||||
const booker = getBooker({
|
||||
email: blockedEmail,
|
||||
name: "Domain Blocked Booker",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
organizationId: org.id,
|
||||
});
|
||||
|
||||
await createOrganizationWatchlistEntry(org.id, {
|
||||
type: WatchlistType.DOMAIN,
|
||||
value: blockedDomain,
|
||||
action: "BLOCK",
|
||||
});
|
||||
|
||||
// Use the child team ID for the eventType
|
||||
const teamId = org.children && org.children.length > 0 ? org.children[0].id : null;
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData(
|
||||
{
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 30,
|
||||
length: 30,
|
||||
teamId,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
},
|
||||
{ id: org.id }
|
||||
)
|
||||
);
|
||||
|
||||
await mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
user: organizer.username,
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createdBooking = await handleNewBooking({
|
||||
bookingData: mockBookingData,
|
||||
});
|
||||
|
||||
expectDecoyBookingResponse(createdBooking);
|
||||
expect(createdBooking.attendees[0].email).toBe(blockedEmail);
|
||||
await expectNoBookingInDatabase(blockedEmail);
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
test(
|
||||
"should NOT block booking when email is in a different organization's watchlist",
|
||||
async () => {
|
||||
const handleNewBooking = getNewBookingHandler();
|
||||
const blockedEmail = "different-org-user@example.com";
|
||||
|
||||
// Create two different organizations with teams
|
||||
const organizerOrg = await createOrganization({
|
||||
name: "Organizer Org",
|
||||
slug: "organizer-org",
|
||||
withTeam: true,
|
||||
});
|
||||
|
||||
const differentOrg = await createOrganization({
|
||||
name: "Different Org",
|
||||
slug: "different-org",
|
||||
withTeam: true,
|
||||
});
|
||||
|
||||
const booker = getBooker({
|
||||
email: blockedEmail,
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
organizationId: organizerOrg.id,
|
||||
});
|
||||
|
||||
// Block email in a DIFFERENT organization
|
||||
await createOrganizationWatchlistEntry(differentOrg.id, {
|
||||
type: WatchlistType.EMAIL,
|
||||
value: blockedEmail,
|
||||
action: "BLOCK",
|
||||
});
|
||||
|
||||
// Use the child team ID for the eventType
|
||||
const teamId =
|
||||
organizerOrg.children && organizerOrg.children.length > 0 ? organizerOrg.children[0].id : null;
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData(
|
||||
{
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 30,
|
||||
length: 30,
|
||||
teamId,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
},
|
||||
{ id: organizerOrg.id }
|
||||
)
|
||||
);
|
||||
|
||||
await mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
user: organizer.username,
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createdBooking = await handleNewBooking({
|
||||
bookingData: mockBookingData,
|
||||
});
|
||||
|
||||
// Should NOT be a decoy response - booking should succeed
|
||||
expect(createdBooking).not.toHaveProperty("isShortCircuitedBooking");
|
||||
expect(createdBooking.status).toBe(BookingStatus.ACCEPTED);
|
||||
expect(createdBooking.id).not.toBe(0);
|
||||
expect(createdBooking.attendees[0].email).toBe(blockedEmail);
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
test(
|
||||
"should block booking for managed event when email is in organization watchlist",
|
||||
async () => {
|
||||
const handleNewBooking = getNewBookingHandler();
|
||||
const blockedEmail = "managed-event-spammer@example.com";
|
||||
|
||||
// Create organization with a team
|
||||
const org = await createOrganization({
|
||||
name: "Managed Event Org",
|
||||
slug: "managed-event-org",
|
||||
withTeam: true,
|
||||
});
|
||||
|
||||
const booker = getBooker({
|
||||
email: blockedEmail,
|
||||
name: "Managed Event Booker",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
organizationId: org.id,
|
||||
});
|
||||
|
||||
await createOrganizationWatchlistEntry(org.id, {
|
||||
type: WatchlistType.EMAIL,
|
||||
value: blockedEmail,
|
||||
action: "BLOCK",
|
||||
});
|
||||
|
||||
// Get the child team ID
|
||||
const teamId = org.children && org.children.length > 0 ? org.children[0].id : null;
|
||||
|
||||
// Create a parent event type and a managed (child) event type
|
||||
await createBookingScenario(
|
||||
getScenarioData(
|
||||
{
|
||||
eventTypes: [
|
||||
// Parent event type
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 30,
|
||||
length: 30,
|
||||
teamId,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
// Managed event type (child) - has parent but no teamId
|
||||
{
|
||||
id: 2,
|
||||
slotInterval: 30,
|
||||
length: 30,
|
||||
parent: { id: 1 }, // References parent event type
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
},
|
||||
{ id: org.id }
|
||||
)
|
||||
);
|
||||
|
||||
await mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
},
|
||||
});
|
||||
|
||||
// Try to book the managed event (id: 2)
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
user: organizer.username,
|
||||
eventTypeId: 2, // Booking the managed event
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createdBooking = await handleNewBooking({
|
||||
bookingData: mockBookingData,
|
||||
});
|
||||
|
||||
// Should return a decoy response since email is blocked in the organization
|
||||
expectDecoyBookingResponse(createdBooking);
|
||||
expect(createdBooking.attendees[0].email).toBe(blockedEmail);
|
||||
await expectNoBookingInDatabase(blockedEmail);
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
test(
|
||||
"should block booking for user event in organization when email is in organization watchlist",
|
||||
async () => {
|
||||
const handleNewBooking = getNewBookingHandler();
|
||||
const blockedEmail = "user-event-spammer@example.com";
|
||||
|
||||
// Create organization
|
||||
const org = await createOrganization({
|
||||
name: "User Event Org",
|
||||
slug: "user-event-org",
|
||||
withTeam: false,
|
||||
});
|
||||
|
||||
const booker = getBooker({
|
||||
email: blockedEmail,
|
||||
name: "User Event Booker",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
organizationId: org.id,
|
||||
});
|
||||
|
||||
await createOrganizationWatchlistEntry(org.id, {
|
||||
type: WatchlistType.EMAIL,
|
||||
value: blockedEmail,
|
||||
action: "BLOCK",
|
||||
});
|
||||
|
||||
// Create a user event (no teamId) but with a profile linking to the organization
|
||||
await createBookingScenario(
|
||||
getScenarioData(
|
||||
{
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 30,
|
||||
length: 30,
|
||||
// User Event Type has userId set
|
||||
userId: 101,
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
},
|
||||
{ id: org.id }
|
||||
)
|
||||
);
|
||||
|
||||
await mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
user: organizer.username,
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createdBooking = await handleNewBooking({
|
||||
bookingData: mockBookingData,
|
||||
});
|
||||
|
||||
// Should return a decoy response since email is blocked in the organization
|
||||
expectDecoyBookingResponse(createdBooking);
|
||||
expect(createdBooking.attendees[0].email).toBe(blockedEmail);
|
||||
await expectNoBookingInDatabase(blockedEmail);
|
||||
},
|
||||
timeout
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user