Skip to content

Commit

Permalink
add e2e testing on webhooks and booking happy-path (calcom#936)
Browse files Browse the repository at this point in the history
  • Loading branch information
KATT committed Oct 18, 2021
1 parent 86d2928 commit 9e69029
Show file tree
Hide file tree
Showing 16 changed files with 271 additions and 80 deletions.
4 changes: 3 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"files": ["playwright/**/*.{js,jsx,tsx,ts}"],
"rules": {
"no-undef": "off",
"@typescript-eslint/no-non-null-assertion": "off"
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-implicit-any": "off"
}
}
],
Expand Down
4 changes: 3 additions & 1 deletion components/booking/AvailableTimes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
return (
<div key={slot.time.format()}>
<Link href={bookingUrl}>
<a className="block font-medium mb-2 bg-white dark:bg-gray-600 text-primary-500 dark:text-neutral-200 border border-primary-500 dark:border-transparent rounded-sm hover:text-white hover:bg-primary-500 dark:hover:border-black py-4 dark:hover:bg-black">
<a
className="block font-medium mb-2 bg-white dark:bg-gray-600 text-primary-500 dark:text-neutral-200 border border-primary-500 dark:border-transparent rounded-sm hover:text-white hover:bg-primary-500 dark:hover:border-black py-4 dark:hover:bg-black"
data-testid="time">
{slot.time.format(timeFormat)}
</a>
</Link>
Expand Down
33 changes: 20 additions & 13 deletions components/booking/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
import dayjs, { Dayjs } from "dayjs";
import dayjsBusinessDays from "dayjs-business-days";
// Then, include dayjs-business-time
import dayjsBusinessTime from "dayjs-business-time";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { useEffect, useState } from "react";
Expand All @@ -9,11 +10,12 @@ import classNames from "@lib/classNames";
import { useLocale } from "@lib/hooks/useLocale";
import getSlots from "@lib/slots";

dayjs.extend(dayjsBusinessDays);
dayjs.extend(dayjsBusinessTime);
dayjs.extend(utc);
dayjs.extend(timezone);

const DatePicker = ({
// FIXME prop types
function DatePicker({
weekStart,
onDatePicked,
workingHours,
Expand All @@ -26,7 +28,7 @@ const DatePicker = ({
periodDays,
periodCountCalendarDays,
minimumBookingNotice,
}) => {
}: any): JSX.Element {
const { t } = useLocale();
const [days, setDays] = useState<({ disabled: boolean; date: number } | null)[]>([]);

Expand All @@ -47,11 +49,11 @@ const DatePicker = ({

// Handle month changes
const incrementMonth = () => {
setSelectedMonth(selectedMonth + 1);
setSelectedMonth((selectedMonth ?? 0) + 1);
};

const decrementMonth = () => {
setSelectedMonth(selectedMonth - 1);
setSelectedMonth((selectedMonth ?? 0) - 1);
};

const inviteeDate = (): Dayjs => (date || dayjs()).month(selectedMonth);
Expand All @@ -72,7 +74,7 @@ const DatePicker = ({
case "rolling": {
const periodRollingEndDay = periodCountCalendarDays
? dayjs().tz(organizerTimeZone).add(periodDays, "days").endOf("day")
: dayjs().tz(organizerTimeZone).businessDaysAdd(periodDays, "days").endOf("day");
: dayjs().tz(organizerTimeZone).addBusinessTime(periodDays, "days").endOf("day");
return (
date.endOf("day").isBefore(dayjs().utcOffset(date.utcOffset())) ||
date.endOf("day").isAfter(periodRollingEndDay) ||
Expand Down Expand Up @@ -145,10 +147,13 @@ const DatePicker = ({
<div className="w-1/2 text-right text-gray-600 dark:text-gray-400">
<button
onClick={decrementMonth}
className={
"group mr-2 p-1" + (selectedMonth <= dayjs().month() && "text-gray-400 dark:text-gray-600")
}
disabled={selectedMonth <= dayjs().month()}>
className={classNames(
"group mr-2 p-1",
typeof selectedMonth === "number" &&
selectedMonth <= dayjs().month() &&
"text-gray-400 dark:text-gray-600"
)}
disabled={typeof selectedMonth === "number" && selectedMonth <= dayjs().month()}>
<ChevronLeftIcon className="group-hover:text-black dark:group-hover:text-white w-5 h-5" />
</button>
<button className="group p-1" onClick={incrementMonth}>
Expand Down Expand Up @@ -190,7 +195,9 @@ const DatePicker = ({
: !day.disabled
? " bg-gray-100 dark:bg-gray-600"
: ""
)}>
)}
data-testid="day"
data-disabled={day.disabled}>
{day.date}
</button>
)}
Expand All @@ -199,6 +206,6 @@ const DatePicker = ({
</div>
</div>
);
};
}

export default DatePicker;
4 changes: 2 additions & 2 deletions jest.playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ const opts = {
executablePath: process.env.PLAYWRIGHT_CHROME_EXECUTABLE_PATH,
};

console.log("⚙️ Playwright options:", opts);
console.log("⚙️ Playwright options:", JSON.stringify(opts, null, 4));

module.exports = {
verbose: true,
preset: "jest-playwright-preset",
transform: {
"^.+\\.ts$": "ts-jest",
},
testMatch: ["<rootDir>/playwright/**/?(*.)+(spec|test).[jt]s?(x)"],
testMatch: ["<rootDir>/playwright/**/*(*.)@(spec|test).[jt]s?(x)"],
testEnvironmentOptions: {
"jest-playwright": {
browsers: ["chromium" /*, 'firefox', 'webkit'*/],
Expand Down
5 changes: 2 additions & 3 deletions lib/asStringOrNull.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ export function asNumberOrThrow(str: unknown) {
}

export function asStringOrThrow(str: unknown): string {
const type = typeof str;
if (type !== "string") {
throw new Error(`Expected "string" - got ${type}`);
if (typeof str !== "string") {
throw new Error(`Expected "string" - got ${typeof str}`);
}
return str;
}
17 changes: 11 additions & 6 deletions lib/webhooks/sendPayload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,26 @@ const sendPayload = (
): Promise<string | Response> =>
new Promise((resolve, reject) => {
if (!subscriberUrl || !payload) {
return reject("Missing required elements to send webhook payload.");
return reject(new Error("Missing required elements to send webhook payload."));
}
const body = {
triggerEvent: triggerEvent,
createdAt: createdAt,
payload: payload,
};

fetch(subscriberUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
triggerEvent: triggerEvent,
createdAt: createdAt,
payload: payload,
}),
body: JSON.stringify(body),
})
.then((response) => {
if (!response.ok) {
reject(new Error(`Response code ${response.status}`));
return;
}
resolve(response);
})
.catch((err) => {
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"test": "jest",
"test-playwright": "jest --config jest.playwright.config.js",
"test-playwright-lcov": "cross-env PLAYWRIGHT_HEADLESS=1 PLAYWRIGHT_COVERAGE=1 yarn test-playwright && nyc report --reporter=lcov",
"test-codegen": "yarn playwright codegen http://localhost:3000",
"type-check": "tsc --pretty --noEmit",
"build": "next build",
"start": "next start",
Expand Down Expand Up @@ -58,7 +59,7 @@
"bcryptjs": "^2.4.3",
"classnames": "^2.3.1",
"dayjs": "^1.10.6",
"dayjs-business-days": "^1.0.4",
"dayjs-business-time": "^1.0.4",
"googleapis": "^84.0.0",
"handlebars": "^4.7.7",
"ical.js": "^1.4.0",
Expand Down Expand Up @@ -103,7 +104,7 @@
"@types/jest": "^27.0.1",
"@types/lodash": "^4.14.175",
"@types/micro": "^7.3.6",
"@types/node": "^16.6.1",
"@types/node": "^16.10.2",
"@types/nodemailer": "^6.4.4",
"@types/qrcode": "^1.4.1",
"@types/react": "^17.0.18",
Expand Down
7 changes: 6 additions & 1 deletion pages/_error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ type AugmentedNextPageContext = Omit<NextPageContext, "err"> & {

const log = logger.getChildLogger({ prefix: ["[error]"] });

export function getErrorFromUnknown(cause: unknown): Error & { statusCode?: number } {
export function getErrorFromUnknown(cause: unknown): Error & {
// status code error
statusCode?: number;
// prisma error
code?: unknown;
} {
if (cause instanceof Error) {
return cause;
}
Expand Down
22 changes: 12 additions & 10 deletions pages/api/book/event.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { SchedulingType, Prisma, Credential } from "@prisma/client";
import { Credential, Prisma, SchedulingType } from "@prisma/client";
import async from "async";
import dayjs from "dayjs";
import dayjsBusinessDays from "dayjs-business-days";
import dayjsBusinessTime from "dayjs-business-time";
import isBetween from "dayjs/plugin/isBetween";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import type { NextApiRequest, NextApiResponse } from "next";
import { getErrorFromUnknown } from "pages/_error";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";

Expand All @@ -29,7 +30,7 @@ export interface DailyReturnType {
created_at: string;
}

dayjs.extend(dayjsBusinessDays);
dayjs.extend(dayjsBusinessTime);
dayjs.extend(utc);
dayjs.extend(isBetween);
dayjs.extend(timezone);
Expand Down Expand Up @@ -98,15 +99,15 @@ function isAvailable(busyTimes: BufferedBusyTimes, time: string, length: number)

function isOutOfBounds(
time: dayjs.ConfigType,
{ periodType, periodDays, periodCountCalendarDays, periodStartDate, periodEndDate, timeZone }
{ periodType, periodDays, periodCountCalendarDays, periodStartDate, periodEndDate, timeZone }: any // FIXME types
): boolean {
const date = dayjs(time);

switch (periodType) {
case "rolling": {
const periodRollingEndDay = periodCountCalendarDays
? dayjs().tz(timeZone).add(periodDays, "days").endOf("day")
: dayjs().tz(timeZone).businessDaysAdd(periodDays, "days").endOf("day");
: dayjs().tz(timeZone).addBusinessTime(periodDays, "days").endOf("day");
return date.endOf("day").isAfter(periodRollingEndDay);
}

Expand Down Expand Up @@ -298,7 +299,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
startTime: dayjs(evt.startTime).toDate(),
endTime: dayjs(evt.endTime).toDate(),
description: evt.description,
confirmed: !eventType.requiresConfirmation || !!rescheduleUid,
confirmed: !eventType?.requiresConfirmation || !!rescheduleUid,
location: evt.location,
eventType: {
connect: {
Expand All @@ -323,9 +324,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
let booking: Booking | null = null;
try {
booking = await createBooking();
} catch (e) {
log.error(`Booking ${eventTypeId} failed`, "Error when saving booking to db", e.message);
if (e.code === "P2002") {
} catch (_err) {
const err = getErrorFromUnknown(_err);
log.error(`Booking ${eventTypeId} failed`, "Error when saving booking to db", err.message);
if (err.code === "P2002") {
res.status(409).json({ message: "booking.conflict" });
return;
}
Expand Down Expand Up @@ -361,7 +363,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
);

const videoBusyTimes = (await getBusyVideoTimes(credentials)).filter((time) => time);
calendarBusyTimes.push(...videoBusyTimes);
calendarBusyTimes.push(...(videoBusyTimes as any[])); // FIXME add types
console.log("calendarBusyTimes==>>>", calendarBusyTimes);

const bufferedBusyTimes: BufferedBusyTimes = calendarBusyTimes.map((a) => ({
Expand Down
26 changes: 10 additions & 16 deletions pages/api/webhooks/[hook]/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/client";

import { getSession } from "@lib/auth";
import prisma from "@lib/prisma";

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req });
if (!session) {
const userId = session?.user?.id;
if (!userId) {
return res.status(401).json({ message: "Not authenticated" });
}

// GET /api/webhook/{hook}
const webhooks = await prisma.webhook.findFirst({
const webhook = await prisma.webhook.findFirst({
where: {
id: String(req.query.hook),
userId: session.user.id,
userId,
},
});
if (!webhook) {
return res.status(404).json({ message: "Invalid Webhook" });
}
if (req.method === "GET") {
return res.status(200).json({ webhooks: webhooks });
return res.status(200).json({ webhook });
}

// DELETE /api/webhook/{hook}
Expand All @@ -31,19 +35,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}

if (req.method === "PATCH") {
const webhook = await prisma.webhook.findUnique({
where: {
id: req.query.hook as string,
},
});

if (!webhook) {
return res.status(404).json({ message: "Invalid Webhook" });
}

await prisma.webhook.update({
where: {
id: req.query.hook as string,
id: webhook.id,
},
data: {
subscriberUrl: req.body.subscriberUrl,
Expand Down
3 changes: 2 additions & 1 deletion pages/integrations/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ function WebhookDialogForm(props: {
});
return (
<Form
data-testid="WebhookDialogForm"
form={form}
onSubmit={(event) => {
form
Expand Down Expand Up @@ -248,7 +249,7 @@ function WebhookEmbed(props: { webhooks: TWebhook[] }) {
<ListItemText component="p">Automation</ListItemText>
</div>
<div>
<Button color="secondary" onClick={() => setNewWebhookModal(true)}>
<Button color="secondary" onClick={() => setNewWebhookModal(true)} data-testid="new_webhook">
{t("new_webhook")}
</Button>
</div>
Expand Down

0 comments on commit 9e69029

Please sign in to comment.