Compare commits
No commits in common. "main" and "12120-chore-upgrade-to-next-1400" have entirely different histories.
main
...
12120-chor
|
@ -0,0 +1,18 @@
|
|||
name: Auto Comment Merge Conflicts
|
||||
on: push
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
auto-comment-merge-conflicts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: codytseng/auto-comment-merge-conflicts@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
comment-body: "Hey there, there is a merge conflict, can you take a look?"
|
||||
wait-ms: 3000
|
||||
max-retries: 5
|
||||
label-name: "🚨 merge conflict"
|
||||
ignore-authors: dependabot,otherAuthor
|
|
@ -1,4 +0,0 @@
|
|||
# Checkly Tests
|
||||
|
||||
Run as `yarn checkly test`
|
||||
Deploy the tests as `yarn checkly deploy`
|
|
@ -1,53 +0,0 @@
|
|||
import type { Page } from "@playwright/test";
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Org", () => {
|
||||
// Because these pages involve next.config.js rewrites, it's better to test them on production
|
||||
test.describe("Embeds - i.cal.com", () => {
|
||||
test("Org Profile Page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await page.screenshot({ path: "screenshot.jpg" });
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
});
|
||||
|
||||
test("Org User(Peer) Page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/peer/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator("text=Peer Richelsen")).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
});
|
||||
|
||||
test("Org User Event(peer/meet) Page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/peer/meet/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
});
|
||||
|
||||
test("Org Team Profile(/sales) page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/sales/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator("text=Cal.com Sales")).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
});
|
||||
|
||||
test("Org Team Event page(/sales/hippa) should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/sales/hipaa/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// This ensures that the route is actually mapped to a page that is using withEmbedSsr
|
||||
async function expectPageToBeServerSideRendered(page: Page) {
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return window.__NEXT_DATA__.props.pageProps.isEmbed;
|
||||
})
|
||||
).toBe(true);
|
||||
}
|
|
@ -3,6 +3,7 @@ import { z } from "zod";
|
|||
import { _DestinationCalendarModel as DestinationCalendar } from "@calcom/prisma/zod";
|
||||
|
||||
export const schemaDestinationCalendarBaseBodyParams = DestinationCalendar.pick({
|
||||
credentialId: true,
|
||||
integration: true,
|
||||
externalId: true,
|
||||
eventTypeId: true,
|
||||
|
@ -14,6 +15,7 @@ const schemaDestinationCalendarCreateParams = z
|
|||
.object({
|
||||
integration: z.string(),
|
||||
externalId: z.string(),
|
||||
credentialId: z.number(),
|
||||
eventTypeId: z.number().optional(),
|
||||
bookingId: z.number().optional(),
|
||||
userId: z.number().optional(),
|
||||
|
@ -45,4 +47,5 @@ export const schemaDestinationCalendarReadPublic = DestinationCalendar.pick({
|
|||
eventTypeId: true,
|
||||
bookingId: true,
|
||||
userId: true,
|
||||
credentialId: true,
|
||||
});
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
|
||||
import {
|
||||
schemaDestinationCalendarEditBodyParams,
|
||||
|
@ -62,251 +56,16 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||
* 404:
|
||||
* description: Destination calendar not found
|
||||
*/
|
||||
type DestinationCalendarType = {
|
||||
userId?: number | null;
|
||||
eventTypeId?: number | null;
|
||||
credentialId: number | null;
|
||||
};
|
||||
|
||||
type UserCredentialType = {
|
||||
id: number;
|
||||
appId: string | null;
|
||||
type: string;
|
||||
userId: number | null;
|
||||
user: {
|
||||
email: string;
|
||||
} | null;
|
||||
teamId: number | null;
|
||||
key: Prisma.JsonValue;
|
||||
invalid: boolean | null;
|
||||
};
|
||||
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin, prisma, query, body } = req;
|
||||
const { prisma, query, body } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const parsedBody = schemaDestinationCalendarEditBodyParams.parse(body);
|
||||
const assignedUserId = isAdmin ? parsedBody.userId || userId : userId;
|
||||
|
||||
validateIntegrationInput(parsedBody);
|
||||
const destinationCalendarObject: DestinationCalendarType = await getDestinationCalendar(id, prisma);
|
||||
await validateRequestAndOwnership({ destinationCalendarObject, parsedBody, assignedUserId, prisma });
|
||||
|
||||
const userCredentials = await getUserCredentials({
|
||||
credentialId: destinationCalendarObject.credentialId,
|
||||
userId: assignedUserId,
|
||||
prisma,
|
||||
});
|
||||
const credentialId = await verifyCredentialsAndGetId({
|
||||
parsedBody,
|
||||
userCredentials,
|
||||
currentCredentialId: destinationCalendarObject.credentialId,
|
||||
});
|
||||
// If the user has passed eventTypeId, we need to remove userId from the update data to make sure we don't link it to user as well
|
||||
if (parsedBody.eventTypeId) parsedBody.userId = undefined;
|
||||
const destinationCalendar = await prisma.destinationCalendar.update({
|
||||
where: { id },
|
||||
data: { ...parsedBody, credentialId },
|
||||
data: parsedBody,
|
||||
});
|
||||
return { destinationCalendar: schemaDestinationCalendarReadPublic.parse(destinationCalendar) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves user credentials associated with a given credential ID and user ID and validates if the credentials belong to this user
|
||||
*
|
||||
* @param credentialId - The ID of the credential to fetch. If not provided, an error is thrown.
|
||||
* @param userId - The user ID against which the credentials need to be verified.
|
||||
* @param prisma - An instance of PrismaClient for database operations.
|
||||
*
|
||||
* @returns - An array containing the matching user credentials.
|
||||
*
|
||||
* @throws HttpError - If `credentialId` is not provided or no associated credentials are found in the database.
|
||||
*/
|
||||
async function getUserCredentials({
|
||||
credentialId,
|
||||
userId,
|
||||
prisma,
|
||||
}: {
|
||||
credentialId: number | null;
|
||||
userId: number;
|
||||
prisma: PrismaClient;
|
||||
}) {
|
||||
if (!credentialId) {
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `Destination calendar missing credential id`,
|
||||
});
|
||||
}
|
||||
const userCredentials = await prisma.credential.findMany({
|
||||
where: { id: credentialId, userId },
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
|
||||
if (!userCredentials || userCredentials.length === 0) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: `Bad request, no associated credentials found`,
|
||||
});
|
||||
}
|
||||
return userCredentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provided credentials and retrieves the associated credential ID.
|
||||
*
|
||||
* This function checks if the `integration` and `externalId` properties from the parsed body are present.
|
||||
* If both properties exist, it fetches the connected calendar credentials using the provided user credentials
|
||||
* and checks for a matching external ID and integration from the list of connected calendars.
|
||||
*
|
||||
* If a match is found, it updates the `credentialId` with the one from the connected calendar.
|
||||
* Otherwise, it throws an HTTP error with a 400 status indicating an invalid credential ID.
|
||||
*
|
||||
* If the parsed body does not contain the necessary properties, the function
|
||||
* returns the `credentialId` from the destination calendar object.
|
||||
*
|
||||
* @param parsedBody - The parsed body from the incoming request, validated against a predefined schema.
|
||||
* Checked if it contain properties like `integration` and `externalId`.
|
||||
* @param userCredentials - An array of user credentials used to fetch the connected calendar credentials.
|
||||
* @param destinationCalendarObject - An object representing the destination calendar. Primarily used
|
||||
* to fetch the default `credentialId`.
|
||||
*
|
||||
* @returns - The verified `credentialId` either from the matched connected calendar in case of updating the destination calendar,
|
||||
* or the provided destination calendar object in other cases.
|
||||
*
|
||||
* @throws HttpError - If no matching connected calendar is found for the given `integration` and `externalId`.
|
||||
*/
|
||||
async function verifyCredentialsAndGetId({
|
||||
parsedBody,
|
||||
userCredentials,
|
||||
currentCredentialId,
|
||||
}: {
|
||||
parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>;
|
||||
userCredentials: UserCredentialType[];
|
||||
currentCredentialId: number | null;
|
||||
}) {
|
||||
if (parsedBody.integration && parsedBody.externalId) {
|
||||
const calendarCredentials = getCalendarCredentials(userCredentials);
|
||||
|
||||
const { connectedCalendars } = await getConnectedCalendars(
|
||||
calendarCredentials,
|
||||
[],
|
||||
parsedBody.externalId
|
||||
);
|
||||
const eligibleCalendars = connectedCalendars[0]?.calendars?.filter((calendar) => !calendar.readOnly);
|
||||
const calendar = eligibleCalendars?.find(
|
||||
(c) => c.externalId === parsedBody.externalId && c.integration === parsedBody.integration
|
||||
);
|
||||
|
||||
if (!calendar?.credentialId)
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "Bad request, credential id invalid",
|
||||
});
|
||||
return calendar?.credentialId;
|
||||
}
|
||||
return currentCredentialId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the request for updating a destination calendar.
|
||||
*
|
||||
* This function checks the validity of the provided eventTypeId against the existing destination calendar object
|
||||
* in the sense that if the destination calendar is not linked to an event type, the eventTypeId can not be provided.
|
||||
*
|
||||
* It also ensures that the eventTypeId, if provided, belongs to the assigned user.
|
||||
*
|
||||
* @param destinationCalendarObject - An object representing the destination calendar.
|
||||
* @param parsedBody - The parsed body from the incoming request, validated against a predefined schema.
|
||||
* @param assignedUserId - The user ID assigned for the operation, which might be an admin or a regular user.
|
||||
* @param prisma - An instance of PrismaClient for database operations.
|
||||
*
|
||||
* @throws HttpError - If the validation fails or inconsistencies are detected in the request data.
|
||||
*/
|
||||
async function validateRequestAndOwnership({
|
||||
destinationCalendarObject,
|
||||
parsedBody,
|
||||
assignedUserId,
|
||||
prisma,
|
||||
}: {
|
||||
destinationCalendarObject: DestinationCalendarType;
|
||||
parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>;
|
||||
assignedUserId: number;
|
||||
prisma: PrismaClient;
|
||||
}) {
|
||||
if (parsedBody.eventTypeId) {
|
||||
if (!destinationCalendarObject.eventTypeId) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: `The provided destination calendar can not be linked to an event type`,
|
||||
});
|
||||
}
|
||||
|
||||
const userEventType = await prisma.eventType.findFirst({
|
||||
where: { id: parsedBody.eventTypeId },
|
||||
select: { userId: true },
|
||||
});
|
||||
|
||||
if (!userEventType || userEventType.userId !== assignedUserId) {
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `Event type with ID ${parsedBody.eventTypeId} not found`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsedBody.eventTypeId) {
|
||||
if (destinationCalendarObject.eventTypeId) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: `The provided destination calendar can only be linked to an event type`,
|
||||
});
|
||||
}
|
||||
if (destinationCalendarObject.userId !== assignedUserId) {
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: `Forbidden`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the destination calendar based on the provided ID as the path parameter, specifically `credentialId` and `eventTypeId`.
|
||||
*
|
||||
* If no matching destination calendar is found for the provided ID, an HTTP error with a 404 status
|
||||
* indicating that the desired destination calendar was not found is thrown.
|
||||
*
|
||||
* @param id - The ID of the destination calendar to be retrieved.
|
||||
* @param prisma - An instance of PrismaClient for database operations.
|
||||
*
|
||||
* @returns - An object containing details of the matching destination calendar, specifically `credentialId` and `eventTypeId`.
|
||||
*
|
||||
* @throws HttpError - If no destination calendar matches the provided ID.
|
||||
*/
|
||||
async function getDestinationCalendar(id: number, prisma: PrismaClient) {
|
||||
const destinationCalendarObject = await prisma.destinationCalendar.findFirst({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
select: { userId: true, eventTypeId: true, credentialId: true },
|
||||
});
|
||||
|
||||
if (!destinationCalendarObject) {
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `Destination calendar with ID ${id} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
return destinationCalendarObject;
|
||||
}
|
||||
|
||||
function validateIntegrationInput(parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>) {
|
||||
if (parsedBody.integration && !parsedBody.externalId) {
|
||||
throw new HttpError({ statusCode: 400, message: "External Id is required with integration value" });
|
||||
}
|
||||
if (!parsedBody.integration && parsedBody.externalId) {
|
||||
throw new HttpError({ statusCode: 400, message: "Integration value is required with external ID" });
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
|
||||
import {
|
||||
schemaDestinationCalendarReadPublic,
|
||||
|
@ -40,6 +38,9 @@ import {
|
|||
* externalId:
|
||||
* type: string
|
||||
* description: 'The external ID of the integration'
|
||||
* credentialId:
|
||||
* type: integer
|
||||
* description: 'The credential ID it is associated with'
|
||||
* eventTypeId:
|
||||
* type: integer
|
||||
* description: 'The ID of the eventType it is associated with'
|
||||
|
@ -64,38 +65,20 @@ async function postHandler(req: NextApiRequest) {
|
|||
const parsedBody = schemaDestinationCalendarCreateBodyParams.parse(body);
|
||||
await checkPermissions(req, userId);
|
||||
|
||||
const assignedUserId = isAdmin && parsedBody.userId ? parsedBody.userId : userId;
|
||||
const assignedUserId = isAdmin ? parsedBody.userId || userId : userId;
|
||||
|
||||
/* Check if credentialId data matches the ownership and integration passed in */
|
||||
const userCredentials = await prisma.credential.findMany({
|
||||
where: {
|
||||
type: parsedBody.integration,
|
||||
userId: assignedUserId,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
const credential = await prisma.credential.findFirst({
|
||||
where: { type: parsedBody.integration, userId: assignedUserId },
|
||||
select: { id: true, type: true, userId: true },
|
||||
});
|
||||
|
||||
if (userCredentials.length === 0)
|
||||
if (!credential)
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "Bad request, credential id invalid",
|
||||
});
|
||||
|
||||
const calendarCredentials = getCalendarCredentials(userCredentials);
|
||||
|
||||
const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, [], parsedBody.externalId);
|
||||
|
||||
const eligibleCalendars = connectedCalendars[0]?.calendars?.filter((calendar) => !calendar.readOnly);
|
||||
const calendar = eligibleCalendars?.find(
|
||||
(c) => c.externalId === parsedBody.externalId && c.integration === parsedBody.integration
|
||||
);
|
||||
if (!calendar?.credentialId)
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "Bad request, credential id invalid",
|
||||
});
|
||||
const credentialId = calendar.credentialId;
|
||||
|
||||
if (parsedBody.eventTypeId) {
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: { id: parsedBody.eventTypeId, userId: parsedBody.userId },
|
||||
|
@ -108,9 +91,7 @@ async function postHandler(req: NextApiRequest) {
|
|||
parsedBody.userId = undefined;
|
||||
}
|
||||
|
||||
const destination_calendar = await prisma.destinationCalendar.create({
|
||||
data: { ...parsedBody, credentialId },
|
||||
});
|
||||
const destination_calendar = await prisma.destinationCalendar.create({ data: { ...parsedBody } });
|
||||
|
||||
return {
|
||||
destinationCalendar: schemaDestinationCalendarReadPublic.parse(destination_calendar),
|
||||
|
|
|
@ -141,6 +141,17 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
: []),
|
||||
];
|
||||
|
||||
const showRecordingActions: ActionType[] = [
|
||||
{
|
||||
id: "view_recordings",
|
||||
label: t("view_recordings"),
|
||||
onClick: () => {
|
||||
setViewRecordingsDialogIsOpen(true);
|
||||
},
|
||||
disabled: mutation.isLoading,
|
||||
},
|
||||
];
|
||||
|
||||
let bookedActions: ActionType[] = [
|
||||
{
|
||||
id: "cancel",
|
||||
|
@ -259,21 +270,11 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
const bookingLink = buildBookingLink();
|
||||
|
||||
const title = booking.title;
|
||||
// To be used after we run query on legacy bookings
|
||||
// const showRecordingsButtons = booking.isRecorded && isPast && isConfirmed;
|
||||
|
||||
const showRecordingsButtons = !!(booking.isRecorded && isPast && isConfirmed);
|
||||
const checkForRecordingsButton =
|
||||
!showRecordingsButtons && (booking.location === "integrations:daily" || booking?.location?.trim() === "");
|
||||
|
||||
const showRecordingActions: ActionType[] = [
|
||||
{
|
||||
id: checkForRecordingsButton ? "check_for_recordings" : "view_recordings",
|
||||
label: checkForRecordingsButton ? t("check_for_recordings") : t("view_recordings"),
|
||||
onClick: () => {
|
||||
setViewRecordingsDialogIsOpen(true);
|
||||
},
|
||||
disabled: mutation.isLoading,
|
||||
},
|
||||
];
|
||||
const showRecordingsButtons =
|
||||
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -298,7 +299,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
paymentCurrency={booking.payment[0].currency}
|
||||
/>
|
||||
)}
|
||||
{(showRecordingsButtons || checkForRecordingsButton) && (
|
||||
{showRecordingsButtons && (
|
||||
<ViewRecordingsDialog
|
||||
booking={booking}
|
||||
isOpenDialog={viewRecordingsDialogIsOpen}
|
||||
|
@ -468,9 +469,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
</>
|
||||
) : null}
|
||||
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
||||
{(showRecordingsButtons || checkForRecordingsButton) && (
|
||||
<TableActions actions={showRecordingActions} />
|
||||
)}
|
||||
{showRecordingsButtons && <TableActions actions={showRecordingActions} />}
|
||||
{isCancelled && booking.rescheduled && (
|
||||
<div className="hidden h-full items-center md:flex">
|
||||
<RequestSentMessage />
|
||||
|
|
|
@ -102,16 +102,6 @@ const matcherConfigRootPath = {
|
|||
source: "/",
|
||||
};
|
||||
|
||||
const matcherConfigRootPathEmbed = {
|
||||
has: [
|
||||
{
|
||||
type: "host",
|
||||
value: orgHostPath,
|
||||
},
|
||||
],
|
||||
source: "/embed",
|
||||
};
|
||||
|
||||
const matcherConfigUserRoute = {
|
||||
has: [
|
||||
{
|
||||
|
@ -255,10 +245,6 @@ const nextConfig = {
|
|||
...matcherConfigRootPath,
|
||||
destination: "/team/:orgSlug?isOrgProfile=1",
|
||||
},
|
||||
{
|
||||
...matcherConfigRootPathEmbed,
|
||||
destination: "/team/:orgSlug/embed?isOrgProfile=1",
|
||||
},
|
||||
{
|
||||
...matcherConfigUserRoute,
|
||||
destination: "/org/:orgSlug/:user",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.4.6",
|
||||
"version": "3.4.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
|
|
@ -62,46 +62,6 @@ const triggerWebhook = async ({
|
|||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
const checkIfUserIsPartOfTheSameTeam = async (
|
||||
teamId: number | undefined | null,
|
||||
userId: number,
|
||||
userEmail: string | undefined | null
|
||||
) => {
|
||||
if (!teamId) return false;
|
||||
|
||||
const getUserQuery = () => {
|
||||
if (!!userEmail) {
|
||||
return {
|
||||
OR: [
|
||||
{
|
||||
id: userId,
|
||||
},
|
||||
{
|
||||
email: userEmail,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
id: userId,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
user: getUserQuery(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return !!team;
|
||||
};
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
|
||||
return res.status(405).json({ message: "No SendGrid API key or email" });
|
||||
|
@ -177,22 +137,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||
|
||||
const isUserAttendeeOrOrganiser =
|
||||
booking?.user?.id === session.user.id ||
|
||||
attendeesList.find(
|
||||
(attendee) => attendee.id === session.user.id || attendee.email === session.user.email
|
||||
);
|
||||
attendeesList.find((attendee) => attendee.id === session.user.id);
|
||||
|
||||
if (!isUserAttendeeOrOrganiser) {
|
||||
const isUserMemberOfTheTeam = checkIfUserIsPartOfTheSameTeam(
|
||||
booking?.eventType?.teamId,
|
||||
session.user.id,
|
||||
session.user.email
|
||||
);
|
||||
|
||||
if (!isUserMemberOfTheTeam) {
|
||||
return res.status(403).send({
|
||||
message: "Unauthorised",
|
||||
});
|
||||
}
|
||||
return res.status(403).send({
|
||||
message: "Unauthorised",
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.booking.update({
|
||||
|
@ -252,7 +202,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||
|
||||
return res.status(403).json({ message: "User does not have team plan to send out emails" });
|
||||
} catch (err) {
|
||||
console.warn("Error in /recorded-daily-video", err);
|
||||
console.warn("something_went_wrong", err);
|
||||
return res.status(500).json({ message: "something went wrong" });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -342,17 +342,14 @@ export default function Success(props: SuccessProps) {
|
|||
<div
|
||||
className={classNames(
|
||||
shouldAlignCentrally ? "text-center" : "",
|
||||
"flex items-end justify-center px-4 pb-20 pt-4 sm:flex sm:p-0"
|
||||
"flex items-end justify-center px-4 pb-20 pt-4 sm:block sm:p-0"
|
||||
)}>
|
||||
<div
|
||||
className={classNames(
|
||||
"main my-4 flex flex-col transition-opacity sm:my-0 ",
|
||||
isEmbed ? "" : " inset-0"
|
||||
)}
|
||||
className={classNames("my-4 transition-opacity sm:my-0", isEmbed ? "" : " inset-0")}
|
||||
aria-hidden="true">
|
||||
<div
|
||||
className={classNames(
|
||||
"inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl",
|
||||
"main inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl",
|
||||
!isBackgroundTransparent && " bg-default dark:bg-muted border-booker border-booker-width",
|
||||
"px-8 pb-4 pt-5 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle"
|
||||
)}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
import { getServerSideProps as _getServerSideProps } from "./index";
|
||||
|
||||
export { default } from "./index";
|
||||
|
||||
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
|
@ -283,8 +283,8 @@ const ProfileView = () => {
|
|||
/>
|
||||
|
||||
<div className="border-subtle mt-6 rounded-lg rounded-b-none border border-b-0 p-6">
|
||||
<Label className="mb-0 text-base font-semibold text-red-700">{t("danger_zone")}</Label>
|
||||
<p className="text-subtle text-sm">{t("account_deletion_cannot_be_undone")}</p>
|
||||
<Label className="text-base font-semibold text-red-700">{t("danger_zone")}</Label>
|
||||
<p className="text-subtle">{t("account_deletion_cannot_be_undone")}</p>
|
||||
</div>
|
||||
{/* Delete account Dialog */}
|
||||
<Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}>
|
||||
|
|
|
@ -164,13 +164,14 @@ const PasswordView = ({ user }: PasswordViewProps) => {
|
|||
<>
|
||||
<Meta title={t("password")} description={t("password_description")} borderInShellHeader={true} />
|
||||
{user && user.identityProvider !== IdentityProvider.CAL ? (
|
||||
<div className="border-subtle rounded-b-xl border border-t-0 px-4 py-6 sm:px-6">
|
||||
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">
|
||||
{t("account_managed_by_identity_provider", {
|
||||
provider: identityProviderNameMap[user.identityProvider],
|
||||
})}
|
||||
</h2>
|
||||
|
||||
<div>
|
||||
<div className="mt-6">
|
||||
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">
|
||||
{t("account_managed_by_identity_provider", {
|
||||
provider: identityProviderNameMap[user.identityProvider],
|
||||
})}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-subtle mt-1 text-sm">
|
||||
{t("account_managed_by_identity_provider_description", {
|
||||
provider: identityProviderNameMap[user.identityProvider],
|
||||
|
@ -179,7 +180,7 @@ const PasswordView = ({ user }: PasswordViewProps) => {
|
|||
</div>
|
||||
) : (
|
||||
<Form form={formMethods} handleSubmit={handleSubmit}>
|
||||
<div className="border-subtle border-x px-4 py-6 sm:px-6">
|
||||
<div className="border-x px-4 py-6 sm:px-6">
|
||||
{formMethods.formState.errors.apiError && (
|
||||
<div className="pb-6">
|
||||
<Alert severity="error" message={formMethods.formState.errors.apiError?.message} />
|
||||
|
|
|
@ -1,441 +0,0 @@
|
|||
import { loginUser } from "../fixtures/regularBookings";
|
||||
import { test } from "../lib/fixtures";
|
||||
|
||||
test.describe("Booking With Phone Question and Each Other Question", () => {
|
||||
const bookingOptions = { hasPlaceholder: true, isRequired: true };
|
||||
|
||||
test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||
await loginUser(users);
|
||||
await page.goto("/event-types");
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and Address Question", () => {
|
||||
test("Select required and Address required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and Address question (both required)",
|
||||
secondQuestion: "address",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and Address not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", false, "address test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and Address question (only select required)",
|
||||
secondQuestion: "address",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and checkbox group Question", () => {
|
||||
test("Select required and checkbox group required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and Checkbox question (both required)",
|
||||
secondQuestion: "checkbox",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and checkbox group not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and Checkbox question (only Select required)",
|
||||
secondQuestion: "checkbox",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and checkbox Question", () => {
|
||||
test("Select required and checkbox required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and boolean question (both required)",
|
||||
secondQuestion: "boolean",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and checkbox not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and boolean question (only select required)",
|
||||
secondQuestion: "boolean",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and Long text Question", () => {
|
||||
test("Select required and Long text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and textarea question (both required)",
|
||||
secondQuestion: "textarea",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and Long text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", false, "textarea test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and textarea question (only select required)",
|
||||
secondQuestion: "textarea",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and Multi email Question", () => {
|
||||
test("Select required and Multi email required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and multiemail question (both required)",
|
||||
secondQuestion: "multiemail",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and Multi email not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
false,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and multiemail question (only select required)",
|
||||
secondQuestion: "multiemail",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and multiselect Question", () => {
|
||||
test("Select required and multiselect text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and multiselect question (both required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and multiselect text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and multiselect question (only select required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and Number Question", () => {
|
||||
test("Select required and Number required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("number", "number-test", "number test", true, "number test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and number question (both required)",
|
||||
secondQuestion: "number",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and Number not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("number", "number-test", "number test", false, "number test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and number question (only select required)",
|
||||
secondQuestion: "number",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and Phone Question", () => {
|
||||
test("Select required and select required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and phone question (both required)",
|
||||
secondQuestion: "phone",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and Phone not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", false, "phone test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and phone question (only select required)",
|
||||
secondQuestion: "phone",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and Radio group Question", () => {
|
||||
test("Select required and Radio group required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and radio question (both required)",
|
||||
secondQuestion: "radio",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and Radio group not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and radio question (only select required)",
|
||||
secondQuestion: "radio",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and Short text question", () => {
|
||||
test("Select required and Short text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and text question (both required)",
|
||||
secondQuestion: "text",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and Short text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("text", "text-test", "text test", false, "text test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and text question (only select required)",
|
||||
secondQuestion: "text",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,7 +1,5 @@
|
|||
import { expect, type Page } from "@playwright/test";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
import type { createUsersFixture } from "./users";
|
||||
|
||||
const reschedulePlaceholderText = "Let others know why you need to reschedule";
|
||||
|
@ -40,12 +38,6 @@ type fillAndConfirmBookingParams = {
|
|||
|
||||
type UserFixture = ReturnType<typeof createUsersFixture>;
|
||||
|
||||
function isLastDayOfMonth(): boolean {
|
||||
const today = dayjs();
|
||||
const endOfMonth = today.endOf("month");
|
||||
return today.isSame(endOfMonth, "day");
|
||||
}
|
||||
|
||||
const fillQuestion = async (eventTypePage: Page, questionType: string, customLocators: customLocators) => {
|
||||
const questionActions: QuestionActions = {
|
||||
phone: async () => {
|
||||
|
@ -111,6 +103,7 @@ const fillQuestion = async (eventTypePage: Page, questionType: string, customLoc
|
|||
await eventTypePage.getByPlaceholder(`${questionType} test`).fill("text test");
|
||||
},
|
||||
};
|
||||
|
||||
if (questionActions[questionType]) {
|
||||
await questionActions[questionType]();
|
||||
}
|
||||
|
@ -121,17 +114,6 @@ export async function loginUser(users: UserFixture) {
|
|||
await pro.apiLogin();
|
||||
}
|
||||
|
||||
const goToNextMonthIfNoAvailabilities = async (eventTypePage: Page) => {
|
||||
try {
|
||||
if (isLastDayOfMonth()) {
|
||||
await eventTypePage.getByTestId("view_next_month").waitFor({ timeout: 6000 });
|
||||
await eventTypePage.getByTestId("view_next_month").click();
|
||||
}
|
||||
} catch (err) {
|
||||
console.info("No need to click on view next month button");
|
||||
}
|
||||
};
|
||||
|
||||
export function createBookingPageFixture(page: Page) {
|
||||
return {
|
||||
goToEventType: async (eventType: string) => {
|
||||
|
@ -172,13 +154,19 @@ export function createBookingPageFixture(page: Page) {
|
|||
return eventtypePromise;
|
||||
},
|
||||
selectTimeSlot: async (eventTypePage: Page) => {
|
||||
await goToNextMonthIfNoAvailabilities(eventTypePage);
|
||||
while (await eventTypePage.getByRole("button", { name: "View next" }).isVisible()) {
|
||||
await eventTypePage.getByRole("button", { name: "View next" }).click();
|
||||
}
|
||||
await eventTypePage.getByTestId("time").first().click();
|
||||
},
|
||||
clickReschedule: async () => {
|
||||
await page.getByText("Reschedule").click();
|
||||
},
|
||||
|
||||
navigateToAvailableTimeSlot: async () => {
|
||||
while (await page.getByRole("button", { name: "View next" }).isVisible()) {
|
||||
await page.getByRole("button", { name: "View next" }).click();
|
||||
}
|
||||
},
|
||||
selectFirstAvailableTime: async () => {
|
||||
await page.getByTestId("time").first().click();
|
||||
},
|
||||
|
@ -198,7 +186,6 @@ export function createBookingPageFixture(page: Page) {
|
|||
},
|
||||
|
||||
rescheduleBooking: async (eventTypePage: Page) => {
|
||||
await goToNextMonthIfNoAvailabilities(eventTypePage);
|
||||
await eventTypePage.getByText("Reschedule").click();
|
||||
while (await eventTypePage.getByRole("button", { name: "View next" }).isVisible()) {
|
||||
await eventTypePage.getByRole("button", { name: "View next" }).click();
|
||||
|
@ -235,7 +222,7 @@ export function createBookingPageFixture(page: Page) {
|
|||
|
||||
// Change the selector for specifics cases related to select question
|
||||
const shouldChangeSelectLocator = (question: string, secondQuestion: string): boolean =>
|
||||
question === "select" && ["multiemail", "multiselect", "address"].includes(secondQuestion);
|
||||
question === "select" && ["multiemail", "multiselect"].includes(secondQuestion);
|
||||
|
||||
const shouldUseLastRadioGroupLocator = (question: string, secondQuestion: string): boolean =>
|
||||
question === "radio" && secondQuestion === "checkbox";
|
||||
|
|
|
@ -605,7 +605,7 @@
|
|||
"hide_book_a_team_member": "Hide Book a Team Member Button",
|
||||
"hide_book_a_team_member_description": "Hide Book a Team Member Button from your public pages.",
|
||||
"danger_zone": "Danger zone",
|
||||
"account_deletion_cannot_be_undone":"Be Careful. Account deletion cannot be undone.",
|
||||
"account_deletion_cannot_be_undone":"Careful. Account deletion cannot be undone.",
|
||||
"back": "Back",
|
||||
"cancel": "Cancel",
|
||||
"cancel_all_remaining": "Cancel all remaining",
|
||||
|
@ -1296,7 +1296,6 @@
|
|||
"select_calendars": "Select which calendars you want to check for conflicts to prevent double bookings.",
|
||||
"check_for_conflicts": "Check for conflicts",
|
||||
"view_recordings": "View recordings",
|
||||
"check_for_recordings":"Check for recordings",
|
||||
"adding_events_to": "Adding events to",
|
||||
"follow_system_preferences": "Follow system preferences",
|
||||
"custom_brand_colors": "Custom brand colors",
|
||||
|
|
|
@ -66,7 +66,6 @@ type InputUser = Omit<typeof TestData.users.example, "defaultScheduleId"> & {
|
|||
id: number;
|
||||
defaultScheduleId?: number | null;
|
||||
credentials?: InputCredential[];
|
||||
organizationId?: number | null;
|
||||
selectedCalendars?: InputSelectedCalendar[];
|
||||
schedules: {
|
||||
// Allows giving id in the input directly so that it can be referenced somewhere else as well
|
||||
|
@ -265,21 +264,8 @@ async function addBookingsToDb(
|
|||
})[]
|
||||
) {
|
||||
log.silly("TestData: Creating Bookings", JSON.stringify(bookings));
|
||||
|
||||
function getDateObj(time: string | Date) {
|
||||
return time instanceof Date ? time : new Date(time);
|
||||
}
|
||||
|
||||
// Make sure that we store the date in Date object always. This is to ensure consistency which Prisma does but not prismock
|
||||
log.silly("Handling Prismock bug-3");
|
||||
const fixedBookings = bookings.map((booking) => {
|
||||
const startTime = getDateObj(booking.startTime);
|
||||
const endTime = getDateObj(booking.endTime);
|
||||
return { ...booking, startTime, endTime };
|
||||
});
|
||||
|
||||
await prismock.booking.createMany({
|
||||
data: fixedBookings,
|
||||
data: bookings,
|
||||
});
|
||||
log.silly(
|
||||
"TestData: Bookings as in DB",
|
||||
|
@ -420,7 +406,6 @@ async function addUsers(users: InputUser[]) {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
return newUser;
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
@ -461,16 +446,6 @@ export async function createBookingScenario(data: ScenarioData) {
|
|||
};
|
||||
}
|
||||
|
||||
export async function createOrganization(orgData: { name: string; slug: string }) {
|
||||
const org = await prismock.team.create({
|
||||
data: {
|
||||
name: orgData.name,
|
||||
slug: orgData.slug,
|
||||
},
|
||||
});
|
||||
return org;
|
||||
}
|
||||
|
||||
// async function addPaymentsToDb(payments: Prisma.PaymentCreateInput[]) {
|
||||
// await prismaMock.payment.createMany({
|
||||
// data: payments,
|
||||
|
@ -747,7 +722,6 @@ export function getOrganizer({
|
|||
}) {
|
||||
return {
|
||||
...TestData.users.example,
|
||||
organizationId: null as null | number,
|
||||
name,
|
||||
email,
|
||||
id,
|
||||
|
@ -759,33 +733,24 @@ export function getOrganizer({
|
|||
};
|
||||
}
|
||||
|
||||
export function getScenarioData(
|
||||
{
|
||||
organizer,
|
||||
eventTypes,
|
||||
usersApartFromOrganizer = [],
|
||||
apps = [],
|
||||
webhooks,
|
||||
bookings,
|
||||
}: // hosts = [],
|
||||
{
|
||||
organizer: ReturnType<typeof getOrganizer>;
|
||||
eventTypes: ScenarioData["eventTypes"];
|
||||
apps?: ScenarioData["apps"];
|
||||
usersApartFromOrganizer?: ScenarioData["users"];
|
||||
webhooks?: ScenarioData["webhooks"];
|
||||
bookings?: ScenarioData["bookings"];
|
||||
// hosts?: ScenarioData["hosts"];
|
||||
},
|
||||
org?: { id: number | null } | undefined | null
|
||||
) {
|
||||
export function getScenarioData({
|
||||
organizer,
|
||||
eventTypes,
|
||||
usersApartFromOrganizer = [],
|
||||
apps = [],
|
||||
webhooks,
|
||||
bookings,
|
||||
}: // hosts = [],
|
||||
{
|
||||
organizer: ReturnType<typeof getOrganizer>;
|
||||
eventTypes: ScenarioData["eventTypes"];
|
||||
apps?: ScenarioData["apps"];
|
||||
usersApartFromOrganizer?: ScenarioData["users"];
|
||||
webhooks?: ScenarioData["webhooks"];
|
||||
bookings?: ScenarioData["bookings"];
|
||||
// hosts?: ScenarioData["hosts"];
|
||||
}) {
|
||||
const users = [organizer, ...usersApartFromOrganizer];
|
||||
if (org) {
|
||||
users.forEach((user) => {
|
||||
user.organizationId = org.id;
|
||||
});
|
||||
}
|
||||
|
||||
eventTypes.forEach((eventType) => {
|
||||
if (
|
||||
eventType.users?.filter((eventTypeUser) => {
|
||||
|
@ -932,7 +897,6 @@ export function mockCalendar(
|
|||
url: "https://UNUSED_URL",
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
deleteEvent: async (...rest: any[]) => {
|
||||
log.silly("mockCalendar.deleteEvent", JSON.stringify({ rest }));
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
|
@ -1057,7 +1021,6 @@ export function mockVideoApp({
|
|||
...videoMeetingData,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
deleteMeeting: async (...rest: any[]) => {
|
||||
log.silly("MockVideoApiAdapter.deleteMeeting", JSON.stringify(rest));
|
||||
deleteMeetingCalls.push({
|
||||
|
@ -1190,6 +1153,7 @@ export async function mockPaymentSuccessWebhookFromStripe({ externalId }: { exte
|
|||
await handleStripePaymentSuccess(getMockedStripePaymentEvent({ paymentIntentId: externalId }));
|
||||
} catch (e) {
|
||||
log.silly("mockPaymentSuccessWebhookFromStripe:catch", JSON.stringify(e));
|
||||
|
||||
webhookResponse = e as HttpError;
|
||||
}
|
||||
return { webhookResponse };
|
||||
|
|
|
@ -2,14 +2,10 @@ import prismaMock from "../../../../../tests/libs/__mocks__/prisma";
|
|||
|
||||
import type { WebhookTriggerEvents, Booking, BookingReference, DestinationCalendar } from "@prisma/client";
|
||||
import { parse } from "node-html-parser";
|
||||
import type { VEvent } from "node-ical";
|
||||
import ical from "node-ical";
|
||||
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";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
@ -19,73 +15,42 @@ import type { Fixtures } from "@calcom/web/test/fixtures/fixtures";
|
|||
|
||||
import type { InputEventType } from "./bookingScenario";
|
||||
|
||||
// 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
|
||||
// Alternative could be that we use some other library to do the timezone conversion?
|
||||
function formatDateToWhenFormat({ start, end }: { start: Date; end: Date }, timeZone: string) {
|
||||
const startTime = dayjs(start).tz(timeZone);
|
||||
return `${startTime.format(`dddd, LL`)} | ${startTime.format("h:mma")} - ${dayjs(end)
|
||||
.tz(timeZone)
|
||||
.format("h:mma")} (${timeZone})`;
|
||||
}
|
||||
|
||||
type Recurrence = {
|
||||
freq: number;
|
||||
interval: number;
|
||||
count: number;
|
||||
};
|
||||
type ExpectedEmail = {
|
||||
/**
|
||||
* Checks the main heading of the email - Also referred to as title in code at some places
|
||||
*/
|
||||
heading?: string;
|
||||
links?: { text: string; href: string }[];
|
||||
/**
|
||||
* Checks the sub heading of the email - Also referred to as subTitle in code
|
||||
*/
|
||||
subHeading?: string;
|
||||
/**
|
||||
* Checks the <title> tag - Not sure what's the use of it, as it is not shown in UI it seems.
|
||||
*/
|
||||
titleTag?: string;
|
||||
to: string;
|
||||
bookingTimeRange?: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
timeZone: string;
|
||||
};
|
||||
// TODO: Implement these and more
|
||||
// what?: string;
|
||||
// when?: string;
|
||||
// who?: string;
|
||||
// where?: string;
|
||||
// additionalNotes?: string;
|
||||
// footer?: {
|
||||
// rescheduleLink?: string;
|
||||
// cancelLink?: string;
|
||||
// };
|
||||
ics?: {
|
||||
filename: string;
|
||||
iCalUID: string;
|
||||
recurrence?: Recurrence;
|
||||
};
|
||||
/**
|
||||
* Checks that there is no
|
||||
*/
|
||||
noIcs?: true;
|
||||
appsStatus?: AppsStatus[];
|
||||
};
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace jest {
|
||||
interface Matchers<R> {
|
||||
toHaveEmail(expectedEmail: ExpectedEmail, to: string): R;
|
||||
toHaveEmail(
|
||||
expectedEmail: {
|
||||
title?: string;
|
||||
to: string;
|
||||
noIcs?: true;
|
||||
ics?: {
|
||||
filename: string;
|
||||
iCalUID: string;
|
||||
};
|
||||
appsStatus?: AppsStatus[];
|
||||
},
|
||||
to: string
|
||||
): R;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect.extend({
|
||||
toHaveEmail(emails: Fixtures["emails"], expectedEmail: ExpectedEmail, to: string) {
|
||||
toHaveEmail(
|
||||
emails: Fixtures["emails"],
|
||||
expectedEmail: {
|
||||
title?: string;
|
||||
to: string;
|
||||
ics: {
|
||||
filename: string;
|
||||
iCalUID: string;
|
||||
};
|
||||
noIcs: true;
|
||||
appsStatus: AppsStatus[];
|
||||
},
|
||||
to: string
|
||||
) {
|
||||
const { isNot } = this;
|
||||
const testEmail = emails.get().find((email) => email.to.includes(to));
|
||||
const emailsToLog = emails
|
||||
|
@ -101,7 +66,6 @@ expect.extend({
|
|||
}
|
||||
const ics = testEmail.icalEvent;
|
||||
const icsObject = ics?.content ? ical.sync.parseICS(ics?.content) : null;
|
||||
const iCalUidData = icsObject ? icsObject[expectedEmail.ics?.iCalUID || ""] : null;
|
||||
|
||||
let isToAddressExpected = true;
|
||||
const isIcsFilenameExpected = expectedEmail.ics ? ics?.filename === expectedEmail.ics.filename : true;
|
||||
|
@ -111,18 +75,13 @@ expect.extend({
|
|||
const emailDom = parse(testEmail.html);
|
||||
|
||||
const actualEmailContent = {
|
||||
titleTag: emailDom.querySelector("title")?.innerText,
|
||||
heading: emailDom.querySelector('[data-testid="heading"]')?.innerText,
|
||||
subHeading: emailDom.querySelector('[data-testid="subHeading"]')?.innerText,
|
||||
when: emailDom.querySelector('[data-testid="when"]')?.innerText,
|
||||
links: emailDom.querySelectorAll("a[href]").map((link) => ({
|
||||
text: link.innerText,
|
||||
href: link.getAttribute("href"),
|
||||
})),
|
||||
title: emailDom.querySelector("title")?.innerText,
|
||||
subject: emailDom.querySelector("subject")?.innerText,
|
||||
};
|
||||
|
||||
const expectedEmailContent = getExpectedEmailContent(expectedEmail);
|
||||
assertHasRecurrence(expectedEmail.ics?.recurrence, (iCalUidData as VEvent)?.rrule?.toString() || "");
|
||||
const expectedEmailContent = {
|
||||
title: expectedEmail.title,
|
||||
};
|
||||
|
||||
const isEmailContentMatched = this.equals(
|
||||
actualEmailContent,
|
||||
|
@ -155,7 +114,7 @@ expect.extend({
|
|||
return {
|
||||
pass: false,
|
||||
actual: ics?.filename,
|
||||
expected: expectedEmail.ics?.filename,
|
||||
expected: expectedEmail.ics.filename,
|
||||
message: () => `ICS Filename ${isNot ? "is" : "is not"} matching`,
|
||||
};
|
||||
}
|
||||
|
@ -164,18 +123,11 @@ expect.extend({
|
|||
return {
|
||||
pass: false,
|
||||
actual: JSON.stringify(icsObject),
|
||||
expected: expectedEmail.ics?.iCalUID,
|
||||
expected: expectedEmail.ics.iCalUID,
|
||||
message: () => `Expected ICS UID ${isNot ? "is" : "isn't"} present in actual`,
|
||||
};
|
||||
}
|
||||
|
||||
if (expectedEmail.noIcs && ics) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `${isNot ? "" : "Not"} expected ics file`,
|
||||
};
|
||||
}
|
||||
|
||||
if (expectedEmail.appsStatus) {
|
||||
const actualAppsStatus = emailDom.querySelectorAll('[data-testid="appsStatus"] li').map((li) => {
|
||||
return li.innerText.trim();
|
||||
|
@ -203,50 +155,6 @@ expect.extend({
|
|||
pass: true,
|
||||
message: () => `Email ${isNot ? "is" : "isn't"} correct`,
|
||||
};
|
||||
|
||||
function getExpectedEmailContent(expectedEmail: ExpectedEmail) {
|
||||
const bookingTimeRange = expectedEmail.bookingTimeRange;
|
||||
const when = bookingTimeRange
|
||||
? formatDateToWhenFormat(
|
||||
{
|
||||
start: bookingTimeRange.start,
|
||||
end: bookingTimeRange.end,
|
||||
},
|
||||
bookingTimeRange.timeZone
|
||||
)
|
||||
: null;
|
||||
|
||||
const expectedEmailContent = {
|
||||
titleTag: expectedEmail.titleTag,
|
||||
heading: expectedEmail.heading,
|
||||
subHeading: expectedEmail.subHeading,
|
||||
when: when ? (expectedEmail.ics?.recurrence ? `starting ${when}` : `${when}`) : undefined,
|
||||
links: expect.arrayContaining(expectedEmail.links || []),
|
||||
};
|
||||
// Remove undefined props so that they aren't matched, they are intentionally left undefined because we don't want to match them
|
||||
Object.keys(expectedEmailContent).filter((key) => {
|
||||
if (expectedEmailContent[key as keyof typeof expectedEmailContent] === undefined) {
|
||||
delete expectedEmailContent[key as keyof typeof expectedEmailContent];
|
||||
}
|
||||
});
|
||||
return expectedEmailContent;
|
||||
}
|
||||
|
||||
function assertHasRecurrence(expectedRecurrence: Recurrence | null | undefined, rrule: string) {
|
||||
if (!expectedRecurrence) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expectedRrule = `FREQ=${
|
||||
expectedRecurrence.freq === 0 ? "YEARLY" : expectedRecurrence.freq === 1 ? "MONTHLY" : "WEEKLY"
|
||||
};COUNT=${expectedRecurrence.count};INTERVAL=${expectedRecurrence.interval}`;
|
||||
|
||||
logger.silly({
|
||||
expectedRrule,
|
||||
rrule,
|
||||
});
|
||||
expect(rrule).toContain(expectedRrule);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -327,50 +235,21 @@ export function expectSuccessfulBookingCreationEmails({
|
|||
guests,
|
||||
otherTeamMembers,
|
||||
iCalUID,
|
||||
recurrence,
|
||||
bookingTimeRange,
|
||||
booking,
|
||||
}: {
|
||||
emails: Fixtures["emails"];
|
||||
organizer: { email: string; name: string; timeZone: string };
|
||||
booker: { email: string; name: string; timeZone?: string };
|
||||
guests?: { email: string; name: string; timeZone?: string }[];
|
||||
otherTeamMembers?: { email: string; name: string; timeZone?: string }[];
|
||||
organizer: { email: string; name: string };
|
||||
booker: { email: string; name: string };
|
||||
guests?: { email: string; name: string }[];
|
||||
otherTeamMembers?: { email: string; name: string }[];
|
||||
iCalUID: string;
|
||||
recurrence?: Recurrence;
|
||||
eventDomain?: string;
|
||||
bookingTimeRange?: { start: Date; end: Date };
|
||||
booking: { uid: string; urlOrigin?: string };
|
||||
}) {
|
||||
const bookingUrlOrigin = booking.urlOrigin || WEBAPP_URL;
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
titleTag: "confirmed_event_type_subject",
|
||||
heading: recurrence ? "new_event_scheduled_recurring" : "new_event_scheduled",
|
||||
subHeading: "",
|
||||
links: [
|
||||
{
|
||||
href: `${bookingUrlOrigin}/reschedule/${booking.uid}`,
|
||||
text: "reschedule",
|
||||
},
|
||||
{
|
||||
href: `${bookingUrlOrigin}/booking/${booking.uid}?cancel=true&allRemainingBookings=false`,
|
||||
text: "cancel",
|
||||
},
|
||||
],
|
||||
...(bookingTimeRange
|
||||
? {
|
||||
bookingTimeRange: {
|
||||
...bookingTimeRange,
|
||||
timeZone: organizer.timeZone,
|
||||
},
|
||||
}
|
||||
: null),
|
||||
title: "confirmed_event_type_subject",
|
||||
to: `${organizer.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
iCalUID: `${iCalUID}`,
|
||||
recurrence,
|
||||
iCalUID: iCalUID,
|
||||
},
|
||||
},
|
||||
`${organizer.email}`
|
||||
|
@ -378,34 +257,12 @@ export function expectSuccessfulBookingCreationEmails({
|
|||
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
titleTag: "confirmed_event_type_subject",
|
||||
heading: recurrence ? "your_event_has_been_scheduled_recurring" : "your_event_has_been_scheduled",
|
||||
subHeading: "emailed_you_and_any_other_attendees",
|
||||
...(bookingTimeRange
|
||||
? {
|
||||
bookingTimeRange: {
|
||||
...bookingTimeRange,
|
||||
// Using the default timezone
|
||||
timeZone: booker.timeZone || DEFAULT_TIMEZONE_BOOKER,
|
||||
},
|
||||
}
|
||||
: null),
|
||||
title: "confirmed_event_type_subject",
|
||||
to: `${booker.name} <${booker.email}>`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
iCalUID: iCalUID,
|
||||
recurrence,
|
||||
},
|
||||
links: [
|
||||
{
|
||||
href: `${bookingUrlOrigin}/reschedule/${booking.uid}`,
|
||||
text: "reschedule",
|
||||
},
|
||||
{
|
||||
href: `${bookingUrlOrigin}/booking/${booking.uid}?cancel=true&allRemainingBookings=false`,
|
||||
text: "cancel",
|
||||
},
|
||||
],
|
||||
},
|
||||
`${booker.name} <${booker.email}>`
|
||||
);
|
||||
|
@ -414,33 +271,13 @@ export function expectSuccessfulBookingCreationEmails({
|
|||
otherTeamMembers.forEach((otherTeamMember) => {
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
titleTag: "confirmed_event_type_subject",
|
||||
heading: recurrence ? "new_event_scheduled_recurring" : "new_event_scheduled",
|
||||
subHeading: "",
|
||||
...(bookingTimeRange
|
||||
? {
|
||||
bookingTimeRange: {
|
||||
...bookingTimeRange,
|
||||
timeZone: otherTeamMember.timeZone || DEFAULT_TIMEZONE_BOOKER,
|
||||
},
|
||||
}
|
||||
: null),
|
||||
title: "confirmed_event_type_subject",
|
||||
// Don't know why but organizer and team members of the eventType don'thave their name here like Booker
|
||||
to: `${otherTeamMember.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
iCalUID: iCalUID,
|
||||
},
|
||||
links: [
|
||||
{
|
||||
href: `${bookingUrlOrigin}/reschedule/${booking.uid}`,
|
||||
text: "reschedule",
|
||||
},
|
||||
{
|
||||
href: `${bookingUrlOrigin}/booking/${booking.uid}?cancel=true&allRemainingBookings=false`,
|
||||
text: "cancel",
|
||||
},
|
||||
],
|
||||
},
|
||||
`${otherTeamMember.email}`
|
||||
);
|
||||
|
@ -451,17 +288,7 @@ export function expectSuccessfulBookingCreationEmails({
|
|||
guests.forEach((guest) => {
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
titleTag: "confirmed_event_type_subject",
|
||||
heading: recurrence ? "your_event_has_been_scheduled_recurring" : "your_event_has_been_scheduled",
|
||||
subHeading: "emailed_you_and_any_other_attendees",
|
||||
...(bookingTimeRange
|
||||
? {
|
||||
bookingTimeRange: {
|
||||
...bookingTimeRange,
|
||||
timeZone: guest.timeZone || DEFAULT_TIMEZONE_BOOKER,
|
||||
},
|
||||
}
|
||||
: null),
|
||||
title: "confirmed_event_type_subject",
|
||||
to: `${guest.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
|
@ -484,7 +311,7 @@ export function expectBrokenIntegrationEmails({
|
|||
// Broken Integration email is only sent to the Organizer
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
titleTag: "broken_integration",
|
||||
title: "broken_integration",
|
||||
to: `${organizer.email}`,
|
||||
// No ics goes in case of broken integration email it seems
|
||||
// ics: {
|
||||
|
@ -517,7 +344,7 @@ export function expectCalendarEventCreationFailureEmails({
|
|||
}) {
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
titleTag: "broken_integration",
|
||||
title: "broken_integration",
|
||||
to: `${organizer.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
|
@ -529,7 +356,7 @@ export function expectCalendarEventCreationFailureEmails({
|
|||
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
titleTag: "calendar_event_creation_failure_subject",
|
||||
title: "calendar_event_creation_failure_subject",
|
||||
to: `${booker.name} <${booker.email}>`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
|
@ -551,11 +378,11 @@ export function expectSuccessfulBookingRescheduledEmails({
|
|||
organizer: { email: string; name: string };
|
||||
booker: { email: string; name: string };
|
||||
iCalUID: string;
|
||||
appsStatus?: AppsStatus[];
|
||||
appsStatus: AppsStatus[];
|
||||
}) {
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
titleTag: "event_type_has_been_rescheduled_on_time_date",
|
||||
title: "event_type_has_been_rescheduled_on_time_date",
|
||||
to: `${organizer.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
|
@ -568,7 +395,7 @@ export function expectSuccessfulBookingRescheduledEmails({
|
|||
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
titleTag: "event_type_has_been_rescheduled_on_time_date",
|
||||
title: "event_type_has_been_rescheduled_on_time_date",
|
||||
to: `${booker.name} <${booker.email}>`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
|
@ -588,7 +415,7 @@ export function expectAwaitingPaymentEmails({
|
|||
}) {
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
titleTag: "awaiting_payment_subject",
|
||||
title: "awaiting_payment_subject",
|
||||
to: `${booker.name} <${booker.email}>`,
|
||||
noIcs: true,
|
||||
},
|
||||
|
@ -607,7 +434,7 @@ export function expectBookingRequestedEmails({
|
|||
}) {
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
titleTag: "event_awaiting_approval_subject",
|
||||
title: "event_awaiting_approval_subject",
|
||||
to: `${organizer.email}`,
|
||||
noIcs: true,
|
||||
},
|
||||
|
@ -616,7 +443,7 @@ export function expectBookingRequestedEmails({
|
|||
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
titleTag: "booking_submitted_subject",
|
||||
title: "booking_submitted_subject",
|
||||
to: `${booker.email}`,
|
||||
noIcs: true,
|
||||
},
|
||||
|
@ -802,42 +629,32 @@ export function expectSuccessfulCalendarEventCreationInCalendar(
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
updateEventCalls: any[];
|
||||
},
|
||||
expected:
|
||||
| {
|
||||
calendarId?: string | null;
|
||||
videoCallUrl: string;
|
||||
destinationCalendars?: Partial<DestinationCalendar>[];
|
||||
}
|
||||
| {
|
||||
calendarId?: string | null;
|
||||
videoCallUrl: string;
|
||||
destinationCalendars?: Partial<DestinationCalendar>[];
|
||||
}[]
|
||||
) {
|
||||
const expecteds = expected instanceof Array ? expected : [expected];
|
||||
expect(calendarMock.createEventCalls.length).toBe(expecteds.length);
|
||||
for (let i = 0; i < calendarMock.createEventCalls.length; i++) {
|
||||
const expected = expecteds[i];
|
||||
|
||||
const calEvent = calendarMock.createEventCalls[i][0];
|
||||
|
||||
expect(calEvent).toEqual(
|
||||
expect.objectContaining({
|
||||
destinationCalendar: expected.calendarId
|
||||
? [
|
||||
expect.objectContaining({
|
||||
externalId: expected.calendarId,
|
||||
}),
|
||||
]
|
||||
: expected.destinationCalendars
|
||||
? expect.arrayContaining(expected.destinationCalendars.map((cal) => expect.objectContaining(cal)))
|
||||
: null,
|
||||
videoCallData: expect.objectContaining({
|
||||
url: expected.videoCallUrl,
|
||||
}),
|
||||
})
|
||||
);
|
||||
expected: {
|
||||
calendarId?: string | null;
|
||||
videoCallUrl: string;
|
||||
destinationCalendars: Partial<DestinationCalendar>[];
|
||||
}
|
||||
) {
|
||||
expect(calendarMock.createEventCalls.length).toBe(1);
|
||||
const call = calendarMock.createEventCalls[0];
|
||||
const calEvent = call[0];
|
||||
|
||||
expect(calEvent).toEqual(
|
||||
expect.objectContaining({
|
||||
destinationCalendar: expected.calendarId
|
||||
? [
|
||||
expect.objectContaining({
|
||||
externalId: expected.calendarId,
|
||||
}),
|
||||
]
|
||||
: expected.destinationCalendars
|
||||
? expect.arrayContaining(expected.destinationCalendars.map((cal) => expect.objectContaining(cal)))
|
||||
: null,
|
||||
videoCallData: expect.objectContaining({
|
||||
url: expected.videoCallUrl,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function expectSuccessfulCalendarEventUpdationInCalendar(
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
import { defineConfig } from "checkly";
|
||||
|
||||
/**
|
||||
* See https://www.checklyhq.com/docs/cli/project-structure/
|
||||
*/
|
||||
const config = defineConfig({
|
||||
/* A human friendly name for your project */
|
||||
projectName: "calcom-monorepo",
|
||||
/** A logical ID that needs to be unique across your Checkly account,
|
||||
* See https://www.checklyhq.com/docs/cli/constructs/ to learn more about logical IDs.
|
||||
*/
|
||||
logicalId: "calcom-monorepo",
|
||||
/* An optional URL to your Git repo */
|
||||
repoUrl: "https://github.com/checkly/checkly-cli",
|
||||
/* Sets default values for Checks */
|
||||
checks: {
|
||||
/* A default for how often your Check should run in minutes */
|
||||
frequency: 10,
|
||||
/* Checkly data centers to run your Checks as monitors */
|
||||
locations: ["us-east-1", "eu-west-1"],
|
||||
/* An optional array of tags to organize your Checks */
|
||||
tags: ["Web"],
|
||||
/** The Checkly Runtime identifier, determining npm packages and the Node.js version available at runtime.
|
||||
* See https://www.checklyhq.com/docs/cli/npm-packages/
|
||||
*/
|
||||
runtimeId: "2023.02",
|
||||
/* A glob pattern that matches the Checks inside your repo, see https://www.checklyhq.com/docs/cli/using-check-test-match/ */
|
||||
checkMatch: "**/__checks__/**/*.check.ts",
|
||||
browserChecks: {
|
||||
/* A glob pattern matches any Playwright .spec.ts files and automagically creates a Browser Check. This way, you
|
||||
* can just write native Playwright code. See https://www.checklyhq.com/docs/cli/using-check-test-match/
|
||||
* */
|
||||
testMatch: "**/__checks__/**/*.spec.ts",
|
||||
},
|
||||
},
|
||||
cli: {
|
||||
/* The default datacenter location to use when running npx checkly test */
|
||||
runLocation: "eu-west-1",
|
||||
/* An array of default reporters to use when a reporter is not specified with the "--reporter" flag */
|
||||
reporters: ["list"],
|
||||
},
|
||||
});
|
||||
|
||||
export default config;
|
|
@ -81,7 +81,6 @@
|
|||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@types/jsonwebtoken": "^9.0.3",
|
||||
"c8": "^7.13.0",
|
||||
"checkly": "latest",
|
||||
"dotenv-checker": "^1.1.5",
|
||||
"husky": "^8.0.0",
|
||||
"i18n-unused": "^0.13.0",
|
||||
|
|
|
@ -255,10 +255,7 @@ export class CalendarEventBuilder implements ICalendarEventBuilder {
|
|||
const queryParams = new URLSearchParams();
|
||||
queryParams.set("rescheduleUid", `${booking.uid}`);
|
||||
slug = `${slug}`;
|
||||
|
||||
const rescheduleLink = `${
|
||||
this.calendarEvent.bookerUrl ?? WEBAPP_URL
|
||||
}/${slug}?${queryParams.toString()}`;
|
||||
const rescheduleLink = `${WEBAPP_URL}/${slug}?${queryParams.toString()}`;
|
||||
this.rescheduleLink = rescheduleLink;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
|
|
|
@ -9,7 +9,6 @@ import type {
|
|||
} from "@calcom/types/Calendar";
|
||||
|
||||
class CalendarEventClass implements CalendarEvent {
|
||||
bookerUrl?: string | undefined;
|
||||
type!: string;
|
||||
title!: string;
|
||||
startTime!: string;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { CSSProperties } from "react";
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
import EmailCommonDivider from "./EmailCommonDivider";
|
||||
|
||||
|
@ -19,7 +19,6 @@ const EmailScheduledBodyHeaderContent = (props: {
|
|||
wordBreak: "break-word",
|
||||
}}>
|
||||
<div
|
||||
data-testid="heading"
|
||||
style={{
|
||||
fontFamily: "Roboto, Helvetica, sans-serif",
|
||||
fontSize: 24,
|
||||
|
@ -36,7 +35,6 @@ const EmailScheduledBodyHeaderContent = (props: {
|
|||
<tr>
|
||||
<td align="center" style={{ fontSize: 0, padding: "10px 25px", wordBreak: "break-word" }}>
|
||||
<div
|
||||
data-testid="subHeading"
|
||||
style={{
|
||||
fontFamily: "Roboto, Helvetica, sans-serif",
|
||||
fontSize: 16,
|
||||
|
|
|
@ -61,11 +61,11 @@ export function WhenInfo(props: {
|
|||
!!props.calEvent.cancellationReason && !props.calEvent.cancellationReason.includes("$RCH$")
|
||||
}
|
||||
description={
|
||||
<span data-testid="when">
|
||||
<>
|
||||
{recurringEvent?.count ? `${t("starting")} ` : ""}
|
||||
{getRecipientStart(`dddd, LL | ${timeFormat}`)} - {getRecipientEnd(timeFormat)}{" "}
|
||||
<span style={{ color: "#4B5563" }}>({timeZone})</span>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
withSpacer
|
||||
/>
|
||||
|
|
|
@ -161,8 +161,8 @@ const EmailStep = (props: { translationString: string; iconsrc: string }) => {
|
|||
style={{
|
||||
backgroundColor: "#E5E7EB",
|
||||
borderRadius: "48px",
|
||||
minHeight: "48px",
|
||||
minWidth: "48px",
|
||||
height: "48px",
|
||||
width: "48px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
|
|
|
@ -50,13 +50,8 @@ export default class OrganizerDailyVideoDownloadRecordingEmail extends BaseEmail
|
|||
return this.getFormattedRecipientTime({ time: this.calEvent.endTime, format });
|
||||
}
|
||||
|
||||
protected getLocale(): string {
|
||||
return this.calEvent.organizer.language.locale;
|
||||
}
|
||||
|
||||
protected getFormattedDate() {
|
||||
const organizerTimeFormat = this.calEvent.organizer.timeFormat || TimeFormat.TWELVE_HOUR;
|
||||
|
||||
return `${this.getOrganizerStart(organizerTimeFormat)} - ${this.getOrganizerEnd(
|
||||
organizerTimeFormat
|
||||
)}, ${this.t(this.getOrganizerStart("dddd").toLowerCase())}, ${this.t(
|
||||
|
|
|
@ -53,26 +53,24 @@ import {
|
|||
import { createMockNextJsRequest } from "./lib/createMockNextJsRequest";
|
||||
import { getMockRequestDataForBooking } from "./lib/getMockRequestDataForBooking";
|
||||
import { setupAndTeardown } from "./lib/setupAndTeardown";
|
||||
import { testWithAndWithoutOrg } from "./lib/test";
|
||||
|
||||
export type CustomNextApiRequest = NextApiRequest & Request;
|
||||
|
||||
export type CustomNextApiResponse = NextApiResponse & Response;
|
||||
// Local test runs sometime gets too slow
|
||||
const timeout = process.env.CI ? 5000 : 20000;
|
||||
|
||||
describe("handleNewBooking", () => {
|
||||
setupAndTeardown();
|
||||
|
||||
describe("Fresh/New Booking:", () => {
|
||||
testWithAndWithoutOrg(
|
||||
test(
|
||||
`should create a successful booking with Cal Video(Daily Video) if no explicit location is provided
|
||||
1. Should create a booking in the database
|
||||
2. Should send emails to the booker as well as organizer
|
||||
3. Should create a booking in the event's destination calendar
|
||||
3. Should trigger BOOKING_CREATED webhook
|
||||
`,
|
||||
async ({ emails, org }) => {
|
||||
async ({ emails }) => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
|
@ -91,41 +89,37 @@ describe("handleNewBooking", () => {
|
|||
externalId: "organizer@google-calendar.com",
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "event-type-1@google-calendar.com",
|
||||
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,
|
||||
},
|
||||
],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "event-type-1@google-calendar.com",
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
},
|
||||
org?.organization
|
||||
)
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
mockSuccessfulVideoMeetingCreation({
|
||||
|
@ -201,10 +195,6 @@ describe("handleNewBooking", () => {
|
|||
});
|
||||
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
urlOrigin: org ? org.urlOrigin : WEBAPP_URL,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
emails,
|
||||
|
@ -353,9 +343,6 @@ describe("handleNewBooking", () => {
|
|||
});
|
||||
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
emails,
|
||||
|
@ -501,9 +488,6 @@ describe("handleNewBooking", () => {
|
|||
});
|
||||
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
emails,
|
||||
|
@ -765,9 +749,6 @@ describe("handleNewBooking", () => {
|
|||
});
|
||||
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
emails,
|
||||
|
@ -853,14 +834,11 @@ describe("handleNewBooking", () => {
|
|||
const createdBooking = await handleNewBooking(req);
|
||||
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
emails,
|
||||
// Because no calendar was involved, we don't have an ics UID
|
||||
iCalUID: createdBooking.uid!,
|
||||
iCalUID: createdBooking.uid,
|
||||
});
|
||||
|
||||
expectBookingCreatedWebhookToHaveBeenFired({
|
||||
|
@ -1458,9 +1436,6 @@ describe("handleNewBooking", () => {
|
|||
expectWorkflowToBeTriggered();
|
||||
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
emails,
|
||||
|
@ -1755,9 +1730,6 @@ describe("handleNewBooking", () => {
|
|||
expectWorkflowToBeTriggered();
|
||||
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
emails,
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { getDate } from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
|
||||
export const DEFAULT_TIMEZONE_BOOKER = "Asia/Kolkata";
|
||||
export function getBasicMockRequestDataForBooking() {
|
||||
return {
|
||||
start: `${getDate({ dateIncrement: 1 }).dateString}T04:00:00.000Z`,
|
||||
end: `${getDate({ dateIncrement: 1 }).dateString}T04:30:00.000Z`,
|
||||
eventTypeSlug: "no-confirmation",
|
||||
timeZone: DEFAULT_TIMEZONE_BOOKER,
|
||||
timeZone: "Asia/Calcutta",
|
||||
language: "en",
|
||||
user: "teampro",
|
||||
metadata: {},
|
||||
|
@ -21,8 +20,6 @@ export function getMockRequestDataForBooking({
|
|||
eventTypeId: number;
|
||||
rescheduleUid?: string;
|
||||
bookingUid?: string;
|
||||
recurringEventId?: string;
|
||||
recurringCount?: number;
|
||||
responses: {
|
||||
email: string;
|
||||
name: string;
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
import type { TestFunction } from "vitest";
|
||||
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
import type { Fixtures } from "@calcom/web/test/fixtures/fixtures";
|
||||
import { createOrganization } from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
|
||||
const _testWithAndWithoutOrg = (
|
||||
description: Parameters<typeof testWithAndWithoutOrg>[0],
|
||||
fn: Parameters<typeof testWithAndWithoutOrg>[1],
|
||||
timeout: Parameters<typeof testWithAndWithoutOrg>[2],
|
||||
mode: "only" | "skip" | "run" = "run"
|
||||
) => {
|
||||
const t = mode === "only" ? test.only : mode === "skip" ? test.skip : test;
|
||||
t(
|
||||
`${description} - With org`,
|
||||
async ({ emails, meta, task, onTestFailed, expect, skip }) => {
|
||||
const org = await createOrganization({
|
||||
name: "Test Org",
|
||||
slug: "testorg",
|
||||
});
|
||||
|
||||
await fn({
|
||||
meta,
|
||||
task,
|
||||
onTestFailed,
|
||||
expect,
|
||||
emails,
|
||||
skip,
|
||||
org: {
|
||||
organization: org,
|
||||
urlOrigin: `http://${org.slug}.cal.local:3000`,
|
||||
},
|
||||
});
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
t(
|
||||
`${description}`,
|
||||
async ({ emails, meta, task, onTestFailed, expect, skip }) => {
|
||||
await fn({
|
||||
emails,
|
||||
meta,
|
||||
task,
|
||||
onTestFailed,
|
||||
expect,
|
||||
skip,
|
||||
org: null,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
);
|
||||
};
|
||||
|
||||
export const testWithAndWithoutOrg = (
|
||||
description: string,
|
||||
fn: TestFunction<
|
||||
Fixtures & {
|
||||
org: {
|
||||
organization: { id: number | null };
|
||||
urlOrigin?: string;
|
||||
} | null;
|
||||
}
|
||||
>,
|
||||
timeout?: number
|
||||
) => {
|
||||
_testWithAndWithoutOrg(description, fn, timeout, "run");
|
||||
};
|
||||
|
||||
testWithAndWithoutOrg.only = ((description, fn) => {
|
||||
_testWithAndWithoutOrg(description, fn, "only");
|
||||
}) as typeof _testWithAndWithoutOrg;
|
||||
|
||||
testWithAndWithoutOrg.skip = ((description, fn) => {
|
||||
_testWithAndWithoutOrg(description, fn, "skip");
|
||||
}) as typeof _testWithAndWithoutOrg;
|
|
@ -213,9 +213,6 @@ describe("handleNewBooking", () => {
|
|||
});
|
||||
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
otherTeamMembers,
|
||||
|
@ -528,9 +525,6 @@ describe("handleNewBooking", () => {
|
|||
});
|
||||
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
otherTeamMembers,
|
||||
|
@ -848,9 +842,6 @@ describe("handleNewBooking", () => {
|
|||
});
|
||||
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
otherTeamMembers,
|
||||
|
@ -1065,9 +1056,6 @@ describe("handleNewBooking", () => {
|
|||
});
|
||||
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
otherTeamMembers,
|
||||
|
|
|
@ -95,7 +95,7 @@ const NoAvailabilityOverlay = ({
|
|||
return (
|
||||
<div className="bg-muted border-subtle absolute left-1/2 top-40 -mt-10 w-max -translate-x-1/2 -translate-y-1/2 transform rounded-md border p-8 shadow-sm">
|
||||
<h4 className="text-emphasis mb-4 font-medium">{t("no_availability_in_month", { month: month })}</h4>
|
||||
<Button onClick={nextMonthButton} color="primary" EndIcon={ArrowRight} data-testid="view_next_month">
|
||||
<Button onClick={nextMonthButton} color="primary" EndIcon={ArrowRight}>
|
||||
{t("view_next_month")}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth";
|
||||
import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns";
|
||||
|
||||
|
@ -64,22 +63,5 @@ describe("Test Suite: Date Picker", () => {
|
|||
vi.setSystemTime(vi.getRealSystemTime());
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("it returns the correct responses end of month", () => {
|
||||
// test a date at one minute past midnight, end of month.
|
||||
// we use dayjs() as the system timezone can still modify the Date.
|
||||
vi.useFakeTimers().setSystemTime(dayjs().endOf("month").startOf("day").add(1, "second").toDate());
|
||||
|
||||
const currentDate = new Date();
|
||||
const result = getAvailableDatesInMonth({
|
||||
browsingDate: currentDate,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
|
||||
// Undo the forced time we applied earlier, reset to system default.
|
||||
vi.setSystemTime(vi.getRealSystemTime());
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import dayjs from "@calcom/dayjs";
|
||||
import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns";
|
||||
|
||||
// calculate the available dates in the month:
|
||||
|
@ -22,9 +21,7 @@ export function getAvailableDatesInMonth({
|
|||
);
|
||||
for (
|
||||
let date = browsingDate > minDate ? browsingDate : minDate;
|
||||
// Check if date is before the last date of the month
|
||||
// or is the same day, in the same month, in the same year.
|
||||
date < lastDateOfMonth || dayjs(date).isSame(lastDateOfMonth, "day");
|
||||
date <= lastDateOfMonth;
|
||||
date = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1)
|
||||
) {
|
||||
// intersect included dates
|
||||
|
|
|
@ -5,7 +5,6 @@ import { z } from "zod";
|
|||
|
||||
import { getSession } from "@calcom/features/auth/lib/getSession";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
const teamIdschema = z.object({
|
||||
teamId: z.preprocess((a) => parseInt(z.string().parse(a), 10), z.number().positive()),
|
||||
|
@ -64,75 +63,11 @@ export function checkUserIdentifier(creds: Partial<Credentials>) {
|
|||
}
|
||||
|
||||
export function checkPermission(session: Session | null) {
|
||||
if (
|
||||
(session?.user.role !== "ADMIN" && process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "false") ||
|
||||
!session?.user
|
||||
) {
|
||||
if (session?.user.role !== "ADMIN" && process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "false") {
|
||||
throw new Error("You do not have permission to do this.");
|
||||
}
|
||||
}
|
||||
|
||||
async function getImpersonatedUser({
|
||||
session,
|
||||
teamId,
|
||||
creds,
|
||||
}: {
|
||||
session: Session | null;
|
||||
teamId: number | undefined;
|
||||
creds: Credentials | null;
|
||||
}) {
|
||||
let TeamWhereClause: Prisma.MembershipWhereInput = {
|
||||
disableImpersonation: false, // Ensure they have impersonation enabled
|
||||
accepted: true, // Ensure they are apart of the team and not just invited.
|
||||
team: {
|
||||
id: teamId, // Bring back only the right team
|
||||
},
|
||||
};
|
||||
|
||||
// If you are an admin we dont need to follow this flow -> We can just follow the usual flow
|
||||
// If orgId and teamId are the same we can follow the same flow
|
||||
if (session?.user.org?.id && session.user.org.id !== teamId && session?.user.role !== "ADMIN") {
|
||||
TeamWhereClause = {
|
||||
disableImpersonation: false,
|
||||
accepted: true,
|
||||
team: {
|
||||
id: session.user.org.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Get user who is being impersonated
|
||||
const impersonatedUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ username: creds?.username }, { email: creds?.username }],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
role: true,
|
||||
name: true,
|
||||
email: true,
|
||||
organizationId: true,
|
||||
disableImpersonation: true,
|
||||
locale: true,
|
||||
teams: {
|
||||
where: TeamWhereClause,
|
||||
select: {
|
||||
teamId: true,
|
||||
disableImpersonation: true,
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!impersonatedUser) {
|
||||
throw new Error("This user does not exist");
|
||||
}
|
||||
|
||||
return impersonatedUser;
|
||||
}
|
||||
|
||||
const ImpersonationProvider = CredentialsProvider({
|
||||
id: "impersonation-auth",
|
||||
name: "Impersonation",
|
||||
|
@ -150,7 +85,41 @@ const ImpersonationProvider = CredentialsProvider({
|
|||
checkUserIdentifier(creds);
|
||||
checkPermission(session);
|
||||
|
||||
const impersonatedUser = await getImpersonatedUser({ session, teamId, creds });
|
||||
// Get user who is being impersonated
|
||||
const impersonatedUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ username: creds?.username }, { email: creds?.username }],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
role: true,
|
||||
name: true,
|
||||
email: true,
|
||||
organizationId: true,
|
||||
disableImpersonation: true,
|
||||
locale: true,
|
||||
teams: {
|
||||
where: {
|
||||
disableImpersonation: false, // Ensure they have impersonation enabled
|
||||
accepted: true, // Ensure they are apart of the team and not just invited.
|
||||
team: {
|
||||
id: teamId, // Bring back only the right team
|
||||
},
|
||||
},
|
||||
select: {
|
||||
teamId: true,
|
||||
disableImpersonation: true,
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check if impersonating is allowed for this user
|
||||
if (!impersonatedUser) {
|
||||
throw new Error("This user does not exist");
|
||||
}
|
||||
|
||||
if (session?.user.role === "ADMIN") {
|
||||
if (impersonatedUser.disableImpersonation) {
|
||||
|
|
|
@ -96,10 +96,7 @@ export function subdomainSuffix() {
|
|||
|
||||
export function getOrgFullOrigin(slug: string, options: { protocol: boolean } = { protocol: true }) {
|
||||
if (!slug) return WEBAPP_URL;
|
||||
const orgFullOrigin = `${
|
||||
options.protocol ? `${new URL(WEBAPP_URL).protocol}//` : ""
|
||||
}${slug}.${subdomainSuffix()}`;
|
||||
return orgFullOrigin;
|
||||
return `${options.protocol ? `${new URL(WEBAPP_URL).protocol}//` : ""}${slug}.${subdomainSuffix()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -194,7 +194,7 @@ const OrgAppearanceView = ({
|
|||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-5">
|
||||
<div className="border-subtle rounded-md border p-5">
|
||||
<span className="text-default text-sm">{t("only_owner_change")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -113,7 +113,7 @@ const OrgProfileView = () => {
|
|||
{isOrgAdminOrOwner ? (
|
||||
<OrgProfileForm defaultValues={defaultValues} />
|
||||
) : (
|
||||
<div className="border-subtle flex rounded-b-md border border-t-0 px-4 py-8 sm:px-6">
|
||||
<div className="flex">
|
||||
<div className="flex-grow">
|
||||
<div>
|
||||
<Label className="text-emphasis">{t("org_name")}</Label>
|
||||
|
|
|
@ -147,7 +147,7 @@ const PaymentForm = (props: Props) => {
|
|||
formatParams: { amount: { currency: props.payment.currency } },
|
||||
})}
|
||||
onChange={(e) => setHoldAcknowledged(e.target.checked)}
|
||||
descriptionClassName="text-info font-semibold"
|
||||
descriptionClassName="text-blue-900 font-semibold"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -321,7 +321,7 @@ export default function MemberListItem(props: Props) {
|
|||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await signIn("impersonation-auth", {
|
||||
username: props.member.email,
|
||||
username: props.member.username || props.member.email,
|
||||
teamId: props.team.id,
|
||||
});
|
||||
setShowImpersonateModal(false);
|
||||
|
|
|
@ -174,7 +174,7 @@ export const ViewRecordingsDialog = (props: IViewRecordingsDialog) => {
|
|||
|
||||
return (
|
||||
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||
<DialogContent enableOverflow>
|
||||
<DialogContent>
|
||||
<DialogHeader title={t("recordings_title")} subtitle={subtitle} />
|
||||
{roomName ? (
|
||||
<LicenseRequired>
|
||||
|
|
|
@ -27,7 +27,7 @@ export function ImpersonationMemberModal(props: { state: State; dispatch: Dispat
|
|||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await signIn("impersonation-auth", {
|
||||
username: user.email,
|
||||
username: user.username || user.email,
|
||||
teamId: teamId,
|
||||
});
|
||||
props.dispatch({
|
||||
|
|
|
@ -5,8 +5,7 @@ import dayjs from "@calcom/dayjs";
|
|||
import { buildDateRanges, processDateOverride, processWorkingHours, subtract } from "./date-ranges";
|
||||
|
||||
describe("processWorkingHours", () => {
|
||||
// TEMPORAIRLY SKIPPING THIS TEST - Started failing after 29th Oct
|
||||
it.skip("should return the correct working hours given a specific availability, timezone, and date range", () => {
|
||||
it("should return the correct working hours given a specific availability, timezone, and date range", () => {
|
||||
const item = {
|
||||
days: [1, 2, 3, 4, 5], // Monday to Friday
|
||||
startTime: new Date(Date.UTC(2023, 5, 12, 8, 0)), // 8 AM
|
||||
|
@ -48,8 +47,8 @@ describe("processWorkingHours", () => {
|
|||
|
||||
expect(lastAvailableSlot.start.date()).toBe(31);
|
||||
});
|
||||
// TEMPORAIRLY SKIPPING THIS TEST - Started failing after 29th Oct
|
||||
it.skip("should return the correct working hours in the month were DST ends", () => {
|
||||
|
||||
it("should return the correct working hours in the month were DST ends", () => {
|
||||
const item = {
|
||||
days: [0, 1, 2, 3, 4, 5, 6], // Monday to Sunday
|
||||
startTime: new Date(Date.UTC(2023, 5, 12, 8, 0)), // 8 AM
|
||||
|
|
|
@ -22,7 +22,6 @@ import { TRPCError } from "@trpc/server";
|
|||
import { getDefaultScheduleId } from "../viewer/availability/util";
|
||||
import { updateUserMetadataAllowedKeys, type TUpdateProfileInputSchema } from "./updateProfile.schema";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["updateProfile"] });
|
||||
type UpdateProfileOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
|
@ -36,7 +35,6 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
|
|||
const userMetadata = handleUserMetadata({ ctx, input });
|
||||
const data: Prisma.UserUpdateInput = {
|
||||
...input,
|
||||
avatar: await getAvatarToSet(input.avatar),
|
||||
metadata: userMetadata,
|
||||
};
|
||||
|
||||
|
@ -63,6 +61,12 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
|
|||
}
|
||||
}
|
||||
}
|
||||
if (input.avatar) {
|
||||
data.avatar = await resizeBase64Image(input.avatar);
|
||||
}
|
||||
if (input.avatar === null) {
|
||||
data.avatar = null;
|
||||
}
|
||||
|
||||
if (isPremiumUsername) {
|
||||
const stripeCustomerId = userMetadata?.stripeCustomerId;
|
||||
|
@ -230,17 +234,3 @@ const handleUserMetadata = ({ ctx, input }: UpdateProfileOptions) => {
|
|||
// Required so we don't override and delete saved values
|
||||
return { ...userMetadata, ...cleanMetadata };
|
||||
};
|
||||
|
||||
async function getAvatarToSet(avatar: string | null | undefined) {
|
||||
if (avatar === null || avatar === undefined) {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
if (!avatar.startsWith("data:image")) {
|
||||
// Non Base64 avatar currently could only be the dynamic avatar URL(i.e. /{USER}/avatar.png). If we allow setting that URL, we would get infinite redirects on /user/avatar.ts endpoint
|
||||
log.warn("Non Base64 avatar, ignored it", { avatar });
|
||||
// `undefined` would not ignore the avatar, but `null` would remove it. So, we return `undefined` here.
|
||||
return undefined;
|
||||
}
|
||||
return await resizeBase64Image(avatar);
|
||||
}
|
||||
|
|
|
@ -130,15 +130,14 @@ async function getInfoForAllTeams({ ctx, input }: GetOptions) {
|
|||
|
||||
// Get total team count across all teams the user is in (for pagination)
|
||||
|
||||
const totalTeamMembers = await prisma.$queryRaw<
|
||||
{
|
||||
count: number;
|
||||
}[]
|
||||
>`SELECT COUNT(DISTINCT "userId")::integer from "Membership" WHERE "teamId" IN (${Prisma.join(teamIds)})`;
|
||||
const totalTeamMembers =
|
||||
await prisma.$queryRaw<number>`SELECT COUNT(DISTINCT "userId")::integer from "Membership" WHERE "teamId" IN (${Prisma.join(
|
||||
teamIds
|
||||
)})`;
|
||||
|
||||
return {
|
||||
teamMembers,
|
||||
totalTeamMembers: totalTeamMembers[0].count,
|
||||
totalTeamMembers,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
|||
import { isPrismaObjOrUndefined } from "@calcom/lib";
|
||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||
import { getTranslation } from "@calcom/lib/server";
|
||||
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl";
|
||||
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
@ -168,7 +167,6 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
|||
const builder = new CalendarEventBuilder();
|
||||
builder.init({
|
||||
title: bookingToReschedule.title,
|
||||
bookerUrl: await getBookerUrl(user),
|
||||
type: event && event.title ? event.title : bookingToReschedule.title,
|
||||
startTime: bookingToReschedule.startTime.toISOString(),
|
||||
endTime: bookingToReschedule.endTime.toISOString(),
|
||||
|
|
|
@ -13,6 +13,7 @@ vi.mock("@calcom/prisma", () => ({
|
|||
const handlePrismockBugs = () => {
|
||||
const __updateBooking = prismock.booking.update;
|
||||
const __findManyWebhook = prismock.webhook.findMany;
|
||||
const __findManyBooking = prismock.booking.findMany;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
prismock.booking.update = (...rest: any[]) => {
|
||||
// There is a bug in prismock where it considers `createMany` and `create` itself to have the data directly
|
||||
|
@ -45,6 +46,35 @@ const handlePrismockBugs = () => {
|
|||
// @ts-ignore
|
||||
return __findManyWebhook(...rest);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
prismock.booking.findMany = (...rest: any[]) => {
|
||||
// There is a bug in prismock where it considers `createMany` and `create` itself to have the data directly
|
||||
// In booking flows, we encounter such scenario, so let's fix that here directly till it's fixed in prismock
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const where = rest[0]?.where;
|
||||
if (where?.OR) {
|
||||
logger.silly("Fixed Prismock bug-3");
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
where.OR.forEach((or: any) => {
|
||||
if (or.startTime?.gte) {
|
||||
or.startTime.gte = or.startTime.gte.toISOString ? or.startTime.gte.toISOString() : or.startTime.gte;
|
||||
}
|
||||
if (or.startTime?.lte) {
|
||||
or.startTime.lte = or.startTime.lte.toISOString ? or.startTime.lte.toISOString() : or.startTime.lte;
|
||||
}
|
||||
if (or.endTime?.gte) {
|
||||
or.endTime.lte = or.endTime.gte.toISOString ? or.endTime.gte.toISOString() : or.endTime.gte;
|
||||
}
|
||||
if (or.endTime?.lte) {
|
||||
or.endTime.lte = or.endTime.lte.toISOString ? or.endTime.lte.toISOString() : or.endTime.lte;
|
||||
}
|
||||
});
|
||||
}
|
||||
return __findManyBooking(...rest);
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -2,6 +2,9 @@ import { defineConfig } from "vitest/config";
|
|||
|
||||
process.env.INTEGRATION_TEST_MODE = "true";
|
||||
|
||||
// We can't set it during tests because it is used as soon as _metadata.ts is imported which happens before tests start running
|
||||
process.env.DAILY_API_KEY = "MOCK_DAILY_API_KEY";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
coverage: {
|
||||
|
@ -10,12 +13,3 @@ export default defineConfig({
|
|||
testTimeout: 500000,
|
||||
},
|
||||
});
|
||||
|
||||
setEnvVariablesThatAreUsedBeforeSetup();
|
||||
|
||||
function setEnvVariablesThatAreUsedBeforeSetup() {
|
||||
// We can't set it during tests because it is used as soon as _metadata.ts is imported which happens before tests start running
|
||||
process.env.DAILY_API_KEY = "MOCK_DAILY_API_KEY";
|
||||
// With same env variable, we can test both non org and org booking scenarios
|
||||
process.env.NEXT_PUBLIC_WEBAPP_URL = "http://app.cal.local:3000";
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue