diff --git a/apps/web/test/fixtures/fixtures.ts b/apps/web/test/fixtures/fixtures.ts new file mode 100644 index 0000000000..121c188bb3 --- /dev/null +++ b/apps/web/test/fixtures/fixtures.ts @@ -0,0 +1,20 @@ +// my-test.ts +import { test as base } from "vitest"; + +import { getTestEmails } from "@calcom/lib/testEmails"; + +export interface Fixtures { + emails: ReturnType; +} + +export const test = base.extend({ + emails: async ({}, use) => { + await use(getEmailsFixture()); + }, +}); + +function getEmailsFixture() { + return { + get: getTestEmails, + }; +} diff --git a/apps/web/test/lib/getSchedule.test.ts b/apps/web/test/lib/getSchedule.test.ts index c7205a5ee3..7a7b5edf1b 100644 --- a/apps/web/test/lib/getSchedule.test.ts +++ b/apps/web/test/lib/getSchedule.test.ts @@ -1,23 +1,14 @@ -import type { - EventType as PrismaEventType, - User as PrismaUser, - Booking as PrismaBooking, - App as PrismaApp, -} from "@prisma/client"; - import CalendarManagerMock from "../../../../tests/libs/__mocks__/CalendarManager"; import prismaMock from "../../../../tests/libs/__mocks__/prisma"; import { diff } from "jest-diff"; -import { v4 as uuidv4 } from "uuid"; import { describe, expect, vi, beforeEach, afterEach, test } from "vitest"; -import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; -import type { SchedulingType } from "@calcom/prisma/enums"; import type { BookingStatus } from "@calcom/prisma/enums"; import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types"; import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { getDate, getGoogleCalendarCredential, createBookingScenario} from "../utils/bookingScenario"; // TODO: Mock properly prismaMock.eventType.findUnique.mockResolvedValue(null); @@ -129,6 +120,7 @@ const TestData = { }, users: { example: { + name: "Example", username: "example", defaultScheduleId: 1, email: "example@example.com", @@ -151,63 +143,6 @@ const TestData = { }, }; -type App = { - slug: string; - dirName: string; -}; - -type InputCredential = typeof TestData.credentials.google; - -type InputSelectedCalendar = typeof TestData.selectedCalendars.google; - -type InputUser = typeof TestData.users.example & { id: number } & { - credentials?: InputCredential[]; - selectedCalendars?: InputSelectedCalendar[]; - schedules: { - id: number; - name: string; - availability: { - userId: number | null; - eventTypeId: number | null; - days: number[]; - startTime: Date; - endTime: Date; - date: string | null; - }[]; - timeZone: string; - }[]; -}; - -type InputEventType = { - id: number; - title?: string; - length?: number; - offsetStart?: number; - slotInterval?: number; - minimumBookingNotice?: number; - users?: { id: number }[]; - hosts?: { id: number }[]; - schedulingType?: SchedulingType; - beforeEventBuffer?: number; - afterEventBuffer?: number; -}; - -type InputBooking = { - userId?: number; - eventTypeId: number; - startTime: string; - endTime: string; - title?: string; - status: BookingStatus; - attendees?: { email: string }[]; -}; - -type InputHost = { - id: number; - userId: number; - eventTypeId: number; - isFixed: boolean; -}; const cleanup = async () => { await prisma.eventType.deleteMany(); @@ -241,7 +176,6 @@ describe("getSchedule", () => { ]); const scenarioData = { - hosts: [], eventTypes: [ { id: 1, @@ -350,7 +284,6 @@ describe("getSchedule", () => { endTime: `${plus2DateString}T06:15:00.000Z`, }, ], - hosts: [], }); // Day Plus 2 is completely free - It only has non accepted bookings @@ -449,7 +382,6 @@ describe("getSchedule", () => { schedules: [TestData.schedules.IstWorkHours], }, ], - hosts: [], }); const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); @@ -550,7 +482,6 @@ describe("getSchedule", () => { schedules: [TestData.schedules.IstWorkHours], }, ], - hosts: [], }); const { dateString: todayDateString } = getDate(); const { dateString: minus1DateString } = getDate({ dateIncrement: -1 }); @@ -634,7 +565,6 @@ describe("getSchedule", () => { selectedCalendars: [TestData.selectedCalendars.google], }, ], - hosts: [], apps: [TestData.apps.googleCalendar], }; @@ -710,7 +640,6 @@ describe("getSchedule", () => { }, ], apps: [TestData.apps.googleCalendar], - hosts: [], }; createBookingScenario(scenarioData); @@ -768,7 +697,6 @@ describe("getSchedule", () => { selectedCalendars: [TestData.selectedCalendars.google], }, ], - hosts: [], apps: [TestData.apps.googleCalendar], }; @@ -834,7 +762,6 @@ describe("getSchedule", () => { schedules: [TestData.schedules.IstWorkHoursWithDateOverride(plus2DateString)], }, ], - hosts: [], }; createBookingScenario(scenarioData); @@ -913,15 +840,6 @@ describe("getSchedule", () => { endTime: `${plus2DateString}T04:15:00.000Z`, }, ], - hosts: [ - // This user is a host of our Collective event - { - id: 1, - eventTypeId: 1, - userId: 101, - isFixed: true, - }, - ], }); // Requesting this user's availability for their @@ -1022,7 +940,6 @@ describe("getSchedule", () => { endTime: `${plus2DateString}T05:45:00.000Z`, }, ], - hosts: [], }); const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({ @@ -1162,7 +1079,6 @@ describe("getSchedule", () => { endTime: `${plus3DateString}T04:15:00.000Z`, }, ], - hosts: [], }); const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule({ input: { @@ -1224,219 +1140,3 @@ describe("getSchedule", () => { }); }); -function getGoogleCalendarCredential() { - return { - type: "google_calendar", - key: { - scope: - "https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/calendar.readonly", - token_type: "Bearer", - expiry_date: 1656999025367, - access_token: "ACCESS_TOKEN", - refresh_token: "REFRESH_TOKEN", - }, - }; -} - -function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser[]) { - const baseEventType = { - title: "Base EventType Title", - slug: "base-event-type-slug", - timeZone: null, - beforeEventBuffer: 0, - afterEventBuffer: 0, - schedulingType: null, - - //TODO: What is the purpose of periodStartDate and periodEndDate? Test these? - periodStartDate: new Date("2022-01-21T09:03:48.000Z"), - periodEndDate: new Date("2022-01-21T09:03:48.000Z"), - periodCountCalendarDays: false, - periodDays: 30, - seatsPerTimeSlot: null, - metadata: {}, - minimumBookingNotice: 0, - offsetStart: 0, - }; - const foundEvents: Record = {}; - const eventTypesWithUsers = eventTypes.map((eventType) => { - if (!eventType.slotInterval && !eventType.length) { - throw new Error("eventTypes[number]: slotInterval or length must be defined"); - } - if (foundEvents[eventType.id]) { - throw new Error(`eventTypes[number]: id ${eventType.id} is not unique`); - } - foundEvents[eventType.id] = true; - const users = - eventType.users?.map((userWithJustId) => { - return usersStore.find((user) => user.id === userWithJustId.id); - }) || []; - return { - ...baseEventType, - ...eventType, - users, - }; - }); - - logger.silly("TestData: Creating EventType", eventTypes); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - prismaMock.eventType.findUnique.mockImplementation(({ where }) => { - return new Promise((resolve) => { - const eventType = eventTypesWithUsers.find((e) => e.id === where.id) as unknown as PrismaEventType & { - users: PrismaUser[]; - }; - resolve(eventType); - }); - }); -} - -async function addBookings(bookings: InputBooking[], eventTypes: InputEventType[]) { - logger.silly("TestData: Creating Bookings", bookings); - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - prismaMock.booking.findMany.mockImplementation((findManyArg) => { - const where = findManyArg?.where || {}; - return new Promise((resolve) => { - resolve( - bookings - // We can improve this filter to support the entire where clause but that isn't necessary yet. So, handle what we know we pass to `findMany` and is needed - .filter((booking) => { - /** - * A user is considered busy within a given time period if there - * is a booking they own OR host. This function mocks some of the logic - * for each condition. For details see the following ticket: - * https://github.com/calcom/cal.com/issues/6374 - */ - - // ~~ FIRST CONDITION ensures that this booking is owned by this user - // and that the status is what we want - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const statusIn = where.OR[0].status?.in || []; - const firstConditionMatches = - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - statusIn.includes(booking.status) && booking.userId === where.OR[0].userId; - - // We return this booking if either condition is met - return firstConditionMatches; - }) - .map((booking) => ({ - uid: uuidv4(), - title: "Test Booking Title", - ...booking, - eventType: eventTypes.find((eventType) => eventType.id === booking.eventTypeId), - })) as unknown as PrismaBooking[] - ); - }); - }); -} - -function addUsers(users: InputUser[]) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - prismaMock.user.findUniqueOrThrow.mockImplementation((findUniqueArgs) => { - return new Promise((resolve) => { - resolve({ - email: `IntegrationTestUser${findUniqueArgs?.where.id}@example.com`, - } as unknown as PrismaUser); - }); - }); - - prismaMock.user.findMany.mockResolvedValue( - users.map((user) => { - return { - ...user, - username: `IntegrationTestUser${user.id}`, - email: `IntegrationTestUser${user.id}@example.com`, - }; - }) as unknown as PrismaUser[] - ); -} -type ScenarioData = { - // TODO: Support multiple bookings and add tests with that. - bookings?: InputBooking[]; - users: InputUser[]; - hosts: InputHost[]; - credentials?: InputCredential[]; - apps?: App[]; - selectedCalendars?: InputSelectedCalendar[]; - eventTypes: InputEventType[]; - calendarBusyTimes?: { - start: string; - end: string; - }[]; -}; - -function createBookingScenario(data: ScenarioData) { - logger.silly("TestData: Creating Scenario", data); - - addUsers(data.users); - - const eventType = addEventTypes(data.eventTypes, data.users); - if (data.apps) { - prismaMock.app.findMany.mockResolvedValue(data.apps as PrismaApp[]); - // FIXME: How do we know which app to return? - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - prismaMock.app.findUnique.mockImplementation(({ where: { slug: whereSlug } }) => { - return new Promise((resolve) => { - if (!data.apps) { - resolve(null); - return; - } - resolve((data.apps.find(({ slug }) => slug == whereSlug) as PrismaApp) || null); - }); - }); - } - data.bookings = data.bookings || []; - addBookings(data.bookings, data.eventTypes); - - return { - eventType, - }; -} - -/** - * This fn indents to dynamically compute day, month, year for the purpose of testing. - * We are not using DayJS because that's actually being tested by this code. - * - `dateIncrement` adds the increment to current day - * - `monthIncrement` adds the increment to current month - * - `yearIncrement` adds the increment to current year - */ -const getDate = (param: { dateIncrement?: number; monthIncrement?: number; yearIncrement?: number } = {}) => { - let { dateIncrement, monthIncrement, yearIncrement } = param; - dateIncrement = dateIncrement || 0; - monthIncrement = monthIncrement || 0; - yearIncrement = yearIncrement || 0; - - let _date = new Date().getDate() + dateIncrement; - let year = new Date().getFullYear() + yearIncrement; - - // Make it start with 1 to match with DayJS requiremet - let _month = new Date().getMonth() + monthIncrement + 1; - - // If last day of the month(As _month is plus 1 already it is going to be the 0th day of next month which is the last day of current month) - const lastDayOfMonth = new Date(year, _month, 0).getDate(); - const numberOfDaysForNextMonth = +_date - +lastDayOfMonth; - if (numberOfDaysForNextMonth > 0) { - _date = numberOfDaysForNextMonth; - _month = _month + 1; - } - - if (_month === 13) { - _month = 1; - year = year + 1; - } - - const date = _date < 10 ? "0" + _date : _date; - const month = _month < 10 ? "0" + _month : _month; - - return { - date, - month, - year, - dateString: `${year}-${month}-${date}`, - }; -}; diff --git a/apps/web/test/utils/bookingScenario.ts b/apps/web/test/utils/bookingScenario.ts new file mode 100644 index 0000000000..8b0f448e41 --- /dev/null +++ b/apps/web/test/utils/bookingScenario.ts @@ -0,0 +1,742 @@ +import type { + EventType as PrismaEventType, + User as PrismaUser, + Booking as PrismaBooking, + App as PrismaApp, +} from "@prisma/client"; +import type { Prisma } from "@prisma/client"; +import type { WebhookTriggerEvents } from "@prisma/client"; +import { v4 as uuidv4 } from "uuid"; +import { expect } from "vitest"; +import "vitest-fetch-mock"; + +import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; +import logger from "@calcom/lib/logger"; +import type { SchedulingType } from "@calcom/prisma/enums"; +import type { BookingStatus } from "@calcom/prisma/enums"; +import type { EventBusyDate } from "@calcom/types/Calendar"; +import type { Fixtures } from "@calcom/web/test/fixtures/fixtures"; + +import appStoreMock from "../../../../tests/libs/__mocks__/app-store"; +import i18nMock from "../../../../tests/libs/__mocks__/libServerI18n"; +import prismaMock from "../../../../tests/libs/__mocks__/prisma"; + +type App = { + slug: string; + dirName: string; +}; + +type InputWebhook = { + appId: string | null; + userId?: number | null; + teamId?: number | null; + eventTypeId?: number; + active: boolean; + eventTriggers: WebhookTriggerEvents[]; + subscriberUrl: string; +}; +/** + * Data to be mocked + */ +type ScenarioData = { + // hosts: { id: number; eventTypeId?: number; userId?: number; isFixed?: boolean }[]; + /** + * Prisma would return these eventTypes + */ + eventTypes: InputEventType[]; + /** + * Prisma would return these users + */ + users: InputUser[]; + /** + * Prisma would return these apps + */ + apps?: App[]; + bookings?: InputBooking[]; + webhooks?: InputWebhook[]; +}; + +type InputCredential = typeof TestData.credentials.google; + +type InputSelectedCalendar = typeof TestData.selectedCalendars.google; + +type InputUser = typeof TestData.users.example & { id: number } & { + credentials?: InputCredential[]; + selectedCalendars?: InputSelectedCalendar[]; + schedules: { + id: number; + name: string; + availability: { + userId: number | null; + eventTypeId: number | null; + days: number[]; + startTime: Date; + endTime: Date; + date: string | null; + }[]; + timeZone: string; + }[]; +}; + +type InputEventType = { + id: number; + title?: string; + length?: number; + offsetStart?: number; + slotInterval?: number; + minimumBookingNotice?: number; + /** + * These user ids are `ScenarioData["users"]["id"]` + */ + users?: { id: number }[]; + hosts?: { id: number }[]; + schedulingType?: SchedulingType; + beforeEventBuffer?: number; + afterEventBuffer?: number; + requiresConfirmation?: boolean; +}; + +type InputBooking = { + userId?: number; + eventTypeId: number; + startTime: string; + endTime: string; + title?: string; + status: BookingStatus; + attendees?: { email: string }[]; +}; + +const Timezones = { + "+5:30": "Asia/Kolkata", + "+6:00": "Asia/Dhaka", +}; + +function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser[]) { + const baseEventType = { + title: "Base EventType Title", + slug: "base-event-type-slug", + timeZone: null, + beforeEventBuffer: 0, + afterEventBuffer: 0, + schedulingType: null, + + //TODO: What is the purpose of periodStartDate and periodEndDate? Test these? + periodStartDate: new Date("2022-01-21T09:03:48.000Z"), + periodEndDate: new Date("2022-01-21T09:03:48.000Z"), + periodCountCalendarDays: false, + periodDays: 30, + seatsPerTimeSlot: null, + metadata: {}, + minimumBookingNotice: 0, + offsetStart: 0, + }; + const foundEvents: Record = {}; + const eventTypesWithUsers = eventTypes.map((eventType) => { + if (!eventType.slotInterval && !eventType.length) { + throw new Error("eventTypes[number]: slotInterval or length must be defined"); + } + if (foundEvents[eventType.id]) { + throw new Error(`eventTypes[number]: id ${eventType.id} is not unique`); + } + foundEvents[eventType.id] = true; + const users = + eventType.users?.map((userWithJustId) => { + return usersStore.find((user) => user.id === userWithJustId.id); + }) || []; + return { + ...baseEventType, + ...eventType, + workflows: [], + users, + }; + }); + + logger.silly("TestData: Creating EventType", eventTypes); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const eventTypeMock = ({ where }) => { + return new Promise((resolve) => { + const eventType = eventTypesWithUsers.find((e) => e.id === where.id) as unknown as PrismaEventType & { + users: PrismaUser[]; + }; + resolve(eventType); + }); + }; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + prismaMock.eventType.findUnique.mockImplementation(eventTypeMock); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + prismaMock.eventType.findUniqueOrThrow.mockImplementation(eventTypeMock); +} + +async function addBookings(bookings: InputBooking[], eventTypes: InputEventType[]) { + logger.silly("TestData: Creating Bookings", bookings); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + prismaMock.booking.findMany.mockImplementation((findManyArg) => { + const where = findManyArg?.where || {}; + return new Promise((resolve) => { + resolve( + bookings + // We can improve this filter to support the entire where clause but that isn't necessary yet. So, handle what we know we pass to `findMany` and is needed + .filter((booking) => { + /** + * A user is considered busy within a given time period if there + * is a booking they own OR host. This function mocks some of the logic + * for each condition. For details see the following ticket: + * https://github.com/calcom/cal.com/issues/6374 + */ + + // ~~ FIRST CONDITION ensures that this booking is owned by this user + // and that the status is what we want + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const statusIn = where.OR[0].status?.in || []; + const firstConditionMatches = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + statusIn.includes(booking.status) && booking.userId === where.OR[0].userId; + + // We return this booking if either condition is met + return firstConditionMatches; + }) + .map((booking) => ({ + uid: uuidv4(), + title: "Test Booking Title", + ...booking, + eventType: eventTypes.find((eventType) => eventType.id === booking.eventTypeId), + })) as unknown as PrismaBooking[] + ); + }); + }); +} + +async function addWebhooks(webhooks: InputWebhook[]) { + prismaMock.webhook.findMany.mockResolvedValue( + webhooks.map((webhook) => { + return { + ...webhook, + payloadTemplate: null, + secret: null, + id: uuidv4(), + createdAt: new Date(), + userId: webhook.userId || null, + eventTypeId: webhook.eventTypeId || null, + teamId: webhook.teamId || null, + }; + }) + ); +} + +function addUsers(users: InputUser[]) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + prismaMock.user.findUniqueOrThrow.mockImplementation((findUniqueArgs) => { + return new Promise((resolve) => { + resolve({ + email: `IntegrationTestUser${findUniqueArgs?.where.id}@example.com`, + } as unknown as PrismaUser); + }); + }); + + prismaMock.user.findMany.mockResolvedValue( + users.map((user) => { + return { + ...user, + username: `IntegrationTestUser${user.id}`, + email: `IntegrationTestUser${user.id}@example.com`, + }; + }) as unknown as PrismaUser[] + ); +} + +export async function createBookingScenario(data: ScenarioData) { + logger.silly("TestData: Creating Scenario", data); + addUsers(data.users); + + const eventType = addEventTypes(data.eventTypes, data.users); + if (data.apps) { + prismaMock.app.findMany.mockResolvedValue(data.apps as PrismaApp[]); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + const appMock = ({ where: { slug: whereSlug } }) => { + return new Promise((resolve) => { + if (!data.apps) { + resolve(null); + return; + } + + const foundApp = data.apps.find(({ slug }) => slug == whereSlug); + //TODO: Pass just the app name in data.apps and maintain apps in a separate object or load them dyamically + resolve( + ({ + ...foundApp, + ...(foundApp?.slug ? TestData.apps[foundApp.slug as keyof typeof TestData.apps] || {} : {}), + enabled: true, + createdAt: new Date(), + updatedAt: new Date(), + categories: [], + } as PrismaApp) || null + ); + }); + }; + // FIXME: How do we know which app to return? + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + prismaMock.app.findUnique.mockImplementation(appMock); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + prismaMock.app.findFirst.mockImplementation(appMock); + } + data.bookings = data.bookings || []; + allowSuccessfulBookingCreation(); + addBookings(data.bookings, data.eventTypes); + // mockBusyCalendarTimes([]); + addWebhooks(data.webhooks || []); + return { + eventType, + }; +} + +/** + * This fn indents to /ally compute day, month, year for the purpose of testing. + * We are not using DayJS because that's actually being tested by this code. + * - `dateIncrement` adds the increment to current day + * - `monthIncrement` adds the increment to current month + * - `yearIncrement` adds the increment to current year + */ +export const getDate = ( + param: { dateIncrement?: number; monthIncrement?: number; yearIncrement?: number } = {} +) => { + let { dateIncrement, monthIncrement, yearIncrement } = param; + dateIncrement = dateIncrement || 0; + monthIncrement = monthIncrement || 0; + yearIncrement = yearIncrement || 0; + + let _date = new Date().getDate() + dateIncrement; + let year = new Date().getFullYear() + yearIncrement; + + // Make it start with 1 to match with DayJS requiremet + let _month = new Date().getMonth() + monthIncrement + 1; + + // If last day of the month(As _month is plus 1 already it is going to be the 0th day of next month which is the last day of current month) + const lastDayOfMonth = new Date(year, _month, 0).getDate(); + const numberOfDaysForNextMonth = +_date - +lastDayOfMonth; + if (numberOfDaysForNextMonth > 0) { + _date = numberOfDaysForNextMonth; + _month = _month + 1; + } + + if (_month === 13) { + _month = 1; + year = year + 1; + } + + const date = _date < 10 ? "0" + _date : _date; + const month = _month < 10 ? "0" + _month : _month; + + return { + date, + month, + year, + dateString: `${year}-${month}-${date}`, + }; +}; + +export function getMockedCredential({ + metadataLookupKey, + key, +}: { + metadataLookupKey: string; + key: { + expiry_date?: number; + token_type?: string; + access_token?: string; + refresh_token?: string; + scope: string; + }; +}) { + return { + type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type, + appId: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].slug, + key: { + expiry_date: Date.now() + 1000000, + token_type: "Bearer", + access_token: "ACCESS_TOKEN", + refresh_token: "REFRESH_TOKEN", + ...key, + }, + }; +} + +export function getGoogleCalendarCredential() { + return getMockedCredential({ + metadataLookupKey: "googlecalendar", + key: { + scope: + "https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/calendar.readonly", + }, + }); +} + +export function getZoomAppCredential() { + return getMockedCredential({ + metadataLookupKey: "zoomvideo", + key: { + scope: "meeting:writed", + }, + }); +} + +export const TestData = { + selectedCalendars: { + google: { + integration: "google_calendar", + externalId: "john@example.com", + }, + }, + credentials: { + google: getGoogleCalendarCredential(), + }, + schedules: { + IstWorkHours: { + id: 1, + name: "9:30AM to 6PM in India - 4:00AM to 12:30PM in GMT", + availability: [ + { + userId: null, + eventTypeId: null, + days: [0, 1, 2, 3, 4, 5, 6], + startTime: new Date("1970-01-01T09:30:00.000Z"), + endTime: new Date("1970-01-01T18:00:00.000Z"), + date: null, + }, + ], + timeZone: Timezones["+5:30"], + }, + IstWorkHoursWithDateOverride: (dateString: string) => ({ + id: 1, + name: "9:30AM to 6PM in India - 4:00AM to 12:30PM in GMT but with a Date Override for 2PM to 6PM IST(in GST time it is 8:30AM to 12:30PM)", + availability: [ + { + userId: null, + eventTypeId: null, + days: [0, 1, 2, 3, 4, 5, 6], + startTime: new Date("1970-01-01T09:30:00.000Z"), + endTime: new Date("1970-01-01T18:00:00.000Z"), + date: null, + }, + { + userId: null, + eventTypeId: null, + days: [0, 1, 2, 3, 4, 5, 6], + startTime: new Date(`1970-01-01T14:00:00.000Z`), + endTime: new Date(`1970-01-01T18:00:00.000Z`), + date: dateString, + }, + ], + timeZone: Timezones["+5:30"], + }), + }, + users: { + example: { + name: "Example", + email: "example@example.com", + username: "example", + defaultScheduleId: 1, + timeZone: Timezones["+5:30"], + }, + }, + apps: { + "google-calendar": { + slug: "google-calendar", + dirName: "whatever", + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + keys: { + expiry_date: Infinity, + client_id: "client_id", + client_secret: "client_secret", + redirect_uris: ["http://localhost:3000/auth/callback"], + }, + }, + "daily-video": { + slug: "daily-video", + dirName: "whatever", + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + keys: { + expiry_date: Infinity, + api_key: "", + scale_plan: "false", + client_id: "client_id", + client_secret: "client_secret", + redirect_uris: ["http://localhost:3000/auth/callback"], + }, + }, + }, +}; + +function allowSuccessfulBookingCreation() { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + prismaMock.booking.create.mockImplementation(function (booking) { + return booking.data; + }); +} + +export class MockError extends Error { + constructor(message: string) { + super(message); + this.name = "MockError"; + } +} + +export function getOrganizer({ + name, + email, + id, + schedules, + credentials, + selectedCalendars, +}: { + name: string; + email: string; + id: number; + schedules: InputUser["schedules"]; + credentials?: InputCredential[]; + selectedCalendars?: InputSelectedCalendar[]; +}) { + return { + ...TestData.users.example, + name, + email, + id, + schedules, + credentials, + selectedCalendars, + }; +} + +export function getScenarioData({ + organizer, + eventTypes, + usersApartFromOrganizer = [], + apps = [], + webhooks, +}: // hosts = [], +{ + organizer: ReturnType; + eventTypes: ScenarioData["eventTypes"]; + apps: ScenarioData["apps"]; + usersApartFromOrganizer?: ScenarioData["users"]; + webhooks?: ScenarioData["webhooks"]; + // hosts?: ScenarioData["hosts"]; +}) { + const users = [organizer, ...usersApartFromOrganizer]; + eventTypes.forEach((eventType) => { + if ( + eventType.users?.filter((eventTypeUser) => { + return !users.find((userToCreate) => userToCreate.id === eventTypeUser.id); + }).length + ) { + throw new Error(`EventType ${eventType.id} has users that are not present in ScenarioData["users"]`); + } + }); + return { + // hosts: [...hosts], + eventTypes: [...eventTypes], + users, + apps: [...apps], + webhooks, + }; +} + +export function mockEnableEmailFeature() { + prismaMock.feature.findMany.mockResolvedValue([ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + { + slug: "emails", + // It's a kill switch + enabled: false, + }, + ]); +} + +export function mockNoTranslations() { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + i18nMock.getTranslation.mockImplementation(() => { + return new Promise((resolve) => { + const identityFn = (key: string) => key; + resolve(identityFn); + }); + }); +} + +export function mockCalendarToHaveNoBusySlots(metadataLookupKey: keyof typeof appStoreMetadata) { + const appStoreLookupKey = metadataLookupKey; + appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockResolvedValue({ + lib: { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + CalendarService: function MockCalendarService() { + return { + createEvent: () => { + return Promise.resolve({ + type: "daily_video", + id: "dailyEventName", + password: "dailyvideopass", + url: "http://dailyvideo.example.com", + }); + }, + getAvailability: (): Promise => { + return new Promise((resolve) => { + resolve([]); + }); + }, + }; + }, + }, + }); +} + +export function mockSuccessfulVideoMeetingCreation({ + metadataLookupKey, + appStoreLookupKey, +}: { + metadataLookupKey: string; + appStoreLookupKey?: string; +}) { + appStoreLookupKey = appStoreLookupKey || metadataLookupKey; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockImplementation(() => { + return new Promise((resolve) => { + resolve({ + lib: { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + VideoApiAdapter: () => ({ + createMeeting: () => { + return Promise.resolve({ + type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type, + id: "MOCK_ID", + password: "MOCK_PASS", + url: `http://mock-${metadataLookupKey}.example.com`, + }); + }, + }), + }, + }); + }); + }); +} + +export function mockErrorOnVideoMeetingCreation({ + metadataLookupKey, + appStoreLookupKey, +}: { + metadataLookupKey: string; + appStoreLookupKey?: string; +}) { + appStoreLookupKey = appStoreLookupKey || metadataLookupKey; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + appStoreMock.default[appStoreLookupKey].mockImplementation(() => { + return new Promise((resolve) => { + resolve({ + lib: { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + VideoApiAdapter: () => ({ + createMeeting: () => { + throw new MockError("Error creating Video meeting"); + }, + }), + }, + }); + }); + }); +} + +export function expectWebhookToHaveBeenCalledWith( + subscriberUrl: string, + data: { + triggerEvent: WebhookTriggerEvents; + payload: { metadata: Record; responses: Record }; + } +) { + const fetchCalls = fetchMock.mock.calls; + const webhookFetchCall = fetchCalls.find((call) => call[0] === subscriberUrl); + if (!webhookFetchCall) { + throw new Error(`Webhook not called with ${subscriberUrl}`); + } + expect(webhookFetchCall[0]).toBe(subscriberUrl); + const body = webhookFetchCall[1]?.body; + const parsedBody = JSON.parse((body as string) || "{}"); + console.log({ payload: parsedBody.payload }); + expect(parsedBody.triggerEvent).toBe(data.triggerEvent); + parsedBody.payload.metadata.videoCallUrl = parsedBody.payload.metadata.videoCallUrl + ? parsedBody.payload.metadata.videoCallUrl.replace(/\/video\/[a-zA-Z0-9]{22}/, "/video/DYNAMIC_UID") + : parsedBody.payload.metadata.videoCallUrl; + expect(parsedBody.payload.metadata).toContain(data.payload.metadata); + expect(parsedBody.payload.responses).toEqual(data.payload.responses); +} + +export function expectWorkflowToBeTriggered() { + // TODO: Implement this. +} + +export function expectBookingToBeInDatabase(booking: Partial) { + const createBookingCalledWithArgs = prismaMock.booking.create.mock.calls[0]; + expect(createBookingCalledWithArgs[0].data).toEqual(expect.objectContaining(booking)); +} + +export function getBooker({ name, email }: { name: string; email: string }) { + return { + name, + email, + }; +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + interface Matchers { + toHaveEmail(expectedEmail: { htmlToContain?: string; to: string }): R; + } + } +} + +expect.extend({ + toHaveEmail( + testEmail: ReturnType[number], + expectedEmail: { + //TODO: Support email HTML parsing to target specific elements + htmlToContain?: string; + to: string; + } + ) { + let isHtmlContained = true; + let isToAddressExpected = true; + if (expectedEmail.htmlToContain) { + isHtmlContained = testEmail.html.includes(expectedEmail.htmlToContain); + } + isToAddressExpected = expectedEmail.to === testEmail.to; + + return { + pass: isHtmlContained && isToAddressExpected, + message: () => { + if (!isHtmlContained) { + return `Email HTML is not as expected. Expected:"${expectedEmail.htmlToContain}" isn't contained in "${testEmail.html}"`; + } + + return `Email To address is not as expected. Expected:${expectedEmail.to} isn't contained in ${testEmail.to}`; + }, + }; + }, +}); diff --git a/package.json b/package.json index ebf7d7bb55..badd51a1ea 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,8 @@ "prettier": "^2.8.6", "tsc-absolute": "^1.0.0", "typescript": "^4.9.4", - "vitest": "^0.31.1", + "vitest": "^0.34.3", + "vitest-fetch-mock": "^0.2.2", "vitest-mock-extended": "^1.1.3" }, "dependencies": { diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index cc8d5c55b9..a56648bcab 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -10,6 +10,7 @@ import { appKeysSchema as calVideoKeysSchema } from "@calcom/app-store/dailyvide import { getEventLocationTypeFromApp } from "@calcom/app-store/locations"; import { MeetLocationType } from "@calcom/app-store/locations"; import getApps from "@calcom/app-store/utils"; +import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; import { createdEventSchema } from "@calcom/prisma/zod-utils"; import type { NewCalendarEventType } from "@calcom/types/Calendar"; @@ -436,7 +437,14 @@ export default class EventManager { * This might happen if someone tries to use a location with a missing credential, so we fallback to Cal Video. * @todo remove location from event types that has missing credentials * */ - if (!videoCredential) videoCredential = { ...FAKE_DAILY_CREDENTIAL, appName: "FAKE" }; + if (!videoCredential) { + logger.warn( + 'Falling back to "daily" video integration for event with location: ' + + event.location + + " because credential is missing for the app" + ); + videoCredential = { ...FAKE_DAILY_CREDENTIAL, appName: "FAKE" }; + } return videoCredential; } diff --git a/packages/emails/templates/_base-email.ts b/packages/emails/templates/_base-email.ts index b89149d75a..b3a7ff7bf8 100644 --- a/packages/emails/templates/_base-email.ts +++ b/packages/emails/templates/_base-email.ts @@ -6,6 +6,7 @@ import dayjs from "@calcom/dayjs"; import { getFeatureFlagMap } from "@calcom/features/flags/server/utils"; import { getErrorFromUnknown } from "@calcom/lib/errors"; import { serverConfig } from "@calcom/lib/serverConfig"; +import { setTestEmail } from "@calcom/lib/testEmails"; import prisma from "@calcom/prisma"; export default class BaseEmail { @@ -34,6 +35,16 @@ export default class BaseEmail { return new Promise((r) => r("Skipped Sending Email due to active Kill Switch")); } + if (process.env.INTEGRATION_TEST_MODE === "true") { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-expect-error + setTestEmail(this.getNodeMailerPayload()); + console.log( + "Skipped Sending Email as process.env.NEXT_PUBLIC_UNIT_TESTS is set. Emails are available in globalThis.testEmails" + ); + return new Promise((r) => r("Skipped sendEmail for Unit Tests")); + } + const payload = this.getNodeMailerPayload(); const parseSubject = z.string().safeParse(payload?.subject); const payloadWithUnEscapedSubject = { diff --git a/packages/features/bookings/lib/handleNewBooking.test.ts b/packages/features/bookings/lib/handleNewBooking.test.ts new file mode 100644 index 0000000000..a2fb67a0ea --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking.test.ts @@ -0,0 +1,511 @@ +/** + * How to ensure that unmocked prisma queries aren't called? + */ +import type { Request, Response } from "express"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { createMocks } from "node-mocks-http"; +import { describe, expect, beforeEach } from "vitest"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { BookingStatus } from "@calcom/prisma/enums"; +import { test } from "@calcom/web/test/fixtures/fixtures"; +import { + createBookingScenario, + getDate, + expectWorkflowToBeTriggered, + getGoogleCalendarCredential, + TestData, + getOrganizer, + getBooker, + getScenarioData, + expectBookingToBeInDatabase, + getZoomAppCredential, + mockEnableEmailFeature, + mockNoTranslations, + mockErrorOnVideoMeetingCreation, + mockSuccessfulVideoMeetingCreation, + mockCalendarToHaveNoBusySlots, + expectWebhookToHaveBeenCalledWith, + MockError, +} from "@calcom/web/test/utils/bookingScenario"; + +type CustomNextApiRequest = NextApiRequest & Request; +type CustomNextApiResponse = NextApiResponse & Response; +// Local test runs sometime gets too slow +const timeout = process.env.CI ? 5000 : 20000; + +describe.sequential("handleNewBooking", () => { + beforeEach(() => { + // Required to able to generate token in email in some cases + process.env.CALENDSO_ENCRYPTION_KEY="abcdefghjnmkljhjklmnhjklkmnbhjui" + mockNoTranslations(); + mockEnableEmailFeature(); + globalThis.testEmails = []; + fetchMock.resetMocks(); + }); + + describe.sequential("Frontend:", () => { + 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 trigger BOOKING_CREATED webhook + `, + async ({ emails }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }); + + const mockBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: "integrations:daily" }, + }, + }, + }); + + const { req } = createMockNextJsRequest({ + method: "POST", + body: mockBookingData, + }); + + const scenarioData = getScenarioData({ + webhooks: [ + { + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl: "http://my-webhook.example.com", + active: true, + eventTypeId: 1, + appId: null, + }, + ], + eventTypes: [ + { + id: 1, + slotInterval: 45, + length: 45, + users: [ + { + id: 101, + }, + ], + }, + ], + organizer, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }); + + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + }); + + mockCalendarToHaveNoBusySlots("googlecalendar"); + createBookingScenario(scenarioData); + + const createdBooking = await handleNewBooking(req); + expect(createdBooking.responses).toContain({ + email: booker.email, + name: booker.name, + }); + + expect(createdBooking).toContain({ + location: "integrations:daily", + }); + + expectBookingToBeInDatabase({ + description: "", + eventType: { + connect: { + id: mockBookingData.eventTypeId, + }, + }, + status: BookingStatus.ACCEPTED, + }); + + expectWorkflowToBeTriggered(); + + const testEmails = emails.get(); + expect(testEmails[0]).toHaveEmail({ + htmlToContain: "confirmed_event_type_subject", + to: `${organizer.email}`, + }); + expect(testEmails[1]).toHaveEmail({ + htmlToContain: "confirmed_event_type_subject", + to: `${booker.name} <${booker.email}>`, + }); + expect(testEmails[1].html).toContain("confirmed_event_type_subject"); + expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", { + triggerEvent: "BOOKING_CREATED", + payload: { + metadata: { + videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, + }, + responses: { + name: { label: "your_name", value: "Booker" }, + email: { label: "email_address", value: "booker@example.com" }, + location: { + label: "location", + value: { optionValue: "", value: "integrations:daily" }, + }, + title: { label: "what_is_this_meeting_about" }, + notes: { label: "additional_notes" }, + guests: { label: "additional_guests" }, + rescheduleReason: { label: "reason_for_reschedule" }, + }, + }, + }); + }, + timeout + ); + + test( + `should submit a booking request for event requiring confirmation + 1. Should create a booking in the database with status PENDING + 2. Should send emails to the booker as well as organizer for booking request and awaiting approval + 3. Should trigger BOOKING_REQUESTED webhook + `, + async ({ emails }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }); + + const mockBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: "integrations:daily" }, + }, + }, + }); + + const { req } = createMockNextJsRequest({ + method: "POST", + body: mockBookingData, + }); + + const scenarioData = 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, + requiresConfirmation: true, + length: 45, + users: [ + { + id: 101, + }, + ], + }, + ], + organizer, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }); + + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + }); + + mockCalendarToHaveNoBusySlots("googlecalendar"); + createBookingScenario(scenarioData); + + const createdBooking = await handleNewBooking(req); + expect(createdBooking.responses).toContain({ + email: booker.email, + name: booker.name, + }); + + expect(createdBooking).toContain({ + location: "integrations:daily", + }); + + expectBookingToBeInDatabase({ + description: "", + eventType: { + connect: { + id: mockBookingData.eventTypeId, + }, + }, + status: BookingStatus.PENDING, + }); + + expectWorkflowToBeTriggered(); + + const testEmails = emails.get(); + expect(testEmails[0]).toHaveEmail({ + htmlToContain: "event_awaiting_approval_subject", + to: `${organizer.email}`, + }); + + expect(testEmails[1]).toHaveEmail({ + htmlToContain: "booking_submitted_subject", + to: `${booker.email}`, + }); + + expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", { + triggerEvent: "BOOKING_REQUESTED", + payload: { + metadata: { + // In a Pending Booking Request, we don't send the video call url + videoCallUrl: undefined, + }, + responses: { + name: { label: "your_name", value: "Booker" }, + email: { label: "email_address", value: "booker@example.com" }, + location: { + label: "location", + value: { optionValue: "", value: "integrations:daily" }, + }, + title: { label: "what_is_this_meeting_about" }, + notes: { label: "additional_notes" }, + guests: { label: "additional_guests" }, + rescheduleReason: { label: "reason_for_reschedule" }, + }, + }, + }); + }, + timeout + ); + + test( + `if booking with Cal Video(Daily Video) fails, booking creation fails with uncaught error`, + async ({}) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const booker = getBooker({ + email: "booker@example.org", + name: "Booker", + }); + const organizer = TestData.users.example; + const { req } = createMockNextJsRequest({ + method: "POST", + body: getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: "integrations:daily" }, + }, + }, + }), + }); + + const scenarioData = { + hosts: [], + eventTypes: [ + { + id: 1, + slotInterval: 45, + length: 45, + users: [ + { + id: 101, + }, + ], + }, + ], + users: [ + { + ...organizer, + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }, + ], + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }; + + mockErrorOnVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + }); + mockCalendarToHaveNoBusySlots("googlecalendar"); + + createBookingScenario(scenarioData); + + try { + await handleNewBooking(req); + } catch (e) { + expect(e).toBeInstanceOf(MockError); + expect((e as { message: string }).message).toBe("Error creating Video meeting"); + } + }, + timeout + ); + + test( + `should create a successful booking with Zoom if used`, + async ({ emails }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getZoomAppCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }); + + const { req } = createMockNextJsRequest({ + method: "POST", + body: getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: "integrations:zoom" }, + }, + }, + }), + }); + + const bookingScenario = getScenarioData({ + organizer, + eventTypes: [ + { + id: 1, + slotInterval: 45, + length: 45, + users: [ + { + id: 101, + }, + ], + }, + ], + apps: [TestData.apps["daily-video"]], + webhooks: [ + { + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl: "http://my-webhook.example.com", + active: true, + eventTypeId: 1, + appId: null, + }, + ], + }); + + createBookingScenario(bookingScenario); + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "zoomvideo", + }); + await handleNewBooking(req); + + const testEmails = emails.get(); + + expect(testEmails[0]).toHaveEmail({ + htmlToContain: "confirmed_event_type_subject", + to: `${organizer.email}`, + }); + + expect(testEmails[1]).toHaveEmail({ + htmlToContain: "confirmed_event_type_subject", + to: `${booker.name} <${booker.email}>`, + }); + + expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", { + triggerEvent: "BOOKING_CREATED", + payload: { + metadata: { + videoCallUrl: "http://mock-zoomvideo.example.com", + }, + responses: { + name: { label: "your_name", value: "Booker" }, + email: { label: "email_address", value: "booker@example.com" }, + location: { + label: "location", + value: { optionValue: "", value: "integrations:zoom" }, + }, + title: { label: "what_is_this_meeting_about" }, + notes: { label: "additional_notes" }, + guests: { label: "additional_guests" }, + rescheduleReason: { label: "reason_for_reschedule" }, + }, + }, + }); + }, + timeout + ); + }); +}); + +function createMockNextJsRequest(...args: Parameters) { + return createMocks(...args); +} + +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: "Asia/Calcutta", + language: "en", + bookingUid: "bvCmP5rSquAazGSA7hz7ZP", + user: "teampro", + metadata: {}, + hasHashedBookingLink: false, + hashedLink: null, + }; +} + +function getMockRequestDataForBooking({ + data, +}: { + data: Partial> & { + eventTypeId: number; + responses: { + email: string; + name: string; + location: { optionValue: ""; value: string }; + }; + }; +}) { + return { + ...getBasicMockRequestDataForBooking(), + ...data, + }; +} diff --git a/packages/lib/testEmails.ts b/packages/lib/testEmails.ts new file mode 100644 index 0000000000..fff8da1079 --- /dev/null +++ b/packages/lib/testEmails.ts @@ -0,0 +1,18 @@ +declare global { + // eslint-disable-next-line no-var + var testEmails: { + to: string; + from: string; + subject: string; + html: string; + }[]; +} + +export const setTestEmail = (email: (typeof globalThis.testEmails)[number]) => { + globalThis.testEmails = globalThis.testEmails || []; + globalThis.testEmails.push(email); +}; + +export const getTestEmails = () => { + return globalThis.testEmails; +}; diff --git a/setupVitest.ts b/setupVitest.ts new file mode 100644 index 0000000000..18104fa24c --- /dev/null +++ b/setupVitest.ts @@ -0,0 +1,7 @@ +import { vi } from "vitest"; +import createFetchMock from "vitest-fetch-mock"; + +const fetchMocker = createFetchMock(vi); + +// sets globalThis.fetch and globalThis.fetchMock to our mocked version +fetchMocker.enableMocks(); diff --git a/tests/libs/__mocks__/app-store.ts b/tests/libs/__mocks__/app-store.ts new file mode 100644 index 0000000000..2b74913d72 --- /dev/null +++ b/tests/libs/__mocks__/app-store.ts @@ -0,0 +1,17 @@ +import { beforeEach, vi } from "vitest"; +import { mockReset, mockDeep } from "vitest-mock-extended"; + +import type * as appStore from "@calcom/app-store"; + +vi.mock("@calcom/app-store", () => appStoreMock); + +beforeEach(() => { + mockReset(appStoreMock); +}); + +const appStoreMock = mockDeep({ + fallbackMockImplementation: () => { + throw new Error("Unimplemented"); + }, +}); +export default appStoreMock; diff --git a/tests/libs/__mocks__/libServerI18n.ts b/tests/libs/__mocks__/libServerI18n.ts new file mode 100644 index 0000000000..81d201d40d --- /dev/null +++ b/tests/libs/__mocks__/libServerI18n.ts @@ -0,0 +1,13 @@ +import { beforeEach, vi } from "vitest"; +import { mockReset, mockDeep } from "vitest-mock-extended"; + +import type * as i18n from "@calcom/lib/server/i18n"; + +vi.mock("@calcom/lib/server/i18n", () => i18nMock); + +beforeEach(() => { + mockReset(i18nMock); +}); + +const i18nMock = mockDeep(); +export default i18nMock; diff --git a/tests/libs/__mocks__/prisma.ts b/tests/libs/__mocks__/prisma.ts index d285b349a7..2b3153db07 100644 --- a/tests/libs/__mocks__/prisma.ts +++ b/tests/libs/__mocks__/prisma.ts @@ -5,6 +5,7 @@ import type { PrismaClient } from "@calcom/prisma"; vi.mock("@calcom/prisma", () => ({ default: prisma, + prisma, availabilityUserSelect: vi.fn(), userSelect: vi.fn(), })); diff --git a/tests/libs/__mocks__/reminderScheduler.ts b/tests/libs/__mocks__/reminderScheduler.ts new file mode 100644 index 0000000000..0881b17a68 --- /dev/null +++ b/tests/libs/__mocks__/reminderScheduler.ts @@ -0,0 +1,13 @@ +import { beforeEach, vi } from "vitest"; +import { mockReset, mockDeep } from "vitest-mock-extended"; + +import type * as reminderScheduler from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler"; + +vi.mock("@calcom/features/ee/workflows/lib/reminders/reminderScheduler", () => reminderSchedulerMock); + +beforeEach(() => { + mockReset(reminderSchedulerMock); +}); + +const reminderSchedulerMock = mockDeep(); +export default reminderSchedulerMock; diff --git a/tests/libs/__mocks__/videoClient.ts b/tests/libs/__mocks__/videoClient.ts new file mode 100644 index 0000000000..eb83c80ec3 --- /dev/null +++ b/tests/libs/__mocks__/videoClient.ts @@ -0,0 +1,13 @@ +import { beforeEach, vi } from "vitest"; +import { mockReset, mockDeep } from "vitest-mock-extended"; + +import type * as videoClient from "@calcom/core/videoClient"; + +vi.mock("@calcom/core/videoClient", () => videoClientMock); + +beforeEach(() => { + mockReset(videoClientMock); +}); + +const videoClientMock = mockDeep(); +export default videoClientMock; diff --git a/vitest.config.ts b/vitest.config.ts index 6eea9436fc..d5aa8a132d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,9 +1,12 @@ import { defineConfig } from "vitest/config"; +process.env.INTEGRATION_TEST_MODE = "true"; + export default defineConfig({ test: { coverage: { - provider: "c8", + provider: "v8", }, + testTimeout: 500000, }, }); diff --git a/vitest.workspace.ts b/vitest.workspace.ts index 6bda5c3c32..0940105554 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -17,6 +17,7 @@ const workspaces = packagedEmbedTestsOnly include: ["packages/**/*.{test,spec}.{ts,js}", "apps/**/*.{test,spec}.{ts,js}"], // TODO: Ignore the api until tests are fixed exclude: ["apps/api/**/*", "**/node_modules/**/*", "packages/embeds/**/*"], + setupFiles: ["setupVitest.ts"], }, }, { diff --git a/yarn.lock b/yarn.lock index 13ce309530..f5b5591f33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6985,7 +6985,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.13, @jridgewell/sourcemap-codec@npm:^1.4.14": +"@jridgewell/sourcemap-codec@npm:^1.4.13, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15": version: 1.4.15 resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" checksum: b881c7e503db3fc7f3c1f35a1dd2655a188cc51a3612d76efc8a6eb74728bef5606e6758ee77423e564092b4a518aba569bbb21c9bac5ab7a35b0c6ae7e344c8 @@ -13215,57 +13215,56 @@ __metadata: languageName: node linkType: hard -"@vitest/expect@npm:0.31.1": - version: 0.31.1 - resolution: "@vitest/expect@npm:0.31.1" +"@vitest/expect@npm:0.34.3": + version: 0.34.3 + resolution: "@vitest/expect@npm:0.34.3" dependencies: - "@vitest/spy": 0.31.1 - "@vitest/utils": 0.31.1 + "@vitest/spy": 0.34.3 + "@vitest/utils": 0.34.3 chai: ^4.3.7 - checksum: 0d1e135ae753d913231eae830da00ee42afca53d354898fb43f97e82398dcf17298c02e9989dd6b19b9b2909989248ef76d203d63f6af6f9159dc96959ea654b + checksum: 79afaa37d2efb7bb5503332caf389860b2261f198dbe61557e8061262b628d18658e59eb51d1808ecd35fc59f4bb4d04c0e0f97a27c7db02584ab5b424147b8d languageName: node linkType: hard -"@vitest/runner@npm:0.31.1": - version: 0.31.1 - resolution: "@vitest/runner@npm:0.31.1" +"@vitest/runner@npm:0.34.3": + version: 0.34.3 + resolution: "@vitest/runner@npm:0.34.3" dependencies: - "@vitest/utils": 0.31.1 - concordance: ^5.0.4 + "@vitest/utils": 0.34.3 p-limit: ^4.0.0 - pathe: ^1.1.0 - checksum: cc8702e21b799d5e941409cb2afe6d0e576b4f3ac99df4a1393a8cd11b57f6b0b06e756cc24e2739812d095fbfd0824e22e861dbd6a71769ca387d485ade6fb5 + pathe: ^1.1.1 + checksum: 945580eaa58e8edbe29a64059bc2a524a9e85117b6d600fdb457cfe84cbfb81bf6d7e98e1227e7cb4e7399992c8fe8d83d0791d0385ff005dc1a4d9da125443b languageName: node linkType: hard -"@vitest/snapshot@npm:0.31.1": - version: 0.31.1 - resolution: "@vitest/snapshot@npm:0.31.1" +"@vitest/snapshot@npm:0.34.3": + version: 0.34.3 + resolution: "@vitest/snapshot@npm:0.34.3" dependencies: - magic-string: ^0.30.0 - pathe: ^1.1.0 - pretty-format: ^27.5.1 - checksum: de05fa9136864f26f0804baf3ae8068f67de28015f29047329c84e67fb33be7305c9e52661b016e834d30f4081c136b3b6d8d4054c024a5d52b22a7f90fc4be0 + magic-string: ^0.30.1 + pathe: ^1.1.1 + pretty-format: ^29.5.0 + checksum: 234893e91a1efd4bdbbde047a68de40975e02ead8407724ce8ca4a24edf0fb2d725f8a3efceb104965388407b598faf22407aadfbf4164cc74b3cf1e0e9f4543 languageName: node linkType: hard -"@vitest/spy@npm:0.31.1": - version: 0.31.1 - resolution: "@vitest/spy@npm:0.31.1" +"@vitest/spy@npm:0.34.3": + version: 0.34.3 + resolution: "@vitest/spy@npm:0.34.3" dependencies: - tinyspy: ^2.1.0 - checksum: 8b06cf25fcc028c16106ec82f4ceb84d6dfa04d06f651bca4738ce2b99796d1fc4e0c10319767240755eff8ede2bff9d31d5a901fe92828d319c65001581137b + tinyspy: ^2.1.1 + checksum: a2b64b9c357a56ad2f2340ecd225ffe787e61afba4ffb24a6670aad3fc90ea2606ed48daa188ed62b3ef67d55c0259fda6b101143d6c91b58c9ac4298d8be4f9 languageName: node linkType: hard -"@vitest/utils@npm:0.31.1": - version: 0.31.1 - resolution: "@vitest/utils@npm:0.31.1" +"@vitest/utils@npm:0.34.3": + version: 0.34.3 + resolution: "@vitest/utils@npm:0.34.3" dependencies: - concordance: ^5.0.4 + diff-sequences: ^29.4.3 loupe: ^2.3.6 - pretty-format: ^27.5.1 - checksum: 58016c185455e3814632cb77e37368c846bde5e342f8b4a66fa229bde64f455ca39abebc9c12e2483696ee38bc17b3c4300379f7a3b18d1087f24f474448a8d8 + pretty-format: ^29.5.0 + checksum: aeb8ef7fd98b32cb6c403796880d0aa8f5411bbdb249bb23b3301a70e1b7d1ee025ddb204aae8c1db5756f6ac428c49ebbb8e2ed23ce185c8a659b67413efa85 languageName: node linkType: hard @@ -13864,6 +13863,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.10.0, acorn@npm:^8.9.0": + version: 8.10.0 + resolution: "acorn@npm:8.10.0" + bin: + acorn: bin/acorn + checksum: 538ba38af0cc9e5ef983aee196c4b8b4d87c0c94532334fa7e065b2c8a1f85863467bb774231aae91613fcda5e68740c15d97b1967ae3394d20faddddd8af61d + languageName: node + linkType: hard + "acorn@npm:^8.5.0": version: 8.7.1 resolution: "acorn@npm:8.7.1" @@ -15276,13 +15284,6 @@ __metadata: languageName: node linkType: hard -"blueimp-md5@npm:^2.10.0": - version: 2.19.0 - resolution: "blueimp-md5@npm:2.19.0" - checksum: 28095dcbd2c67152a2938006e8d7c74c3406ba6556071298f872505432feb2c13241b0476644160ee0a5220383ba94cb8ccdac0053b51f68d168728f9c382530 - languageName: node - linkType: hard - "bmp-js@npm:^0.1.0": version: 0.1.0 resolution: "bmp-js@npm:0.1.0" @@ -15972,7 +15973,8 @@ __metadata: tsc-absolute: ^1.0.0 turbo: ^1.10.1 typescript: ^4.9.4 - vitest: ^0.31.1 + vitest: ^0.34.3 + vitest-fetch-mock: ^0.2.2 vitest-mock-extended: ^1.1.3 languageName: unknown linkType: soft @@ -17078,22 +17080,6 @@ __metadata: languageName: node linkType: hard -"concordance@npm:^5.0.4": - version: 5.0.4 - resolution: "concordance@npm:5.0.4" - dependencies: - date-time: ^3.1.0 - esutils: ^2.0.3 - fast-diff: ^1.2.0 - js-string-escape: ^1.0.1 - lodash: ^4.17.15 - md5-hex: ^3.0.1 - semver: ^7.3.2 - well-known-symbols: ^2.0.0 - checksum: 749153ba711492feb7c3d2f5bb04c107157440b3e39509bd5dd19ee7b3ac751d1e4cd75796d9f702e0a713312dbc661421c68aa4a2c34d5f6d91f47e3a1c64a6 - languageName: node - linkType: hard - "concurrently@npm:^7.6.0": version: 7.6.0 resolution: "concurrently@npm:7.6.0" @@ -17514,6 +17500,15 @@ __metadata: languageName: node linkType: hard +"cross-fetch@npm:^3.0.6": + version: 3.1.8 + resolution: "cross-fetch@npm:3.1.8" + dependencies: + node-fetch: ^2.6.12 + checksum: 78f993fa099eaaa041122ab037fe9503ecbbcb9daef234d1d2e0b9230a983f64d645d088c464e21a247b825a08dc444a6e7064adfa93536d3a9454b4745b3632 + languageName: node + linkType: hard + "cross-spawn@npm:7.0.3, cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" @@ -17936,15 +17931,6 @@ __metadata: languageName: node linkType: hard -"date-time@npm:^3.1.0": - version: 3.1.0 - resolution: "date-time@npm:3.1.0" - dependencies: - time-zone: ^1.0.0 - checksum: f9cfcd1b15dfeabab15c0b9d18eb9e4e2d9d4371713564178d46a8f91ad577a290b5178b80050718d02d9c0cf646f8a875011e12d1ed05871e9f72c72c8a8fe6 - languageName: node - linkType: hard - "datocms-listen@npm:^0.1.9": version: 0.1.15 resolution: "datocms-listen@npm:0.1.15" @@ -19946,7 +19932,7 @@ __metadata: languageName: node linkType: hard -"esutils@npm:^2.0.2, esutils@npm:^2.0.3": +"esutils@npm:^2.0.2": version: 2.0.3 resolution: "esutils@npm:2.0.3" checksum: 22b5b08f74737379a840b8ed2036a5fb35826c709ab000683b092d9054e5c2a82c27818f12604bfc2a9a76b90b6834ef081edbc1c7ae30d1627012e067c6ec87 @@ -20401,13 +20387,6 @@ __metadata: languageName: node linkType: hard -"fast-diff@npm:^1.2.0": - version: 1.3.0 - resolution: "fast-diff@npm:1.3.0" - checksum: d22d371b994fdc8cce9ff510d7b8dc4da70ac327bcba20df607dd5b9cae9f908f4d1028f5fe467650f058d1e7270235ae0b8230809a262b4df587a3b3aa216c3 - languageName: node - linkType: hard - "fast-equals@npm:^1.6.3": version: 1.6.3 resolution: "fast-equals@npm:1.6.3" @@ -26526,12 +26505,12 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.0": - version: 0.30.0 - resolution: "magic-string@npm:0.30.0" +"magic-string@npm:^0.30.1": + version: 0.30.3 + resolution: "magic-string@npm:0.30.3" dependencies: - "@jridgewell/sourcemap-codec": ^1.4.13 - checksum: 7bdf22e27334d8a393858a16f5f840af63a7c05848c000fd714da5aa5eefa09a1bc01d8469362f25cc5c4a14ec01b46557b7fff8751365522acddf21e57c488d + "@jridgewell/sourcemap-codec": ^1.4.15 + checksum: a5a9ddf9bd3bf49a2de1048bf358464f1bda7b3cc1311550f4a0ba8f81a4070e25445d53a5ee28850161336f1bff3cf28aa3320c6b4aeff45ce3e689f300b2f3 languageName: node linkType: hard @@ -26725,15 +26704,6 @@ __metadata: languageName: node linkType: hard -"md5-hex@npm:^3.0.1": - version: 3.0.1 - resolution: "md5-hex@npm:3.0.1" - dependencies: - blueimp-md5: ^2.10.0 - checksum: 6799a19e8bdd3e0c2861b94c1d4d858a89220488d7885c1fa236797e367d0c2e5f2b789e05309307083503f85be3603a9686a5915568a473137d6b4117419cc2 - languageName: node - linkType: hard - "md5.js@npm:^1.3.4": version: 1.3.5 resolution: "md5.js@npm:1.3.5" @@ -27796,6 +27766,18 @@ __metadata: languageName: node linkType: hard +"mlly@npm:^1.4.0": + version: 1.4.1 + resolution: "mlly@npm:1.4.1" + dependencies: + acorn: ^8.10.0 + pathe: ^1.1.1 + pkg-types: ^1.0.3 + ufo: ^1.3.0 + checksum: b2b59ab3d70196127be4e54609d2a442bd252345727138940fb245672a238b2fbdd431e8c75ec5c741ff90410ce488c5fd6446d5d3e6476d21dbf4c3fa35d4a0 + languageName: node + linkType: hard + "mock-fs@npm:^4.1.0": version: 4.14.0 resolution: "mock-fs@npm:4.14.0" @@ -28446,6 +28428,20 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^2.6.12": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: ^5.0.0 + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: d76d2f5edb451a3f05b15115ec89fc6be39de37c6089f1b6368df03b91e1633fd379a7e01b7ab05089a25034b2023d959b47e59759cb38d88341b2459e89d6e5 + languageName: node + linkType: hard + "node-forge@npm:1.3.1, node-forge@npm:^1.0.0": version: 1.3.1 resolution: "node-forge@npm:1.3.1" @@ -29889,6 +29885,13 @@ __metadata: languageName: node linkType: hard +"pathe@npm:^1.1.1": + version: 1.1.1 + resolution: "pathe@npm:1.1.1" + checksum: 34ab3da2e5aa832ebc6a330ffe3f73d7ba8aec6e899b53b8ec4f4018de08e40742802deb12cf5add9c73b7bf719b62c0778246bd376ca62b0fb23e0dde44b759 + languageName: node + linkType: hard + "pathval@npm:^1.1.1": version: 1.1.1 resolution: "pathval@npm:1.1.1" @@ -30718,7 +30721,7 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:^27.0.2, pretty-format@npm:^27.5.1": +"pretty-format@npm:^27.0.2": version: 27.5.1 resolution: "pretty-format@npm:27.5.1" dependencies: @@ -34508,10 +34511,10 @@ __metadata: languageName: node linkType: hard -"std-env@npm:^3.3.2": - version: 3.3.3 - resolution: "std-env@npm:3.3.3" - checksum: 6665f6d8bd63aae432d3eb9abbd7322847ad0d902603e6dce1e8051b4f42ceeb4f7f96a4faf70bb05ce65ceee2dc982502b701575c8a58b1bfad29f3dbb19f81 +"std-env@npm:^3.3.3": + version: 3.4.3 + resolution: "std-env@npm:3.4.3" + checksum: bef186fb2baddda31911234b1e58fa18f181eb6930616aaec3b54f6d5db65f2da5daaa5f3b326b98445a7d50ca81d6fe8809ab4ebab85ecbe4a802f1b40921bf languageName: node linkType: hard @@ -35811,13 +35814,6 @@ __metadata: languageName: node linkType: hard -"time-zone@npm:^1.0.0": - version: 1.0.0 - resolution: "time-zone@npm:1.0.0" - checksum: e46f5a69b8c236dcd8e91e29d40d4e7a3495ed4f59888c3f84ce1d9678e20461421a6ba41233509d47dd94bc18f1a4377764838b21b584663f942b3426dcbce8 - languageName: node - linkType: hard - "timed-out@npm:^4.0.0, timed-out@npm:^4.0.1": version: 4.0.1 resolution: "timed-out@npm:4.0.1" @@ -35903,17 +35899,17 @@ __metadata: languageName: node linkType: hard -"tinypool@npm:^0.5.0": - version: 0.5.0 - resolution: "tinypool@npm:0.5.0" - checksum: 4e0dfd8f28666d541c1d92304222edc4613f05d74fe2243c8520d466a2cc6596011a7072c1c41a7de7522351b82fda07e8038198e8f43673d8d69401c5903f8c +"tinypool@npm:^0.7.0": + version: 0.7.0 + resolution: "tinypool@npm:0.7.0" + checksum: fdcccd5c750574fce51f8801a877f8284e145d12b79cd5f2d72bfbddfe20c895e915555bc848e122bb6aa968098e7ac4fe1e8e88104904d518dc01cccd18a510 languageName: node linkType: hard -"tinyspy@npm:^2.1.0": - version: 2.1.0 - resolution: "tinyspy@npm:2.1.0" - checksum: cb83c1f74a79dd5934018bad94f60a304a29d98a2d909ea45fc367f7b80b21b0a7d8135a2ce588deb2b3ba56c7c607258b2a03e6001d89e4d564f9a95cc6a81f +"tinyspy@npm:^2.1.1": + version: 2.1.1 + resolution: "tinyspy@npm:2.1.1" + checksum: cfe669803a7f11ca912742b84c18dcc4ceecaa7661c69bc5eb608a8a802d541c48aba220df8929f6c8cd09892ad37cb5ba5958ddbbb57940e91d04681d3cee73 languageName: node linkType: hard @@ -36860,6 +36856,13 @@ __metadata: languageName: node linkType: hard +"ufo@npm:^1.3.0": + version: 1.3.0 + resolution: "ufo@npm:1.3.0" + checksum: 01f0be86cd5c205ad1b49ebea985e000a4542c503ee75398302b0f5e4b9a6d9cd8e77af2dc614ab7bea08805fdfd9a85191fb3b5ee3df383cb936cf65e9db30d + languageName: node + linkType: hard + "uglify-js@npm:^3.1.4": version: 3.15.3 resolution: "uglify-js@npm:3.15.3" @@ -37840,19 +37843,19 @@ __metadata: languageName: node linkType: hard -"vite-node@npm:0.31.1": - version: 0.31.1 - resolution: "vite-node@npm:0.31.1" +"vite-node@npm:0.34.3": + version: 0.34.3 + resolution: "vite-node@npm:0.34.3" dependencies: cac: ^6.7.14 debug: ^4.3.4 - mlly: ^1.2.0 - pathe: ^1.1.0 + mlly: ^1.4.0 + pathe: ^1.1.1 picocolors: ^1.0.0 vite: ^3.0.0 || ^4.0.0 bin: vite-node: vite-node.mjs - checksum: f70ffa3f6dcb4937cdc99f59bf360d42de83c556ba9a19eb1c3504ef20db4c1d1afa644d9a8e63240e851c0c95773b64c526bdb3eb4794b5e941ddcd57124aa9 + checksum: 366c4f3fb7c038e2180abc6b18cfbac3b8684cd878eaf7ebf1ffb07d95d2ea325713fc575a7949a13bb00cfe264acbc28c02e2836b8647e1f443fe631c17805a languageName: node linkType: hard @@ -37930,6 +37933,17 @@ __metadata: languageName: node linkType: hard +"vitest-fetch-mock@npm:^0.2.2": + version: 0.2.2 + resolution: "vitest-fetch-mock@npm:0.2.2" + dependencies: + cross-fetch: ^3.0.6 + peerDependencies: + vitest: ">=0.16.0" + checksum: fa160f301171cd45dbf7d782880b6b6063fc74b9dd1965ef9206545e812ca8696e6be76662afbac822c6bf850fbb66cf8fb066af646e0e159f5a87ab25c97a02 + languageName: node + linkType: hard + "vitest-mock-extended@npm:^1.1.3": version: 1.1.3 resolution: "vitest-mock-extended@npm:1.1.3" @@ -37942,34 +37956,33 @@ __metadata: languageName: node linkType: hard -"vitest@npm:^0.31.1": - version: 0.31.1 - resolution: "vitest@npm:0.31.1" +"vitest@npm:^0.34.3": + version: 0.34.3 + resolution: "vitest@npm:0.34.3" dependencies: "@types/chai": ^4.3.5 "@types/chai-subset": ^1.3.3 "@types/node": "*" - "@vitest/expect": 0.31.1 - "@vitest/runner": 0.31.1 - "@vitest/snapshot": 0.31.1 - "@vitest/spy": 0.31.1 - "@vitest/utils": 0.31.1 - acorn: ^8.8.2 + "@vitest/expect": 0.34.3 + "@vitest/runner": 0.34.3 + "@vitest/snapshot": 0.34.3 + "@vitest/spy": 0.34.3 + "@vitest/utils": 0.34.3 + acorn: ^8.9.0 acorn-walk: ^8.2.0 cac: ^6.7.14 chai: ^4.3.7 - concordance: ^5.0.4 debug: ^4.3.4 local-pkg: ^0.4.3 - magic-string: ^0.30.0 - pathe: ^1.1.0 + magic-string: ^0.30.1 + pathe: ^1.1.1 picocolors: ^1.0.0 - std-env: ^3.3.2 + std-env: ^3.3.3 strip-literal: ^1.0.1 tinybench: ^2.5.0 - tinypool: ^0.5.0 + tinypool: ^0.7.0 vite: ^3.0.0 || ^4.0.0 - vite-node: 0.31.1 + vite-node: 0.34.3 why-is-node-running: ^2.2.2 peerDependencies: "@edge-runtime/vm": "*" @@ -37999,7 +38012,7 @@ __metadata: optional: true bin: vitest: vitest.mjs - checksum: b3f64a36102edc5b8594c085da648c838c0d275c620bd3b780624f936903b9c06579d6ef137fe9859e468f16deb8f154a50f009093119f9adb8b60ff1b7597ee + checksum: 4535d080feede94db5015eb60c6ed5f7b0d8cd67f12072de5ae1faded133cc640043c0c2646ef51ab9b61c2f885589da57458a65e82cf91a25cf954470018a40 languageName: node linkType: hard @@ -38703,13 +38716,6 @@ __metadata: languageName: node linkType: hard -"well-known-symbols@npm:^2.0.0": - version: 2.0.0 - resolution: "well-known-symbols@npm:2.0.0" - checksum: 4f54bbc3012371cb4d228f436891b8e7536d34ac61a57541890257e96788608e096231e0121ac24d08ef2f908b3eb2dc0adba35023eaeb2a7df655da91415402 - languageName: node - linkType: hard - "whatwg-encoding@npm:^2.0.0": version: 2.0.0 resolution: "whatwg-encoding@npm:2.0.0"