Add getSchedule tests (#3233)
* Add getSchedule tests * Add first integration test * Update turbo.json * Make sure unit tests run asap * Worker threads * Improve team event test * Remove unrelated changes * Improve tests readability * Update CalendarManager.ts * Add README * Debug tests * Temporarily disabled build errors * Fix failing tests * Remove unncessary logs Co-authored-by: zomars <zomars@me.com>pull/3450/head^2
parent
2eedf74eb7
commit
471420c1d4
|
@ -3,7 +3,7 @@ import type { Config } from "@jest/types";
|
|||
const config: Config.InitialOptions = {
|
||||
verbose: true,
|
||||
roots: ["<rootDir>"],
|
||||
testMatch: ["**/tests/**/*.+(ts|tsx|js)", "**/?(*.)+(spec|test).+(ts|tsx|js)"],
|
||||
testMatch: ["**/test/lib/**/*.(spec|test).(ts|tsx|js)"],
|
||||
testPathIgnorePatterns: ["<rootDir>/.next", "<rootDir>/playwright/"],
|
||||
transform: {
|
||||
"^.+\\.(js|jsx|ts|tsx)$": ["babel-jest", { presets: ["next/babel"] }],
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
||||
"dev": "next dev",
|
||||
"dx": "yarn dev",
|
||||
"test": "jest",
|
||||
"test": "dotenv -e ./test/.env.test -- jest",
|
||||
"db-setup-tests": "dotenv -e ./test/.env.test -- yarn workspace @calcom/prisma prisma migrate deploy",
|
||||
"test-e2e": "cd ../.. && yarn playwright test --config=tests/config/playwright.config.ts --project=chromium",
|
||||
"playwright-report": "playwright show-report playwright/reports/playwright-html-report",
|
||||
"test-codegen": "yarn playwright codegen http://localhost:3000",
|
||||
|
@ -89,6 +90,7 @@
|
|||
"next-mdx-remote": "^4.0.3",
|
||||
"next-seo": "^4.26.0",
|
||||
"next-transpile-modules": "^9.0.0",
|
||||
"nock": "^13.2.8",
|
||||
"nodemailer": "^6.7.5",
|
||||
"otplib": "^12.0.1",
|
||||
"qrcode": "^1.5.0",
|
||||
|
|
|
@ -158,8 +158,10 @@ test.describe("Embed Code Generator Tests", () => {
|
|||
});
|
||||
|
||||
test.describe("Event Type Edit Page", () => {
|
||||
//TODO: Instead of hardcoding, browse through actual events, as this ID might change in future
|
||||
const sixtyMinProEventId = "6";
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/event-types/3");
|
||||
await page.goto(`/event-types/${sixtyMinProEventId}`);
|
||||
});
|
||||
|
||||
test("open Embed Dialog for the Event Type", async ({ page }) => {
|
||||
|
@ -167,14 +169,14 @@ test.describe("Embed Code Generator Tests", () => {
|
|||
|
||||
await expectToBeNavigatingToEmbedTypesDialog(page, {
|
||||
eventTypeId,
|
||||
basePage: "/event-types/3",
|
||||
basePage: `/event-types/${sixtyMinProEventId}`,
|
||||
});
|
||||
|
||||
chooseEmbedType(page, "inline");
|
||||
|
||||
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
|
||||
eventTypeId,
|
||||
basePage: "/event-types/3",
|
||||
basePage: `/event-types/${sixtyMinProEventId}`,
|
||||
embedType: "inline",
|
||||
});
|
||||
|
||||
|
@ -186,7 +188,7 @@ test.describe("Embed Code Generator Tests", () => {
|
|||
|
||||
await expectToContainValidPreviewIframe(page, {
|
||||
embedType: "inline",
|
||||
calLink: "pro/30min",
|
||||
calLink: "pro/60min",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -91,12 +91,14 @@ test.describe("Event Types tests", () => {
|
|||
});
|
||||
|
||||
test("can duplicate an existing event type", async ({ page }) => {
|
||||
const firstTitle = await page.locator("[data-testid=event-type-title-3]").innerText();
|
||||
const firstFullSlug = await page.locator("[data-testid=event-type-slug-3]").innerText();
|
||||
// TODO: Locate the actual EventType available in list. This ID might change in future
|
||||
const eventTypeId = "6";
|
||||
const firstTitle = await page.locator(`[data-testid=event-type-title-${eventTypeId}]`).innerText();
|
||||
const firstFullSlug = await page.locator(`[data-testid=event-type-slug-${eventTypeId}]`).innerText();
|
||||
const firstSlug = firstFullSlug.split("/")[2];
|
||||
|
||||
await page.click("[data-testid=event-type-options-3]");
|
||||
await page.click("[data-testid=event-type-duplicate-3]");
|
||||
await page.click(`[data-testid=event-type-options-${eventTypeId}]`);
|
||||
await page.click(`[data-testid=event-type-duplicate-${eventTypeId}]`);
|
||||
|
||||
const url = page.url();
|
||||
const params = new URLSearchParams(url);
|
||||
|
|
|
@ -5,6 +5,7 @@ import type { CurrentSeats } from "@calcom/core/getUserAvailability";
|
|||
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
||||
import dayjs, { Dayjs } from "@calcom/dayjs";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { availabilityUserSelect } from "@calcom/prisma";
|
||||
import { TimeRange } from "@calcom/types/schedule";
|
||||
|
||||
|
@ -94,171 +95,192 @@ const checkForAvailability = ({
|
|||
export const slotsRouter = createRouter().query("getSchedule", {
|
||||
input: getScheduleSchema,
|
||||
async resolve({ input, ctx }) {
|
||||
if (input.debug === true) {
|
||||
logger.setSettings({ minLevel: "debug" });
|
||||
}
|
||||
const startPrismaEventTypeGet = performance.now();
|
||||
const eventType = await ctx.prisma.eventType.findUnique({
|
||||
where: {
|
||||
id: input.eventTypeId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
minimumBookingNotice: true,
|
||||
length: true,
|
||||
seatsPerTimeSlot: true,
|
||||
timeZone: true,
|
||||
slotInterval: true,
|
||||
beforeEventBuffer: true,
|
||||
afterEventBuffer: true,
|
||||
schedulingType: true,
|
||||
periodType: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
periodCountCalendarDays: true,
|
||||
periodDays: true,
|
||||
schedule: {
|
||||
select: {
|
||||
availability: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
availability: {
|
||||
select: {
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
days: true,
|
||||
},
|
||||
},
|
||||
users: {
|
||||
select: {
|
||||
username: true,
|
||||
...availabilityUserSelect,
|
||||
},
|
||||
return await getSchedule(input, ctx);
|
||||
},
|
||||
});
|
||||
|
||||
export async function getSchedule(
|
||||
input: {
|
||||
timeZone?: string | undefined;
|
||||
eventTypeId?: number | undefined;
|
||||
usernameList?: string[] | undefined;
|
||||
debug?: boolean | undefined;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
},
|
||||
ctx: { prisma: typeof prisma }
|
||||
) {
|
||||
if (input.debug === true) {
|
||||
logger.setSettings({ minLevel: "debug" });
|
||||
}
|
||||
if (process.env.INTEGRATION_TEST_MODE === "true") {
|
||||
logger.setSettings({ minLevel: "silly" });
|
||||
}
|
||||
const startPrismaEventTypeGet = performance.now();
|
||||
const eventType = await ctx.prisma.eventType.findUnique({
|
||||
where: {
|
||||
id: input.eventTypeId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
minimumBookingNotice: true,
|
||||
length: true,
|
||||
seatsPerTimeSlot: true,
|
||||
timeZone: true,
|
||||
slotInterval: true,
|
||||
beforeEventBuffer: true,
|
||||
afterEventBuffer: true,
|
||||
schedulingType: true,
|
||||
periodType: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
periodCountCalendarDays: true,
|
||||
periodDays: true,
|
||||
schedule: {
|
||||
select: {
|
||||
availability: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
availability: {
|
||||
select: {
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
days: true,
|
||||
},
|
||||
},
|
||||
users: {
|
||||
select: {
|
||||
username: true,
|
||||
...availabilityUserSelect,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const endPrismaEventTypeGet = performance.now();
|
||||
logger.debug(
|
||||
`Prisma eventType get took ${endPrismaEventTypeGet - startPrismaEventTypeGet}ms for event:${
|
||||
input.eventTypeId
|
||||
}`
|
||||
);
|
||||
if (!eventType) {
|
||||
throw new TRPCError({ code: "NOT_FOUND" });
|
||||
}
|
||||
|
||||
const startTime =
|
||||
input.timeZone === "Etc/GMT"
|
||||
? dayjs.utc(input.startTime)
|
||||
: dayjs(input.startTime).utc().tz(input.timeZone);
|
||||
const endTime =
|
||||
input.timeZone === "Etc/GMT" ? dayjs.utc(input.endTime) : dayjs(input.endTime).utc().tz(input.timeZone);
|
||||
|
||||
if (!startTime.isValid() || !endTime.isValid()) {
|
||||
throw new TRPCError({ message: "Invalid time range given.", code: "BAD_REQUEST" });
|
||||
}
|
||||
let currentSeats: CurrentSeats | undefined = undefined;
|
||||
|
||||
const userSchedules = await Promise.all(
|
||||
eventType.users.map(async (currentUser) => {
|
||||
const {
|
||||
busy,
|
||||
workingHours,
|
||||
currentSeats: _currentSeats,
|
||||
} = await getUserAvailability(
|
||||
{
|
||||
userId: currentUser.id,
|
||||
dateFrom: startTime.format(),
|
||||
dateTo: endTime.format(),
|
||||
eventTypeId: input.eventTypeId,
|
||||
afterEventBuffer: eventType.afterEventBuffer,
|
||||
},
|
||||
{ user: currentUser, eventType, currentSeats }
|
||||
);
|
||||
if (!currentSeats && _currentSeats) currentSeats = _currentSeats;
|
||||
|
||||
return {
|
||||
workingHours,
|
||||
busy,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const workingHours = userSchedules.flatMap((s) => s.workingHours);
|
||||
|
||||
const slots: Record<string, Slot[]> = {};
|
||||
const availabilityCheckProps = {
|
||||
eventLength: eventType.length,
|
||||
beforeBufferTime: eventType.beforeEventBuffer,
|
||||
currentSeats,
|
||||
};
|
||||
const isWithinBounds = (_time: Parameters<typeof isOutOfBounds>[0]) =>
|
||||
!isOutOfBounds(_time, {
|
||||
periodType: eventType.periodType,
|
||||
periodStartDate: eventType.periodStartDate,
|
||||
periodEndDate: eventType.periodEndDate,
|
||||
periodCountCalendarDays: eventType.periodCountCalendarDays,
|
||||
periodDays: eventType.periodDays,
|
||||
});
|
||||
const endPrismaEventTypeGet = performance.now();
|
||||
logger.debug(`Prisma eventType get took ${endPrismaEventTypeGet - startPrismaEventTypeGet}ms`);
|
||||
if (!eventType) {
|
||||
throw new TRPCError({ code: "NOT_FOUND" });
|
||||
}
|
||||
|
||||
const startTime =
|
||||
input.timeZone === "Etc/GMT"
|
||||
? dayjs.utc(input.startTime)
|
||||
: dayjs(input.startTime).utc().tz(input.timeZone);
|
||||
const endTime =
|
||||
input.timeZone === "Etc/GMT" ? dayjs.utc(input.endTime) : dayjs(input.endTime).utc().tz(input.timeZone);
|
||||
let time = startTime;
|
||||
let getSlotsTime = 0;
|
||||
let checkForAvailabilityTime = 0;
|
||||
let getSlotsCount = 0;
|
||||
let checkForAvailabilityCount = 0;
|
||||
do {
|
||||
const startGetSlots = performance.now();
|
||||
// get slots retrieves the available times for a given day
|
||||
const times = getSlots({
|
||||
inviteeDate: time,
|
||||
eventLength: eventType.length,
|
||||
workingHours,
|
||||
minimumBookingNotice: eventType.minimumBookingNotice,
|
||||
frequency: eventType.slotInterval || eventType.length,
|
||||
});
|
||||
const endGetSlots = performance.now();
|
||||
getSlotsTime += endGetSlots - startGetSlots;
|
||||
getSlotsCount++;
|
||||
// if ROUND_ROBIN - slots stay available on some() - if normal / COLLECTIVE - slots only stay available on every()
|
||||
const filterStrategy =
|
||||
!eventType.schedulingType || eventType.schedulingType === SchedulingType.COLLECTIVE
|
||||
? ("every" as const)
|
||||
: ("some" as const);
|
||||
|
||||
if (!startTime.isValid() || !endTime.isValid()) {
|
||||
throw new TRPCError({ message: "Invalid time range given.", code: "BAD_REQUEST" });
|
||||
}
|
||||
let currentSeats: CurrentSeats | undefined = undefined;
|
||||
|
||||
const userSchedules = await Promise.all(
|
||||
eventType.users.map(async (currentUser) => {
|
||||
const {
|
||||
busy,
|
||||
workingHours,
|
||||
currentSeats: _currentSeats,
|
||||
} = await getUserAvailability(
|
||||
{
|
||||
userId: currentUser.id,
|
||||
dateFrom: startTime.format(),
|
||||
dateTo: endTime.format(),
|
||||
eventTypeId: input.eventTypeId,
|
||||
afterEventBuffer: eventType.afterEventBuffer,
|
||||
},
|
||||
{ user: currentUser, eventType, currentSeats }
|
||||
);
|
||||
if (!currentSeats && _currentSeats) currentSeats = _currentSeats;
|
||||
|
||||
return {
|
||||
workingHours,
|
||||
busy,
|
||||
};
|
||||
const filteredTimes = times.filter(isWithinBounds).filter((time) =>
|
||||
userSchedules[filterStrategy]((schedule) => {
|
||||
const startCheckForAvailability = performance.now();
|
||||
const result = checkForAvailability({ time, ...schedule, ...availabilityCheckProps });
|
||||
const endCheckForAvailability = performance.now();
|
||||
checkForAvailabilityCount++;
|
||||
checkForAvailabilityTime += endCheckForAvailability - startCheckForAvailability;
|
||||
return result;
|
||||
})
|
||||
);
|
||||
|
||||
const workingHours = userSchedules.flatMap((s) => s.workingHours);
|
||||
slots[time.format("YYYY-MM-DD")] = filteredTimes.map((time) => ({
|
||||
time: time.toISOString(),
|
||||
users: eventType.users.map((user) => user.username || ""),
|
||||
// Conditionally add the attendees and booking id to slots object if there is already a booking during that time
|
||||
...(currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString()) && {
|
||||
attendees:
|
||||
currentSeats[
|
||||
currentSeats.findIndex((booking) => booking.startTime.toISOString() === time.toISOString())
|
||||
]._count.attendees,
|
||||
bookingUid:
|
||||
currentSeats[
|
||||
currentSeats.findIndex((booking) => booking.startTime.toISOString() === time.toISOString())
|
||||
].uid,
|
||||
}),
|
||||
}));
|
||||
time = time.add(1, "day");
|
||||
} while (time.isBefore(endTime));
|
||||
|
||||
const slots: Record<string, Slot[]> = {};
|
||||
const availabilityCheckProps = {
|
||||
eventLength: eventType.length,
|
||||
beforeBufferTime: eventType.beforeEventBuffer,
|
||||
currentSeats,
|
||||
};
|
||||
const isWithinBounds = (_time: Parameters<typeof isOutOfBounds>[0]) =>
|
||||
!isOutOfBounds(_time, {
|
||||
periodType: eventType.periodType,
|
||||
periodStartDate: eventType.periodStartDate,
|
||||
periodEndDate: eventType.periodEndDate,
|
||||
periodCountCalendarDays: eventType.periodCountCalendarDays,
|
||||
periodDays: eventType.periodDays,
|
||||
});
|
||||
logger.debug(`getSlots took ${getSlotsTime}ms and executed ${getSlotsCount} times`);
|
||||
|
||||
let time = startTime;
|
||||
let getSlotsTime = 0;
|
||||
let checkForAvailabilityTime = 0;
|
||||
let getSlotsCount = 0;
|
||||
let checkForAvailabilityCount = 0;
|
||||
do {
|
||||
const startGetSlots = performance.now();
|
||||
// get slots retrieves the available times for a given day
|
||||
const times = getSlots({
|
||||
inviteeDate: time,
|
||||
eventLength: eventType.length,
|
||||
workingHours,
|
||||
minimumBookingNotice: eventType.minimumBookingNotice,
|
||||
frequency: eventType.slotInterval || eventType.length,
|
||||
});
|
||||
const endGetSlots = performance.now();
|
||||
getSlotsTime += endGetSlots - startGetSlots;
|
||||
getSlotsCount++;
|
||||
// if ROUND_ROBIN - slots stay available on some() - if normal / COLLECTIVE - slots only stay available on every()
|
||||
const filterStrategy =
|
||||
!eventType.schedulingType || eventType.schedulingType === SchedulingType.COLLECTIVE
|
||||
? ("every" as const)
|
||||
: ("some" as const);
|
||||
|
||||
const filteredTimes = times.filter(isWithinBounds).filter((time) =>
|
||||
userSchedules[filterStrategy]((schedule) => {
|
||||
const startCheckForAvailability = performance.now();
|
||||
const result = checkForAvailability({ time, ...schedule, ...availabilityCheckProps });
|
||||
const endCheckForAvailability = performance.now();
|
||||
checkForAvailabilityCount++;
|
||||
checkForAvailabilityTime += endCheckForAvailability - startCheckForAvailability;
|
||||
return result;
|
||||
})
|
||||
);
|
||||
|
||||
slots[time.format("YYYY-MM-DD")] = filteredTimes.map((time) => ({
|
||||
time: time.toISOString(),
|
||||
users: eventType.users.map((user) => user.username || ""),
|
||||
// Conditionally add the attendees and booking id to slots object if there is already a booking during that time
|
||||
...(currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString()) && {
|
||||
attendees:
|
||||
currentSeats[
|
||||
currentSeats.findIndex((booking) => booking.startTime.toISOString() === time.toISOString())
|
||||
]._count.attendees,
|
||||
bookingUid:
|
||||
currentSeats[
|
||||
currentSeats.findIndex((booking) => booking.startTime.toISOString() === time.toISOString())
|
||||
].uid,
|
||||
}),
|
||||
}));
|
||||
time = time.add(1, "day");
|
||||
} while (time.isBefore(endTime));
|
||||
|
||||
logger.debug(`getSlots took ${getSlotsTime}ms and executed ${getSlotsCount} times`);
|
||||
|
||||
logger.debug(
|
||||
`checkForAvailability took ${checkForAvailabilityTime}ms and executed ${checkForAvailabilityCount} times`
|
||||
);
|
||||
|
||||
return {
|
||||
slots,
|
||||
};
|
||||
},
|
||||
});
|
||||
logger.debug(
|
||||
`checkForAvailability took ${checkForAvailabilityTime}ms and executed ${checkForAvailabilityCount} times`
|
||||
);
|
||||
logger.silly(`Available slots: ${JSON.stringify(slots)}`);
|
||||
return {
|
||||
slots,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
DATABASE_URL="postgresql://prisma:prisma@localhost:5433/tests"
|
||||
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
|
||||
INTEGRATION_TEST_MODE=true
|
|
@ -0,0 +1,12 @@
|
|||
# Unit and Integration Tests
|
||||
|
||||
Make sure you have copied .env.test.example to .env.test
|
||||
|
||||
You can run all jest tests as
|
||||
|
||||
`yarn test`
|
||||
|
||||
You can run tests matching specific description by following command
|
||||
`yarn test -t getSchedule`
|
||||
|
||||
Tip: Use `--watchAll` flag to run tests on every change
|
|
@ -0,0 +1,15 @@
|
|||
# Set the version of docker compose to use
|
||||
version: '3.9'
|
||||
|
||||
# The containers that compose the project
|
||||
services:
|
||||
db:
|
||||
image: postgres:13
|
||||
restart: always
|
||||
container_name: integration-tests-prisma
|
||||
ports:
|
||||
- '5433:5432'
|
||||
environment:
|
||||
POSTGRES_USER: prisma
|
||||
POSTGRES_PASSWORD: prisma
|
||||
POSTGRES_DB: tests
|
|
@ -0,0 +1,579 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import nock from "nock";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import logger from "@calcom/lib/logger";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus, PeriodType } from "@calcom/prisma/client";
|
||||
|
||||
import { getSchedule } from "../../server/routers/viewer/slots";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace jest {
|
||||
interface Matchers<R> {
|
||||
toHaveTimeSlots(expectedSlots: string[], date: { dateString: string }): R;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect.extend({
|
||||
toHaveTimeSlots(schedule, expectedSlots: string[], { dateString }: { dateString: string }) {
|
||||
expect(schedule.slots[`${dateString}`]).toBeDefined();
|
||||
expect(schedule.slots[`${dateString}`].map((slot: { time: string }) => slot.time)).toEqual(
|
||||
expectedSlots.map((slotTime) => `${dateString}T${slotTime}`)
|
||||
);
|
||||
return {
|
||||
pass: true,
|
||||
message: () => "has correct timeslots ",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const getDate = (param: { dateIncrement?: number; monthIncrement?: number; yearIncrement?: number } = {}) => {
|
||||
let { dateIncrement, monthIncrement, yearIncrement } = param;
|
||||
dateIncrement = dateIncrement || 0;
|
||||
monthIncrement = monthIncrement || 0;
|
||||
yearIncrement = yearIncrement || 0;
|
||||
const year = new Date().getFullYear() + yearIncrement;
|
||||
// Make it start with 1 to match with DayJS requiremet
|
||||
let _month = new Date().getMonth() + monthIncrement + 1;
|
||||
if (_month === 13) {
|
||||
_month = 1;
|
||||
}
|
||||
const month = _month < 10 ? "0" + _month : _month;
|
||||
|
||||
let _date = new Date().getDate() + dateIncrement;
|
||||
|
||||
// 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)
|
||||
if (_date === new Date(year, _month, 0).getDate()) {
|
||||
_date = 1;
|
||||
}
|
||||
|
||||
const date = _date < 10 ? "0" + _date : _date;
|
||||
// console.log("Date, month, year:", date, month, year);
|
||||
return {
|
||||
date,
|
||||
month,
|
||||
year,
|
||||
dateString: `${year}-${month}-${date}`,
|
||||
};
|
||||
};
|
||||
|
||||
const ctx = {
|
||||
prisma,
|
||||
};
|
||||
|
||||
type App = {
|
||||
slug: string;
|
||||
dirName: string;
|
||||
};
|
||||
type User = {
|
||||
credentials?: Credential[];
|
||||
selectedCalendars?: SelectedCalendar[];
|
||||
};
|
||||
|
||||
type Credential = { key: any; type: string };
|
||||
type SelectedCalendar = {
|
||||
integration: string;
|
||||
externalId: string;
|
||||
};
|
||||
|
||||
type EventType = {
|
||||
id?: number;
|
||||
title?: string;
|
||||
length: number;
|
||||
periodType: PeriodType;
|
||||
slotInterval: number;
|
||||
minimumBookingNotice: number;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
};
|
||||
|
||||
type Booking = {
|
||||
userId: number;
|
||||
eventTypeId: number;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
title?: string;
|
||||
status: BookingStatus;
|
||||
};
|
||||
|
||||
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",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function addEventTypeToDB(data: {
|
||||
eventType: EventType;
|
||||
selectedCalendars?: SelectedCalendar[];
|
||||
credentials?: Credential[];
|
||||
users?: User[];
|
||||
usersConnectedToTheEvent?: { id: number }[];
|
||||
numUsers?: number;
|
||||
}) {
|
||||
data.selectedCalendars = data.selectedCalendars || [];
|
||||
data.credentials = data.credentials || [];
|
||||
const userCreate = {
|
||||
id: 100,
|
||||
username: "hariom",
|
||||
email: "hariombalhara@gmail.com",
|
||||
schedules: {
|
||||
create: {
|
||||
name: "Schedule1",
|
||||
availability: {
|
||||
create: {
|
||||
userId: null,
|
||||
eventTypeId: null,
|
||||
days: [0, 1, 2, 3, 4, 5, 6],
|
||||
startTime: "1970-01-01T09:30:00.000Z",
|
||||
endTime: "1970-01-01T18:00:00.000Z",
|
||||
date: null,
|
||||
},
|
||||
},
|
||||
timeZone: "Asia/Kolkata",
|
||||
},
|
||||
},
|
||||
};
|
||||
const usersCreate: typeof userCreate[] = [];
|
||||
|
||||
if (!data.users && !data.numUsers && !data.usersConnectedToTheEvent) {
|
||||
throw new Error("Either users, numUsers or usersConnectedToTheEvent must be provided");
|
||||
}
|
||||
if (!data.users && data.numUsers) {
|
||||
data.users = [];
|
||||
for (let i = 0; i < data.numUsers; i++) {
|
||||
data.users.push({
|
||||
credentials: undefined,
|
||||
selectedCalendars: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (data.users?.length) {
|
||||
data.users.forEach((user, index) => {
|
||||
const newUserCreate = {
|
||||
...userCreate,
|
||||
...user,
|
||||
credentials: { create: user.credentials },
|
||||
selectedCalendars: { create: user.selectedCalendars },
|
||||
};
|
||||
newUserCreate.id = index + 1;
|
||||
newUserCreate.username = `IntegrationTestUser${newUserCreate.id}`;
|
||||
newUserCreate.email = `IntegrationTestUser${newUserCreate.id}@example.com`;
|
||||
usersCreate.push(newUserCreate);
|
||||
});
|
||||
} else {
|
||||
usersCreate.push({ ...userCreate });
|
||||
}
|
||||
|
||||
const prismaData: Prisma.EventTypeCreateArgs["data"] = {
|
||||
title: "Test EventType Title",
|
||||
slug: "testslug",
|
||||
timeZone: null,
|
||||
beforeEventBuffer: 0,
|
||||
afterEventBuffer: 0,
|
||||
schedulingType: null,
|
||||
periodStartDate: "2022-01-21T09:03:48.000Z",
|
||||
periodEndDate: "2022-01-21T09:03:48.000Z",
|
||||
periodCountCalendarDays: false,
|
||||
periodDays: 30,
|
||||
users: {
|
||||
create: usersCreate,
|
||||
connect: data.usersConnectedToTheEvent,
|
||||
},
|
||||
...data.eventType,
|
||||
};
|
||||
logger.silly("TestData: Creating EventType", prismaData);
|
||||
|
||||
return await prisma.eventType.create({
|
||||
data: prismaData,
|
||||
select: {
|
||||
id: true,
|
||||
users: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function addBookingToDB(data: { booking: Booking }) {
|
||||
const prismaData = {
|
||||
uid: uuidv4(),
|
||||
title: "Test Booking Title",
|
||||
...data.booking,
|
||||
};
|
||||
logger.silly("TestData: Creating Booking", prismaData);
|
||||
|
||||
return await prisma.booking.create({
|
||||
data: prismaData,
|
||||
});
|
||||
}
|
||||
|
||||
async function createBookingScenario(data: {
|
||||
booking?: Omit<Booking, "eventTypeId" | "userId">;
|
||||
users?: User[];
|
||||
numUsers?: number;
|
||||
credentials?: Credential[];
|
||||
apps?: App[];
|
||||
selectedCalendars?: SelectedCalendar[];
|
||||
eventType: EventType;
|
||||
/**
|
||||
* User must already be existing
|
||||
* */
|
||||
usersConnectedToTheEvent?: { id: number }[];
|
||||
}) {
|
||||
// if (!data.eventType.userId) {
|
||||
// data.eventType.userId =
|
||||
// (data.users ? data.users[0]?.id : null) || data.usersConnect ? data.usersConnect[0]?.id : null;
|
||||
// }
|
||||
const eventType = await addEventTypeToDB(data);
|
||||
if (data.apps) {
|
||||
await prisma.app.createMany({
|
||||
data: data.apps,
|
||||
});
|
||||
}
|
||||
if (data.booking) {
|
||||
// TODO: What about if there are multiple users of the eventType?
|
||||
const userId = eventType.users[0].id;
|
||||
const eventTypeId = eventType.id;
|
||||
|
||||
await addBookingToDB({ ...data, booking: { ...data.booking, userId, eventTypeId } });
|
||||
}
|
||||
return {
|
||||
eventType,
|
||||
};
|
||||
}
|
||||
|
||||
const cleanup = async () => {
|
||||
await prisma.eventType.deleteMany();
|
||||
await prisma.user.deleteMany();
|
||||
await prisma.schedule.deleteMany();
|
||||
await prisma.selectedCalendar.deleteMany();
|
||||
await prisma.credential.deleteMany();
|
||||
await prisma.booking.deleteMany();
|
||||
await prisma.app.deleteMany();
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
describe("getSchedule", () => {
|
||||
describe("User Event", () => {
|
||||
test("correctly identifies unavailable slots from Cal Bookings", async () => {
|
||||
// const { dateString: todayDateString } = getDate();
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
||||
|
||||
// An event with one accepted booking
|
||||
const { eventType } = await createBookingScenario({
|
||||
eventType: {
|
||||
minimumBookingNotice: 1440,
|
||||
length: 30,
|
||||
slotInterval: 45,
|
||||
periodType: "UNLIMITED" as PeriodType,
|
||||
},
|
||||
numUsers: 1,
|
||||
booking: {
|
||||
status: "ACCEPTED",
|
||||
startTime: `${plus3DateString}T04:00:00.000Z`,
|
||||
endTime: `${plus3DateString}T04:15:00.000Z`,
|
||||
},
|
||||
});
|
||||
|
||||
// const scheduleLyingWithinMinBookingNotice = await getSchedule(
|
||||
// {
|
||||
// eventTypeId: eventType.id,
|
||||
// startTime: `${todayDateString}T18:30:00.000Z`,
|
||||
// endTime: `${plus1DateString}T18:29:59.999Z`,
|
||||
// timeZone: "Asia/Kolkata",
|
||||
// },
|
||||
// ctx
|
||||
// );
|
||||
|
||||
// expect(scheduleLyingWithinMinBookingNotice).toHaveTimeSlots([], {
|
||||
// dateString: plus1DateString,
|
||||
// });
|
||||
|
||||
const scheduleOnCompletelyFreeDay = await getSchedule(
|
||||
{
|
||||
eventTypeId: eventType.id,
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: "Asia/Kolkata",
|
||||
},
|
||||
ctx
|
||||
);
|
||||
|
||||
expect(scheduleOnCompletelyFreeDay).toHaveTimeSlots(
|
||||
[
|
||||
"04:00:00.000Z",
|
||||
"04:45:00.000Z",
|
||||
"05:30:00.000Z",
|
||||
"06:15:00.000Z",
|
||||
"07:00:00.000Z",
|
||||
"07:45:00.000Z",
|
||||
"08:30:00.000Z",
|
||||
"09:15:00.000Z",
|
||||
"10:00:00.000Z",
|
||||
"10:45:00.000Z",
|
||||
"11:30:00.000Z",
|
||||
],
|
||||
{
|
||||
dateString: plus2DateString,
|
||||
}
|
||||
);
|
||||
|
||||
const scheduleForDayWithOneBooking = await getSchedule(
|
||||
{
|
||||
eventTypeId: eventType.id,
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: "Asia/Kolkata", // GMT+5:30
|
||||
},
|
||||
ctx
|
||||
);
|
||||
expect(scheduleForDayWithOneBooking).toHaveTimeSlots(
|
||||
[
|
||||
"04:45:00.000Z",
|
||||
"05:30:00.000Z",
|
||||
"06:15:00.000Z",
|
||||
"07:00:00.000Z",
|
||||
"07:45:00.000Z",
|
||||
"08:30:00.000Z",
|
||||
"09:15:00.000Z",
|
||||
"10:00:00.000Z",
|
||||
"10:45:00.000Z",
|
||||
"11:30:00.000Z",
|
||||
],
|
||||
{
|
||||
dateString: plus3DateString,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("correctly identifies unavailable slots from calendar", async () => {
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
|
||||
// An event with one accepted booking
|
||||
const { eventType } = await createBookingScenario({
|
||||
eventType: {
|
||||
minimumBookingNotice: 1440,
|
||||
length: 30,
|
||||
slotInterval: 45,
|
||||
periodType: "UNLIMITED" as PeriodType,
|
||||
seatsPerTimeSlot: null,
|
||||
},
|
||||
users: [
|
||||
{
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [
|
||||
{
|
||||
integration: "google_calendar",
|
||||
externalId: "john@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
apps: [
|
||||
{
|
||||
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"],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
nock("https://oauth2.googleapis.com").post("/token").reply(200, {
|
||||
access_token: "access_token",
|
||||
expiry_date: Infinity,
|
||||
});
|
||||
|
||||
// Google Calendar with 11th July having many events
|
||||
nock("https://www.googleapis.com")
|
||||
.post("/calendar/v3/freeBusy")
|
||||
.reply(200, {
|
||||
calendars: [
|
||||
{
|
||||
busy: [
|
||||
{
|
||||
start: `${plus2DateString}T04:30:00.000Z`,
|
||||
end: `${plus2DateString}T23:00:00.000Z`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule(
|
||||
{
|
||||
eventTypeId: eventType.id,
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: "Asia/Kolkata",
|
||||
},
|
||||
ctx
|
||||
);
|
||||
|
||||
// As per Google Calendar Availability, only 4PM GMT slot would be available
|
||||
expect(scheduleForDayWithAGoogleCalendarBooking).toHaveTimeSlots([`04:00:00.000Z`], {
|
||||
dateString: plus2DateString,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Team Event", () => {
|
||||
test("correctly identifies unavailable slots from calendar", async () => {
|
||||
const { dateString: todayDateString } = getDate();
|
||||
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
|
||||
// An event having two users with one accepted booking
|
||||
const { eventType: teamEventType } = await createBookingScenario({
|
||||
eventType: {
|
||||
id: 1,
|
||||
minimumBookingNotice: 0,
|
||||
length: 30,
|
||||
slotInterval: 45,
|
||||
periodType: "UNLIMITED" as PeriodType,
|
||||
seatsPerTimeSlot: null,
|
||||
},
|
||||
numUsers: 2,
|
||||
booking: {
|
||||
status: "ACCEPTED",
|
||||
startTime: `${plus2DateString}T04:00:00.000Z`,
|
||||
endTime: `${plus2DateString}T04:15:00.000Z`,
|
||||
},
|
||||
});
|
||||
|
||||
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
startTime: `${todayDateString}T18:30:00.000Z`,
|
||||
endTime: `${plus1DateString}T18:29:59.999Z`,
|
||||
timeZone: "Asia/Kolkata",
|
||||
},
|
||||
ctx
|
||||
);
|
||||
|
||||
expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots(
|
||||
[
|
||||
`04:00:00.000Z`,
|
||||
`04:45:00.000Z`,
|
||||
`05:30:00.000Z`,
|
||||
`06:15:00.000Z`,
|
||||
`07:00:00.000Z`,
|
||||
`07:45:00.000Z`,
|
||||
`08:30:00.000Z`,
|
||||
`09:15:00.000Z`,
|
||||
`10:00:00.000Z`,
|
||||
`10:45:00.000Z`,
|
||||
`11:30:00.000Z`,
|
||||
],
|
||||
{
|
||||
dateString: plus1DateString,
|
||||
}
|
||||
);
|
||||
|
||||
const scheduleForTeamEventOnADayWithOneBooking = await getSchedule(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: "Asia/Kolkata",
|
||||
},
|
||||
ctx
|
||||
);
|
||||
|
||||
expect(scheduleForTeamEventOnADayWithOneBooking).toHaveTimeSlots(
|
||||
[
|
||||
`04:45:00.000Z`,
|
||||
`05:30:00.000Z`,
|
||||
`06:15:00.000Z`,
|
||||
`07:00:00.000Z`,
|
||||
`07:45:00.000Z`,
|
||||
`08:30:00.000Z`,
|
||||
`09:15:00.000Z`,
|
||||
`10:00:00.000Z`,
|
||||
`10:45:00.000Z`,
|
||||
`11:30:00.000Z`,
|
||||
],
|
||||
{ dateString: plus2DateString }
|
||||
);
|
||||
|
||||
// An event with user 2 of team event
|
||||
await createBookingScenario({
|
||||
eventType: {
|
||||
id: 2,
|
||||
minimumBookingNotice: 0,
|
||||
length: 30,
|
||||
slotInterval: 45,
|
||||
periodType: "UNLIMITED" as PeriodType,
|
||||
seatsPerTimeSlot: null,
|
||||
},
|
||||
usersConnectedToTheEvent: [
|
||||
{
|
||||
id: teamEventType.users[1].id,
|
||||
},
|
||||
],
|
||||
booking: {
|
||||
status: "ACCEPTED",
|
||||
startTime: `${plus2DateString}T05:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T05:45:00.000Z`,
|
||||
},
|
||||
});
|
||||
|
||||
const scheduleOfTeamEventHavingAUserWithBlockedTimeInAnotherEvent = await getSchedule(
|
||||
{
|
||||
eventTypeId: 1,
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: "Asia/Kolkata",
|
||||
},
|
||||
ctx
|
||||
);
|
||||
|
||||
// A user with blocked time in another event, doesn't impact Team Event availability
|
||||
expect(scheduleOfTeamEventHavingAUserWithBlockedTimeInAnotherEvent).toHaveTimeSlots(
|
||||
[
|
||||
`04:45:00.000Z`,
|
||||
`05:30:00.000Z`,
|
||||
`06:15:00.000Z`,
|
||||
`07:00:00.000Z`,
|
||||
`07:45:00.000Z`,
|
||||
`08:30:00.000Z`,
|
||||
`09:15:00.000Z`,
|
||||
`10:00:00.000Z`,
|
||||
`10:45:00.000Z`,
|
||||
`11:30:00.000Z`,
|
||||
],
|
||||
{ dateString: plus2DateString }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -42,7 +42,7 @@
|
|||
"test-playwright": "yarn playwright test --config=tests/config/playwright.config.ts",
|
||||
"embed-tests-quick": "turbo run embed-tests-quick",
|
||||
"embed-tests": "turbo run embed-tests",
|
||||
"test-e2e": "turbo run test-e2e --scope=\"@calcom/web\" --concurrency=1",
|
||||
"test-e2e": "turbo run test --scope=\"@calcom/web\" && yarn turbo run test-e2e --scope=\"@calcom/web\" --concurrency=1",
|
||||
"type-check": "turbo run type-check",
|
||||
"app-store": "yarn workspace @calcom/app-store-cli cli",
|
||||
"app-store:build": "yarn workspace @calcom/app-store-cli build",
|
||||
|
|
|
@ -101,20 +101,19 @@ const getCachedResults = async (
|
|||
/** We extract external Ids so we don't cache too much */
|
||||
const selectedCalendarIds = passedSelectedCalendars.map((sc) => sc.externalId);
|
||||
/** We create a unque hash key based on the input data */
|
||||
const cacheKey = createHash("md5")
|
||||
.update(JSON.stringify({ id, selectedCalendarIds, dateFrom, dateTo }))
|
||||
.digest("hex");
|
||||
const cacheKey = JSON.stringify({ id, selectedCalendarIds, dateFrom, dateTo });
|
||||
const cacheHashedKey = createHash("md5").update(cacheKey).digest("hex");
|
||||
/** Check if we already have cached data and return */
|
||||
const cachedAvailability = cache.get(cacheKey);
|
||||
const cachedAvailability = cache.get(cacheHashedKey);
|
||||
if (cachedAvailability) {
|
||||
log.debug(`Cache HIT: Calendar Availability for key`, { id, selectedCalendarIds, dateFrom, dateTo });
|
||||
log.debug(`Cache HIT: Calendar Availability for key: ${cacheKey}`);
|
||||
return cachedAvailability;
|
||||
}
|
||||
log.debug(`Cache MISS: Calendar Availability for key`, { id, selectedCalendarIds, dateFrom, dateTo });
|
||||
log.debug(`Cache MISS: Calendar Availability for key ${cacheKey}`);
|
||||
/** If we don't then we actually fetch external calendars (which can be very slow) */
|
||||
const availability = await c.getAvailability(dateFrom, dateTo, passedSelectedCalendars);
|
||||
/** We save the availability to a few seconds so recurrent calls are nearly instant */
|
||||
cache.put(cacheKey, availability, CACHING_TIME);
|
||||
cache.put(cacheHashedKey, availability, CACHING_TIME);
|
||||
return availability;
|
||||
});
|
||||
const awaitedResults = await Promise.all(results);
|
||||
|
|
|
@ -16,6 +16,13 @@ export async function getBusyTimes(params: {
|
|||
selectedCalendars: SelectedCalendar[];
|
||||
}) {
|
||||
const { credentials, userId, eventTypeId, startTime, endTime, selectedCalendars } = params;
|
||||
logger.silly(
|
||||
`Checking Busy time from Cal Bookings in range ${startTime} to ${endTime} for input ${JSON.stringify({
|
||||
userId,
|
||||
eventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
})}`
|
||||
);
|
||||
const startPrismaBookingGet = performance.now();
|
||||
const busyTimes: EventBusyDate[] = await prisma.booking
|
||||
.findMany({
|
||||
|
@ -29,11 +36,13 @@ export async function getBusyTimes(params: {
|
|||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
},
|
||||
})
|
||||
.then((bookings) => bookings.map(({ startTime, endTime }) => ({ end: endTime, start: startTime })));
|
||||
logger.silly(`Busy Time from Cal Bookings ${JSON.stringify(busyTimes)}`);
|
||||
const endPrismaBookingGet = performance.now();
|
||||
logger.debug(`prisma booking get took ${endPrismaBookingGet - startPrismaBookingGet}ms`);
|
||||
if (credentials.length > 0) {
|
||||
|
@ -46,7 +55,6 @@ export async function getBusyTimes(params: {
|
|||
busyTimes.push(...videoBusyTimes);
|
||||
*/
|
||||
}
|
||||
|
||||
return busyTimes;
|
||||
}
|
||||
|
||||
|
|
|
@ -87,10 +87,10 @@ export function getWorkingHours(
|
|||
utcOffset;
|
||||
const endTime =
|
||||
dayjs.utc(schedule.endTime).get("hour") * 60 + dayjs.utc(schedule.endTime).get("minute") - utcOffset;
|
||||
|
||||
// add to working hours, keeping startTime and endTimes between bounds (0-1439)
|
||||
const sameDayStartTime = Math.max(MINUTES_DAY_START, Math.min(MINUTES_DAY_END, startTime));
|
||||
const sameDayEndTime = Math.max(MINUTES_DAY_START, Math.min(MINUTES_DAY_END, endTime));
|
||||
|
||||
if (sameDayStartTime !== sameDayEndTime) {
|
||||
workingHours.push({
|
||||
days: schedule.days,
|
||||
|
|
|
@ -3,7 +3,6 @@ import { Logger } from "tslog";
|
|||
import { IS_PRODUCTION } from "./constants";
|
||||
|
||||
const logger = new Logger({
|
||||
minLevel: "info",
|
||||
dateTimePattern: "hour:minute:second.millisecond timeZoneName",
|
||||
displayFunctionName: false,
|
||||
displayFilePath: "hidden",
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@prisma/client": "^4.1.0"
|
||||
"@prisma/client": "^4.1.0",
|
||||
"dotenv-cli": "^6.0.0"
|
||||
},
|
||||
"main": "index.ts",
|
||||
"types": "index.d.ts",
|
||||
|
|
|
@ -9,7 +9,6 @@ import prisma from ".";
|
|||
import "./seed-app-store";
|
||||
|
||||
require("dotenv").config({ path: "../../.env" });
|
||||
|
||||
async function createUserAndEventType(opts: {
|
||||
user: {
|
||||
email: string;
|
||||
|
@ -87,7 +86,7 @@ async function createUserAndEventType(opts: {
|
|||
});
|
||||
|
||||
console.log(
|
||||
`\t📆 Event type ${eventTypeData.slug}, length ${eventTypeData.length}min - ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}/${eventTypeData.slug}`
|
||||
`\t📆 Event type ${eventTypeData.slug} with id ${id}, length ${eventTypeData.length}min - ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}/${eventTypeData.slug}`
|
||||
);
|
||||
for (const bookingInput of bookingInputs) {
|
||||
await prisma.booking.create({
|
||||
|
|
15
turbo.json
15
turbo.json
|
@ -126,14 +126,24 @@
|
|||
"test": {
|
||||
"dependsOn": ["^test"]
|
||||
},
|
||||
"@calcom/web#db-setup-tests": {
|
||||
"cache": false
|
||||
},
|
||||
"@calcom/web#test": {
|
||||
"cache": false,
|
||||
"dependsOn": ["@calcom/web#db-setup-tests"]
|
||||
},
|
||||
"test-e2e": {
|
||||
"cache": false,
|
||||
"dependsOn": ["@calcom/prisma#db-seed", "@calcom/web#test", "@calcom/web#build"]
|
||||
"dependsOn": ["@calcom/prisma#db-seed", "@calcom/web#build"]
|
||||
},
|
||||
"type-check": {
|
||||
"cache": false,
|
||||
"outputs": []
|
||||
},
|
||||
"@calcom/prisma#db-reset": {
|
||||
"cache": false
|
||||
},
|
||||
"@calcom/app-store-cli#build": {
|
||||
"cache": false,
|
||||
"inputs": ["../../app-store/**/**"],
|
||||
|
@ -168,5 +178,6 @@
|
|||
"inputs": ["./.env.appStore.example", "./.env.appStore"],
|
||||
"outputs": ["./.env.appStore"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"globalDependencies": ["yarn.lock"]
|
||||
}
|
||||
|
|
34
yarn.lock
34
yarn.lock
|
@ -6744,12 +6744,27 @@ dotenv-checker@^1.1.5:
|
|||
gradient-string "2.0.0"
|
||||
inquirer "8.2.1"
|
||||
|
||||
dotenv-cli@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-6.0.0.tgz#8a30cbc59d0a8aaa166b2fee0a9a55e23a1223ab"
|
||||
integrity sha512-qXlCOi3UMDhCWFKe0yq5sg3X+pJAz+RQDiFN38AMSbUrnY3uZshSfDJUAge951OS7J9gwLZGfsBlWRSOYz/TRg==
|
||||
dependencies:
|
||||
cross-spawn "^7.0.3"
|
||||
dotenv "^16.0.0"
|
||||
dotenv-expand "^8.0.1"
|
||||
minimist "^1.2.5"
|
||||
|
||||
dotenv-expand@^8.0.1:
|
||||
version "8.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-8.0.3.tgz#29016757455bcc748469c83a19b36aaf2b83dd6e"
|
||||
integrity sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==
|
||||
|
||||
dotenv@^10.0.0:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
|
||||
integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
|
||||
|
||||
dotenv@^16.0.1:
|
||||
dotenv@^16.0.0, dotenv@^16.0.1:
|
||||
version "16.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d"
|
||||
integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==
|
||||
|
@ -10966,7 +10981,7 @@ json-stable-stringify-without-jsonify@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||
integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
|
||||
|
||||
json-stringify-safe@~5.0.1:
|
||||
json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
|
||||
|
@ -12820,6 +12835,16 @@ nice-try@^1.0.4:
|
|||
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
||||
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
|
||||
|
||||
nock@^13.2.8:
|
||||
version "13.2.8"
|
||||
resolved "https://registry.yarnpkg.com/nock/-/nock-13.2.8.tgz#e2043ccaa8e285508274575e090a7fe1e46b90f1"
|
||||
integrity sha512-JT42FrXfQRpfyL4cnbBEJdf4nmBpVP0yoCcSBr+xkT8Q1y3pgtaCKHGAAOIFcEJ3O3t0QbVAmid0S0f2bj3Wpg==
|
||||
dependencies:
|
||||
debug "^4.1.0"
|
||||
json-stringify-safe "^5.0.1"
|
||||
lodash "^4.17.21"
|
||||
propagate "^2.0.0"
|
||||
|
||||
node-addon-api@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32"
|
||||
|
@ -14037,6 +14062,11 @@ prop-types@^15.0.0, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2,
|
|||
object-assign "^4.1.1"
|
||||
react-is "^16.13.1"
|
||||
|
||||
propagate@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45"
|
||||
integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==
|
||||
|
||||
property-expr@^2.0.4:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4"
|
||||
|
|
Loading…
Reference in New Issue