From 26e50d24b00c9f8f24e9bec92ed2f51cee79fbdd Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Fri, 19 Jan 2024 13:22:28 +0000 Subject: [PATCH] test(core): add tests for events utils (#13265) This adds tests for the following utils found in `core`: - event utils - getAggregateWorkingHours - getCalendarsEvents Co-authored-by: Keith Williams --- packages/core/event.test.ts | 394 ++++++++++++++++++ .../core/getAggregateWorkingHours.test.ts | 326 +++++++++++++++ packages/core/getCalendarsEvents.test.ts | 210 ++++++++++ 3 files changed, 930 insertions(+) create mode 100644 packages/core/event.test.ts create mode 100644 packages/core/getAggregateWorkingHours.test.ts create mode 100644 packages/core/getCalendarsEvents.test.ts diff --git a/packages/core/event.test.ts b/packages/core/event.test.ts new file mode 100644 index 0000000000..4e4195b570 --- /dev/null +++ b/packages/core/event.test.ts @@ -0,0 +1,394 @@ +import type { TFunction } from "next-i18next"; +import { describe, expect, it, vi } from "vitest"; + +import * as event from "./event"; + +describe("event tests", () => { + describe("fn: getEventName", () => { + it("should return event_between_users message if no name", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + t: tFunc as TFunction, + }); + + expect(result).toBe("foo"); + + const lastCall = tFunc.mock.lastCall; + expect(lastCall).toEqual([ + "event_between_users", + { + eventName: "example event type", + host: "example host", + attendeeName: "example attendee", + interpolation: { + escapeValue: false, + }, + }, + ]); + }); + + it("should return event_between_users message if no name with team set", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + teamName: "example team name", + t: tFunc as TFunction, + }); + + expect(result).toBe("foo"); + + const lastCall = tFunc.mock.lastCall; + expect(lastCall).toEqual([ + "event_between_users", + { + eventName: "example event type", + host: "example team name", + attendeeName: "example attendee", + interpolation: { + escapeValue: false, + }, + }, + ]); + }); + + it("should return event name if no vars used", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + eventName: "example event name", + t: tFunc as TFunction, + }); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("example event name"); + }); + + it("should support templating of event type", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + eventName: "event type: {Event type title}", + t: tFunc as TFunction, + }); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("event type: example event type"); + }); + + it("should support templating of scheduler", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + eventName: "scheduler: {Scheduler}", + t: tFunc as TFunction, + }); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("scheduler: example attendee"); + }); + + it("should support templating of organiser", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + eventName: "organiser: {Organiser}", + t: tFunc as TFunction, + }); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("organiser: example host"); + }); + + it("should support templating of user", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + eventName: "user: {USER}", + t: tFunc as TFunction, + }); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("user: example attendee"); + }); + + it("should support templating of attendee", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + eventName: "attendee: {ATTENDEE}", + t: tFunc as TFunction, + }); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("attendee: example attendee"); + }); + + it("should support templating of host", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + eventName: "host: {HOST}", + t: tFunc as TFunction, + }); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("host: example host"); + }); + + it("should support templating of attendee with host/attendee", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + eventName: "host or attendee: {HOST/ATTENDEE}", + t: tFunc as TFunction, + }); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("host or attendee: example attendee"); + }); + + it("should support templating of host with host/attendee", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName( + { + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + eventName: "host or attendee: {HOST/ATTENDEE}", + t: tFunc as TFunction, + }, + true + ); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("host or attendee: example host"); + }); + + it("should support templating of custom booking fields", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + eventName: "custom field: {customField}", + bookingFields: { + customField: "example custom field", + }, + t: tFunc as TFunction, + }); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("custom field: example custom field"); + }); + + it("should support templating of custom booking fields with values", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + eventName: "custom field: {customField}", + bookingFields: { + customField: { + value: "example custom field", + }, + }, + t: tFunc as TFunction, + }); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("custom field: example custom field"); + }); + + it("should support templating of custom booking fields with non-string values", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + eventName: "custom field: {customField}", + bookingFields: { + customField: { + value: 808, + }, + }, + t: tFunc as TFunction, + }); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("custom field: 808"); + }); + + it("should support templating of custom booking fields with no value", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + eventName: "custom field: {customField}", + bookingFields: { + customField: { + value: undefined, + }, + }, + t: tFunc as TFunction, + }); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("custom field: "); + }); + + it("should support templating of location via {Location}", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + location: "attendeeInPerson", + eventName: "location: {Location}", + t: tFunc as TFunction, + }); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("location: in_person_attendee_address"); + }); + + it("should support templating of location via {LOCATION}", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + location: "attendeeInPerson", + eventName: "location: {LOCATION}", + t: tFunc as TFunction, + }); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("location: in_person_attendee_address"); + }); + + it("should strip location template if none set", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + eventName: "location: {Location}", + t: tFunc as TFunction, + }); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("location: "); + }); + + it("should strip location template if empty", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + location: "", + eventName: "location: {Location}", + t: tFunc as TFunction, + }); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("location: "); + }); + + it("should template {Location} as passed location if unknown type", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + location: "unknownNonsense", + eventName: "location: {Location}", + t: tFunc as TFunction, + }); + + expect(tFunc).not.toHaveBeenCalled(); + expect(result).toBe("location: unknownNonsense"); + }); + }); + + describe("fn: validateCustomEventName", () => { + it("should be valid when no variables used", () => { + expect(event.validateCustomEventName("foo", "error message")).toBe(true); + }); + + [ + "Event type title", + "Organiser", + "Scheduler", + "Location", + "LOCATION", + "HOST/ATTENDEE", + "HOST", + "ATTENDEE", + "USER", + ].forEach((value) => { + it(`should support {${value}} variable`, () => { + expect(event.validateCustomEventName(`foo {${value}} bar`, "error message")).toBe(true); + + expect(event.validateCustomEventName(`{${value}} bar`, "error message")).toBe(true); + + expect(event.validateCustomEventName(`foo {${value}}`, "error message")).toBe(true); + }); + }); + + it("should support booking field variables", () => { + expect( + event.validateCustomEventName("foo{customField}bar", "error message", { + customField: true, + }) + ).toBe(true); + }); + + it("should return error when invalid variable used", () => { + expect(event.validateCustomEventName("foo{nonsenseField}bar", "error message")).toBe("error message"); + }); + }); +}); diff --git a/packages/core/getAggregateWorkingHours.test.ts b/packages/core/getAggregateWorkingHours.test.ts new file mode 100644 index 0000000000..f775139437 --- /dev/null +++ b/packages/core/getAggregateWorkingHours.test.ts @@ -0,0 +1,326 @@ +import { describe, it, expect } from "vitest"; + +import type { WorkingHours } from "@calcom/types/schedule"; + +import { getAggregateWorkingHours } from "./getAggregateWorkingHours"; + +describe("getAggregateWorkingHours", () => { + it("should return all schedules if no scheduling type", () => { + const workingHours: WorkingHours[] = [ + { + days: [1, 2, 3], + startTime: 0, + endTime: 720, + }, + ]; + const result = getAggregateWorkingHours( + [ + { + busy: [], + timeZone: "Europe/London", + workingHours, + dateOverrides: [], + }, + { + busy: [], + timeZone: "Europe/London", + workingHours, + dateOverrides: [], + }, + ], + null + ); + + expect(result).toEqual([...workingHours, ...workingHours]); + }); + + it("should return all schedules if no fixed users exist", () => { + const workingHours: WorkingHours[] = [ + { + days: [1, 2, 3], + startTime: 0, + endTime: 720, + }, + ]; + const result = getAggregateWorkingHours( + [ + { + busy: [], + timeZone: "Europe/London", + workingHours, + dateOverrides: [], + }, + { + busy: [], + timeZone: "Europe/London", + workingHours, + dateOverrides: [], + }, + ], + "MANAGED" + ); + + expect(result).toEqual([...workingHours, ...workingHours]); + }); + + it("should consider all schedules fixed if collective", () => { + const workingHoursA: WorkingHours[] = [ + { + days: [1, 2], + startTime: 0, + endTime: 200, + }, + ]; + const workingHoursB: WorkingHours[] = [ + { + days: [2, 3], + startTime: 100, + endTime: 300, + }, + ]; + const result = getAggregateWorkingHours( + [ + { + busy: [], + timeZone: "Europe/London", + workingHours: workingHoursA, + dateOverrides: [], + user: { + isFixed: false, + }, + }, + { + busy: [], + timeZone: "Europe/London", + workingHours: workingHoursB, + dateOverrides: [], + user: { + isFixed: false, + }, + }, + ], + "COLLECTIVE" + ); + + expect(result).toEqual([ + { + days: [2], + startTime: 100, + endTime: 200, + }, + ]); + }); + + it("should include loose host hours", () => { + const workingHoursA: WorkingHours[] = [ + { + days: [1, 2], + startTime: 0, + endTime: 200, + }, + ]; + const workingHoursB: WorkingHours[] = [ + { + days: [2, 3], + startTime: 100, + endTime: 300, + }, + ]; + const result = getAggregateWorkingHours( + [ + { + busy: [], + timeZone: "Europe/London", + workingHours: workingHoursA, + dateOverrides: [], + user: { + isFixed: false, + }, + }, + { + busy: [], + timeZone: "Europe/London", + workingHours: workingHoursB, + dateOverrides: [], + user: { + isFixed: true, + }, + }, + ], + "GROUP" + ); + + expect(result).toEqual([ + { + days: [2], + startTime: 100, + endTime: 200, + userId: undefined, + }, + ]); + }); + + it("should return last user's hours if no intersection", () => { + const workingHoursA: WorkingHours[] = [ + { + days: [1], + startTime: 0, + endTime: 200, + }, + ]; + const workingHoursB: WorkingHours[] = [ + { + days: [2], + startTime: 100, + endTime: 300, + }, + ]; + const result = getAggregateWorkingHours( + [ + { + busy: [], + timeZone: "Europe/London", + workingHours: workingHoursA, + dateOverrides: [], + user: { + isFixed: true, + }, + }, + { + busy: [], + timeZone: "Europe/London", + workingHours: workingHoursB, + dateOverrides: [], + user: { + isFixed: true, + }, + }, + ], + "GROUP" + ); + + expect(result).toEqual([...workingHoursB]); + }); + + it("should include user IDs when not collective", () => { + const workingHoursA: WorkingHours[] = [ + { + days: [1, 2], + startTime: 0, + endTime: 200, + userId: "userA", + }, + ]; + const workingHoursB: WorkingHours[] = [ + { + days: [2, 3], + startTime: 100, + endTime: 300, + userId: "userB", + }, + ]; + const result = getAggregateWorkingHours( + [ + { + busy: [], + timeZone: "Europe/London", + workingHours: workingHoursA, + dateOverrides: [], + user: { + isFixed: true, + }, + }, + { + busy: [], + timeZone: "Europe/London", + workingHours: workingHoursB, + dateOverrides: [], + user: { + isFixed: true, + }, + }, + ], + "GROUP" + ); + + expect(result).toEqual([ + { + userId: "userA", + days: [2], + startTime: 100, + endTime: 200, + }, + ]); + }); + + it("should handle multiple intersections", () => { + const workingHoursA: WorkingHours[] = [ + { + days: [1, 2], + startTime: 0, + endTime: 200, + }, + { + days: [3, 4], + startTime: 100, + endTime: 300, + }, + ]; + const workingHoursB: WorkingHours[] = [ + { + days: [2, 3], + startTime: 100, + endTime: 300, + }, + { + days: [4, 5], + startTime: 0, + endTime: 200, + }, + ]; + const result = getAggregateWorkingHours( + [ + { + busy: [], + timeZone: "Europe/London", + workingHours: workingHoursA, + dateOverrides: [], + user: { + isFixed: true, + }, + }, + { + busy: [], + timeZone: "Europe/London", + workingHours: workingHoursB, + dateOverrides: [], + user: { + isFixed: true, + }, + }, + ], + "GROUP" + ); + + expect(result).toEqual([ + { + days: [2], + startTime: 100, + endTime: 200, + userId: undefined, + }, + { + days: [3], + startTime: 100, + endTime: 300, + userId: undefined, + }, + { + days: [4], + startTime: 100, + endTime: 200, + userId: undefined, + }, + ]); + }); +}); diff --git a/packages/core/getCalendarsEvents.test.ts b/packages/core/getCalendarsEvents.test.ts new file mode 100644 index 0000000000..5179baab80 --- /dev/null +++ b/packages/core/getCalendarsEvents.test.ts @@ -0,0 +1,210 @@ +import type { SelectedCalendar } from "@prisma/client"; +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; + +import GoogleCalendarService from "@calcom/app-store/googlecalendar/lib/CalendarService"; +import OfficeCalendarService from "@calcom/app-store/office365calendar/lib/CalendarService"; +import logger from "@calcom/lib/logger"; +import type { EventBusyDate } from "@calcom/types/Calendar"; +import type { CredentialPayload } from "@calcom/types/Credential"; + +import getCalendarsEvents from "./getCalendarsEvents"; + +describe("getCalendarsEvents", () => { + let credential: CredentialPayload; + + beforeEach(() => { + vi.spyOn(logger.constructor.prototype, "debug"); + + credential = { + id: 303, + type: "google_calendar", + key: { + scope: "example scope", + token_type: "Bearer", + expiry_date: Date.now() + 84000, + access_token: "access token", + refresh_token: "refresh token", + }, + userId: 808, + teamId: null, + appId: "exampleApp", + subscriptionId: null, + paymentStatus: null, + billingCycleStart: null, + invalid: false, + }; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should return empty array if no calendar credentials", async () => { + const result = await getCalendarsEvents( + [ + { + ...credential, + type: "totally_unrelated", + }, + ], + "2010-12-01", + "2010-12-02", + [] + ); + + expect(result).toEqual([]); + }); + + it("should return unknown calendars as empty", async () => { + const result = await getCalendarsEvents( + [ + { + ...credential, + type: "unknown_calendar", + }, + ], + "2010-12-01", + "2010-12-02", + [] + ); + + expect(result).toEqual([[]]); + }); + + it("should return unmatched calendars as empty", async () => { + const selectedCalendar: SelectedCalendar = { + credentialId: 100, + externalId: "externalId", + integration: "office365_calendar", + userId: 200, + }; + const result = await getCalendarsEvents( + [ + { + ...credential, + type: "google_calendar", + }, + ], + "2010-12-01", + "2010-12-02", + [selectedCalendar] + ); + + expect(result).toEqual([[]]); + }); + + it("should return availability from selected calendar", async () => { + const availability: EventBusyDate[] = [ + { + start: new Date(2010, 11, 2), + end: new Date(2010, 11, 3), + }, + { + start: new Date(2010, 11, 2, 4), + end: new Date(2010, 11, 2, 16), + }, + ]; + + const getAvailabilitySpy = vi + .spyOn(GoogleCalendarService.prototype, "getAvailability") + .mockReturnValue(Promise.resolve(availability)); + + const selectedCalendar: SelectedCalendar = { + credentialId: 100, + externalId: "externalId", + integration: "google_calendar", + userId: 200, + }; + const result = await getCalendarsEvents( + [ + { + ...credential, + type: "google_calendar", + }, + ], + "2010-12-01", + "2010-12-04", + [selectedCalendar] + ); + + expect(getAvailabilitySpy).toHaveBeenCalledWith("2010-12-01", "2010-12-04", [selectedCalendar]); + expect(result).toEqual([ + availability.map((av) => ({ + ...av, + source: "exampleApp", + })), + ]); + }); + + it("should return availability from multiple calendars", async () => { + const googleAvailability: EventBusyDate[] = [ + { + start: new Date(2010, 11, 2), + end: new Date(2010, 11, 3), + }, + ]; + const officeAvailability: EventBusyDate[] = [ + { + start: new Date(2010, 11, 2, 4), + end: new Date(2010, 11, 2, 16), + }, + ]; + + const getGoogleAvailabilitySpy = vi + .spyOn(GoogleCalendarService.prototype, "getAvailability") + .mockReturnValue(Promise.resolve(googleAvailability)); + const getOfficeAvailabilitySpy = vi + .spyOn(OfficeCalendarService.prototype, "getAvailability") + .mockReturnValue(Promise.resolve(officeAvailability)); + + const selectedGoogleCalendar: SelectedCalendar = { + credentialId: 100, + externalId: "externalId", + integration: "google_calendar", + userId: 200, + }; + const selectedOfficeCalendar: SelectedCalendar = { + credentialId: 100, + externalId: "externalId", + integration: "office365_calendar", + userId: 200, + }; + const result = await getCalendarsEvents( + [ + { + ...credential, + type: "google_calendar", + }, + { + ...credential, + type: "office365_calendar", + key: { + access_token: "access", + refresh_token: "refresh", + expires_in: Date.now() + 86400, + }, + }, + ], + "2010-12-01", + "2010-12-04", + [selectedGoogleCalendar, selectedOfficeCalendar] + ); + + expect(getGoogleAvailabilitySpy).toHaveBeenCalledWith("2010-12-01", "2010-12-04", [ + selectedGoogleCalendar, + ]); + expect(getOfficeAvailabilitySpy).toHaveBeenCalledWith("2010-12-01", "2010-12-04", [ + selectedOfficeCalendar, + ]); + expect(result).toEqual([ + googleAvailability.map((av) => ({ + ...av, + source: "exampleApp", + })), + officeAvailability.map((av) => ({ + ...av, + source: "exampleApp", + })), + ]); + }); +});