test: Booking flow App failure and ics file tests and improved logging (#11646)

This commit is contained in:
Hariom Balhara 2023-10-02 16:21:04 +05:30 committed by GitHub
parent 07bfaec24e
commit 3ca7456885
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 513 additions and 239 deletions

View File

@ -12,6 +12,7 @@ import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { handleStripePaymentSuccess } from "@calcom/features/ee/payments/api/webhook";
import type { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import type { SchedulingType } from "@calcom/prisma/enums";
import type { BookingStatus } from "@calcom/prisma/enums";
import type { NewCalendarEventType } from "@calcom/types/Calendar";
@ -19,6 +20,8 @@ import type { EventBusyDate } from "@calcom/types/Calendar";
import { getMockPaymentService } from "./MockPaymentService";
logger.setSettings({ minLevel: "silly" });
const log = logger.getChildLogger({ prefix: ["[bookingScenario]"] });
type App = {
slug: string;
dirName: string;
@ -123,7 +126,6 @@ const Timezones = {
"+5:30": "Asia/Kolkata",
"+6:00": "Asia/Dhaka",
};
logger.setSettings({ minLevel: "silly" });
async function addEventTypesToDb(
eventTypes: (Omit<Prisma.EventTypeCreateInput, "users" | "worflows" | "destinationCalendar"> & {
@ -135,11 +137,11 @@ async function addEventTypesToDb(
destinationCalendar?: any;
})[]
) {
logger.silly("TestData: Add EventTypes to DB", JSON.stringify(eventTypes));
log.silly("TestData: Add EventTypes to DB", JSON.stringify(eventTypes));
await prismock.eventType.createMany({
data: eventTypes,
});
logger.silly(
log.silly(
"TestData: All EventTypes in DB are",
JSON.stringify({
eventTypes: await prismock.eventType.findMany({
@ -197,7 +199,7 @@ async function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser
: eventType.destinationCalendar,
};
});
logger.silly("TestData: Creating EventType", JSON.stringify(eventTypesWithUsers));
log.silly("TestData: Creating EventType", JSON.stringify(eventTypesWithUsers));
await addEventTypesToDb(eventTypesWithUsers);
}
@ -216,7 +218,7 @@ async function addBookingsToDb(
await prismock.booking.createMany({
data: bookings,
});
logger.silly(
log.silly(
"TestData: Booking as in DB",
JSON.stringify({
bookings: await prismock.booking.findMany({
@ -229,7 +231,7 @@ async function addBookingsToDb(
}
async function addBookings(bookings: InputBooking[]) {
logger.silly("TestData: Creating Bookings", JSON.stringify(bookings));
log.silly("TestData: Creating Bookings", JSON.stringify(bookings));
const allBookings = [...bookings].map((booking) => {
if (booking.references) {
addBookingReferencesToDB(
@ -277,19 +279,22 @@ async function addWebhooksToDb(webhooks: any[]) {
}
async function addWebhooks(webhooks: InputWebhook[]) {
logger.silly("TestData: Creating Webhooks", webhooks);
log.silly("TestData: Creating Webhooks", safeStringify(webhooks));
await addWebhooksToDb(webhooks);
}
async function addUsersToDb(users: (Prisma.UserCreateInput & { schedules: Prisma.ScheduleCreateInput[] })[]) {
logger.silly("TestData: Creating Users", JSON.stringify(users));
log.silly("TestData: Creating Users", JSON.stringify(users));
await prismock.user.createMany({
data: users,
});
logger.silly("Added users to Db", {
allUsers: await prismock.user.findMany(),
});
log.silly(
"Added users to Db",
safeStringify({
allUsers: await prismock.user.findMany(),
})
);
}
async function addUsers(users: InputUser[]) {
@ -339,7 +344,7 @@ async function addUsers(users: InputUser[]) {
}
export async function createBookingScenario(data: ScenarioData) {
logger.silly("TestData: Creating Scenario", JSON.stringify({ data }));
log.silly("TestData: Creating Scenario", JSON.stringify({ data }));
await addUsers(data.users);
const eventType = await addEventTypes(data.eventTypes, data.users);
@ -690,6 +695,7 @@ export function enableEmailFeature() {
}
export function mockNoTranslations() {
log.silly("Mocking i18n.getTranslation to return identity function");
// @ts-expect-error FIXME
i18nMock.getTranslation.mockImplementation(() => {
return new Promise((resolve) => {
@ -707,10 +713,14 @@ export function mockCalendar(
metadataLookupKey: keyof typeof appStoreMetadata,
calendarData?: {
create?: {
id?: string;
uid?: string;
iCalUID?: string;
};
update?: {
id?: string;
uid: string;
iCalUID?: string;
};
busySlots?: { start: `${string}Z`; end: `${string}Z` }[];
creationCrash?: boolean;
@ -727,7 +737,7 @@ export function mockCalendar(
uid: "UPDATED_MOCK_ID",
},
};
logger.silly(`Mocking ${appStoreLookupKey} on appStoreMock`);
log.silly(`Mocking ${appStoreLookupKey} on appStoreMock`);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const createEventCalls: any[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -745,13 +755,15 @@ export function mockCalendar(
throw new Error("MockCalendarService.createEvent fake error");
}
const [calEvent, credentialId] = rest;
logger.silly("mockCalendar.createEvent", JSON.stringify({ calEvent, credentialId }));
log.silly("mockCalendar.createEvent", JSON.stringify({ calEvent, credentialId }));
createEventCalls.push(rest);
return Promise.resolve({
type: app.type,
additionalInfo: {},
uid: "PROBABLY_UNUSED_UID",
id: normalizedCalendarData.create?.uid || "FALLBACK_MOCK_ID",
// A Calendar is always expected to return an id.
id: normalizedCalendarData.create?.id || "FALLBACK_MOCK_CALENDAR_EVENT_ID",
iCalUID: normalizedCalendarData.create?.iCalUID,
// Password and URL seems useless for CalendarService, plan to remove them if that's the case
password: "MOCK_PASSWORD",
url: "https://UNUSED_URL",
@ -763,13 +775,15 @@ export function mockCalendar(
throw new Error("MockCalendarService.updateEvent fake error");
}
const [uid, event, externalCalendarId] = rest;
logger.silly("mockCalendar.updateEvent", JSON.stringify({ uid, event, externalCalendarId }));
log.silly("mockCalendar.updateEvent", JSON.stringify({ uid, event, externalCalendarId }));
// eslint-disable-next-line prefer-rest-params
updateEventCalls.push(rest);
return Promise.resolve({
type: app.type,
additionalInfo: {},
uid: "PROBABLY_UNUSED_UID",
iCalUID: normalizedCalendarData.update?.iCalUID,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
id: normalizedCalendarData.update?.uid || "FALLBACK_MOCK_ID",
// Password and URL seems useless for CalendarService, plan to remove them if that's the case
@ -797,14 +811,7 @@ export function mockCalendar(
export function mockCalendarToHaveNoBusySlots(
metadataLookupKey: keyof typeof appStoreMetadata,
calendarData?: {
create: {
uid?: string;
};
update?: {
uid: string;
};
}
calendarData?: Parameters<typeof mockCalendar>[1]
) {
calendarData = calendarData || {
create: {
@ -848,10 +855,7 @@ export function mockVideoApp({
password: "MOCK_PASS",
url: `http://mock-${metadataLookupKey}.example.com`,
};
logger.silly(
"mockSuccessfulVideoMeetingCreation",
JSON.stringify({ metadataLookupKey, appStoreLookupKey })
);
log.silly("mockSuccessfulVideoMeetingCreation", JSON.stringify({ metadataLookupKey, appStoreLookupKey }));
const createMeetingCalls: any[] = [];
const updateMeetingCalls: any[] = [];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -888,7 +892,7 @@ export function mockVideoApp({
if (!calEvent.organizer) {
throw new Error("calEvent.organizer is not defined");
}
logger.silly(
log.silly(
"mockSuccessfulVideoMeetingCreation.updateMeeting",
JSON.stringify({ bookingRef, calEvent })
);
@ -1019,7 +1023,7 @@ export async function mockPaymentSuccessWebhookFromStripe({ externalId }: { exte
try {
await handleStripePaymentSuccess(getMockedStripePaymentEvent({ paymentIntentId: externalId }));
} catch (e) {
logger.silly("mockPaymentSuccessWebhookFromStripe:catch", JSON.stringify(e));
log.silly("mockPaymentSuccessWebhookFromStripe:catch", JSON.stringify(e));
webhookResponse = e as HttpError;
}

View File

@ -1,10 +1,12 @@
import prismaMock from "../../../../../tests/libs/__mocks__/prisma";
import type { WebhookTriggerEvents, Booking, BookingReference } from "@prisma/client";
import ical from "node-ical";
import { expect } from "vitest";
import "vitest-fetch-mock";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { BookingStatus } from "@calcom/prisma/enums";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { Fixtures } from "@calcom/web/test/fixtures/fixtures";
@ -15,7 +17,19 @@ declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace jest {
interface Matchers<R> {
toHaveEmail(expectedEmail: { htmlToContain?: string; to: string }, to: string): R;
toHaveEmail(
expectedEmail: {
//TODO: Support email HTML parsing to target specific elements
htmlToContain?: string;
to: string;
noIcs?: true;
ics?: {
filename: string;
iCalUID: string;
};
},
to: string
): R;
}
}
}
@ -27,11 +41,18 @@ expect.extend({
//TODO: Support email HTML parsing to target specific elements
htmlToContain?: string;
to: string;
ics: {
filename: string;
iCalUID: string;
};
noIcs: true;
},
to: string
) {
const testEmail = emails.get().find((email) => email.to.includes(to));
const emailsToLog = emails.get().map((email) => ({ to: email.to, html: email.html }));
const emailsToLog = emails
.get()
.map((email) => ({ to: email.to, html: email.html, ics: email.icalEvent }));
if (!testEmail) {
logger.silly("All Emails", JSON.stringify({ numEmails: emailsToLog.length, emailsToLog }));
return {
@ -39,11 +60,20 @@ expect.extend({
message: () => `No email sent to ${to}`,
};
}
const ics = testEmail.icalEvent;
const icsObject = ics?.content ? ical.sync.parseICS(ics?.content) : null;
let isHtmlContained = true;
let isToAddressExpected = true;
const isIcsFilenameExpected = expectedEmail.ics ? ics?.filename === expectedEmail.ics.filename : true;
const isIcsUIDExpected = expectedEmail.ics
? !!(icsObject ? icsObject[expectedEmail.ics.iCalUID] : null)
: true;
if (expectedEmail.htmlToContain) {
isHtmlContained = testEmail.html.includes(expectedEmail.htmlToContain);
}
isToAddressExpected = expectedEmail.to === testEmail.to;
if (!isHtmlContained || !isToAddressExpected) {
@ -51,12 +81,29 @@ expect.extend({
}
return {
pass: isHtmlContained && isToAddressExpected,
pass:
isHtmlContained &&
isToAddressExpected &&
(expectedEmail.noIcs ? true : isIcsFilenameExpected && isIcsUIDExpected),
message: () => {
if (!isHtmlContained) {
return `Email HTML is not as expected. Expected:"${expectedEmail.htmlToContain}" isn't contained in "${testEmail.html}"`;
}
return `Email To address is not as expected. Expected:${expectedEmail.to} isn't equal to ${testEmail.to}`;
if (!isToAddressExpected) {
return `Email To address is not as expected. Expected:${expectedEmail.to} isn't equal to ${testEmail.to}`;
}
if (!isIcsFilenameExpected) {
return `ICS Filename is not as expected. Expected:${expectedEmail.ics.filename} isn't equal to ${ics?.filename}`;
}
if (!isIcsUIDExpected) {
return `ICS UID is not as expected. Expected:${
expectedEmail.ics.iCalUID
} isn't present in ${JSON.stringify(icsObject)}`;
}
throw new Error("Unknown error");
},
};
},
@ -73,7 +120,7 @@ export function expectWebhookToHaveBeenCalledWith(
const webhooksToSubscriberUrl = fetchCalls.filter((call) => {
return call[0] === subscriberUrl;
});
logger.silly("Scanning fetchCalls for webhook", fetchCalls);
logger.silly("Scanning fetchCalls for webhook", safeStringify(fetchCalls));
const webhookFetchCall = webhooksToSubscriberUrl.find((call) => {
const body = call[1]?.body;
const parsedBody = JSON.parse((body as string) || "{}");
@ -135,15 +182,21 @@ export function expectSuccessfulBookingCreationEmails({
emails,
organizer,
booker,
iCalUID,
}: {
emails: Fixtures["emails"];
organizer: { email: string; name: string };
booker: { email: string; name: string };
iCalUID: string;
}) {
expect(emails).toHaveEmail(
{
htmlToContain: "<title>confirmed_event_type_subject</title>",
to: `${organizer.email}`,
ics: {
filename: "event.ics",
iCalUID: iCalUID,
},
},
`${organizer.email}`
);
@ -152,6 +205,10 @@ export function expectSuccessfulBookingCreationEmails({
{
htmlToContain: "<title>confirmed_event_type_subject</title>",
to: `${booker.name} <${booker.email}>`,
ics: {
filename: "event.ics",
iCalUID: iCalUID,
},
},
`${booker.name} <${booker.email}>`
);
@ -160,17 +217,20 @@ export function expectSuccessfulBookingCreationEmails({
export function expectBrokenIntegrationEmails({
emails,
organizer,
booker,
}: {
emails: Fixtures["emails"];
organizer: { email: string; name: string };
booker: { email: string; name: string };
}) {
// Broken Integration email is only sent to the Organizer
expect(emails).toHaveEmail(
{
htmlToContain: "<title>broken_integration</title>",
to: `${organizer.email}`,
// No ics goes in case of broken integration email it seems
// ics: {
// filename: "event.ics",
// iCalUID: iCalUID,
// },
},
`${organizer.email}`
);
@ -188,15 +248,21 @@ export function expectCalendarEventCreationFailureEmails({
emails,
organizer,
booker,
iCalUID,
}: {
emails: Fixtures["emails"];
organizer: { email: string; name: string };
booker: { email: string; name: string };
iCalUID: string;
}) {
expect(emails).toHaveEmail(
{
htmlToContain: "<title>broken_integration</title>",
to: `${organizer.email}`,
ics: {
filename: "event.ics",
iCalUID,
},
},
`${organizer.email}`
);
@ -205,6 +271,10 @@ export function expectCalendarEventCreationFailureEmails({
{
htmlToContain: "<title>calendar_event_creation_failure_subject</title>",
to: `${booker.name} <${booker.email}>`,
ics: {
filename: "event.ics",
iCalUID,
},
},
`${booker.name} <${booker.email}>`
);
@ -214,15 +284,21 @@ export function expectSuccessfulBookingRescheduledEmails({
emails,
organizer,
booker,
iCalUID,
}: {
emails: Fixtures["emails"];
organizer: { email: string; name: string };
booker: { email: string; name: string };
iCalUID: string;
}) {
expect(emails).toHaveEmail(
{
htmlToContain: "<title>event_type_has_been_rescheduled_on_time_date</title>",
to: `${organizer.email}`,
ics: {
filename: "event.ics",
iCalUID,
},
},
`${organizer.email}`
);
@ -231,6 +307,10 @@ export function expectSuccessfulBookingRescheduledEmails({
{
htmlToContain: "<title>event_type_has_been_rescheduled_on_time_date</title>",
to: `${booker.name} <${booker.email}>`,
ics: {
filename: "event.ics",
iCalUID,
},
},
`${booker.name} <${booker.email}>`
);
@ -247,6 +327,7 @@ export function expectAwaitingPaymentEmails({
{
htmlToContain: "<title>awaiting_payment_subject</title>",
to: `${booker.name} <${booker.email}>`,
noIcs: true,
},
`${booker.email}`
);
@ -265,6 +346,7 @@ export function expectBookingRequestedEmails({
{
htmlToContain: "<title>event_awaiting_approval_subject</title>",
to: `${organizer.email}`,
noIcs: true,
},
`${organizer.email}`
);
@ -273,6 +355,7 @@ export function expectBookingRequestedEmails({
{
htmlToContain: "<title>booking_submitted_subject</title>",
to: `${booker.email}`,
noIcs: true,
},
`${booker.email}`
);
@ -346,7 +429,7 @@ export function expectBookingCreatedWebhookToHaveBeenFired({
subscriberUrl: string;
location: string;
paidEvent?: boolean;
videoCallUrl?: string;
videoCallUrl?: string | null;
}) {
if (!paidEvent) {
expectWebhookToHaveBeenCalledWith(subscriberUrl, {
@ -545,6 +628,7 @@ export function expectSuccessfulVideoMeetingUpdationInCalendar(
expect(calendarEvent).toEqual(expect.objectContaining(expected.calEvent));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function expectBookingInDBToBeRescheduledFromTo({ from, to }: { from: any; to: any }) {
// Expect previous booking to be cancelled
await expectBookingToBeInDatabase({

View File

@ -87,6 +87,7 @@
"jsdom": "^22.0.0",
"lint-staged": "^12.5.0",
"mailhog": "^4.16.0",
"node-ical": "^0.16.1",
"prettier": "^2.8.6",
"prismock": "^1.21.1",
"tsc-absolute": "^1.0.0",

View File

@ -42,7 +42,7 @@ export const getCalendar = async (credential: CredentialPayload | null): Promise
log.warn(`calendar of type ${calendarType} is not implemented`);
return null;
}
log.info("calendarApp", calendarApp.lib.CalendarService);
log.info("Got calendarApp", calendarApp.lib.CalendarService);
const CalendarService = calendarApp.lib.CalendarService;
return new CalendarService(credential);
};

View File

@ -256,7 +256,10 @@ export const createEvent = async (
if (error?.calError) {
calError = error.calError;
}
log.error("createEvent failed", JSON.stringify(error), calEvent);
log.error(
"createEvent failed",
safeStringify({ error, calEvent: getPiiFreeCalendarEvent(calEvent) })
);
// @TODO: This code will be off till we can investigate an error with it
//https://github.com/calcom/cal.com/issues/3949
// await sendBrokenIntegrationEmail(calEvent, "calendar");
@ -266,7 +269,13 @@ export const createEvent = async (
if (!creationResult) {
logger.silly("createEvent failed", { success, uid, creationResult, originalEvent: calEvent, calError });
}
log.debug(
"Created calendar event",
JSON.stringify({
calEvent: getPiiFreeCalendarEvent(calEvent),
creationResult,
})
);
return {
appName: credential.appId || "",
type: credential.type,

View File

@ -10,6 +10,7 @@ import { appKeysSchema as calVideoKeysSchema } from "@calcom/app-store/dailyvide
import { getEventLocationTypeFromApp, MeetLocationType } from "@calcom/app-store/locations";
import getApps from "@calcom/app-store/utils";
import logger from "@calcom/lib/logger";
import { getPiiFreeUser } from "@calcom/lib/piiFreeData";
import { safeStringify } from "@calcom/lib/safeStringify";
import prisma from "@calcom/prisma";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
@ -27,6 +28,7 @@ import type {
import { createEvent, updateEvent } from "./CalendarManager";
import { createMeeting, updateMeeting } from "./videoClient";
const log = logger.getChildLogger({ prefix: ["EventManager"] });
export const isDedicatedIntegration = (location: string): boolean => {
return location !== MeetLocationType && location.includes("integrations:");
};
@ -81,7 +83,7 @@ export default class EventManager {
* @param user
*/
constructor(user: EventManagerUser) {
logger.silly("Initializing EventManager", JSON.stringify({ user }));
log.silly("Initializing EventManager", safeStringify({ user: getPiiFreeUser(user) }));
const appCredentials = getApps(user.credentials, true).flatMap((app) =>
app.credentials.map((creds) => ({ ...creds, appName: app.name }))
);
@ -124,6 +126,7 @@ export default class EventManager {
const [mainHostDestinationCalendar] =
(evt.destinationCalendar as [undefined | NonNullable<typeof evt.destinationCalendar>[number]]) ?? [];
if (evt.location === MeetLocationType && mainHostDestinationCalendar?.integration !== "google_calendar") {
log.warn("Falling back to Cal Video integration as Google Calendar not installed");
evt["location"] = "integrations:daily";
}
const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null;
@ -362,7 +365,7 @@ export default class EventManager {
[] as DestinationCalendar[]
);
for (const destination of destinationCalendars) {
logger.silly("Creating Calendar event", JSON.stringify({ destination }));
log.silly("Creating Calendar event", JSON.stringify({ destination }));
if (destination.credentialId) {
let credential = this.calendarCredentials.find((c) => c.id === destination.credentialId);
if (!credential) {
@ -402,7 +405,7 @@ export default class EventManager {
}
}
} else {
logger.silly(
log.silly(
"No destination Calendar found, falling back to first connected calendar",
safeStringify({
calendarCredentials: this.calendarCredentials,
@ -416,7 +419,7 @@ export default class EventManager {
const [credential] = this.calendarCredentials.filter((cred) => !cred.type.endsWith("other_calendar"));
if (credential) {
const createdEvent = await createEvent(credential, event);
logger.silly("Created Calendar event", { createdEvent });
log.silly("Created Calendar event", safeStringify({ createdEvent }));
if (createdEvent) {
createdEvents.push(createdEvent);
}
@ -465,7 +468,7 @@ export default class EventManager {
* @todo remove location from event types that has missing credentials
* */
if (!videoCredential) {
logger.warn(
log.warn(
'Falling back to "daily" video integration for event with location: ' +
event.location +
" because credential is missing for the app"
@ -513,7 +516,7 @@ export default class EventManager {
): Promise<Array<EventResult<NewCalendarEventType>>> {
let calendarReference: PartialReference[] | undefined = undefined,
credential;
logger.silly("updateAllCalendarEvents", JSON.stringify({ event, booking, newBookingId }));
log.silly("updateAllCalendarEvents", JSON.stringify({ event, booking, newBookingId }));
try {
// If a newBookingId is given, update that calendar event
let newBooking;
@ -575,7 +578,7 @@ export default class EventManager {
(credential) => credential.type === reference?.type
);
for (const credential of credentials) {
logger.silly("updateAllCalendarEvents-credential", JSON.stringify({ credentials }));
log.silly("updateAllCalendarEvents-credential", JSON.stringify({ credentials }));
result.push(updateEvent(credential, event, bookingRefUid, calenderExternalId));
}
}

View File

@ -3,6 +3,7 @@ import type { SelectedCalendar } from "@prisma/client";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import logger from "@calcom/lib/logger";
import { getPiiFreeCredential, getPiiFreeSelectedCalendar } from "@calcom/lib/piiFreeData";
import { safeStringify } from "@calcom/lib/safeStringify";
import { performance } from "@calcom/lib/server/perfObserver";
import type { EventBusyDate } from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";
@ -35,6 +36,13 @@ const getCalendarsEvents = async (
const selectedCalendarIds = passedSelectedCalendars.map((sc) => sc.externalId);
/** If we don't then we actually fetch external calendars (which can be very slow) */
performance.mark("eventBusyDatesStart");
log.debug(
`Getting availability for`,
safeStringify({
calendarService: c.constructor.name,
selectedCalendars: passedSelectedCalendars.map(getPiiFreeSelectedCalendar),
})
);
const eventBusyDates = await c.getAvailability(dateFrom, dateTo, passedSelectedCalendars);
performance.mark("eventBusyDatesEnd");
performance.measure(
@ -52,11 +60,14 @@ const getCalendarsEvents = async (
"getBusyCalendarTimesStart",
"getBusyCalendarTimesEnd"
);
log.debug({
calendarCredentials: calendarCredentials.map(getPiiFreeCredential),
selectedCalendars: selectedCalendars.map(getPiiFreeSelectedCalendar),
calendarEvents: awaitedResults,
});
log.debug(
"Result",
safeStringify({
calendarCredentials: calendarCredentials.map(getPiiFreeCredential),
selectedCalendars: selectedCalendars.map(getPiiFreeSelectedCalendar),
calendarEvents: awaitedResults,
})
);
return awaitedResults;
};

View File

@ -6,7 +6,7 @@ import { getDailyAppKeys } from "@calcom/app-store/dailyvideo/lib/getDailyAppKey
import { sendBrokenIntegrationEmail } from "@calcom/emails";
import { getUid } from "@calcom/lib/CalEventParser";
import logger from "@calcom/lib/logger";
import { getPiiFreeCalendarEvent } from "@calcom/lib/piiFreeData";
import { getPiiFreeCalendarEvent, getPiiFreeCredential } from "@calcom/lib/piiFreeData";
import { safeStringify } from "@calcom/lib/safeStringify";
import { prisma } from "@calcom/prisma";
import type { GetRecordingsResponseSchema } from "@calcom/prisma/zod-utils";
@ -25,7 +25,7 @@ const getVideoAdapters = async (withCredentials: CredentialPayload[]): Promise<V
for (const cred of withCredentials) {
const appName = cred.type.split("_").join(""); // Transform `zoom_video` to `zoomvideo`;
logger.silly("getVideoAdapters", JSON.stringify({ appName, cred }));
log.silly("Getting video adapter for", safeStringify({ appName, cred: getPiiFreeCredential(cred) }));
const appImportFn = appStore[appName as keyof typeof appStore];
// Static Link Video Apps don't exist in packages/app-store/index.ts(it's manually maintained at the moment) and they aren't needed there anyway.
@ -55,7 +55,14 @@ const getBusyVideoTimes = async (withCredentials: CredentialPayload[]) =>
const createMeeting = async (credential: CredentialPayload, calEvent: CalendarEvent) => {
const uid: string = getUid(calEvent);
log.silly("videoClient:createMeeting", JSON.stringify({ credential, uid, calEvent }));
log.silly(
"createMeeting",
safeStringify({
credential: getPiiFreeCredential(credential),
uid,
calEvent: getPiiFreeCalendarEvent(calEvent),
})
);
if (!credential || !credential.appId) {
throw new Error(
"Credentials must be set! Video platforms are optional, so this method shouldn't even be called when no video credentials are set."
@ -159,7 +166,10 @@ const updateMeeting = async (
const deleteMeeting = async (credential: CredentialPayload | null, uid: string): Promise<unknown> => {
if (credential) {
const videoAdapter = (await getVideoAdapters([credential]))[0];
logger.debug("videoAdapter inside deleteMeeting", { credential, uid });
log.debug(
"Calling deleteMeeting for",
safeStringify({ credential: getPiiFreeCredential(credential), uid })
);
// There are certain video apps with no video adapter defined. e.g. riverby,whereby
if (videoAdapter) {
return videoAdapter.deleteMeeting(uid);

View File

@ -2,7 +2,6 @@ import type { TFunction } from "next-i18next";
import { guessEventLocationType } from "@calcom/app-store/locations";
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import logger from "@calcom/lib/logger";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { Info } from "./Info";
@ -12,7 +11,6 @@ export function LocationInfo(props: { calEvent: CalendarEvent; t: TFunction }) {
// We would not be able to determine provider name for DefaultEventLocationTypes
const providerName = guessEventLocationType(props.calEvent.location)?.label;
logger.debug(`LocationInfo: ${JSON.stringify(props.calEvent)} ${providerName}`);
const location = props.calEvent.location;
let meetingUrl = location?.search(/^https?:/) !== -1 ? location : undefined;

View File

@ -147,7 +147,8 @@ describe("handleNewBooking", () => {
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
uid: "MOCK_ID",
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
@ -193,8 +194,8 @@ describe("handleNewBooking", () => {
},
{
type: "google_calendar",
uid: "MOCK_ID",
meetingId: "MOCK_ID",
uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
meetingPassword: "MOCK_PASSWORD",
meetingUrl: "https://UNUSED_URL",
},
@ -207,7 +208,13 @@ describe("handleNewBooking", () => {
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
});
expectSuccessfulBookingCreationEmails({ booker, organizer, emails });
expectSuccessfulBookingCreationEmails({
booker,
organizer,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
});
expectBookingCreatedWebhookToHaveBeenFired({
booker,
organizer,
@ -281,9 +288,12 @@ describe("handleNewBooking", () => {
},
});
// Mock a Scenario where iCalUID isn't returned by Google Calendar in which case booking UID is used as the ics UID
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "GOOGLE_CALENDAR_EVENT_ID",
uid: "MOCK_ID",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
@ -329,8 +339,8 @@ describe("handleNewBooking", () => {
},
{
type: "google_calendar",
uid: "MOCK_ID",
meetingId: "MOCK_ID",
uid: "GOOGLE_CALENDAR_EVENT_ID",
meetingId: "GOOGLE_CALENDAR_EVENT_ID",
meetingPassword: "MOCK_PASSWORD",
meetingUrl: "https://UNUSED_URL",
},
@ -346,7 +356,12 @@ describe("handleNewBooking", () => {
calendarId: null,
});
expectSuccessfulBookingCreationEmails({ booker, organizer, emails });
expectSuccessfulBookingCreationEmails({
booker,
organizer,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
});
expectBookingCreatedWebhookToHaveBeenFired({
booker,
organizer,
@ -425,6 +440,8 @@ describe("handleNewBooking", () => {
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
uid: "MOCK_ID",
id: "GOOGLE_CALENDAR_EVENT_ID",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
@ -470,8 +487,8 @@ describe("handleNewBooking", () => {
},
{
type: "google_calendar",
uid: "MOCK_ID",
meetingId: "MOCK_ID",
uid: "GOOGLE_CALENDAR_EVENT_ID",
meetingId: "GOOGLE_CALENDAR_EVENT_ID",
meetingPassword: "MOCK_PASSWORD",
meetingUrl: "https://UNUSED_URL",
},
@ -484,7 +501,13 @@ describe("handleNewBooking", () => {
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
});
expectSuccessfulBookingCreationEmails({ booker, organizer, emails });
expectSuccessfulBookingCreationEmails({
booker,
organizer,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
});
expectBookingCreatedWebhookToHaveBeenFired({
booker,
organizer,
@ -498,7 +521,7 @@ describe("handleNewBooking", () => {
test(
`an error in creating a calendar event should not stop the booking creation - Current behaviour is wrong as the booking is created but no-one is notified of it`,
async ({ emails }) => {
async ({}) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const booker = getBooker({
email: "booker@example.com",
@ -546,7 +569,7 @@ describe("handleNewBooking", () => {
})
);
const calendarMock = mockCalendarToCrashOnCreateEvent("googlecalendar");
const _calendarMock = mockCalendarToCrashOnCreateEvent("googlecalendar");
const mockBookingData = getMockRequestDataForBooking({
data: {
@ -672,9 +695,15 @@ describe("handleNewBooking", () => {
},
}),
});
await handleNewBooking(req);
const createdBooking = await handleNewBooking(req);
expectSuccessfulBookingCreationEmails({ booker, organizer, emails });
expectSuccessfulBookingCreationEmails({
booker,
organizer,
emails,
// Because no calendar was involved, we don't have an ics UID
iCalUID: createdBooking.uid,
});
expectBookingCreatedWebhookToHaveBeenFired({
booker,
@ -893,95 +922,10 @@ describe("handleNewBooking", () => {
})
);
const calendarMock = mockCalendar("googlecalendar", {
create: {
uid: "MOCK_ID",
},
busySlots: [
{
start: `${plus1DateString}T05:00:00.000Z`,
end: `${plus1DateString}T05:15:00.000Z`,
},
],
});
const mockBookingData = getMockRequestDataForBooking({
data: {
start: `${getDate({ dateIncrement: 1 }).dateString}T04:00:00.000Z`,
end: `${getDate({ dateIncrement: 1 }).dateString}T05:30:00.000Z`,
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
await expect(async () => await handleNewBooking(req)).rejects.toThrowError(
"No available users found"
);
},
timeout
);
test(
`should fail a booking if there is already a booking in the organizer's selectedCalendars(Single Calendar) with the overlapping time`,
async () => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const organizerId = 101;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: organizerId,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
users: [
{
id: 101,
},
],
},
],
organizer,
apps: [TestData.apps["google-calendar"]],
})
);
const calendarMock = mockCalendar("googlecalendar", {
const _calendarMock = mockCalendar("googlecalendar", {
create: {
uid: "MOCK_ID",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
busySlots: [
{
@ -1075,7 +1019,11 @@ describe("handleNewBooking", () => {
metadataLookupKey: "dailyvideo",
});
mockCalendarToHaveNoBusySlots("googlecalendar");
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
const mockBookingData = getMockRequestDataForBooking({
data: {
@ -1193,7 +1141,11 @@ describe("handleNewBooking", () => {
metadataLookupKey: "dailyvideo",
});
mockCalendarToHaveNoBusySlots("googlecalendar");
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
const mockBookingData = getMockRequestDataForBooking({
data: {
@ -1231,7 +1183,12 @@ describe("handleNewBooking", () => {
expectWorkflowToBeTriggered();
expectSuccessfulBookingCreationEmails({ booker, organizer, emails });
expectSuccessfulBookingCreationEmails({
booker,
organizer,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
});
expectBookingCreatedWebhookToHaveBeenFired({
booker,
@ -1306,7 +1263,11 @@ describe("handleNewBooking", () => {
metadataLookupKey: "dailyvideo",
});
mockCalendarToHaveNoBusySlots("googlecalendar");
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
const mockBookingData = getMockRequestDataForBooking({
data: {
@ -1489,7 +1450,11 @@ describe("handleNewBooking", () => {
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
});
mockCalendarToHaveNoBusySlots("googlecalendar");
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
await createBookingScenario(scenarioData);
const createdBooking = await handleNewBooking(req);
@ -1512,7 +1477,12 @@ describe("handleNewBooking", () => {
expectWorkflowToBeTriggered();
expectSuccessfulBookingCreationEmails({ booker, organizer, emails });
expectSuccessfulBookingCreationEmails({
booker,
organizer,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
});
expectBookingCreatedWebhookToHaveBeenFired({
booker,
organizer,
@ -1918,6 +1888,7 @@ describe("handleNewBooking", () => {
},
update: {
uid: "UPDATED_MOCK_ID",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
@ -2027,7 +1998,12 @@ describe("handleNewBooking", () => {
uid: "MOCK_ID",
});
expectSuccessfulBookingRescheduledEmails({ booker, organizer, emails });
expectSuccessfulBookingRescheduledEmails({
booker,
organizer,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
});
expectBookingRescheduledWebhookToHaveBeenFired({
booker,
organizer,
@ -2132,6 +2108,7 @@ describe("handleNewBooking", () => {
uid: "MOCK_ID",
},
update: {
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
uid: "UPDATED_MOCK_ID",
},
});
@ -2223,7 +2200,12 @@ describe("handleNewBooking", () => {
uid: "MOCK_ID",
});
expectSuccessfulBookingRescheduledEmails({ booker, organizer, emails });
expectSuccessfulBookingRescheduledEmails({
booker,
organizer,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
});
expectBookingRescheduledWebhookToHaveBeenFired({
booker,
organizer,
@ -2237,7 +2219,7 @@ describe("handleNewBooking", () => {
test(
`an error in updating a calendar event should not stop the rescheduling - Current behaviour is wrong as the booking is resheduled but no-one is notified of it`,
async ({ emails }) => {
async ({}) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const booker = getBooker({
email: "booker@example.com",
@ -2315,7 +2297,7 @@ describe("handleNewBooking", () => {
})
);
const calendarMock = mockCalendarToCrashOnUpdateEvent("googlecalendar");
const _calendarMock = mockCalendarToCrashOnUpdateEvent("googlecalendar");
const mockBookingData = getMockRequestDataForBooking({
data: {

View File

@ -35,6 +35,7 @@ import {
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
import { userOrgQuery } from "@calcom/features/ee/organizations/lib/orgDomains";
import {
allowDisablingAttendeeConfirmationEmails,
@ -60,6 +61,7 @@ import { HttpError } from "@calcom/lib/http-error";
import isOutOfBounds, { BookingDateInPastError } from "@calcom/lib/isOutOfBounds";
import logger from "@calcom/lib/logger";
import { handlePayment } from "@calcom/lib/payment/handlePayment";
import { getPiiFreeCalendarEvent, getPiiFreeEventType, getPiiFreeUser } from "@calcom/lib/piiFreeData";
import { safeStringify } from "@calcom/lib/safeStringify";
import { checkBookingLimits, checkDurationLimits, getLuckyUser } from "@calcom/lib/server";
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl";
@ -688,25 +690,14 @@ async function handler(
prefix: ["book:user", `${eventTypeId}:${reqBody.user}/${eventTypeSlug}`],
});
if (isLoggingEnabled({ eventTypeId, usernameOrTeamName: reqBody.user })) {
if (isEventTypeLoggingEnabled({ eventTypeId, usernameOrTeamName: reqBody.user })) {
logger.setSettings({ minLevel: "silly" });
}
loggerWithEventDetails.debug(
"handleNewBooking called",
JSON.stringify({
eventTypeId,
eventTypeSlug,
startTime: reqBody.start,
endTime: reqBody.end,
rescheduleUid: reqBody.rescheduleUid,
})
);
const fullName = getFullName(bookerName);
const tGuests = await getTranslation("en", "common");
loggerWithEventDetails.debug(`Booking eventType ${eventTypeId} started`);
const dynamicUserList = Array.isArray(reqBody.user) ? reqBody.user : getUsernameList(reqBody.user);
if (!eventType) throw new HttpError({ statusCode: 404, message: "eventType.notFound" });
@ -714,6 +705,30 @@ async function handler(
!!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType);
const paymentAppData = getPaymentAppData(eventType);
loggerWithEventDetails.debug(
`Booking eventType ${eventTypeId} started`,
safeStringify({
reqBody: {
user: reqBody.user,
eventTypeId,
eventTypeSlug,
startTime: reqBody.start,
endTime: reqBody.end,
rescheduleUid: reqBody.rescheduleUid,
location: location,
},
isTeamEventType,
eventType: getPiiFreeEventType(eventType),
dynamicUserList,
paymentAppData: {
enabled: paymentAppData.enabled,
price: paymentAppData.price,
paymentOption: paymentAppData.paymentOption,
currency: paymentAppData.currency,
appId: paymentAppData.appId,
},
})
);
let timeOutOfBounds = false;
try {
@ -837,6 +852,13 @@ async function handler(
: user.isFixed || eventType.schedulingType !== SchedulingType.ROUND_ROBIN,
}));
loggerWithEventDetails.debug(
"Concerned users",
safeStringify({
users: users.map(getPiiFreeUser),
})
);
let locationBodyString = location;
// TODO: It's definition should be moved to getLocationValueForDb
@ -921,6 +943,13 @@ async function handler(
const luckyUsers: typeof users = [];
const luckyUserPool = availableUsers.filter((user) => !user.isFixed);
loggerWithEventDetails.debug(
"Computed available users",
safeStringify({
availableUsers: availableUsers.map((user) => user.id),
luckyUserPool: luckyUserPool.map((user) => user.id),
})
);
// loop through all non-fixed hosts and get the lucky users
while (luckyUserPool.length > 0 && luckyUsers.length < 1 /* TODO: Add variable */) {
const newLuckyUser = await getLuckyUser("MAXIMIZE_AVAILABILITY", {
@ -946,6 +975,7 @@ async function handler(
}
const [organizerUser] = users;
const tOrganizer = await getTranslation(organizerUser?.locale ?? "en", "common");
const allCredentials = await getAllCredentials(organizerUser, eventType);
@ -1988,6 +2018,17 @@ async function handler(
type Booking = Prisma.PromiseReturnType<typeof createBooking>;
let booking: (Booking & { appsStatus?: AppsStatus[] }) | null = null;
loggerWithEventDetails.debug(
"Going to create booking in DB now",
safeStringify({
organizerUser: organizerUser.id,
attendeesList: attendeesList.map((guest) => ({ timeZone: guest.timeZone })),
requiresConfirmation,
isConfirmedByDefault,
userReschedulingIsOwner,
})
);
try {
booking = await createBooking();
@ -2177,7 +2218,7 @@ async function handler(
};
loggerWithEventDetails.error(
`Booking ${organizerUser.username} failed`,
`Failure in creating events in some of the integrations ${organizerUser.username} failed`,
safeStringify({ error, results })
);
} else {
@ -2201,6 +2242,7 @@ async function handler(
const googleCalResult = results[googleCalIndex];
if (!googleCalResult) {
loggerWithEventDetails.warn("Google Calendar not installed but using Google Meet as location");
results.push({
...googleMeetResult,
success: false,
@ -2263,6 +2305,12 @@ async function handler(
}
}
loggerWithEventDetails.debug(
"Sending scheduled emails for booking confirmation",
safeStringify({
calEvent: getPiiFreeCalendarEvent(evt),
})
);
await sendScheduledEmails(
{
...evt,
@ -2285,9 +2333,16 @@ async function handler(
!!booking;
if (!isConfirmedByDefault && noEmail !== true && !bookingRequiresPayment) {
loggerWithEventDetails.debug(
`Booking ${organizerUser.username} requires confirmation, sending request emails`,
safeStringify({
calEvent: getPiiFreeCalendarEvent(evt),
})
);
await sendOrganizerRequestEmail({ ...evt, additionalNotes });
await sendAttendeeRequestEmail({ ...evt, additionalNotes }, attendeesList[0]);
}
const metadata = videoCallUrl
? {
videoCallUrl: getVideoCallUrlFromCalEvent(evt),
@ -2311,6 +2366,7 @@ async function handler(
};
if (bookingRequiresPayment) {
loggerWithEventDetails.debug(`Booking ${organizerUser.username} requires payment`);
// Load credentials.app.categories
const credentialPaymentAppCategories = await prisma.credential.findMany({
where: {
@ -2555,31 +2611,3 @@ const findBookingQuery = async (bookingId: number) => {
// Don't leak any sensitive data
return foundBooking;
};
function isLoggingEnabled({
eventTypeId,
usernameOrTeamName,
}: {
eventTypeId?: number | null;
usernameOrTeamName?: string | string[];
}) {
const usernameOrTeamnamesList =
usernameOrTeamName instanceof Array ? usernameOrTeamName : [usernameOrTeamName];
// eslint-disable-next-line turbo/no-undeclared-env-vars
const bookingLoggingEventIds = process.env.BOOKING_LOGGING_EVENT_IDS || "";
const isEnabled = bookingLoggingEventIds.split(",").some((id) => {
if (Number(id.trim()) === eventTypeId) {
return true;
}
});
if (isEnabled) {
return true;
}
// eslint-disable-next-line turbo/no-undeclared-env-vars
const bookingLoggingUsername = process.env.BOOKING_LOGGING_USER_OR_TEAM_NAME || "";
return bookingLoggingUsername.split(",").some((u) => {
return usernameOrTeamnamesList.some((foundUsername) => foundUsername === u.trim());
});
}

View File

@ -0,0 +1,27 @@
export function isEventTypeLoggingEnabled({
eventTypeId,
usernameOrTeamName,
}: {
eventTypeId?: number | null;
usernameOrTeamName?: string | string[];
}) {
const usernameOrTeamnamesList =
usernameOrTeamName instanceof Array ? usernameOrTeamName : [usernameOrTeamName];
// eslint-disable-next-line turbo/no-undeclared-env-vars
const bookingLoggingEventIds = process.env.BOOKING_LOGGING_EVENT_IDS || "";
const isEnabled = bookingLoggingEventIds.split(",").some((id) => {
if (Number(id.trim()) === eventTypeId) {
return true;
}
});
if (isEnabled) {
return true;
}
// eslint-disable-next-line turbo/no-undeclared-env-vars
const bookingLoggingUsername = process.env.BOOKING_LOGGING_USER_OR_TEAM_NAME || "";
return bookingLoggingUsername.split(",").some((u) => {
return usernameOrTeamnamesList.some((foundUsername) => foundUsername === u.trim());
});
}

View File

@ -1,5 +1,6 @@
import type { Credential, SelectedCalendar } from "@prisma/client";
import type { Credential, SelectedCalendar, DestinationCalendar } from "@prisma/client";
import type { EventType } from "@calcom/prisma/client";
import type { CalendarEvent } from "@calcom/types/Calendar";
export function getPiiFreeCalendarEvent(calEvent: CalendarEvent) {
@ -15,6 +16,7 @@ export function getPiiFreeCalendarEvent(calEvent: CalendarEvent) {
recurrence: calEvent.recurrence,
requiresConfirmation: calEvent.requiresConfirmation,
uid: calEvent.uid,
iCalUID: calEvent.iCalUID,
/**
* Let's just get a boolean value for PII sensitive fields so that we atleast know if it's present or not
*/
@ -66,10 +68,54 @@ export function getPiiFreeSelectedCalendar(selectedCalendar: Partial<SelectedCal
return {
integration: selectedCalendar.integration,
userId: selectedCalendar.userId,
/**
* Let's just get a boolean value for PII sensitive fields so that we atleast know if it's present or not
*/
externalId: !!selectedCalendar.externalId,
// Get first 3 characters of externalId, so that we know what it could be but not the full value
externalId: selectedCalendar.externalId?.slice(0, 3),
credentialId: !!selectedCalendar.credentialId,
};
}
export function getPiiFreeDestinationCalendar(destinationCalendar: Partial<DestinationCalendar>) {
return {
integration: destinationCalendar.integration,
userId: destinationCalendar.userId,
/**
* Let's just get a boolean value for PII sensitive fields so that we atleast know if it's present or not
*/
externalId: !!destinationCalendar.externalId,
credentialId: !!destinationCalendar.credentialId,
};
}
export function getPiiFreeEventType(eventType: Partial<Omit<EventType, "recurringEvent">>) {
return {
id: eventType.id,
schedulingType: eventType.schedulingType,
seatsPerTimeSlot: eventType.seatsPerTimeSlot,
};
}
export function getPiiFreeUser(user: {
id?: number;
username?: string | null;
isFixed?: boolean;
timeZone?: string;
allowDynamicBooking?: boolean | null;
defaultScheduleId?: number | null;
organizationId?: number | null;
credentials?: Credential[];
destinationCalendar?: DestinationCalendar | null;
}) {
return {
id: user.id,
username: user.username,
isFixed: user.isFixed,
timeZone: user.timeZone,
allowDynamicBooking: user.allowDynamicBooking,
defaultScheduleId: user.defaultScheduleId,
organizationId: user.organizationId,
credentials: user.credentials?.map(getPiiFreeCredential),
destinationCalendar: user.destinationCalendar
? getPiiFreeDestinationCalendar(user.destinationCalendar)
: user.destinationCalendar,
};
}

View File

@ -1,6 +1,7 @@
import logger from "@calcom/lib/logger";
import type { MembershipRole } from "@calcom/prisma/enums";
import { safeStringify } from "../safeStringify";
import type { ConsoleUserInfoType, TeamInfoType, WebUserInfoType } from "./ISyncService";
import services from "./services";
import CloseComService from "./services/CloseComService";
@ -9,7 +10,7 @@ const log = logger.getChildLogger({ prefix: [`[[SyncServiceManager] `] });
export const createConsoleUser = async (user: ConsoleUserInfoType | null | undefined) => {
if (user) {
log.debug("createConsoleUser", { user });
log.debug("createConsoleUser", safeStringify({ user }));
try {
Promise.all(
services.map(async (serviceClass) => {
@ -24,16 +25,16 @@ export const createConsoleUser = async (user: ConsoleUserInfoType | null | undef
})
);
} catch (e) {
log.warn("createConsoleUser", e);
log.warn("createConsoleUser", safeStringify({ error: e }));
}
} else {
log.warn("createConsoleUser:noUser", { user });
log.warn("createConsoleUser:noUser", safeStringify({ user }));
}
};
export const createWebUser = async (user: WebUserInfoType | null | undefined) => {
if (user) {
log.debug("createWebUser", { user });
log.debug("createWebUser", safeStringify({ user }));
try {
Promise.all(
services.map(async (serviceClass) => {
@ -48,16 +49,16 @@ export const createWebUser = async (user: WebUserInfoType | null | undefined) =>
})
);
} catch (e) {
log.warn("createWebUser", e);
log.warn("createWebUser", safeStringify({ error: e }));
}
} else {
log.warn("createWebUser:noUser", { user });
log.warn("createWebUser:noUser", safeStringify({ user }));
}
};
export const updateWebUser = async (user: WebUserInfoType | null | undefined) => {
if (user) {
log.debug("updateWebUser", { user });
log.debug("updateWebUser", safeStringify({ user }));
try {
Promise.all(
services.map(async (serviceClass) => {
@ -72,10 +73,10 @@ export const updateWebUser = async (user: WebUserInfoType | null | undefined) =>
})
);
} catch (e) {
log.warn("updateWebUser", e);
log.warn("updateWebUser", safeStringify({ error: e }));
}
} else {
log.warn("updateWebUser:noUser", { user });
log.warn("updateWebUser:noUser", safeStringify({ user }));
}
};

View File

@ -1,6 +1,10 @@
declare global {
// eslint-disable-next-line no-var
var testEmails: {
icalEvent?: {
filename: string;
content: string;
};
to: string;
from: string;
subject: string;

View File

@ -8,6 +8,7 @@ import { getUserAvailability } from "@calcom/core/getUserAvailability";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains";
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import isTimeOutOfBounds from "@calcom/lib/isOutOfBounds";
import logger from "@calcom/lib/logger";
@ -266,25 +267,30 @@ export function getRegularOrDynamicEventType(
export async function getAvailableSlots({ input, ctx }: GetScheduleOptions) {
const orgDetails = orgDomainConfig(ctx?.req?.headers.host ?? "");
if (input.debug === true) {
logger.setSettings({ minLevel: "debug" });
}
if (process.env.INTEGRATION_TEST_MODE === "true") {
logger.setSettings({ minLevel: "silly" });
}
const startPrismaEventTypeGet = performance.now();
const eventType = await getRegularOrDynamicEventType(input, orgDetails);
const endPrismaEventTypeGet = performance.now();
logger.debug(
`Prisma eventType get took ${endPrismaEventTypeGet - startPrismaEventTypeGet}ms for event:${
input.eventTypeId
}`
);
if (!eventType) {
throw new TRPCError({ code: "NOT_FOUND" });
}
if (isEventTypeLoggingEnabled({ eventTypeId: eventType.id })) {
logger.setSettings({ minLevel: "debug" });
}
const loggerWithEventDetails = logger.getChildLogger({
prefix: ["getAvailableSlots", `${eventType.id}:${input.usernameList}/${input.eventTypeSlug}`],
});
loggerWithEventDetails.debug(
`Prisma eventType get took ${endPrismaEventTypeGet - startPrismaEventTypeGet}ms for event:${
input.eventTypeId
}`
);
const getStartTime = (startTimeInput: string, timeZone?: string) => {
const startTimeMin = dayjs.utc().add(eventType.minimumBookingNotice, "minutes");
const startTime = timeZone === "Etc/GMT" ? dayjs.utc(startTimeInput) : dayjs(startTimeInput).tz(timeZone);
@ -598,12 +604,12 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions) {
Object.create(null)
);
logger.debug(`getSlots took ${getSlotsTime}ms and executed ${getSlotsCount} times`);
loggerWithEventDetails.debug(`getSlots took ${getSlotsTime}ms and executed ${getSlotsCount} times`);
logger.debug(
loggerWithEventDetails.debug(
`checkForAvailability took ${checkForAvailabilityTime}ms and executed ${checkForAvailabilityCount} times`
);
logger.silly(`Available slots: ${JSON.stringify(computedAvailableSlots)}`);
loggerWithEventDetails.debug(`Available slots: ${JSON.stringify(computedAvailableSlots)}`);
return {
slots: computedAvailableSlots,

View File

@ -15519,6 +15519,17 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:1.4.0":
version: 1.4.0
resolution: "axios@npm:1.4.0"
dependencies:
follow-redirects: ^1.15.0
form-data: ^4.0.0
proxy-from-env: ^1.1.0
checksum: 7fb6a4313bae7f45e89d62c70a800913c303df653f19eafec88e56cea2e3821066b8409bc68be1930ecca80e861c52aa787659df0ffec6ad4d451c7816b9386b
languageName: node
linkType: hard
"axios@npm:>=0.21.2, axios@npm:^0.26.0, axios@npm:^0.26.1":
version: 0.26.1
resolution: "axios@npm:0.26.1"
@ -16677,6 +16688,7 @@ __metadata:
lint-staged: ^12.5.0
lucide-react: ^0.171.0
mailhog: ^4.16.0
node-ical: ^0.16.1
prettier: ^2.8.6
prismock: ^1.21.1
tsc-absolute: ^1.0.0
@ -27381,6 +27393,13 @@ __metadata:
languageName: node
linkType: hard
"luxon@npm:^1.21.3":
version: 1.28.1
resolution: "luxon@npm:1.28.1"
checksum: 2c62999adea79645fd1e931d685f61aeb33eda7f39fbf79b84261a233b62f9590f48336654ee9efd405848bd9e13dc7fe087c701b7126187201a88998dc68b04
languageName: node
linkType: hard
"luxon@npm:^1.26.0":
version: 1.28.0
resolution: "luxon@npm:1.28.0"
@ -28729,6 +28748,15 @@ __metadata:
languageName: node
linkType: hard
"moment-timezone@npm:^0.5.31":
version: 0.5.43
resolution: "moment-timezone@npm:0.5.43"
dependencies:
moment: ^2.29.4
checksum: 8075c897ed8a044f992ef26fe8cdbcad80caf974251db424cae157473cca03be2830de8c74d99341b76edae59f148c9d9d19c1c1d9363259085688ec1cf508d0
languageName: node
linkType: hard
"moment-timezone@npm:^0.5.34":
version: 0.5.34
resolution: "moment-timezone@npm:0.5.34"
@ -28745,6 +28773,13 @@ __metadata:
languageName: node
linkType: hard
"moment@npm:^2.29.4":
version: 2.29.4
resolution: "moment@npm:2.29.4"
checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e
languageName: node
linkType: hard
"mongodb-connection-string-url@npm:^2.6.0":
version: 2.6.0
resolution: "mongodb-connection-string-url@npm:2.6.0"
@ -29438,6 +29473,18 @@ __metadata:
languageName: node
linkType: hard
"node-ical@npm:^0.16.1":
version: 0.16.1
resolution: "node-ical@npm:0.16.1"
dependencies:
axios: 1.4.0
moment-timezone: ^0.5.31
rrule: 2.6.4
uuid: ^9.0.0
checksum: be917c2a38b5514d4ae8c3ae6563d298fa772082b76f428c534eaa2a3f30acbf97a4d010a04ac0d39851c838e41fe65b7f2467cb35b70d1587f870361309a8d8
languageName: node
linkType: hard
"node-int64@npm:^0.4.0":
version: 0.4.0
resolution: "node-int64@npm:0.4.0"
@ -34330,6 +34377,19 @@ __metadata:
languageName: node
linkType: hard
"rrule@npm:2.6.4":
version: 2.6.4
resolution: "rrule@npm:2.6.4"
dependencies:
luxon: ^1.21.3
tslib: ^1.10.0
dependenciesMeta:
luxon:
optional: true
checksum: 510cddbdfae9d91dab84fe484d54cd0a46f2a9cfd58c24fe470b9341f511c3999abc06fd33735884a08b369f700d87cfd49b46999f2f30b11f5b07c43858d695
languageName: node
linkType: hard
"rrule@npm:^2.7.1":
version: 2.7.1
resolution: "rrule@npm:2.7.1"
@ -37547,7 +37607,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:^1.0.0, tslib@npm:^1.11.1, tslib@npm:^1.8.1, tslib@npm:^1.9.3":
"tslib@npm:^1.0.0, tslib@npm:^1.10.0, tslib@npm:^1.11.1, tslib@npm:^1.8.1, tslib@npm:^1.9.3":
version: 1.14.1
resolution: "tslib@npm:1.14.1"
checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd