Compare commits

...

12 Commits

Author SHA1 Message Date
Joe Au-Yeung 70531b202d Clean up 2023-09-20 22:56:18 -04:00
Joe Au-Yeung 6fa2726537 Merge branch 'main' into test/msw-gcal 2023-09-20 22:49:19 -04:00
Joe Au-Yeung 937fe300ba WIP 2023-09-20 22:48:26 -04:00
Joe Au-Yeung f3379cd3f1 Merge branch 'main' into test/msw-gcal 2023-09-20 13:54:16 -04:00
Joe Au-Yeung ff80cd110c WIP GCal Testing 2023-09-13 11:38:59 -04:00
Joe Au-Yeung 342cafdd4b Merge branch 'main' into test/msw-gcal 2023-09-01 21:45:25 -04:00
Joe Au-Yeung d53f9b07eb Merge branch 'main' into test/msw-gcal 2023-08-25 14:38:18 -04:00
Joe Au-Yeung 9aa600035f Abstract testing function 2023-08-22 21:47:06 -04:00
Joe Au-Yeung 61201375c1 Abstract NextApi types 2023-08-22 21:46:47 -04:00
Joe Au-Yeung ba04740e21 Create msw server 2023-08-22 21:46:26 -04:00
Joe Au-Yeung 9bbec71e17 Create mock prisma client 2023-08-22 21:46:04 -04:00
Joe Au-Yeung 170d6232fc Add vitest to googlecalendar package 2023-08-22 21:45:45 -04:00
19 changed files with 774 additions and 1684 deletions

View File

@ -151,7 +151,7 @@ describe("POST /api/bookings", () => {
);
});
});
describe("Success", () => {
describe("Regular event-type", () => {
test("Creates one single booking", async () => {

View File

@ -0,0 +1,10 @@
const path = require("path");
const i18nConfig = require("@calcom/config/next-i18next.config");
/** @type {import("next-i18next").UserConfig} */
const config = {
...i18nConfig,
localePath: path.resolve("../web/public/static/locales"),
};
module.exports = config;

10
next-i18next.config.js Normal file
View File

@ -0,0 +1,10 @@
const path = require("path");
const i18nConfig = require("@calcom/config/next-i18next.config");
/** @type {import("next-i18next").UserConfig} */
const config = {
...i18nConfig,
localePath: path.resolve("apps/web/public/static/locales"),
};
module.exports = config;

View File

@ -15,6 +15,7 @@ let client_secret = "";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { code } = req.query;
const state = decodeOAuthState(req);
console.log("🚀 ~ file: callback.ts:18 ~ handler ~ state:", state);
if (code && typeof code !== "string") {
res.status(400).json({ message: "`code` must be a string" });

View File

@ -199,6 +199,7 @@ export default class GoogleCalendarService implements Calendar {
useDefault: true,
},
guestsCanSeeOtherGuests: !!calEventRaw.seatsPerTimeSlot ? calEventRaw.seatsShowAttendees : true,
iCalUID: calEventRaw.iCalUID || calEventRaw.uid,
};
if (calEventRaw.location) {

View File

@ -9,7 +9,14 @@
"@calcom/prisma": "*",
"googleapis": "^84.0.0"
},
"scripts": {
"test": "vitest",
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
"@calcom/types": "*"
"@calcom/types": "*",
"msw": "1.2.3",
"node-mocks-http": "^1.11.0",
"vitest": "^0.34.2"
}
}

View File

@ -0,0 +1,72 @@
import { rest } from "msw";
import { testExpiryDate } from "../../gcal.test";
export const handlers = [
// Handles a POST request
rest.get("https://accounts.google.com/o/oauth2/v2/auth", (req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200)
);
}),
rest.get("https://accounts.google.com/v3/signin/identifier", (req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200)
);
}),
rest.post(`https://oauth2.googleapis.com/token`, (req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json({
access_token: "access_token",
refresh_token: "refresh_token",
expiry_date: testExpiryDate,
scope:
"https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events",
token_type: "Bearer",
})
);
}),
rest.get(`/api/integrations/googlecalendar/add`, (req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200)
);
}),
rest.post("https://www.googleapis.com/calendar/v3/calendars/primary/events", async (req, res, ctx) => {
const eventData = await req.json();
const organizer = eventData.attendees.find((attendee: any) => attendee.organizer);
return res(
ctx.status(200),
ctx.json({
// ...eventData,
id: 12345,
iCalUID: 67890,
kind: "calendar#event",
etag: "12345",
status: "confirmed",
reminders: { useDefault: true },
summary: eventData.title,
location: eventData.location,
organizer: {
email: organizer.email,
displayName: organizer.name,
self: true,
},
start: {
dateTime: eventData.startTime,
timeZone: organizer.timeZone,
},
end: {
dateTime: eventData.endTime,
timeZone: organizer.timeZone,
},
})
);
}),
];
// https://accounts.google.com/v3/signin/identifier

View File

@ -0,0 +1,6 @@
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
// This configures a request mocking server with the given request handlers.
export const server = setupServer(...handlers);

View File

@ -0,0 +1,257 @@
import { server } from "./__mocks__/server/server";
import { mockCredential } from "@calcom/prisma/__mocks__/mockCredential";
import type { TFunction } from "next-i18next";
import { createMocks } from "node-mocks-http";
import { test, expect, describe, beforeAll, afterAll, afterEach, vi, beforeEach } from "vitest";
import dayjs from "@calcom/dayjs";
import { default as handleNewBooking } from "@calcom/features/bookings/lib/handleNewBooking";
import { buildCalendarEvent } from "@calcom/lib/test/builder";
import { expectFunctionToBeCalledNthTimesWithArgs } from "@calcom/lib/test/expectFunctionToBeCalledNthTimesWithArgs";
import type { CustomNextApiRequest, CustomNextApiResponse } from "@calcom/lib/test/types";
import prisma from "@calcom/prisma";
import { default as prismaMock } from "@calcom/prisma/__mocks__";
import { default as addHandler } from "../api/add";
import { default as callbackHandler } from "../api/callback";
import { default as CalendarService } from "../lib/CalendarService";
vi.mock("@calcom/lib/getIP", () => ({
_esModule: true,
default: vi.fn().mockReturnValue("127.0.0.1"),
}));
const mockT: TFunction = vi.fn();
vi.mock("");
export const testExpiryDate = Date.now();
describe("Google oauth endpoints", () => {
beforeEach(() => {
vi.restoreAllMocks();
});
beforeAll(() => server.listen());
afterAll(() => server.close());
afterEach(() => server.resetHandlers());
describe("Google calendar oauth flows", () => {
test("OAuth URL should contain the correct scopes", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
});
prismaMock.app.findUnique.mockResolvedValueOnce({
slug: "google-calendar",
keys: { client_id: "client_id", client_secret: "client_secret" },
dirName: "googlecalendar",
categories: ["calendar"],
createdAt: new Date(),
updatedAt: new Date(),
enabled: true,
});
await addHandler(req, res);
const responseJSON = JSON.parse(res._getData());
const authURL = responseJSON.url;
expect(authURL).toEqual(expect.stringContaining("access_type=offline"));
expect(authURL).toContain("https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.readonly");
expect(authURL).toContain("https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.events");
});
test("OAuth callback should create the correct credentials", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
code: "testcode",
},
session: {
user: {
id: 123,
},
},
});
prismaMock.app.findUnique.mockResolvedValueOnce({
slug: "google-calendar",
keys: { client_id: "client_id", client_secret: "client_secret" },
dirName: "googlecalendar",
categories: ["calendar"],
createdAt: new Date(),
updatedAt: new Date(),
enabled: true,
});
await callbackHandler(req, res);
expectFunctionToBeCalledNthTimesWithArgs(
prismaMock.credential.create,
1,
expect.objectContaining({
data: {
type: "google_calendar",
key: {
scope:
"https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events",
token_type: "Bearer",
access_token: "access_token",
refresh_token: "refresh_token",
expiry_date: testExpiryDate,
},
userId: req.session?.user.id,
appId: "google-calendar",
},
})
);
expect(res._getStatusCode()).toBe(302);
});
});
test("Oauth callback should create Google Meet credentials if installGoogleVideo is true", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
code: "testcode",
state: JSON.stringify({ installGoogleVideo: true }),
},
session: {
user: {
id: 123,
},
},
});
prismaMock.app.findUnique.mockResolvedValueOnce({
slug: "google-calendar",
keys: { client_id: "client_id", client_secret: "client_secret" },
dirName: "googlecalendar",
categories: ["calendar"],
createdAt: new Date(),
updatedAt: new Date(),
enabled: true,
});
await callbackHandler(req, res);
expect(prismaMock.credential.create).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
data: {
type: "google_calendar",
key: {
scope:
"https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events",
token_type: "Bearer",
access_token: "access_token",
refresh_token: "refresh_token",
expiry_date: testExpiryDate,
},
userId: req.session?.user.id,
appId: "google-calendar",
},
})
);
expect(prismaMock.credential.create).toHaveBeenNthCalledWith(2, {
data: {
type: "google_video",
key: {},
userId: req.session?.user.id,
appId: "google-meet",
},
});
});
describe("handle sending data to Google", () => {
test("Create event", async () => {
const credential = mockCredential({
type: "google_calendar",
key: {
scope:
"https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events",
token_type: "Bearer",
access_token: "access_token",
refresh_token: "refresh_token",
expiry_date: testExpiryDate + 60 * 60 * 1000,
},
userId: 123,
appId: "google-calendar",
});
const calendarEventRaw = buildCalendarEvent({
attendees: [
{
name: "test",
email: "test@test.com",
timeZone: "America/Montevideo",
language: { translate: mockT, locale: "en" },
},
],
});
console.log("🚀 ~ file: gcal.test.ts:188 ~ test ~ calendarEventRaw:", calendarEventRaw);
const googleCalendarService = new CalendarService(credential);
const response = await googleCalendarService.createEvent(calendarEventRaw, credential.id);
console.log("🚀 ~ file: gcal.test.ts:187 ~ test ~ response:", response);
expect(response).toEqual(
expect.objectContaining({
uid: "",
id: "12345",
kind: "calendar#event",
etag: "12345",
iCalUID: calendarEventRaw.iCalUID,
type: "google_calendar",
password: "",
url: "",
summary: calendarEventRaw.title,
status: "confirmed",
reminders: { useDefault: true },
location: calendarEventRaw.location,
organizer: {
email: calendarEventRaw.organizer.email,
displayName: calendarEventRaw.organizer.name,
self: true,
},
additionalInfo: { hangoutLink: "" },
start: {
dateTime: calendarEventRaw.startTime,
timeZone: calendarEventRaw.organizer.timeZone,
},
end: {
dateTime: calendarEventRaw.endTime,
timeZone: calendarEventRaw.organizer.timeZone,
},
attendees: calendarEventRaw.attendees.map((attendee) => ({
email: attendee.email,
})),
})
);
});
test("API call to handleNewBooking", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
name: "test",
start: dayjs().add(1, "hour").format(),
end: dayjs().add(1, "day").format(),
eventTypeId: 3,
email: "test@example.com",
location: "Cal.com Video",
timeZone: "America/Montevideo",
language: "en",
customInputs: [],
metadata: {},
userId: 4,
},
userId: 4,
prisma,
});
// prismaMock.eventType.findUniqueOrThrow.mockResolvedValue(buildEventType());
// prismaMock.booking.findMany.mockResolvedValue([]);
await handleNewBooking(req, res);
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
expect(prismaMock.booking.create).toHaveBeenCalledTimes(1);
});
});
});

View File

@ -0,0 +1,10 @@
const path = require("path");
const i18nConfig = require("@calcom/config/next-i18next.config");
/** @type {import("next-i18next").UserConfig} */
const config = {
...i18nConfig,
localePath: path.resolve("/apps/web/public/static/locales"),
};
module.exports = config;

View File

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { getCalendarCredentials } from "./CalendarManager";
import { getCalendarCredentials } from "../CalendarManager";
describe("CalendarManager tests", () => {
describe("fn: getCalendarCredentials", () => {

View File

@ -379,7 +379,7 @@ async function ensureAvailableUsers(
}
) {
const availableUsers: IsFixedAwareUser[] = [];
const duration = dayjs(input.dateTo).diff(input.dateFrom, 'minute');
const duration = dayjs(input.dateTo).diff(input.dateFrom, "minute");
const originalBookingDuration = input.originalRescheduledBooking
? dayjs(input.originalRescheduledBooking.endTime).diff(
@ -668,6 +668,7 @@ async function handler(
...eventType,
bookingFields: getBookingFieldsWithSystemFields(eventType),
};
const {
recurringCount,
allRecurringDates,

View File

@ -227,3 +227,15 @@ export const buildUser = <T extends Partial<UserPayload>>(user?: T): UserPayload
...user,
};
};
export const buildNewBooking = (user: ReturnType<typeof buildUser>, eventTypeId: number) => {
return {
responses: {
email: "test@example.com",
name: "Test",
guests: [],
location: { optionValue: "", value: "integrations:daily" },
},
user: user.username,
};
};

View File

@ -0,0 +1,7 @@
import { expect } from "vitest";
// eslint-disable-next-line
export const expectFunctionToBeCalledNthTimesWithArgs = (fn: Function, n: number, args: any) => {
expect(fn).toHaveBeenCalledTimes(n);
expect(fn).toHaveBeenCalledWith(args);
};

2
packages/lib/test/types.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
export type CustomNextApiRequest = NextApiRequest & Request;
export type CustomNextApiResponse = NextApiResponse & Response;

View File

@ -0,0 +1,17 @@
import type { PrismaClient } from "@prisma/client";
import { beforeEach, vi } from "vitest";
import { mockDeep, mockReset } from "vitest-mock-extended";
vi.mock("@calcom/prisma", () => ({
default: prisma,
availabilityUserSelect: vi.fn(),
userSelect: vi.fn(),
}));
beforeEach(() => {
mockReset(prisma);
});
const prisma = mockDeep<PrismaClient>();
export default prisma;

View File

@ -0,0 +1,28 @@
import type { Credential } from "@prisma/client";
export const mockCredential = ({
type,
key,
userId,
appId,
teamId = 1,
}: {
type: string;
key: object;
userId: number;
appId: string;
teamId?: number;
}) => {
return {
id: 1,
type,
userId,
teamId,
key,
appId,
invalid: false,
user: {
email: "user@example.com",
},
} as Credential & { user: { email: string } };
};

View File

@ -4,6 +4,8 @@ process.env.INTEGRATION_TEST_MODE = "true";
export default defineConfig({
test: {
// clearMocks: true,
// cache: false,
coverage: {
provider: "v8",
},

2007
yarn.lock

File diff suppressed because it is too large Load Diff