test: Integration tests for handleNewBooking (#11044)
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>pull/11164/head^2
parent
d7d7bcd651
commit
f9eb335d0b
|
@ -0,0 +1,20 @@
|
||||||
|
// my-test.ts
|
||||||
|
import { test as base } from "vitest";
|
||||||
|
|
||||||
|
import { getTestEmails } from "@calcom/lib/testEmails";
|
||||||
|
|
||||||
|
export interface Fixtures {
|
||||||
|
emails: ReturnType<typeof getEmailsFixture>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const test = base.extend<Fixtures>({
|
||||||
|
emails: async ({}, use) => {
|
||||||
|
await use(getEmailsFixture());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function getEmailsFixture() {
|
||||||
|
return {
|
||||||
|
get: getTestEmails,
|
||||||
|
};
|
||||||
|
}
|
|
@ -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 CalendarManagerMock from "../../../../tests/libs/__mocks__/CalendarManager";
|
||||||
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
|
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
|
||||||
|
|
||||||
import { diff } from "jest-diff";
|
import { diff } from "jest-diff";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import { describe, expect, vi, beforeEach, afterEach, test } from "vitest";
|
import { describe, expect, vi, beforeEach, afterEach, test } from "vitest";
|
||||||
|
|
||||||
import logger from "@calcom/lib/logger";
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import type { SchedulingType } from "@calcom/prisma/enums";
|
|
||||||
import type { BookingStatus } from "@calcom/prisma/enums";
|
import type { BookingStatus } from "@calcom/prisma/enums";
|
||||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
|
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
|
||||||
import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util";
|
import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util";
|
||||||
|
import { getDate, getGoogleCalendarCredential, createBookingScenario} from "../utils/bookingScenario";
|
||||||
|
|
||||||
// TODO: Mock properly
|
// TODO: Mock properly
|
||||||
prismaMock.eventType.findUnique.mockResolvedValue(null);
|
prismaMock.eventType.findUnique.mockResolvedValue(null);
|
||||||
|
@ -129,6 +120,7 @@ const TestData = {
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
example: {
|
example: {
|
||||||
|
name: "Example",
|
||||||
username: "example",
|
username: "example",
|
||||||
defaultScheduleId: 1,
|
defaultScheduleId: 1,
|
||||||
email: "example@example.com",
|
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 () => {
|
const cleanup = async () => {
|
||||||
await prisma.eventType.deleteMany();
|
await prisma.eventType.deleteMany();
|
||||||
|
@ -241,7 +176,6 @@ describe("getSchedule", () => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const scenarioData = {
|
const scenarioData = {
|
||||||
hosts: [],
|
|
||||||
eventTypes: [
|
eventTypes: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
|
@ -350,7 +284,6 @@ describe("getSchedule", () => {
|
||||||
endTime: `${plus2DateString}T06:15:00.000Z`,
|
endTime: `${plus2DateString}T06:15:00.000Z`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
hosts: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Day Plus 2 is completely free - It only has non accepted bookings
|
// Day Plus 2 is completely free - It only has non accepted bookings
|
||||||
|
@ -449,7 +382,6 @@ describe("getSchedule", () => {
|
||||||
schedules: [TestData.schedules.IstWorkHours],
|
schedules: [TestData.schedules.IstWorkHours],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
hosts: [],
|
|
||||||
});
|
});
|
||||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||||
|
@ -550,7 +482,6 @@ describe("getSchedule", () => {
|
||||||
schedules: [TestData.schedules.IstWorkHours],
|
schedules: [TestData.schedules.IstWorkHours],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
hosts: [],
|
|
||||||
});
|
});
|
||||||
const { dateString: todayDateString } = getDate();
|
const { dateString: todayDateString } = getDate();
|
||||||
const { dateString: minus1DateString } = getDate({ dateIncrement: -1 });
|
const { dateString: minus1DateString } = getDate({ dateIncrement: -1 });
|
||||||
|
@ -634,7 +565,6 @@ describe("getSchedule", () => {
|
||||||
selectedCalendars: [TestData.selectedCalendars.google],
|
selectedCalendars: [TestData.selectedCalendars.google],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
hosts: [],
|
|
||||||
apps: [TestData.apps.googleCalendar],
|
apps: [TestData.apps.googleCalendar],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -710,7 +640,6 @@ describe("getSchedule", () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
apps: [TestData.apps.googleCalendar],
|
apps: [TestData.apps.googleCalendar],
|
||||||
hosts: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
createBookingScenario(scenarioData);
|
createBookingScenario(scenarioData);
|
||||||
|
@ -768,7 +697,6 @@ describe("getSchedule", () => {
|
||||||
selectedCalendars: [TestData.selectedCalendars.google],
|
selectedCalendars: [TestData.selectedCalendars.google],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
hosts: [],
|
|
||||||
apps: [TestData.apps.googleCalendar],
|
apps: [TestData.apps.googleCalendar],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -834,7 +762,6 @@ describe("getSchedule", () => {
|
||||||
schedules: [TestData.schedules.IstWorkHoursWithDateOverride(plus2DateString)],
|
schedules: [TestData.schedules.IstWorkHoursWithDateOverride(plus2DateString)],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
hosts: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
createBookingScenario(scenarioData);
|
createBookingScenario(scenarioData);
|
||||||
|
@ -913,15 +840,6 @@ describe("getSchedule", () => {
|
||||||
endTime: `${plus2DateString}T04:15:00.000Z`,
|
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
|
// Requesting this user's availability for their
|
||||||
|
@ -1022,7 +940,6 @@ describe("getSchedule", () => {
|
||||||
endTime: `${plus2DateString}T05:45:00.000Z`,
|
endTime: `${plus2DateString}T05:45:00.000Z`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
hosts: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({
|
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({
|
||||||
|
@ -1162,7 +1079,6 @@ describe("getSchedule", () => {
|
||||||
endTime: `${plus3DateString}T04:15:00.000Z`,
|
endTime: `${plus3DateString}T04:15:00.000Z`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
hosts: [],
|
|
||||||
});
|
});
|
||||||
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule({
|
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule({
|
||||||
input: {
|
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<number, boolean> = {};
|
|
||||||
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}`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
@ -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<number, boolean> = {};
|
||||||
|
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<typeof getOrganizer>;
|
||||||
|
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<EventBusyDate[]> => {
|
||||||
|
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<string, unknown>; responses: Record<string, unknown> };
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
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<Prisma.BookingCreateInput>) {
|
||||||
|
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<R> {
|
||||||
|
toHaveEmail(expectedEmail: { htmlToContain?: string; to: string }): R;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect.extend({
|
||||||
|
toHaveEmail(
|
||||||
|
testEmail: ReturnType<Fixtures["emails"]["get"]>[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}`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
|
@ -89,7 +89,8 @@
|
||||||
"prettier": "^2.8.6",
|
"prettier": "^2.8.6",
|
||||||
"tsc-absolute": "^1.0.0",
|
"tsc-absolute": "^1.0.0",
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^4.9.4",
|
||||||
"vitest": "^0.31.1",
|
"vitest": "^0.34.3",
|
||||||
|
"vitest-fetch-mock": "^0.2.2",
|
||||||
"vitest-mock-extended": "^1.1.3"
|
"vitest-mock-extended": "^1.1.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { appKeysSchema as calVideoKeysSchema } from "@calcom/app-store/dailyvide
|
||||||
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
|
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
|
||||||
import { MeetLocationType } from "@calcom/app-store/locations";
|
import { MeetLocationType } from "@calcom/app-store/locations";
|
||||||
import getApps from "@calcom/app-store/utils";
|
import getApps from "@calcom/app-store/utils";
|
||||||
|
import logger from "@calcom/lib/logger";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { createdEventSchema } from "@calcom/prisma/zod-utils";
|
import { createdEventSchema } from "@calcom/prisma/zod-utils";
|
||||||
import type { NewCalendarEventType } from "@calcom/types/Calendar";
|
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.
|
* 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
|
* @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;
|
return videoCredential;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import dayjs from "@calcom/dayjs";
|
||||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||||
import { serverConfig } from "@calcom/lib/serverConfig";
|
import { serverConfig } from "@calcom/lib/serverConfig";
|
||||||
|
import { setTestEmail } from "@calcom/lib/testEmails";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
export default class BaseEmail {
|
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"));
|
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 payload = this.getNodeMailerPayload();
|
||||||
const parseSubject = z.string().safeParse(payload?.subject);
|
const parseSubject = z.string().safeParse(payload?.subject);
|
||||||
const payloadWithUnEscapedSubject = {
|
const payloadWithUnEscapedSubject = {
|
||||||
|
|
|
@ -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: "<title>confirmed_event_type_subject</title>",
|
||||||
|
to: `${organizer.email}`,
|
||||||
|
});
|
||||||
|
expect(testEmails[1]).toHaveEmail({
|
||||||
|
htmlToContain: "<title>confirmed_event_type_subject</title>",
|
||||||
|
to: `${booker.name} <${booker.email}>`,
|
||||||
|
});
|
||||||
|
expect(testEmails[1].html).toContain("<title>confirmed_event_type_subject</title>");
|
||||||
|
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: "<title>event_awaiting_approval_subject</title>",
|
||||||
|
to: `${organizer.email}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(testEmails[1]).toHaveEmail({
|
||||||
|
htmlToContain: "<title>booking_submitted_subject</title>",
|
||||||
|
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: "<title>confirmed_event_type_subject</title>",
|
||||||
|
to: `${organizer.email}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(testEmails[1]).toHaveEmail({
|
||||||
|
htmlToContain: "<title>confirmed_event_type_subject</title>",
|
||||||
|
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<typeof createMocks>) {
|
||||||
|
return createMocks<CustomNextApiRequest, CustomNextApiResponse>(...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<ReturnType<typeof getBasicMockRequestDataForBooking>> & {
|
||||||
|
eventTypeId: number;
|
||||||
|
responses: {
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
location: { optionValue: ""; value: string };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
...getBasicMockRequestDataForBooking(),
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
|
@ -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();
|
|
@ -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<typeof appStore>({
|
||||||
|
fallbackMockImplementation: () => {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export default appStoreMock;
|
|
@ -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<typeof i18n>();
|
||||||
|
export default i18nMock;
|
|
@ -5,6 +5,7 @@ import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
vi.mock("@calcom/prisma", () => ({
|
vi.mock("@calcom/prisma", () => ({
|
||||||
default: prisma,
|
default: prisma,
|
||||||
|
prisma,
|
||||||
availabilityUserSelect: vi.fn(),
|
availabilityUserSelect: vi.fn(),
|
||||||
userSelect: vi.fn(),
|
userSelect: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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<typeof reminderScheduler>();
|
||||||
|
export default reminderSchedulerMock;
|
|
@ -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<typeof videoClient>();
|
||||||
|
export default videoClientMock;
|
|
@ -1,9 +1,12 @@
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
process.env.INTEGRATION_TEST_MODE = "true";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
coverage: {
|
coverage: {
|
||||||
provider: "c8",
|
provider: "v8",
|
||||||
},
|
},
|
||||||
|
testTimeout: 500000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,6 +17,7 @@ const workspaces = packagedEmbedTestsOnly
|
||||||
include: ["packages/**/*.{test,spec}.{ts,js}", "apps/**/*.{test,spec}.{ts,js}"],
|
include: ["packages/**/*.{test,spec}.{ts,js}", "apps/**/*.{test,spec}.{ts,js}"],
|
||||||
// TODO: Ignore the api until tests are fixed
|
// TODO: Ignore the api until tests are fixed
|
||||||
exclude: ["apps/api/**/*", "**/node_modules/**/*", "packages/embeds/**/*"],
|
exclude: ["apps/api/**/*", "**/node_modules/**/*", "packages/embeds/**/*"],
|
||||||
|
setupFiles: ["setupVitest.ts"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
278
yarn.lock
278
yarn.lock
|
@ -6985,7 +6985,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 1.4.15
|
||||||
resolution: "@jridgewell/sourcemap-codec@npm:1.4.15"
|
resolution: "@jridgewell/sourcemap-codec@npm:1.4.15"
|
||||||
checksum: b881c7e503db3fc7f3c1f35a1dd2655a188cc51a3612d76efc8a6eb74728bef5606e6758ee77423e564092b4a518aba569bbb21c9bac5ab7a35b0c6ae7e344c8
|
checksum: b881c7e503db3fc7f3c1f35a1dd2655a188cc51a3612d76efc8a6eb74728bef5606e6758ee77423e564092b4a518aba569bbb21c9bac5ab7a35b0c6ae7e344c8
|
||||||
|
@ -13215,57 +13215,56 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@vitest/expect@npm:0.31.1":
|
"@vitest/expect@npm:0.34.3":
|
||||||
version: 0.31.1
|
version: 0.34.3
|
||||||
resolution: "@vitest/expect@npm:0.31.1"
|
resolution: "@vitest/expect@npm:0.34.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vitest/spy": 0.31.1
|
"@vitest/spy": 0.34.3
|
||||||
"@vitest/utils": 0.31.1
|
"@vitest/utils": 0.34.3
|
||||||
chai: ^4.3.7
|
chai: ^4.3.7
|
||||||
checksum: 0d1e135ae753d913231eae830da00ee42afca53d354898fb43f97e82398dcf17298c02e9989dd6b19b9b2909989248ef76d203d63f6af6f9159dc96959ea654b
|
checksum: 79afaa37d2efb7bb5503332caf389860b2261f198dbe61557e8061262b628d18658e59eb51d1808ecd35fc59f4bb4d04c0e0f97a27c7db02584ab5b424147b8d
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@vitest/runner@npm:0.31.1":
|
"@vitest/runner@npm:0.34.3":
|
||||||
version: 0.31.1
|
version: 0.34.3
|
||||||
resolution: "@vitest/runner@npm:0.31.1"
|
resolution: "@vitest/runner@npm:0.34.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vitest/utils": 0.31.1
|
"@vitest/utils": 0.34.3
|
||||||
concordance: ^5.0.4
|
|
||||||
p-limit: ^4.0.0
|
p-limit: ^4.0.0
|
||||||
pathe: ^1.1.0
|
pathe: ^1.1.1
|
||||||
checksum: cc8702e21b799d5e941409cb2afe6d0e576b4f3ac99df4a1393a8cd11b57f6b0b06e756cc24e2739812d095fbfd0824e22e861dbd6a71769ca387d485ade6fb5
|
checksum: 945580eaa58e8edbe29a64059bc2a524a9e85117b6d600fdb457cfe84cbfb81bf6d7e98e1227e7cb4e7399992c8fe8d83d0791d0385ff005dc1a4d9da125443b
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@vitest/snapshot@npm:0.31.1":
|
"@vitest/snapshot@npm:0.34.3":
|
||||||
version: 0.31.1
|
version: 0.34.3
|
||||||
resolution: "@vitest/snapshot@npm:0.31.1"
|
resolution: "@vitest/snapshot@npm:0.34.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
magic-string: ^0.30.0
|
magic-string: ^0.30.1
|
||||||
pathe: ^1.1.0
|
pathe: ^1.1.1
|
||||||
pretty-format: ^27.5.1
|
pretty-format: ^29.5.0
|
||||||
checksum: de05fa9136864f26f0804baf3ae8068f67de28015f29047329c84e67fb33be7305c9e52661b016e834d30f4081c136b3b6d8d4054c024a5d52b22a7f90fc4be0
|
checksum: 234893e91a1efd4bdbbde047a68de40975e02ead8407724ce8ca4a24edf0fb2d725f8a3efceb104965388407b598faf22407aadfbf4164cc74b3cf1e0e9f4543
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@vitest/spy@npm:0.31.1":
|
"@vitest/spy@npm:0.34.3":
|
||||||
version: 0.31.1
|
version: 0.34.3
|
||||||
resolution: "@vitest/spy@npm:0.31.1"
|
resolution: "@vitest/spy@npm:0.34.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
tinyspy: ^2.1.0
|
tinyspy: ^2.1.1
|
||||||
checksum: 8b06cf25fcc028c16106ec82f4ceb84d6dfa04d06f651bca4738ce2b99796d1fc4e0c10319767240755eff8ede2bff9d31d5a901fe92828d319c65001581137b
|
checksum: a2b64b9c357a56ad2f2340ecd225ffe787e61afba4ffb24a6670aad3fc90ea2606ed48daa188ed62b3ef67d55c0259fda6b101143d6c91b58c9ac4298d8be4f9
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@vitest/utils@npm:0.31.1":
|
"@vitest/utils@npm:0.34.3":
|
||||||
version: 0.31.1
|
version: 0.34.3
|
||||||
resolution: "@vitest/utils@npm:0.31.1"
|
resolution: "@vitest/utils@npm:0.34.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
concordance: ^5.0.4
|
diff-sequences: ^29.4.3
|
||||||
loupe: ^2.3.6
|
loupe: ^2.3.6
|
||||||
pretty-format: ^27.5.1
|
pretty-format: ^29.5.0
|
||||||
checksum: 58016c185455e3814632cb77e37368c846bde5e342f8b4a66fa229bde64f455ca39abebc9c12e2483696ee38bc17b3c4300379f7a3b18d1087f24f474448a8d8
|
checksum: aeb8ef7fd98b32cb6c403796880d0aa8f5411bbdb249bb23b3301a70e1b7d1ee025ddb204aae8c1db5756f6ac428c49ebbb8e2ed23ce185c8a659b67413efa85
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -13864,6 +13863,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"acorn@npm:^8.5.0":
|
||||||
version: 8.7.1
|
version: 8.7.1
|
||||||
resolution: "acorn@npm:8.7.1"
|
resolution: "acorn@npm:8.7.1"
|
||||||
|
@ -15276,13 +15284,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"bmp-js@npm:^0.1.0":
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
resolution: "bmp-js@npm:0.1.0"
|
resolution: "bmp-js@npm:0.1.0"
|
||||||
|
@ -15972,7 +15973,8 @@ __metadata:
|
||||||
tsc-absolute: ^1.0.0
|
tsc-absolute: ^1.0.0
|
||||||
turbo: ^1.10.1
|
turbo: ^1.10.1
|
||||||
typescript: ^4.9.4
|
typescript: ^4.9.4
|
||||||
vitest: ^0.31.1
|
vitest: ^0.34.3
|
||||||
|
vitest-fetch-mock: ^0.2.2
|
||||||
vitest-mock-extended: ^1.1.3
|
vitest-mock-extended: ^1.1.3
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
@ -17078,22 +17080,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"concurrently@npm:^7.6.0":
|
||||||
version: 7.6.0
|
version: 7.6.0
|
||||||
resolution: "concurrently@npm:7.6.0"
|
resolution: "concurrently@npm:7.6.0"
|
||||||
|
@ -17514,6 +17500,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"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
|
version: 7.0.3
|
||||||
resolution: "cross-spawn@npm:7.0.3"
|
resolution: "cross-spawn@npm:7.0.3"
|
||||||
|
@ -17936,15 +17931,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"datocms-listen@npm:^0.1.9":
|
||||||
version: 0.1.15
|
version: 0.1.15
|
||||||
resolution: "datocms-listen@npm:0.1.15"
|
resolution: "datocms-listen@npm:0.1.15"
|
||||||
|
@ -19946,7 +19932,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"esutils@npm:^2.0.2, esutils@npm:^2.0.3":
|
"esutils@npm:^2.0.2":
|
||||||
version: 2.0.3
|
version: 2.0.3
|
||||||
resolution: "esutils@npm:2.0.3"
|
resolution: "esutils@npm:2.0.3"
|
||||||
checksum: 22b5b08f74737379a840b8ed2036a5fb35826c709ab000683b092d9054e5c2a82c27818f12604bfc2a9a76b90b6834ef081edbc1c7ae30d1627012e067c6ec87
|
checksum: 22b5b08f74737379a840b8ed2036a5fb35826c709ab000683b092d9054e5c2a82c27818f12604bfc2a9a76b90b6834ef081edbc1c7ae30d1627012e067c6ec87
|
||||||
|
@ -20401,13 +20387,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"fast-equals@npm:^1.6.3":
|
||||||
version: 1.6.3
|
version: 1.6.3
|
||||||
resolution: "fast-equals@npm:1.6.3"
|
resolution: "fast-equals@npm:1.6.3"
|
||||||
|
@ -26526,12 +26505,12 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"magic-string@npm:^0.30.0":
|
"magic-string@npm:^0.30.1":
|
||||||
version: 0.30.0
|
version: 0.30.3
|
||||||
resolution: "magic-string@npm:0.30.0"
|
resolution: "magic-string@npm:0.30.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@jridgewell/sourcemap-codec": ^1.4.13
|
"@jridgewell/sourcemap-codec": ^1.4.15
|
||||||
checksum: 7bdf22e27334d8a393858a16f5f840af63a7c05848c000fd714da5aa5eefa09a1bc01d8469362f25cc5c4a14ec01b46557b7fff8751365522acddf21e57c488d
|
checksum: a5a9ddf9bd3bf49a2de1048bf358464f1bda7b3cc1311550f4a0ba8f81a4070e25445d53a5ee28850161336f1bff3cf28aa3320c6b4aeff45ce3e689f300b2f3
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -26725,15 +26704,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"md5.js@npm:^1.3.4":
|
||||||
version: 1.3.5
|
version: 1.3.5
|
||||||
resolution: "md5.js@npm:1.3.5"
|
resolution: "md5.js@npm:1.3.5"
|
||||||
|
@ -27796,6 +27766,18 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"mock-fs@npm:^4.1.0":
|
||||||
version: 4.14.0
|
version: 4.14.0
|
||||||
resolution: "mock-fs@npm:4.14.0"
|
resolution: "mock-fs@npm:4.14.0"
|
||||||
|
@ -28446,6 +28428,20 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"node-forge@npm:1.3.1, node-forge@npm:^1.0.0":
|
||||||
version: 1.3.1
|
version: 1.3.1
|
||||||
resolution: "node-forge@npm:1.3.1"
|
resolution: "node-forge@npm:1.3.1"
|
||||||
|
@ -29889,6 +29885,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"pathval@npm:^1.1.1":
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
resolution: "pathval@npm:1.1.1"
|
resolution: "pathval@npm:1.1.1"
|
||||||
|
@ -30718,7 +30721,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"pretty-format@npm:^27.0.2, pretty-format@npm:^27.5.1":
|
"pretty-format@npm:^27.0.2":
|
||||||
version: 27.5.1
|
version: 27.5.1
|
||||||
resolution: "pretty-format@npm:27.5.1"
|
resolution: "pretty-format@npm:27.5.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -34508,10 +34511,10 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"std-env@npm:^3.3.2":
|
"std-env@npm:^3.3.3":
|
||||||
version: 3.3.3
|
version: 3.4.3
|
||||||
resolution: "std-env@npm:3.3.3"
|
resolution: "std-env@npm:3.4.3"
|
||||||
checksum: 6665f6d8bd63aae432d3eb9abbd7322847ad0d902603e6dce1e8051b4f42ceeb4f7f96a4faf70bb05ce65ceee2dc982502b701575c8a58b1bfad29f3dbb19f81
|
checksum: bef186fb2baddda31911234b1e58fa18f181eb6930616aaec3b54f6d5db65f2da5daaa5f3b326b98445a7d50ca81d6fe8809ab4ebab85ecbe4a802f1b40921bf
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -35811,13 +35814,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"timed-out@npm:^4.0.0, timed-out@npm:^4.0.1":
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
resolution: "timed-out@npm:4.0.1"
|
resolution: "timed-out@npm:4.0.1"
|
||||||
|
@ -35903,17 +35899,17 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"tinypool@npm:^0.5.0":
|
"tinypool@npm:^0.7.0":
|
||||||
version: 0.5.0
|
version: 0.7.0
|
||||||
resolution: "tinypool@npm:0.5.0"
|
resolution: "tinypool@npm:0.7.0"
|
||||||
checksum: 4e0dfd8f28666d541c1d92304222edc4613f05d74fe2243c8520d466a2cc6596011a7072c1c41a7de7522351b82fda07e8038198e8f43673d8d69401c5903f8c
|
checksum: fdcccd5c750574fce51f8801a877f8284e145d12b79cd5f2d72bfbddfe20c895e915555bc848e122bb6aa968098e7ac4fe1e8e88104904d518dc01cccd18a510
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"tinyspy@npm:^2.1.0":
|
"tinyspy@npm:^2.1.1":
|
||||||
version: 2.1.0
|
version: 2.1.1
|
||||||
resolution: "tinyspy@npm:2.1.0"
|
resolution: "tinyspy@npm:2.1.1"
|
||||||
checksum: cb83c1f74a79dd5934018bad94f60a304a29d98a2d909ea45fc367f7b80b21b0a7d8135a2ce588deb2b3ba56c7c607258b2a03e6001d89e4d564f9a95cc6a81f
|
checksum: cfe669803a7f11ca912742b84c18dcc4ceecaa7661c69bc5eb608a8a802d541c48aba220df8929f6c8cd09892ad37cb5ba5958ddbbb57940e91d04681d3cee73
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -36860,6 +36856,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"uglify-js@npm:^3.1.4":
|
||||||
version: 3.15.3
|
version: 3.15.3
|
||||||
resolution: "uglify-js@npm:3.15.3"
|
resolution: "uglify-js@npm:3.15.3"
|
||||||
|
@ -37840,19 +37843,19 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"vite-node@npm:0.31.1":
|
"vite-node@npm:0.34.3":
|
||||||
version: 0.31.1
|
version: 0.34.3
|
||||||
resolution: "vite-node@npm:0.31.1"
|
resolution: "vite-node@npm:0.34.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: ^6.7.14
|
cac: ^6.7.14
|
||||||
debug: ^4.3.4
|
debug: ^4.3.4
|
||||||
mlly: ^1.2.0
|
mlly: ^1.4.0
|
||||||
pathe: ^1.1.0
|
pathe: ^1.1.1
|
||||||
picocolors: ^1.0.0
|
picocolors: ^1.0.0
|
||||||
vite: ^3.0.0 || ^4.0.0
|
vite: ^3.0.0 || ^4.0.0
|
||||||
bin:
|
bin:
|
||||||
vite-node: vite-node.mjs
|
vite-node: vite-node.mjs
|
||||||
checksum: f70ffa3f6dcb4937cdc99f59bf360d42de83c556ba9a19eb1c3504ef20db4c1d1afa644d9a8e63240e851c0c95773b64c526bdb3eb4794b5e941ddcd57124aa9
|
checksum: 366c4f3fb7c038e2180abc6b18cfbac3b8684cd878eaf7ebf1ffb07d95d2ea325713fc575a7949a13bb00cfe264acbc28c02e2836b8647e1f443fe631c17805a
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -37930,6 +37933,17 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"vitest-mock-extended@npm:^1.1.3":
|
||||||
version: 1.1.3
|
version: 1.1.3
|
||||||
resolution: "vitest-mock-extended@npm:1.1.3"
|
resolution: "vitest-mock-extended@npm:1.1.3"
|
||||||
|
@ -37942,34 +37956,33 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"vitest@npm:^0.31.1":
|
"vitest@npm:^0.34.3":
|
||||||
version: 0.31.1
|
version: 0.34.3
|
||||||
resolution: "vitest@npm:0.31.1"
|
resolution: "vitest@npm:0.34.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/chai": ^4.3.5
|
"@types/chai": ^4.3.5
|
||||||
"@types/chai-subset": ^1.3.3
|
"@types/chai-subset": ^1.3.3
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
"@vitest/expect": 0.31.1
|
"@vitest/expect": 0.34.3
|
||||||
"@vitest/runner": 0.31.1
|
"@vitest/runner": 0.34.3
|
||||||
"@vitest/snapshot": 0.31.1
|
"@vitest/snapshot": 0.34.3
|
||||||
"@vitest/spy": 0.31.1
|
"@vitest/spy": 0.34.3
|
||||||
"@vitest/utils": 0.31.1
|
"@vitest/utils": 0.34.3
|
||||||
acorn: ^8.8.2
|
acorn: ^8.9.0
|
||||||
acorn-walk: ^8.2.0
|
acorn-walk: ^8.2.0
|
||||||
cac: ^6.7.14
|
cac: ^6.7.14
|
||||||
chai: ^4.3.7
|
chai: ^4.3.7
|
||||||
concordance: ^5.0.4
|
|
||||||
debug: ^4.3.4
|
debug: ^4.3.4
|
||||||
local-pkg: ^0.4.3
|
local-pkg: ^0.4.3
|
||||||
magic-string: ^0.30.0
|
magic-string: ^0.30.1
|
||||||
pathe: ^1.1.0
|
pathe: ^1.1.1
|
||||||
picocolors: ^1.0.0
|
picocolors: ^1.0.0
|
||||||
std-env: ^3.3.2
|
std-env: ^3.3.3
|
||||||
strip-literal: ^1.0.1
|
strip-literal: ^1.0.1
|
||||||
tinybench: ^2.5.0
|
tinybench: ^2.5.0
|
||||||
tinypool: ^0.5.0
|
tinypool: ^0.7.0
|
||||||
vite: ^3.0.0 || ^4.0.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
|
why-is-node-running: ^2.2.2
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@edge-runtime/vm": "*"
|
"@edge-runtime/vm": "*"
|
||||||
|
@ -37999,7 +38012,7 @@ __metadata:
|
||||||
optional: true
|
optional: true
|
||||||
bin:
|
bin:
|
||||||
vitest: vitest.mjs
|
vitest: vitest.mjs
|
||||||
checksum: b3f64a36102edc5b8594c085da648c838c0d275c620bd3b780624f936903b9c06579d6ef137fe9859e468f16deb8f154a50f009093119f9adb8b60ff1b7597ee
|
checksum: 4535d080feede94db5015eb60c6ed5f7b0d8cd67f12072de5ae1faded133cc640043c0c2646ef51ab9b61c2f885589da57458a65e82cf91a25cf954470018a40
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -38703,13 +38716,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"whatwg-encoding@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "whatwg-encoding@npm:2.0.0"
|
resolution: "whatwg-encoding@npm:2.0.0"
|
||||||
|
|
Loading…
Reference in New Issue