fix: Fix 'Book a new time' link in request-reschedule for Team Event (#12261)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
co-authored by
Carina Wollendorfer
Alex van Andel
parent
00df493623
commit
9d06f6dd0e
@@ -0,0 +1,275 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { describe } from "vitest";
|
||||
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { TRequestRescheduleInputSchema } from "@calcom/trpc/server/routers/viewer/bookings/requestReschedule.schema";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
import {
|
||||
createBookingScenario,
|
||||
getGoogleCalendarCredential,
|
||||
TestData,
|
||||
getOrganizer,
|
||||
getBooker,
|
||||
getScenarioData,
|
||||
getMockBookingAttendee,
|
||||
getDate,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
import { expectBookingRequestRescheduledEmails } from "@calcom/web/test/utils/bookingScenario/expects";
|
||||
|
||||
import { getSampleUserInSession } from "../utils/bookingScenario/getSampleUserInSession";
|
||||
import { setupAndTeardown } from "../utils/bookingScenario/setupAndTeardown";
|
||||
|
||||
export type CustomNextApiRequest = NextApiRequest & Request;
|
||||
|
||||
export type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
describe("Handler: requestReschedule", () => {
|
||||
setupAndTeardown();
|
||||
|
||||
describe("User Event Booking", () => {
|
||||
test(`should be able to request-reschedule for a user booking
|
||||
1. RequestReschedule emails go to both attendee and the person requesting the reschedule`, async ({
|
||||
emails,
|
||||
}) => {
|
||||
const { requestRescheduleHandler } = await import(
|
||||
"@calcom/trpc/server/routers/viewer/bookings/requestReschedule.handler"
|
||||
);
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const bookingUid = "MOCKED_BOOKING_UID";
|
||||
const eventTypeSlug = "event-type-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,
|
||||
slug: eventTypeSlug,
|
||||
slotInterval: 45,
|
||||
length: 45,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
uid: bookingUid,
|
||||
eventTypeId: 1,
|
||||
userId: 101,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `${plus1DateString}T05:00:00.000Z`,
|
||||
endTime: `${plus1DateString}T05:15:00.000Z`,
|
||||
attendees: [
|
||||
getMockBookingAttendee({
|
||||
id: 2,
|
||||
name: booker.name,
|
||||
email: booker.email,
|
||||
// Booker's locale when the fresh booking happened earlier
|
||||
locale: "hi",
|
||||
// Booker's timezone when the fresh booking happened earlier
|
||||
timeZone: "Asia/Kolkata",
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
const loggedInUser = {
|
||||
organizationId: null,
|
||||
id: 101,
|
||||
username: "reschedule-requester",
|
||||
name: "Reschedule Requester",
|
||||
email: "reschedule-requester@example.com",
|
||||
};
|
||||
await requestRescheduleHandler(
|
||||
getTrpcHandlerData({
|
||||
user: loggedInUser,
|
||||
input: {
|
||||
bookingId: bookingUid,
|
||||
rescheduleReason: "",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expectBookingRequestRescheduledEmails({
|
||||
booking: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
booker,
|
||||
organizer: organizer,
|
||||
loggedInUser,
|
||||
emails,
|
||||
bookNewTimePath: `/${organizer.username}/${eventTypeSlug}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Team Event Booking", () => {
|
||||
test(`should be able to request-reschedule for a team event booking
|
||||
1. RequestReschedule emails go to both attendee and the person requesting the reschedule`, async ({
|
||||
emails,
|
||||
}) => {
|
||||
const { requestRescheduleHandler } = await import(
|
||||
"@calcom/trpc/server/routers/viewer/bookings/requestReschedule.handler"
|
||||
);
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
teams: [
|
||||
{
|
||||
membership: {
|
||||
accepted: true,
|
||||
},
|
||||
team: {
|
||||
id: 1,
|
||||
name: "Team 1",
|
||||
slug: "team-1",
|
||||
},
|
||||
},
|
||||
],
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const bookingUid = "MOCKED_BOOKING_UID";
|
||||
const eventTypeSlug = "event-type-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,
|
||||
slug: eventTypeSlug,
|
||||
slotInterval: 45,
|
||||
teamId: 1,
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
length: 45,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
uid: bookingUid,
|
||||
eventTypeId: 1,
|
||||
userId: 101,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `${plus1DateString}T05:00:00.000Z`,
|
||||
endTime: `${plus1DateString}T05:15:00.000Z`,
|
||||
attendees: [
|
||||
getMockBookingAttendee({
|
||||
id: 2,
|
||||
name: booker.name,
|
||||
email: booker.email,
|
||||
// Booker's locale when the fresh booking happened earlier
|
||||
locale: "hi",
|
||||
// Booker's timezone when the fresh booking happened earlier
|
||||
timeZone: "Asia/Kolkata",
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
const loggedInUser = {
|
||||
organizationId: null,
|
||||
id: 101,
|
||||
username: "reschedule-requester",
|
||||
name: "Reschedule Requester",
|
||||
email: "reschedule-requester@example.com",
|
||||
};
|
||||
await requestRescheduleHandler(
|
||||
getTrpcHandlerData({
|
||||
user: loggedInUser,
|
||||
input: {
|
||||
bookingId: bookingUid,
|
||||
rescheduleReason: "",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expectBookingRequestRescheduledEmails({
|
||||
booking: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
booker,
|
||||
organizer: organizer,
|
||||
loggedInUser,
|
||||
emails,
|
||||
bookNewTimePath: "/team/team-1/event-type-1",
|
||||
});
|
||||
});
|
||||
test.todo("Verify that the email should go to organizer as well as the team members");
|
||||
});
|
||||
});
|
||||
|
||||
function getTrpcHandlerData({
|
||||
input,
|
||||
user,
|
||||
}: {
|
||||
input: TRequestRescheduleInputSchema;
|
||||
user: Partial<Omit<NonNullable<TrpcSessionUser>, "id" | "email" | "username">> &
|
||||
Pick<NonNullable<TrpcSessionUser>, "id" | "email" | "username">;
|
||||
}) {
|
||||
return {
|
||||
ctx: {
|
||||
user: {
|
||||
...getSampleUserInSession(),
|
||||
...user,
|
||||
} satisfies TrpcSessionUser,
|
||||
},
|
||||
input: input,
|
||||
};
|
||||
}
|
||||
@@ -2,15 +2,15 @@ import appStoreMock from "../../../../../tests/libs/__mocks__/app-store";
|
||||
import i18nMock from "../../../../../tests/libs/__mocks__/libServerI18n";
|
||||
import prismock from "../../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import type { BookingReference, Attendee, Booking } from "@prisma/client";
|
||||
import type { BookingReference, Attendee, Booking, Membership } from "@prisma/client";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { WebhookTriggerEvents } from "@prisma/client";
|
||||
import type Stripe from "stripe";
|
||||
import type { getMockRequestDataForBooking } from "test/utils/bookingScenario/getMockRequestDataForBooking";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import "vitest-fetch-mock";
|
||||
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import type { getMockRequestDataForBooking } from "@calcom/features/bookings/lib/handleNewBooking/test/lib/getMockRequestDataForBooking";
|
||||
import { handleStripePaymentSuccess } from "@calcom/features/ee/payments/api/webhook";
|
||||
import type { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
@@ -68,6 +68,14 @@ type InputUser = Omit<typeof TestData.users.example, "defaultScheduleId"> & {
|
||||
credentials?: InputCredential[];
|
||||
organizationId?: number | null;
|
||||
selectedCalendars?: InputSelectedCalendar[];
|
||||
teams?: {
|
||||
membership: Partial<Membership>;
|
||||
team: {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
}[];
|
||||
schedules: {
|
||||
// Allows giving id in the input directly so that it can be referenced somewhere else as well
|
||||
id?: number;
|
||||
@@ -98,6 +106,7 @@ export type InputEventType = {
|
||||
schedulingType?: SchedulingType;
|
||||
beforeEventBuffer?: number;
|
||||
afterEventBuffer?: number;
|
||||
teamId?: number | null;
|
||||
requiresConfirmation?: boolean;
|
||||
destinationCalendar?: Prisma.DestinationCalendarCreateInput;
|
||||
schedule?: InputUser["schedules"][number];
|
||||
@@ -373,6 +382,7 @@ async function addUsersToDb(users: (Prisma.UserCreateInput & { schedules: Prisma
|
||||
allUsers: await prismock.user.findMany({
|
||||
include: {
|
||||
credentials: true,
|
||||
teams: true,
|
||||
schedules: {
|
||||
include: {
|
||||
availability: true,
|
||||
@@ -385,9 +395,32 @@ async function addUsersToDb(users: (Prisma.UserCreateInput & { schedules: Prisma
|
||||
);
|
||||
}
|
||||
|
||||
async function addTeamsToDb(teams: NonNullable<InputUser["teams"]>[number]["team"][]) {
|
||||
log.silly("TestData: Creating Teams", JSON.stringify(teams));
|
||||
await prismock.team.createMany({
|
||||
data: teams,
|
||||
});
|
||||
const addedTeams = await prismock.team.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: teams.map((team) => team.id),
|
||||
},
|
||||
},
|
||||
});
|
||||
log.silly(
|
||||
"Added teams to Db",
|
||||
safeStringify({
|
||||
addedTeams,
|
||||
})
|
||||
);
|
||||
return addedTeams;
|
||||
}
|
||||
|
||||
async function addUsers(users: InputUser[]) {
|
||||
const prismaUsersCreate = users.map((user) => {
|
||||
const newUser = user;
|
||||
const prismaUsersCreate = [];
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
const newUser = users[i];
|
||||
const user = users[i];
|
||||
if (user.schedules) {
|
||||
newUser.schedules = {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@@ -415,6 +448,22 @@ async function addUsers(users: InputUser[]) {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (user.teams) {
|
||||
const addedTeams = await addTeamsToDb(user.teams.map((team) => team.team));
|
||||
newUser.teams = {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
createMany: {
|
||||
data: user.teams.map((team, index) => {
|
||||
return {
|
||||
teamId: addedTeams[index].id,
|
||||
...team.membership,
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
if (user.selectedCalendars) {
|
||||
newUser.selectedCalendars = {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@@ -425,8 +474,8 @@ async function addUsers(users: InputUser[]) {
|
||||
};
|
||||
}
|
||||
|
||||
return newUser;
|
||||
});
|
||||
prismaUsersCreate.push(newUser);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
await addUsersToDb(prismaUsersCreate);
|
||||
@@ -739,6 +788,7 @@ export function getOrganizer({
|
||||
selectedCalendars,
|
||||
destinationCalendar,
|
||||
defaultScheduleId,
|
||||
teams,
|
||||
}: {
|
||||
name: string;
|
||||
email: string;
|
||||
@@ -748,6 +798,7 @@ export function getOrganizer({
|
||||
selectedCalendars?: InputSelectedCalendar[];
|
||||
defaultScheduleId?: number | null;
|
||||
destinationCalendar?: Prisma.DestinationCalendarCreateInput;
|
||||
teams?: InputUser["teams"];
|
||||
}) {
|
||||
return {
|
||||
...TestData.users.example,
|
||||
@@ -760,6 +811,7 @@ export function getOrganizer({
|
||||
selectedCalendars,
|
||||
destinationCalendar,
|
||||
defaultScheduleId,
|
||||
teams,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -1,6 +1,9 @@
|
||||
import { createMocks } from "node-mocks-http";
|
||||
|
||||
import type { CustomNextApiRequest, CustomNextApiResponse } from "../fresh-booking.test";
|
||||
import type {
|
||||
CustomNextApiRequest,
|
||||
CustomNextApiResponse,
|
||||
} from "@calcom/features/bookings/lib/handleNewBooking/test/fresh-booking.test";
|
||||
|
||||
export function createMockNextJsRequest(...args: Parameters<typeof createMocks>) {
|
||||
return createMocks<CustomNextApiRequest, CustomNextApiResponse>(...args);
|
||||
@@ -8,7 +8,6 @@ import { expect } from "vitest";
|
||||
import "vitest-fetch-mock";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { DEFAULT_TIMEZONE_BOOKER } from "@calcom/features/bookings/lib/handleNewBooking/test/lib/getMockRequestDataForBooking";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
@@ -17,7 +16,8 @@ import type { AppsStatus } from "@calcom/types/Calendar";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { Fixtures } from "@calcom/web/test/fixtures/fixtures";
|
||||
|
||||
import type { InputEventType } from "./bookingScenario";
|
||||
import type { InputEventType, getOrganizer } from "./bookingScenario";
|
||||
import { DEFAULT_TIMEZONE_BOOKER } from "./getMockRequestDataForBooking";
|
||||
|
||||
// This is too complex at the moment, I really need to simplify this.
|
||||
// Maybe we can replace the exact match with a partial match approach that would be easier to maintain but we would still need Dayjs to do the timezone conversion
|
||||
@@ -66,7 +66,7 @@ type ExpectedEmail = {
|
||||
// };
|
||||
ics?: {
|
||||
filename: string;
|
||||
iCalUID: string;
|
||||
iCalUID?: string;
|
||||
recurrence?: Recurrence;
|
||||
};
|
||||
/**
|
||||
@@ -96,7 +96,7 @@ expect.extend({
|
||||
logger.silly("All Emails", JSON.stringify({ numEmails: emailsToLog.length, emailsToLog }));
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `No email sent to ${to}`,
|
||||
message: () => `No email sent to ${to}. All emails are ${JSON.stringify(emailsToLog)}`,
|
||||
};
|
||||
}
|
||||
const ics = testEmail.icalEvent;
|
||||
@@ -105,9 +105,10 @@ expect.extend({
|
||||
|
||||
let isToAddressExpected = true;
|
||||
const isIcsFilenameExpected = expectedEmail.ics ? ics?.filename === expectedEmail.ics.filename : true;
|
||||
const isIcsUIDExpected = expectedEmail.ics
|
||||
? !!(icsObject ? icsObject[expectedEmail.ics.iCalUID] : null)
|
||||
: true;
|
||||
const isIcsUIDExpected =
|
||||
expectedEmail.ics && expectedEmail.ics.iCalUID
|
||||
? !!(icsObject ? icsObject[expectedEmail.ics.iCalUID] : null)
|
||||
: true;
|
||||
const emailDom = parse(testEmail.html);
|
||||
|
||||
const actualEmailContent = {
|
||||
@@ -172,7 +173,7 @@ expect.extend({
|
||||
if (expectedEmail.noIcs && ics) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `${isNot ? "" : "Not"} expected ics file`,
|
||||
message: () => `${isNot ? "" : "Not"} expected ics file ${JSON.stringify(ics)}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -584,6 +585,7 @@ export function expectSuccessfulBookingRescheduledEmails({
|
||||
`${booker.name} <${booker.email}>`
|
||||
);
|
||||
}
|
||||
|
||||
export function expectAwaitingPaymentEmails({
|
||||
emails,
|
||||
booker,
|
||||
@@ -630,6 +632,59 @@ export function expectBookingRequestedEmails({
|
||||
);
|
||||
}
|
||||
|
||||
export function expectBookingRequestRescheduledEmails({
|
||||
emails,
|
||||
loggedInUser,
|
||||
booker,
|
||||
booking,
|
||||
bookNewTimePath,
|
||||
organizer,
|
||||
}: {
|
||||
emails: Fixtures["emails"];
|
||||
organizer: ReturnType<typeof getOrganizer>;
|
||||
loggedInUser: {
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
booker: { email: string; name: string };
|
||||
booking: { uid: string; urlOrigin?: string };
|
||||
bookNewTimePath: string;
|
||||
}) {
|
||||
const bookingUrlOrigin = booking.urlOrigin || WEBAPP_URL;
|
||||
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
titleTag: "rescheduled_event_type_subject",
|
||||
heading: "request_reschedule_booking",
|
||||
subHeading: "request_reschedule_subtitle",
|
||||
links: [
|
||||
{
|
||||
href: `${bookingUrlOrigin}${bookNewTimePath}?rescheduleUid=${booking.uid}`,
|
||||
text: "Book a new time",
|
||||
},
|
||||
],
|
||||
to: `${booker.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
},
|
||||
},
|
||||
`${booker.email}`
|
||||
);
|
||||
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
titleTag: "rescheduled_event_type_subject",
|
||||
heading: "request_reschedule_title_organizer",
|
||||
subHeading: "request_reschedule_subtitle_organizer",
|
||||
to: `${loggedInUser.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
},
|
||||
},
|
||||
`${loggedInUser.email}`
|
||||
);
|
||||
}
|
||||
|
||||
export function expectBookingRequestedWebhookToHaveBeenFired({
|
||||
booker,
|
||||
location,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { UserPermissionRole } from "@calcom/prisma/client";
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
|
||||
export const getSampleUserInSession = function () {
|
||||
return {
|
||||
locale: "",
|
||||
avatar: "",
|
||||
organization: {
|
||||
isOrgAdmin: false,
|
||||
metadata: null,
|
||||
},
|
||||
defaultScheduleId: null,
|
||||
name: "",
|
||||
defaultBookerLayouts: null,
|
||||
timeZone: "Asia/Kolkata",
|
||||
selectedCalendars: [],
|
||||
destinationCalendar: null,
|
||||
emailVerified: new Date(),
|
||||
allowDynamicBooking: false,
|
||||
bio: "",
|
||||
weekStart: "",
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
bufferTime: 0,
|
||||
hideBranding: false,
|
||||
timeFormat: 12,
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
brandColor: "#292929",
|
||||
darkBrandColor: "#fafafa",
|
||||
away: false,
|
||||
metadata: null,
|
||||
role: UserPermissionRole.USER,
|
||||
disableImpersonation: false,
|
||||
organizationId: null,
|
||||
theme: "",
|
||||
createdDate: new Date(),
|
||||
trialEndsAt: new Date(),
|
||||
completedOnboarding: false,
|
||||
allowSEOIndexing: false,
|
||||
receiveMonthlyDigestEmail: false,
|
||||
};
|
||||
};
|
||||
+7
@@ -8,10 +8,15 @@ import {
|
||||
export function setupAndTeardown() {
|
||||
beforeEach(() => {
|
||||
// Required to able to generate token in email in some cases
|
||||
//@ts-expect-error - It is a readonly variable
|
||||
process.env.CALENDSO_ENCRYPTION_KEY = "abcdefghjnmkljhjklmnhjklkmnbhjui";
|
||||
//@ts-expect-error - It is a readonly variable
|
||||
process.env.STRIPE_WEBHOOK_SECRET = "MOCK_STRIPE_WEBHOOK_SECRET";
|
||||
// We are setting it in vitest.config.ts because otherwise it's too late to set it.
|
||||
// process.env.DAILY_API_KEY = "MOCK_DAILY_API_KEY";
|
||||
|
||||
// Ensure that Rate Limiting isn't enforced for tests
|
||||
delete process.env.UPSTASH_REDIS_REST_URL;
|
||||
mockNoTranslations();
|
||||
// mockEnableEmailFeature();
|
||||
enableEmailFeature();
|
||||
@@ -19,7 +24,9 @@ export function setupAndTeardown() {
|
||||
fetchMock.resetMocks();
|
||||
});
|
||||
afterEach(() => {
|
||||
//@ts-expect-error - It is a readonly variable
|
||||
delete process.env.CALENDSO_ENCRYPTION_KEY;
|
||||
//@ts-expect-error - It is a readonly variable
|
||||
delete process.env.STRIPE_WEBHOOK_SECRET;
|
||||
delete process.env.DAILY_API_KEY;
|
||||
globalThis.testEmails = [];
|
||||
+4
-4
@@ -67,10 +67,10 @@ export const testWithAndWithoutOrg = (
|
||||
_testWithAndWithoutOrg(description, fn, timeout, "run");
|
||||
};
|
||||
|
||||
testWithAndWithoutOrg.only = ((description, fn) => {
|
||||
_testWithAndWithoutOrg(description, fn, "only");
|
||||
testWithAndWithoutOrg.only = ((description, fn, timeout) => {
|
||||
_testWithAndWithoutOrg(description, fn, timeout, "only");
|
||||
}) as typeof _testWithAndWithoutOrg;
|
||||
|
||||
testWithAndWithoutOrg.skip = ((description, fn) => {
|
||||
_testWithAndWithoutOrg(description, fn, "skip");
|
||||
testWithAndWithoutOrg.skip = ((description, fn, timeout) => {
|
||||
_testWithAndWithoutOrg(description, fn, timeout, "skip");
|
||||
}) as typeof _testWithAndWithoutOrg;
|
||||
@@ -5,12 +5,15 @@ import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { CalendarEventClass } from "./class";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["builders", "CalendarEvent", "builder"] });
|
||||
const translator = short();
|
||||
const userSelect = Prisma.validator<Prisma.UserArgs>()({
|
||||
select: {
|
||||
@@ -149,6 +152,7 @@ export class CalendarEventBuilder implements ICalendarEventBuilder {
|
||||
} catch (error) {
|
||||
throw new Error("Error while getting eventType");
|
||||
}
|
||||
log.debug("getEventFromEventId.resultEventType", safeStringify(resultEventType));
|
||||
return resultEventType;
|
||||
}
|
||||
|
||||
@@ -244,7 +248,7 @@ export class CalendarEventBuilder implements ICalendarEventBuilder {
|
||||
|
||||
let slug = "";
|
||||
if (isTeam && eventType?.team?.slug) {
|
||||
slug = `/team/${eventType.team?.slug}`;
|
||||
slug = `team/${eventType.team?.slug}/${eventType.slug}`;
|
||||
} else if (isDynamic) {
|
||||
const dynamicSlug = isDynamic ? `${booking.dynamicGroupSlugRef}/${booking.dynamicEventSlugRef}` : "";
|
||||
slug = dynamicSlug;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Booking } from "@prisma/client";
|
||||
import type { Booking } from "@prisma/client";
|
||||
|
||||
import { CalendarEventBuilder } from "./builder";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
|
||||
import type { CalendarEventBuilder } from "./builder";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["builders", "CalendarEvent", "director"] });
|
||||
export class CalendarEventDirector {
|
||||
private builder!: CalendarEventBuilder;
|
||||
private existingBooking!: Partial<Booking>;
|
||||
@@ -44,6 +48,10 @@ export class CalendarEventDirector {
|
||||
this.builder.setDescription(this.builder.eventType.description);
|
||||
this.builder.setNotes(this.existingBooking.description);
|
||||
this.builder.buildRescheduleLink(this.existingBooking, this.builder.eventType);
|
||||
log.debug(
|
||||
"buildForRescheduleEmail",
|
||||
safeStringify({ existingBooking: this.existingBooking, builder: this.builder })
|
||||
);
|
||||
} else {
|
||||
throw new Error("buildForRescheduleEmail.missing.params.required");
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { describe } from "vitest";
|
||||
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
|
||||
import { setupAndTeardown } from "./lib/setupAndTeardown";
|
||||
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
|
||||
|
||||
describe("handleNewBooking", () => {
|
||||
setupAndTeardown();
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
mockVideoAppToCrashOnCreateMeeting,
|
||||
BookingLocations,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest";
|
||||
import {
|
||||
expectWorkflowToBeTriggered,
|
||||
expectSuccessfulBookingCreationEmails,
|
||||
@@ -49,11 +50,9 @@ import {
|
||||
expectBrokenIntegrationEmails,
|
||||
expectSuccessfulCalendarEventCreationInCalendar,
|
||||
} from "@calcom/web/test/utils/bookingScenario/expects";
|
||||
|
||||
import { createMockNextJsRequest } from "./lib/createMockNextJsRequest";
|
||||
import { getMockRequestDataForBooking } from "./lib/getMockRequestDataForBooking";
|
||||
import { setupAndTeardown } from "./lib/setupAndTeardown";
|
||||
import { testWithAndWithoutOrg } from "./lib/test";
|
||||
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
|
||||
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
|
||||
import { testWithAndWithoutOrg } from "@calcom/web/test/utils/bookingScenario/test";
|
||||
|
||||
export type CustomNextApiRequest = NextApiRequest & Request;
|
||||
|
||||
@@ -940,7 +939,7 @@ describe("handleNewBooking", () => {
|
||||
});
|
||||
await handleNewBooking(req);
|
||||
|
||||
expectBrokenIntegrationEmails({ booker, organizer, emails });
|
||||
expectBrokenIntegrationEmails({ organizer, emails });
|
||||
|
||||
expectBookingCreatedWebhookToHaveBeenFired({
|
||||
booker,
|
||||
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
import { describe } from "vitest";
|
||||
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
|
||||
import { setupAndTeardown } from "./lib/setupAndTeardown";
|
||||
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
|
||||
|
||||
describe("handleNewBooking", () => {
|
||||
setupAndTeardown();
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
mockCalendarToHaveNoBusySlots,
|
||||
getDate,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest";
|
||||
import {
|
||||
expectWorkflowToBeTriggered,
|
||||
expectSuccessfulBookingCreationEmails,
|
||||
@@ -23,10 +24,8 @@ import {
|
||||
expectBookingCreatedWebhookToHaveBeenFired,
|
||||
expectSuccessfulCalendarEventCreationInCalendar,
|
||||
} from "@calcom/web/test/utils/bookingScenario/expects";
|
||||
|
||||
import { createMockNextJsRequest } from "./lib/createMockNextJsRequest";
|
||||
import { getMockRequestDataForBooking } from "./lib/getMockRequestDataForBooking";
|
||||
import { setupAndTeardown } from "./lib/setupAndTeardown";
|
||||
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
|
||||
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
|
||||
|
||||
const DAY_IN_MS = 1000 * 60 * 60 * 24;
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
getMockFailingAppStatus,
|
||||
getMockPassingAppStatus,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest";
|
||||
import {
|
||||
expectWorkflowToBeTriggered,
|
||||
expectBookingToBeInDatabase,
|
||||
@@ -37,10 +38,8 @@ import {
|
||||
expectSuccessfulCalendarEventDeletionInCalendar,
|
||||
expectSuccessfulVideoMeetingDeletionInCalendar,
|
||||
} from "@calcom/web/test/utils/bookingScenario/expects";
|
||||
|
||||
import { createMockNextJsRequest } from "./lib/createMockNextJsRequest";
|
||||
import { getMockRequestDataForBooking } from "./lib/getMockRequestDataForBooking";
|
||||
import { setupAndTeardown } from "./lib/setupAndTeardown";
|
||||
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
|
||||
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
|
||||
|
||||
// Local test runs sometime gets too slow
|
||||
const timeout = process.env.CI ? 5000 : 20000;
|
||||
|
||||
+3
-4
@@ -22,6 +22,7 @@ import {
|
||||
BookingLocations,
|
||||
getZoomAppCredential,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest";
|
||||
import {
|
||||
expectWorkflowToBeTriggered,
|
||||
expectSuccessfulBookingCreationEmails,
|
||||
@@ -30,10 +31,8 @@ import {
|
||||
expectSuccessfulCalendarEventCreationInCalendar,
|
||||
expectSuccessfulVideoMeetingCreation,
|
||||
} from "@calcom/web/test/utils/bookingScenario/expects";
|
||||
|
||||
import { createMockNextJsRequest } from "../lib/createMockNextJsRequest";
|
||||
import { getMockRequestDataForBooking } from "../lib/getMockRequestDataForBooking";
|
||||
import { setupAndTeardown } from "../lib/setupAndTeardown";
|
||||
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
|
||||
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
|
||||
|
||||
export type CustomNextApiRequest = NextApiRequest & Request;
|
||||
|
||||
|
||||
@@ -17,8 +17,11 @@ export const getBrand = async (orgId: number | null) => {
|
||||
metadata: true,
|
||||
},
|
||||
});
|
||||
const metadata = teamMetadataSchema.parse(org?.metadata);
|
||||
const slug = (org?.slug || metadata?.requestedSlug) as string;
|
||||
if (!org) {
|
||||
return null;
|
||||
}
|
||||
const metadata = teamMetadataSchema.parse(org.metadata);
|
||||
const slug = (org.slug || metadata?.requestedSlug) as string;
|
||||
const fullDomain = getOrgFullOrigin(slug);
|
||||
const domainSuffix = subdomainSuffix();
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import { cancelScheduledJobs } from "@calcom/features/webhooks/lib/scheduleTrigg
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import { isPrismaObjOrUndefined } from "@calcom/lib";
|
||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { getTranslation } from "@calcom/lib/server";
|
||||
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl";
|
||||
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
|
||||
@@ -36,11 +38,11 @@ type RequestRescheduleOptions = {
|
||||
};
|
||||
input: TRequestRescheduleInputSchema;
|
||||
};
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["requestRescheduleHandler"] });
|
||||
export const requestRescheduleHandler = async ({ ctx, input }: RequestRescheduleOptions) => {
|
||||
const { user } = ctx;
|
||||
const { bookingId, rescheduleReason: cancellationReason } = input;
|
||||
|
||||
log.debug("Started", safeStringify({ bookingId, cancellationReason, user }));
|
||||
const bookingToReschedule = await prisma.booking.findFirstOrThrow({
|
||||
select: {
|
||||
id: true,
|
||||
@@ -98,6 +100,12 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
||||
if (userTeamIds.indexOf(bookingToReschedule?.eventType?.teamId) === -1) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "User isn't a member on the team" });
|
||||
}
|
||||
log.debug(
|
||||
"Request reschedule for team booking",
|
||||
safeStringify({
|
||||
teamId: bookingToReschedule.eventType?.teamId,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!bookingBelongsToTeam && bookingToReschedule.userId !== user.id) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "User isn't owner of the current booking" });
|
||||
@@ -220,6 +228,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
||||
})
|
||||
);
|
||||
|
||||
log.debug("builder.calendarEvent", safeStringify(builder.calendarEvent));
|
||||
// Send emails
|
||||
await sendRequestRescheduleEmail(builder.calendarEvent, {
|
||||
rescheduleLink: builder.rescheduleLink,
|
||||
|
||||
@@ -12,6 +12,7 @@ vi.mock("@calcom/prisma", () => ({
|
||||
|
||||
const handlePrismockBugs = () => {
|
||||
const __updateBooking = prismock.booking.update;
|
||||
const __findFirstOrThrowBooking = prismock.booking.findFirstOrThrow;
|
||||
const __findManyWebhook = prismock.webhook.findMany;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
prismock.booking.update = (...rest: any[]) => {
|
||||
@@ -34,6 +35,14 @@ const handlePrismockBugs = () => {
|
||||
return __updateBooking(...rest);
|
||||
};
|
||||
|
||||
prismock.booking.findFirstOrThrow = (...rest: any[]) => {
|
||||
const { where } = rest[0];
|
||||
delete where.NOT;
|
||||
logger.silly("Fixed Prismock bug with using NOT in where clause");
|
||||
|
||||
return __findFirstOrThrowBooking(...rest);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
prismock.webhook.findMany = (...rest: any[]) => {
|
||||
// There is some bug in prismock where it can't handle complex where clauses
|
||||
|
||||
Reference in New Issue
Block a user