Skip to content

Commit 6a532c2

Browse files
fix: return 400 instead of 500 for invalid email in iCal generation (calcom#27217)
* 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>
1 parent 6e1070b commit 6a532c2

2 files changed

Lines changed: 97 additions & 2 deletions

File tree

packages/emails/lib/generateIcsString.test.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { describe, expect } from "vitest";
1+
import { describe, expect, vi } from "vitest";
22

33
import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants";
4+
import { ErrorCode } from "@calcom/lib/errorCodes";
5+
import { ErrorWithCode } from "@calcom/lib/errors";
46
import { buildCalendarEvent, buildPerson } from "@calcom/lib/test/builder";
57
import { buildVideoCallData } from "@calcom/lib/test/builder";
68
import type { CalendarEvent } from "@calcom/types/Calendar";
@@ -166,4 +168,90 @@ describe("generateIcsString", () => {
166168
expect(icsString).toEqual(expect.stringContaining(`LOCATION:${event.location}`));
167169
});
168170
});
171+
describe("error handling", () => {
172+
test("throws ErrorWithCode.BadRequest when ics library returns ValidationError", async () => {
173+
// Mock the ics library to return a ValidationError
174+
const ics = await import("ics");
175+
const createEventSpy = vi.spyOn(ics, "createEvent");
176+
177+
// Simulate a Yup ValidationError (which has name: "ValidationError")
178+
const validationError = {
179+
name: "ValidationError",
180+
message: "attendees[0].email must be a valid email",
181+
errors: ["attendees[0].email must be a valid email"],
182+
};
183+
184+
createEventSpy.mockReturnValueOnce({
185+
error: validationError as unknown as Error,
186+
value: undefined,
187+
});
188+
189+
const event = buildCalendarEvent({
190+
iCalSequence: 0,
191+
attendees: [buildPerson()],
192+
});
193+
194+
expect(() =>
195+
generateIcsString({
196+
event,
197+
status: "CONFIRMED",
198+
})
199+
).toThrow(ErrorWithCode);
200+
201+
try {
202+
generateIcsString({
203+
event,
204+
status: "CONFIRMED",
205+
});
206+
} catch (error) {
207+
expect(error).toBeInstanceOf(ErrorWithCode);
208+
expect((error as ErrorWithCode).code).toBe(ErrorCode.BadRequest);
209+
expect((error as ErrorWithCode).message).toBe("attendees[0].email must be a valid email");
210+
}
211+
212+
createEventSpy.mockRestore();
213+
});
214+
215+
test("re-throws non-ValidationError errors as-is", async () => {
216+
const ics = await import("ics");
217+
const createEventSpy = vi.spyOn(ics, "createEvent");
218+
219+
// Simulate a different type of error
220+
const genericError = new Error("Some other error");
221+
222+
createEventSpy.mockReturnValueOnce({
223+
error: genericError,
224+
value: undefined,
225+
});
226+
227+
const event = buildCalendarEvent({
228+
iCalSequence: 0,
229+
attendees: [buildPerson()],
230+
});
231+
232+
expect(() =>
233+
generateIcsString({
234+
event,
235+
status: "CONFIRMED",
236+
})
237+
).toThrow(genericError);
238+
239+
createEventSpy.mockRestore();
240+
});
241+
242+
test("returns ics string when there is no error", () => {
243+
const event = buildCalendarEvent({
244+
iCalSequence: 0,
245+
attendees: [buildPerson()],
246+
});
247+
248+
const icsString = generateIcsString({
249+
event,
250+
status: "CONFIRMED",
251+
});
252+
253+
expect(icsString).toBeDefined();
254+
expect(typeof icsString).toBe("string");
255+
});
256+
});
169257
});

packages/emails/lib/generateIcsString.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { RRule } from "rrule";
66
import { getRichDescription } from "@calcom/lib/CalEventParser";
77
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
88
import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants";
9+
import { ErrorCode } from "@calcom/lib/errorCodes";
10+
import { ErrorWithCode } from "@calcom/lib/errors";
911
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
1012

1113
export enum BookingAction {
@@ -55,7 +57,7 @@ const generateIcsString = ({
5557
status: EventStatus;
5658
partstat?: ParticipationStatus;
5759
t?: TFunction;
58-
}) => {
60+
}): string | undefined => {
5961
const location = getVideoCallUrlFromCalEvent(event) || event.location;
6062

6163
// Taking care of recurrence rule
@@ -111,6 +113,11 @@ const generateIcsString = ({
111113
busyStatus: "BUSY",
112114
});
113115
if (icsEvent.error) {
116+
// The ics library throws Yup ValidationError objects (not Error instances) for invalid data like invalid email formats.
117+
// Convert these to ErrorWithCode.BadRequest so they return 400 instead of falling through to a generic 500.
118+
if (icsEvent.error.name === "ValidationError") {
119+
throw new ErrorWithCode(ErrorCode.BadRequest, icsEvent.error.message);
120+
}
114121
throw icsEvent.error;
115122
}
116123
return icsEvent.value;

0 commit comments

Comments
 (0)