[DELEGATION] Edit attendance and events, delete event for someone (#553)
* [#523] added way to manage attendance for delegated calendars * [DELEGATION] #523 & #524 changed eventURL calculation for delegated events to have the rigth one * [#524] added drag and drop authorisation to delegated event * [#524] added tests * [#524] support for the permission access * [#524] no edit when delegated event is not public
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { VObjectProperty } from "@/features/Calendars/types/CalendarData";
|
||||
import { CalendarEvent, RepetitionObject } from "@/features/Events/EventsTypes";
|
||||
import {
|
||||
calendarEventToJCal,
|
||||
@@ -10,7 +12,7 @@ import {
|
||||
|
||||
describe("parseCalendarEvent", () => {
|
||||
const baseColor = { light: "#00FF00" };
|
||||
const calendarId = "calendar-123";
|
||||
const calendar = { id: "calendar-123" } as Calendar;
|
||||
|
||||
it("parses a full event with MAILTO in caps correctly", () => {
|
||||
const rawData = [
|
||||
@@ -33,12 +35,12 @@ describe("parseCalendarEvent", () => {
|
||||
["TRANSP", {}, "text", "OPAQUE"],
|
||||
["CLASS", {}, "text", "PUBLIC"],
|
||||
["DTSTAMP", {}, "date-time", "2025-07-18T08:00:00Z"],
|
||||
] as unknown as [string, Record<string, string>, string, any];
|
||||
] as VObjectProperty[];
|
||||
|
||||
const result = parseCalendarEvent(
|
||||
rawData,
|
||||
baseColor,
|
||||
calendarId,
|
||||
calendar,
|
||||
"/calendars/test.ics"
|
||||
);
|
||||
|
||||
@@ -95,12 +97,12 @@ describe("parseCalendarEvent", () => {
|
||||
["TRANSP", {}, "text", "OPAQUE"],
|
||||
["CLASS", {}, "text", "PUBLIC"],
|
||||
["DTSTAMP", {}, "date-time", "2025-07-18T08:00:00Z"],
|
||||
] as unknown as [string, Record<string, string>, string, any];
|
||||
] as VObjectProperty[];
|
||||
|
||||
const result = parseCalendarEvent(
|
||||
rawData,
|
||||
baseColor,
|
||||
calendarId,
|
||||
calendar,
|
||||
"/calendars/test.ics"
|
||||
);
|
||||
|
||||
@@ -137,16 +139,16 @@ describe("parseCalendarEvent", () => {
|
||||
});
|
||||
|
||||
it("marks allday true for DATE format", () => {
|
||||
const rawData = [
|
||||
const rawData: VObjectProperty[] = [
|
||||
["UID", {}, "text", "event-2"],
|
||||
["DTSTART", {}, "date", "2025-07-20"],
|
||||
["DTEND", {}, "date", "2025-07-21"],
|
||||
] as any;
|
||||
];
|
||||
|
||||
const result = parseCalendarEvent(
|
||||
rawData,
|
||||
baseColor,
|
||||
calendarId,
|
||||
calendar,
|
||||
"/calendars/test.ics"
|
||||
);
|
||||
|
||||
@@ -154,7 +156,7 @@ describe("parseCalendarEvent", () => {
|
||||
});
|
||||
|
||||
it("appends recurrence-id to UID if present", () => {
|
||||
const rawData: any = [
|
||||
const rawData: VObjectProperty[] = [
|
||||
["UID", {}, "text", "event-2"],
|
||||
["RECURRENCE-ID", {}, "date-time", "2025-07-18T09:00:00Z"],
|
||||
["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"],
|
||||
@@ -163,7 +165,7 @@ describe("parseCalendarEvent", () => {
|
||||
const result = parseCalendarEvent(
|
||||
rawData,
|
||||
baseColor,
|
||||
calendarId,
|
||||
calendar,
|
||||
"/calendars/test.ics"
|
||||
);
|
||||
|
||||
@@ -171,24 +173,26 @@ describe("parseCalendarEvent", () => {
|
||||
});
|
||||
|
||||
it("returns error if UID or start is missing", () => {
|
||||
const rawDataMissingUid: any = [
|
||||
const rawDataMissingUid: VObjectProperty[] = [
|
||||
["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"],
|
||||
];
|
||||
|
||||
const result = parseCalendarEvent(
|
||||
rawDataMissingUid,
|
||||
baseColor,
|
||||
calendarId,
|
||||
calendar,
|
||||
"/calendars/test.ics"
|
||||
);
|
||||
expect(result.error).toMatch(/missing crucial event param/);
|
||||
|
||||
const rawDataMissingStart: any = [["UID", {}, "text", "event-3"]];
|
||||
const rawDataMissingStart: VObjectProperty[] = [
|
||||
["UID", {}, "text", "event-3"],
|
||||
];
|
||||
|
||||
const result2 = parseCalendarEvent(
|
||||
rawDataMissingStart,
|
||||
baseColor,
|
||||
calendarId,
|
||||
calendar,
|
||||
"/calendars/test.ics"
|
||||
);
|
||||
expect(result2.error).toMatch(/missing crucial event param/);
|
||||
@@ -197,7 +201,7 @@ describe("parseCalendarEvent", () => {
|
||||
it("returns computed end when there is no end but a duration", () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date("2025-07-18T00:00:00Z"));
|
||||
|
||||
const rawDataMissing: any = [
|
||||
const rawDataMissing: VObjectProperty[] = [
|
||||
["UID", {}, "text", "event-1"],
|
||||
["DTSTART", {}, "date-time", "2025-07-18T09:00:00"],
|
||||
["duration", {}, "duration", "PT60M"],
|
||||
@@ -206,7 +210,7 @@ describe("parseCalendarEvent", () => {
|
||||
const result = parseCalendarEvent(
|
||||
rawDataMissing,
|
||||
baseColor,
|
||||
calendarId,
|
||||
calendar,
|
||||
"/calendars/test.ics"
|
||||
);
|
||||
expect(result.end).toBeDefined();
|
||||
@@ -216,7 +220,7 @@ describe("parseCalendarEvent", () => {
|
||||
});
|
||||
|
||||
it("returns 30 minutes event if end and duration are missing", () => {
|
||||
const rawDataMissing: any = [
|
||||
const rawDataMissing: VObjectProperty[] = [
|
||||
["UID", {}, "text", "event-1"],
|
||||
["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"],
|
||||
];
|
||||
@@ -224,7 +228,7 @@ describe("parseCalendarEvent", () => {
|
||||
const result = parseCalendarEvent(
|
||||
rawDataMissing,
|
||||
baseColor,
|
||||
calendarId,
|
||||
calendar,
|
||||
"/calendars/test.ics"
|
||||
);
|
||||
expect(result.end).toBeDefined();
|
||||
@@ -234,23 +238,23 @@ describe("parseCalendarEvent", () => {
|
||||
});
|
||||
|
||||
it("parses alarm block correctly", () => {
|
||||
const rawData = [
|
||||
const rawData: VObjectProperty[] = [
|
||||
["UID", {}, "text", "event-5"],
|
||||
["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"],
|
||||
] as any;
|
||||
];
|
||||
|
||||
const valarm: any = [
|
||||
const valarm = [
|
||||
"VALARM",
|
||||
[
|
||||
["ACTION", {}, "text", "DISPLAY"],
|
||||
["TRIGGER", {}, "duration", "-PT15M"],
|
||||
],
|
||||
];
|
||||
] as unknown as VObjectProperty[];
|
||||
|
||||
const result = parseCalendarEvent(
|
||||
rawData,
|
||||
baseColor,
|
||||
calendarId,
|
||||
calendar,
|
||||
"/calendars/test.ics",
|
||||
valarm
|
||||
);
|
||||
@@ -266,12 +270,12 @@ describe("parseCalendarEvent", () => {
|
||||
["DTEND", {}, "date-time", "2025-07-18T10:00:00Z"],
|
||||
["ATTENDEE", {}, "cal-address", "john@example.com"],
|
||||
["ORGANIZER", {}, "cal-address", "jane@example.com"],
|
||||
] as unknown as [string, Record<string, string>, string, any];
|
||||
] as VObjectProperty[];
|
||||
|
||||
const result = parseCalendarEvent(
|
||||
rawData,
|
||||
baseColor,
|
||||
calendarId,
|
||||
calendar,
|
||||
"/calendars/test.ics"
|
||||
);
|
||||
|
||||
@@ -297,12 +301,12 @@ describe("parseCalendarEvent", () => {
|
||||
["UID", {}, "text", "event-tz"],
|
||||
["DTSTART", { tzid: "Asia/Bangkok" }, "date-time", "2025-07-18T09:00:00"],
|
||||
["DTEND", { tzid: "Asia/Bangkok" }, "date-time", "2025-07-18T10:00:00"],
|
||||
] as unknown as [string, Record<string, string>, string, any];
|
||||
] as VObjectProperty[];
|
||||
|
||||
const result = parseCalendarEvent(
|
||||
rawData,
|
||||
baseColor,
|
||||
calendarId,
|
||||
calendar,
|
||||
"/calendars/test.ics"
|
||||
);
|
||||
|
||||
@@ -319,12 +323,12 @@ describe("parseCalendarEvent", () => {
|
||||
["UID", {}, "text", "event-utc"],
|
||||
["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"],
|
||||
["DTEND", {}, "date-time", "2025-07-18T10:00:00Z"],
|
||||
] as unknown as [string, Record<string, string>, string, any];
|
||||
] as VObjectProperty[];
|
||||
|
||||
const result = parseCalendarEvent(
|
||||
rawData,
|
||||
baseColor,
|
||||
calendarId,
|
||||
calendar,
|
||||
"/calendars/test.ics"
|
||||
);
|
||||
|
||||
@@ -337,12 +341,12 @@ describe("parseCalendarEvent", () => {
|
||||
["UID", {}, "text", "event-allday"],
|
||||
["DTSTART", {}, "date", "2025-07-18"],
|
||||
["DTEND", {}, "date", "2025-07-19"],
|
||||
] as unknown as [string, Record<string, string>, string, any];
|
||||
] as VObjectProperty[];
|
||||
|
||||
const result = parseCalendarEvent(
|
||||
rawData,
|
||||
baseColor,
|
||||
calendarId,
|
||||
calendar,
|
||||
"/calendars/test.ics"
|
||||
);
|
||||
|
||||
@@ -391,8 +395,8 @@ describe("calendarEventToJCal", () => {
|
||||
expect(vevent[0]).toBe("vevent");
|
||||
|
||||
const props = vevent[1];
|
||||
const dtstart = props.find((p: any[]) => p[0] === "dtstart");
|
||||
const dtend = props.find((p: any[]) => p[0] === "dtend");
|
||||
const dtstart = props.find((p: VObjectProperty) => p[0] === "dtstart");
|
||||
const dtend = props.find((p: VObjectProperty) => p[0] === "dtend");
|
||||
|
||||
expect(dtstart).toBeDefined();
|
||||
expect(dtstart[1]).toEqual({ tzid: "Europe/Paris" });
|
||||
@@ -474,7 +478,7 @@ describe("calendarEventToJCal", () => {
|
||||
});
|
||||
|
||||
it("converts with alarm included", () => {
|
||||
const mockEvent: any = {
|
||||
const mockEvent = {
|
||||
uid: "event-10",
|
||||
title: "Alarm Event",
|
||||
start: "2025-07-20T07:00:00.000Z",
|
||||
@@ -483,7 +487,7 @@ describe("calendarEventToJCal", () => {
|
||||
allday: false,
|
||||
alarm: { trigger: "-PT10M", action: "DISPLAY" },
|
||||
attendee: [],
|
||||
};
|
||||
} as unknown as CalendarEvent;
|
||||
|
||||
const result = calendarEventToJCal(mockEvent, "owner@example.com");
|
||||
const vevent = result[2][0];
|
||||
@@ -498,7 +502,7 @@ describe("calendarEventToJCal", () => {
|
||||
});
|
||||
|
||||
it("converts all-day events", () => {
|
||||
const mockEvent: any = {
|
||||
const mockEvent = {
|
||||
uid: "event-11",
|
||||
title: "All Day",
|
||||
start: "2025-07-21T00:00:00.000Z",
|
||||
@@ -506,7 +510,7 @@ describe("calendarEventToJCal", () => {
|
||||
timezone: "Europe/Paris",
|
||||
allday: true,
|
||||
attendee: [],
|
||||
};
|
||||
} as unknown as CalendarEvent;
|
||||
|
||||
const result = calendarEventToJCal(mockEvent);
|
||||
const veventProps = result[2][0][1];
|
||||
@@ -843,7 +847,9 @@ describe("calendarEventToJCal", () => {
|
||||
const [vevent] = result[2];
|
||||
const props = vevent[1];
|
||||
|
||||
const sequenceProp = props.find((p: any[]) => p[0] === "sequence");
|
||||
const sequenceProp = props.find(
|
||||
(p: VObjectProperty) => p[0] === "sequence"
|
||||
);
|
||||
expect(sequenceProp).toBeDefined();
|
||||
expect(sequenceProp).toEqual(["sequence", {}, "integer", 3]);
|
||||
});
|
||||
@@ -864,7 +870,9 @@ describe("calendarEventToJCal", () => {
|
||||
const [vevent] = result[2];
|
||||
const props = vevent[1];
|
||||
|
||||
const sequenceProp = props.find((p: any[]) => p[0] === "sequence");
|
||||
const sequenceProp = props.find(
|
||||
(p: VObjectProperty) => p[0] === "sequence"
|
||||
);
|
||||
expect(sequenceProp).toBeDefined();
|
||||
expect(sequenceProp).toEqual(["sequence", {}, "integer", 1]);
|
||||
});
|
||||
@@ -887,7 +895,9 @@ describe("calendarEventToJCal", () => {
|
||||
const [vevent] = result[2];
|
||||
const props = vevent[1];
|
||||
|
||||
const sequenceProp = props.find((p: any[]) => p[0] === "sequence");
|
||||
const sequenceProp = props.find(
|
||||
(p: VObjectProperty) => p[0] === "sequence"
|
||||
);
|
||||
expect(sequenceProp).toBeDefined();
|
||||
expect(sequenceProp).toEqual(["sequence", {}, "integer", 2]);
|
||||
});
|
||||
@@ -910,18 +920,22 @@ describe("calendarEventToJCal", () => {
|
||||
const [vevent] = result[2];
|
||||
const props = vevent[1];
|
||||
|
||||
const sequenceProp = props.find((p: any[]) => p[0] === "sequence");
|
||||
const sequenceProp = props.find(
|
||||
(p: VObjectProperty) => p[0] === "sequence"
|
||||
);
|
||||
expect(sequenceProp).toBeDefined();
|
||||
expect(sequenceProp).toEqual(["sequence", {}, "integer", 5]);
|
||||
|
||||
// Also verify recurrence-id is present
|
||||
const recurrenceIdProp = props.find((p: any[]) => p[0] === "recurrence-id");
|
||||
const recurrenceIdProp = props.find(
|
||||
(p: VObjectProperty) => p[0] === "recurrence-id"
|
||||
);
|
||||
expect(recurrenceIdProp).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("combineMasterDateWithFormTime", () => {
|
||||
const mockFormatDateTime = (iso: string, tz: string) => {
|
||||
const mockFormatDateTime = (iso: string) => {
|
||||
const date = new Date(iso);
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
@@ -1127,12 +1141,6 @@ describe("normalizeTimezone", () => {
|
||||
|
||||
describe("detectRecurringEventChanges", () => {
|
||||
const mockResolveTimezone = (tz: string) => tz;
|
||||
const mockFormatDateTime = (iso: string, tz: string) => {
|
||||
const date = new Date(iso);
|
||||
const hour = String(date.getUTCHours()).padStart(2, "0");
|
||||
const minute = String(date.getUTCMinutes()).padStart(2, "0");
|
||||
return `2025-10-14T${hour}:${minute}`;
|
||||
};
|
||||
|
||||
it("should detect when only time changes", () => {
|
||||
const oldEvent = {
|
||||
@@ -1346,3 +1354,69 @@ describe("detectRecurringEventChanges", () => {
|
||||
expect(result.repetitionRulesChanged).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseCalendarEvent - delegated calendar URL handling", () => {
|
||||
const baseColor = { light: "#00FF00" };
|
||||
const rawData: VObjectProperty[] = [
|
||||
["uid", {}, "text", "event-uid-123"],
|
||||
["dtstart", {}, "date-time", "20240115T100000Z"],
|
||||
["dtend", {}, "date-time", "20240115T110000Z"],
|
||||
["summary", {}, "text", "Test Event"],
|
||||
];
|
||||
|
||||
const eventURL = "/calendars/user2/cal1/event-uid-123.ics";
|
||||
|
||||
describe("non-delegated calendar", () => {
|
||||
const calendar = {
|
||||
id: "user1/cal1",
|
||||
delegated: false,
|
||||
link: "/calendars/user1/cal1.json",
|
||||
} as Calendar;
|
||||
|
||||
it("uses the raw eventURL as-is", () => {
|
||||
const result = parseCalendarEvent(rawData, baseColor, calendar, eventURL);
|
||||
expect(result.URL).toBe(eventURL);
|
||||
});
|
||||
});
|
||||
|
||||
describe("delegated calendar", () => {
|
||||
const calendar = {
|
||||
id: "user2/cal1",
|
||||
delegated: true,
|
||||
link: "/calendars/user2/cal1.json",
|
||||
owner: { emails: ["owner@example.com"] },
|
||||
} as unknown as Calendar;
|
||||
|
||||
it("rewrites URL using calendar link base path", () => {
|
||||
const result = parseCalendarEvent(rawData, baseColor, calendar, eventURL);
|
||||
expect(result.URL).toBe("/calendars/user2/cal1/event-uid-123.ics");
|
||||
});
|
||||
|
||||
it("preserves the event filename from the original URL", () => {
|
||||
const differentSourceURL = "/calendars/someother/path/event-uid-123.ics";
|
||||
const result = parseCalendarEvent(
|
||||
rawData,
|
||||
baseColor,
|
||||
calendar,
|
||||
differentSourceURL
|
||||
);
|
||||
// filename is extracted and rebased onto calendar link
|
||||
expect(result.URL).toMatch(
|
||||
/\/calendars\/user2\/cal1\/event-uid-123\.ics$/
|
||||
);
|
||||
});
|
||||
|
||||
it("still sets calId from calendar.id", () => {
|
||||
const result = parseCalendarEvent(rawData, baseColor, calendar, eventURL);
|
||||
expect(result.calId).toBe("user2/cal1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("empty calendar object (used in deleteEventInstance)", () => {
|
||||
it("does not throw and sets URL to empty string", () => {
|
||||
const result = parseCalendarEvent(rawData, baseColor, {} as Calendar, "");
|
||||
expect(result.URL).toBe("");
|
||||
expect(result.calId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user