* fix: return 400 instead of 500 for invalid email in iCal generation Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * test: add unit tests for error handling in generateIcsString Co-Authored-By: benny@cal.com <sldisek783@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
258 lines
7.7 KiB
TypeScript
258 lines
7.7 KiB
TypeScript
import { describe, expect, vi } from "vitest";
|
|
|
|
import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants";
|
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
|
import { ErrorWithCode } from "@calcom/lib/errors";
|
|
import { buildCalendarEvent, buildPerson } from "@calcom/lib/test/builder";
|
|
import { buildVideoCallData } from "@calcom/lib/test/builder";
|
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
|
import { test } from "@calcom/testing/lib/fixtures/fixtures";
|
|
|
|
import generateIcsString from "./generateIcsString";
|
|
|
|
const assertHasIcsString = (icsString: string | undefined) => {
|
|
if (!icsString) throw new Error("icsString is undefined");
|
|
|
|
expect(icsString).toBeDefined();
|
|
|
|
return icsString;
|
|
};
|
|
|
|
const testIcsStringContains = ({
|
|
icsString,
|
|
event,
|
|
status,
|
|
}: {
|
|
icsString: string;
|
|
event: CalendarEvent;
|
|
status: string;
|
|
}) => {
|
|
const DTSTART = `${event.startTime.split(".")[0].split(":").slice(0, 2).join(":").replace(/[-:]/g, "")}00Z`;
|
|
const DTEND = `${event.endTime.split(".")[0].split(":").slice(0, 2).join(":").replace(/[-:]/g, "")}00Z`;
|
|
const isOrganizerExempt = ORGANIZER_EMAIL_EXEMPT_DOMAINS?.split(",")
|
|
.filter((domain) => domain.trim() !== "")
|
|
.some((domain) => event.organizer.email.toLowerCase().endsWith(domain.toLowerCase()));
|
|
expect(icsString).toEqual(expect.stringContaining(`UID:${event.iCalUID}`));
|
|
// Sometimes the deeply equal stringMatching error appears. Don't want to add flakey tests
|
|
// expect(icsString).toEqual(expect.stringContaining(`SUMMARY:${event.title}`));
|
|
expect(icsString).toEqual(expect.stringContaining(`DTSTART:${DTSTART}`));
|
|
if (event.hideOrganizerEmail && !isOrganizerExempt) {
|
|
expect(icsString).toEqual(expect.stringContaining(`ORGANIZER;CN=${event.organizer.name}`));
|
|
expect(icsString).not.toEqual(expect.stringContaining(`mailto:${event.organizer.email}`));
|
|
} else {
|
|
expect(icsString).toEqual(
|
|
expect.stringContaining(`ORGANIZER;CN=${event.organizer.name}:mailto:${event.organizer.email}`)
|
|
);
|
|
}
|
|
expect(icsString).toEqual(expect.stringContaining(`DTEND:${DTEND}`));
|
|
expect(icsString).toEqual(expect.stringContaining(`STATUS:${status}`));
|
|
// Getting an error expected icsString to deeply equal stringMatching
|
|
// for (const attendee of event.attendees) {
|
|
// expect(icsString).toEqual(
|
|
// expect.stringMatching(
|
|
// `RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=${attendee.name}:mailto:${attendee.email}`
|
|
// )
|
|
// );
|
|
// }
|
|
};
|
|
|
|
describe("generateIcsString", () => {
|
|
describe("booking actions", () => {
|
|
test("when bookingAction is Create", () => {
|
|
const event = buildCalendarEvent({
|
|
iCalSequence: 0,
|
|
attendees: [buildPerson()],
|
|
});
|
|
|
|
const status = "CONFIRMED";
|
|
|
|
const icsString = generateIcsString({
|
|
event: event,
|
|
status,
|
|
});
|
|
|
|
const assertedIcsString = assertHasIcsString(icsString);
|
|
|
|
testIcsStringContains({ icsString: assertedIcsString, event, status });
|
|
});
|
|
test("when bookingAction is Cancel", () => {
|
|
const event = buildCalendarEvent({
|
|
iCalSequence: 0,
|
|
attendees: [buildPerson()],
|
|
});
|
|
const status = "CANCELLED";
|
|
|
|
const icsString = generateIcsString({
|
|
event: event,
|
|
status,
|
|
});
|
|
|
|
const assertedIcsString = assertHasIcsString(icsString);
|
|
|
|
testIcsStringContains({ icsString: assertedIcsString, event, status });
|
|
});
|
|
test("when bookingAction is Reschedule", () => {
|
|
const event = buildCalendarEvent({
|
|
iCalSequence: 0,
|
|
attendees: [buildPerson()],
|
|
});
|
|
const status = "CONFIRMED";
|
|
|
|
const icsString = generateIcsString({
|
|
event: event,
|
|
status,
|
|
});
|
|
|
|
const assertedIcsString = assertHasIcsString(icsString);
|
|
|
|
testIcsStringContains({ icsString: assertedIcsString, event, status });
|
|
});
|
|
test("when bookingAction is RequestReschedule", () => {
|
|
const event = buildCalendarEvent({
|
|
iCalSequence: 0,
|
|
attendees: [buildPerson()],
|
|
});
|
|
const status = "CANCELLED";
|
|
|
|
const icsString = generateIcsString({
|
|
event: event,
|
|
status,
|
|
});
|
|
|
|
const assertedIcsString = assertHasIcsString(icsString);
|
|
|
|
testIcsStringContains({ icsString: assertedIcsString, event, status });
|
|
});
|
|
});
|
|
describe("set location", () => {
|
|
test("Location is a video link", () => {
|
|
const videoCallData = buildVideoCallData();
|
|
const event = buildCalendarEvent(
|
|
{
|
|
iCalSequence: 0,
|
|
attendees: [buildPerson()],
|
|
videoCallData,
|
|
},
|
|
true
|
|
);
|
|
const status = "CANCELLED";
|
|
|
|
const icsString = generateIcsString({
|
|
event: event,
|
|
status,
|
|
});
|
|
|
|
assertHasIcsString(icsString);
|
|
|
|
expect(icsString).toEqual(expect.stringContaining(`LOCATION:${videoCallData.url}`));
|
|
});
|
|
// Could be custom link, address, or phone number
|
|
test("Location is a string", () => {
|
|
const event = buildCalendarEvent(
|
|
{
|
|
iCalSequence: 0,
|
|
attendees: [buildPerson()],
|
|
location: "+1234567890",
|
|
},
|
|
true
|
|
);
|
|
const status = "CANCELLED";
|
|
|
|
const icsString = generateIcsString({
|
|
event: event,
|
|
status,
|
|
});
|
|
|
|
assertHasIcsString(icsString);
|
|
|
|
expect(icsString).toEqual(expect.stringContaining(`LOCATION:${event.location}`));
|
|
});
|
|
});
|
|
describe("error handling", () => {
|
|
test("throws ErrorWithCode.BadRequest when ics library returns ValidationError", async () => {
|
|
// Mock the ics library to return a ValidationError
|
|
const ics = await import("ics");
|
|
const createEventSpy = vi.spyOn(ics, "createEvent");
|
|
|
|
// Simulate a Yup ValidationError (which has name: "ValidationError")
|
|
const validationError = {
|
|
name: "ValidationError",
|
|
message: "attendees[0].email must be a valid email",
|
|
errors: ["attendees[0].email must be a valid email"],
|
|
};
|
|
|
|
createEventSpy.mockReturnValueOnce({
|
|
error: validationError as unknown as Error,
|
|
value: undefined,
|
|
});
|
|
|
|
const event = buildCalendarEvent({
|
|
iCalSequence: 0,
|
|
attendees: [buildPerson()],
|
|
});
|
|
|
|
expect(() =>
|
|
generateIcsString({
|
|
event,
|
|
status: "CONFIRMED",
|
|
})
|
|
).toThrow(ErrorWithCode);
|
|
|
|
try {
|
|
generateIcsString({
|
|
event,
|
|
status: "CONFIRMED",
|
|
});
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(ErrorWithCode);
|
|
expect((error as ErrorWithCode).code).toBe(ErrorCode.BadRequest);
|
|
expect((error as ErrorWithCode).message).toBe("attendees[0].email must be a valid email");
|
|
}
|
|
|
|
createEventSpy.mockRestore();
|
|
});
|
|
|
|
test("re-throws non-ValidationError errors as-is", async () => {
|
|
const ics = await import("ics");
|
|
const createEventSpy = vi.spyOn(ics, "createEvent");
|
|
|
|
// Simulate a different type of error
|
|
const genericError = new Error("Some other error");
|
|
|
|
createEventSpy.mockReturnValueOnce({
|
|
error: genericError,
|
|
value: undefined,
|
|
});
|
|
|
|
const event = buildCalendarEvent({
|
|
iCalSequence: 0,
|
|
attendees: [buildPerson()],
|
|
});
|
|
|
|
expect(() =>
|
|
generateIcsString({
|
|
event,
|
|
status: "CONFIRMED",
|
|
})
|
|
).toThrow(genericError);
|
|
|
|
createEventSpy.mockRestore();
|
|
});
|
|
|
|
test("returns ics string when there is no error", () => {
|
|
const event = buildCalendarEvent({
|
|
iCalSequence: 0,
|
|
attendees: [buildPerson()],
|
|
});
|
|
|
|
const icsString = generateIcsString({
|
|
event,
|
|
status: "CONFIRMED",
|
|
});
|
|
|
|
expect(icsString).toBeDefined();
|
|
expect(typeof icsString).toBe("string");
|
|
});
|
|
});
|
|
});
|