a9e7b1ac82
Co-authored-by: Camille Moussu <cmoussu@linagora.com>
783 lines
21 KiB
TypeScript
783 lines
21 KiB
TypeScript
import { updateSeries } from "@/features/Events/api/updateSeries";
|
|
import {
|
|
deleteEvent,
|
|
importEventFromFile,
|
|
moveEvent,
|
|
putEvent,
|
|
searchEvent,
|
|
} from "@/features/Events/EventApi";
|
|
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
|
import { calendarEventToJCal } from "@/features/Events/utils";
|
|
import { clientConfig } from "@/features/User/oidcAuth";
|
|
import { api } from "@/utils/apiUtils";
|
|
|
|
clientConfig.url = "https://example.com";
|
|
|
|
jest.mock("@/utils/apiUtils");
|
|
|
|
const day = new Date();
|
|
|
|
const mockEvent = {
|
|
uid: "event1",
|
|
title: "Test Event",
|
|
timezone: "UTC",
|
|
calId: "667037022b752d0026472254/cal1",
|
|
URL: "/calendars/667037022b752d0026472254/667037022b752d0026472254/cal1.ics",
|
|
start: day.toISOString(),
|
|
end: day.toISOString(),
|
|
status: "PUBLIC",
|
|
organizer: { cn: "test", cal_address: "test@test.com" },
|
|
attendee: [
|
|
{
|
|
cn: "test",
|
|
cal_address: "test@test.com",
|
|
partstat: "NEEDS-ACTION",
|
|
rsvp: "TRUE",
|
|
role: "REQ-PARTICIPANT",
|
|
cutype: "INDIVIDUAL",
|
|
},
|
|
{
|
|
cn: "John",
|
|
cal_address: "john@test.com",
|
|
partstat: "NEEDS-ACTION",
|
|
rsvp: "TRUE",
|
|
role: "REQ-PARTICIPANT",
|
|
cutype: "INDIVIDUAL",
|
|
},
|
|
],
|
|
} as CalendarEvent;
|
|
|
|
describe("eventApi", () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it("putEvent sends PUT request with JCal body", async () => {
|
|
const mockResponse = { status: 201, url: "/dav/cals/test.ics" };
|
|
(api as unknown as jest.Mock).mockReturnValue(mockResponse);
|
|
const result = await putEvent(mockEvent);
|
|
const expectedResult = calendarEventToJCal(mockEvent);
|
|
expect(api).toHaveBeenCalledWith(
|
|
"dav/calendars/667037022b752d0026472254/667037022b752d0026472254/cal1.ics",
|
|
expect.objectContaining({
|
|
method: "PUT",
|
|
headers: { "content-type": "text/calendar; charset=utf-8" },
|
|
body: JSON.stringify(expectedResult),
|
|
})
|
|
);
|
|
expect(result).toBe(mockResponse);
|
|
});
|
|
|
|
it("putEvent logs when status is 201", async () => {
|
|
const consoleInfoSpy = jest.spyOn(console, "info").mockImplementation();
|
|
const mockResponse = { status: 201, url: "/dav/cals/test.ics" };
|
|
(api as unknown as jest.Mock).mockReturnValue(mockResponse);
|
|
|
|
await putEvent(mockEvent);
|
|
|
|
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
|
"Event created successfully:",
|
|
"/dav/cals/test.ics"
|
|
);
|
|
|
|
consoleInfoSpy.mockRestore();
|
|
});
|
|
|
|
test("moveEvent sends MOVE request with destination header", async () => {
|
|
const mockResponse = { status: 204 };
|
|
(api as unknown as jest.Mock).mockReturnValue({
|
|
json: jest.fn().mockResolvedValue(mockResponse),
|
|
});
|
|
const result = await moveEvent(mockEvent, "newurl.ics");
|
|
|
|
expect(api).toHaveBeenCalledWith(
|
|
"dav/calendars/667037022b752d0026472254/667037022b752d0026472254/cal1.ics",
|
|
expect.objectContaining({
|
|
method: "MOVE",
|
|
headers: {
|
|
destination: "newurl.ics",
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
it("deleteEvent sends DELETE request and returns json response", async () => {
|
|
const mockResponse = { ok: true };
|
|
(api as unknown as jest.Mock).mockReturnValue({
|
|
json: jest.fn().mockResolvedValue(mockResponse),
|
|
});
|
|
|
|
const result = await deleteEvent("/calendars/test.ics");
|
|
|
|
expect(api).toHaveBeenCalledWith("dav/calendars/test.ics", {
|
|
method: "DELETE",
|
|
});
|
|
});
|
|
|
|
it("import event file", async () => {
|
|
const mockResponse = { status: 202 };
|
|
(api.post as unknown as jest.Mock).mockReturnValue({
|
|
json: jest.fn().mockResolvedValue(mockResponse),
|
|
});
|
|
|
|
await importEventFromFile("123456789", "/calendar/calLink.json");
|
|
|
|
expect(api.post).toHaveBeenCalledWith("api/import", {
|
|
body: JSON.stringify({
|
|
fileId: "123456789",
|
|
target: "/calendar/calLink.json",
|
|
}),
|
|
});
|
|
});
|
|
|
|
test("putEvent handles byday field correctly", async () => {
|
|
const mockResponse = { status: 201, url: "/dav/cals/test.ics" };
|
|
(api as unknown as jest.Mock).mockReturnValue(mockResponse);
|
|
|
|
const eventWithByday = {
|
|
...mockEvent,
|
|
repetition: {
|
|
freq: "weekly",
|
|
interval: 1,
|
|
byday: ["MO", "WE", "FR"],
|
|
},
|
|
};
|
|
|
|
await putEvent(eventWithByday);
|
|
const expectedResult = calendarEventToJCal(eventWithByday);
|
|
|
|
expect(api).toHaveBeenCalledWith(
|
|
"dav/calendars/667037022b752d0026472254/667037022b752d0026472254/cal1.ics",
|
|
expect.objectContaining({
|
|
method: "PUT",
|
|
headers: { "content-type": "text/calendar; charset=utf-8" },
|
|
body: JSON.stringify(expectedResult),
|
|
})
|
|
);
|
|
});
|
|
|
|
test("putEvent handles null byday field correctly", async () => {
|
|
const mockResponse = { status: 201, url: "/dav/cals/test.ics" };
|
|
(api as unknown as jest.Mock).mockReturnValue(mockResponse);
|
|
|
|
const eventWithNullByday = {
|
|
...mockEvent,
|
|
repetition: {
|
|
freq: "daily",
|
|
interval: 1,
|
|
byday: null,
|
|
},
|
|
};
|
|
|
|
await putEvent(eventWithNullByday);
|
|
const expectedResult = calendarEventToJCal(eventWithNullByday);
|
|
|
|
expect(api).toHaveBeenCalledWith(
|
|
"dav/calendars/667037022b752d0026472254/667037022b752d0026472254/cal1.ics",
|
|
expect.objectContaining({
|
|
method: "PUT",
|
|
headers: { "content-type": "text/calendar; charset=utf-8" },
|
|
body: JSON.stringify(expectedResult),
|
|
})
|
|
);
|
|
});
|
|
|
|
describe("searchEvent", () => {
|
|
const mockFilters = {
|
|
searchIn: ["user1/calendar1", "user2/calendar2"],
|
|
keywords: "meeting",
|
|
organizers: ["org@example.com"],
|
|
attendees: ["part@example.com"],
|
|
};
|
|
|
|
it("should call API with correct parameters", async () => {
|
|
const mockResponse = {
|
|
_total_hits: 5,
|
|
_embedded: { events: [] },
|
|
};
|
|
|
|
(api.post as jest.Mock).mockReturnValue({
|
|
json: jest.fn().mockResolvedValue(mockResponse),
|
|
});
|
|
|
|
await searchEvent("test", mockFilters);
|
|
|
|
expect(api.post).toHaveBeenCalledWith(
|
|
"calendar/api/events/search?limit=30&offset=0",
|
|
{
|
|
body: JSON.stringify({
|
|
query: "meeting",
|
|
calendars: [
|
|
{ calendarId: "calendar1", userId: "user1" },
|
|
{ calendarId: "calendar2", userId: "user2" },
|
|
],
|
|
organizers: ["org@example.com"],
|
|
attendees: ["part@example.com"],
|
|
}),
|
|
}
|
|
);
|
|
});
|
|
|
|
it("should use query param when keywords is empty", async () => {
|
|
const mockResponse = { _total_hits: 0, _embedded: { events: [] } };
|
|
|
|
(api.post as jest.Mock).mockReturnValue({
|
|
json: jest.fn().mockResolvedValue(mockResponse),
|
|
});
|
|
|
|
await searchEvent("fallback query", {
|
|
...mockFilters,
|
|
keywords: "",
|
|
});
|
|
|
|
expect(api.post).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.objectContaining({
|
|
body: expect.stringContaining('"query":"fallback query"'),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should omit organizers when empty", async () => {
|
|
const mockResponse = { _total_hits: 0, _embedded: { events: [] } };
|
|
|
|
(api.post as jest.Mock).mockReturnValue({
|
|
json: jest.fn().mockResolvedValue(mockResponse),
|
|
});
|
|
|
|
await searchEvent("test", {
|
|
...mockFilters,
|
|
organizers: [],
|
|
});
|
|
|
|
const callArgs = (api.post as jest.Mock).mock.calls[0][1];
|
|
const body = JSON.parse(callArgs.body);
|
|
expect(body.organizers).toBeUndefined();
|
|
});
|
|
|
|
it("should omit participants when empty", async () => {
|
|
const mockResponse = { _total_hits: 0, _embedded: { events: [] } };
|
|
|
|
(api.post as jest.Mock).mockReturnValue({
|
|
json: jest.fn().mockResolvedValue(mockResponse),
|
|
});
|
|
|
|
await searchEvent("test", {
|
|
...mockFilters,
|
|
attendees: [],
|
|
});
|
|
|
|
const callArgs = (api.post as jest.Mock).mock.calls[0][1];
|
|
const body = JSON.parse(callArgs.body);
|
|
expect(body.participants).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("updateSeries", () => {
|
|
const recurringEventICS = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
PRODID:-//Test//EN
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
DTEND:20240201T110000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Old title
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T100000Z
|
|
DTEND:20240202T110000Z
|
|
SUMMARY:Old title
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`;
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
(api.get as jest.Mock).mockResolvedValue({
|
|
text: jest.fn().mockResolvedValue(recurringEventICS),
|
|
});
|
|
|
|
(api as jest.Mock).mockResolvedValue({ ok: true });
|
|
});
|
|
|
|
it("propagates summary changes to overrides", async () => {
|
|
await updateSeries(
|
|
{ ...mockEvent, title: "New title" } as any,
|
|
undefined,
|
|
false
|
|
);
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
|
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === "recurrence-id")
|
|
);
|
|
|
|
const summary = override[1].find(([k]: any) => k === "summary");
|
|
expect(summary[3]).toBe("New title");
|
|
});
|
|
|
|
it("does not remove recurrence-id from overrides", async () => {
|
|
await updateSeries(mockEvent as any, undefined, false);
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
|
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === "recurrence-id")
|
|
);
|
|
|
|
expect(override).toBeDefined();
|
|
});
|
|
|
|
it("increments SEQUENCE on overrides when metadata changes", async () => {
|
|
await updateSeries(
|
|
{ ...mockEvent, title: "Changed title" } as any,
|
|
undefined,
|
|
false
|
|
);
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
|
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === "recurrence-id")
|
|
);
|
|
|
|
const sequence = override[1].find(([k]: any) => k === "sequence");
|
|
expect(sequence[3]).toBe(2);
|
|
});
|
|
|
|
it("propagates description, location, class, and transp changes", async () => {
|
|
const icsWithMultipleFields = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
DTEND:20240201T110000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Title
|
|
DESCRIPTION:Old description
|
|
LOCATION:Old location
|
|
CLASS:PUBLIC
|
|
TRANSP:OPAQUE
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T100000Z
|
|
DTEND:20240202T110000Z
|
|
SUMMARY:Title
|
|
DESCRIPTION:Old description
|
|
LOCATION:Old location
|
|
CLASS:PUBLIC
|
|
TRANSP:OPAQUE
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`;
|
|
|
|
(api.get as jest.Mock).mockResolvedValueOnce({
|
|
text: jest.fn().mockResolvedValue(icsWithMultipleFields),
|
|
});
|
|
|
|
await updateSeries(
|
|
{
|
|
...mockEvent,
|
|
description: "New description",
|
|
location: "New location",
|
|
class: "PRIVATE",
|
|
transp: "TRANSPARENT",
|
|
} as any,
|
|
undefined,
|
|
false
|
|
);
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === "recurrence-id")
|
|
);
|
|
|
|
const description = override[1].find(([k]: any) => k === "description");
|
|
const location = override[1].find(([k]: any) => k === "location");
|
|
const classField = override[1].find(([k]: any) => k === "class");
|
|
const transp = override[1].find(([k]: any) => k === "transp");
|
|
|
|
expect(description[3]).toBe("New description");
|
|
expect(location[3]).toBe("New location");
|
|
expect(classField[3]).toBe("PRIVATE");
|
|
expect(transp[3]).toBe("TRANSPARENT");
|
|
});
|
|
|
|
it("propagates organizer changes", async () => {
|
|
const icsWithOrganizer = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Title
|
|
ORGANIZER;CN=Alice:mailto:alice@example.com
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T100000Z
|
|
SUMMARY:Title
|
|
ORGANIZER;CN=Alice:mailto:alice@example.com
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`;
|
|
|
|
(api.get as jest.Mock).mockResolvedValueOnce({
|
|
text: jest.fn().mockResolvedValue(icsWithOrganizer),
|
|
});
|
|
|
|
await updateSeries(
|
|
{
|
|
...mockEvent,
|
|
organizer: { cn: "Bob", cal_address: "bob@example.com" },
|
|
} as any,
|
|
undefined,
|
|
false
|
|
);
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === "recurrence-id")
|
|
);
|
|
|
|
const organizer = override[1].find(([k]: any) => k === "organizer");
|
|
expect(organizer[3]).toBe("mailto:bob@example.com");
|
|
expect(organizer[1].cn).toBe("Bob");
|
|
});
|
|
|
|
it("propagates attendee changes", async () => {
|
|
const icsWithAttendees = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Title
|
|
ATTENDEE;CN=Alice:mailto:alice@example.com
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T100000Z
|
|
SUMMARY:Title
|
|
ATTENDEE;CN=Alice:mailto:alice@example.com
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`;
|
|
|
|
(api.get as jest.Mock).mockResolvedValueOnce({
|
|
text: jest.fn().mockResolvedValue(icsWithAttendees),
|
|
});
|
|
|
|
await updateSeries(
|
|
{
|
|
...mockEvent,
|
|
attendee: [
|
|
{
|
|
cn: "Bob",
|
|
cal_address: "bob@example.com",
|
|
partstat: "NEEDS-ACTION",
|
|
cutype: "INDIVIDUAL",
|
|
role: "REQ-PARTICIPANT",
|
|
rsvp: "TRUE",
|
|
},
|
|
{
|
|
cn: "Charlie",
|
|
cal_address: "charlie@example.com",
|
|
partstat: "NEEDS-ACTION",
|
|
cutype: "INDIVIDUAL",
|
|
role: "REQ-PARTICIPANT",
|
|
rsvp: "TRUE",
|
|
},
|
|
],
|
|
} as any,
|
|
undefined,
|
|
false
|
|
);
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === "recurrence-id")
|
|
);
|
|
|
|
const attendees = override[1].filter(([k]: any) => k === "attendee");
|
|
expect(attendees).toHaveLength(2);
|
|
expect(attendees[0][3]).toBe("mailto:bob@example.com");
|
|
expect(attendees[1][3]).toBe("mailto:charlie@example.com");
|
|
});
|
|
|
|
it("propagates x-openpaas-videoconference changes", async () => {
|
|
const icsWithVideo = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Title
|
|
X-OPENPAAS-VIDEOCONFERENCE:https://meet.old.com
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T100000Z
|
|
SUMMARY:Title
|
|
X-OPENPAAS-VIDEOCONFERENCE:https://meet.old.com
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`;
|
|
|
|
(api.get as jest.Mock).mockResolvedValueOnce({
|
|
text: jest.fn().mockResolvedValue(icsWithVideo),
|
|
});
|
|
|
|
await updateSeries(
|
|
{
|
|
...mockEvent,
|
|
x_openpass_videoconference: "https://meet.new.com",
|
|
} as any,
|
|
undefined,
|
|
false
|
|
);
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === "recurrence-id")
|
|
);
|
|
|
|
const videoconf = override[1].find(
|
|
([k]: any) => k === "x-openpaas-videoconference"
|
|
);
|
|
expect(videoconf[3]).toBe("https://meet.new.com");
|
|
});
|
|
|
|
it("works with multiple override instances", async () => {
|
|
const icsMultipleOverrides = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
DTEND:20240201T110000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Old
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T100000Z
|
|
DTEND:20240202T110000Z
|
|
SUMMARY:Old
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240203T100000Z
|
|
DTSTART:20240203T100000Z
|
|
DTEND:20240203T110000Z
|
|
SUMMARY:Old
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`;
|
|
|
|
(api.get as jest.Mock).mockResolvedValueOnce({
|
|
text: jest.fn().mockResolvedValue(icsMultipleOverrides),
|
|
});
|
|
|
|
await updateSeries(
|
|
{ ...mockEvent, title: "New title" } as any,
|
|
undefined,
|
|
false
|
|
);
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
|
const overrides = vevents.filter(([, props]: any) =>
|
|
props.some(([k]: any) => k === "recurrence-id")
|
|
);
|
|
|
|
expect(overrides).toHaveLength(2);
|
|
overrides.forEach((override) => {
|
|
const summary = override[1].find(([k]: any) => k === "summary");
|
|
const sequence = override[1].find(([k]: any) => k === "sequence");
|
|
expect(summary[3]).toBe("New title");
|
|
expect(sequence[3]).toBe(2);
|
|
});
|
|
});
|
|
|
|
it("adds sequence field when missing and metadata changes", async () => {
|
|
const icsNoSequence = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
DTEND:20240201T110000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Old
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T100000Z
|
|
DTEND:20240202T110000Z
|
|
SUMMARY:Old
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`;
|
|
|
|
(api.get as jest.Mock).mockResolvedValueOnce({
|
|
text: jest.fn().mockResolvedValue(icsNoSequence),
|
|
});
|
|
|
|
await updateSeries(
|
|
{ ...mockEvent, title: "New title" } as any,
|
|
undefined,
|
|
false
|
|
);
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === "recurrence-id")
|
|
);
|
|
|
|
const sequence = override[1].find(([k]: any) => k === "sequence");
|
|
expect(sequence[3]).toBe(1);
|
|
});
|
|
|
|
it("preserves override-specific fields when propagating metadata", async () => {
|
|
const icsCustomFields = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
DTEND:20240201T110000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Old
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T140000Z
|
|
DTEND:20240202T150000Z
|
|
SUMMARY:Old
|
|
DESCRIPTION:custom-value
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`;
|
|
|
|
(api.get as jest.Mock).mockResolvedValueOnce({
|
|
text: jest.fn().mockResolvedValue(icsCustomFields),
|
|
});
|
|
|
|
await updateSeries(
|
|
{ ...mockEvent, title: "New title" } as any,
|
|
undefined,
|
|
false
|
|
);
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === "recurrence-id")
|
|
);
|
|
|
|
const summary = override[1].find(([k]: any) => k === "summary");
|
|
const dtstart = override[1].find(([k]: any) => k === "dtstart");
|
|
const dtend = override[1].find(([k]: any) => k === "dtend");
|
|
const description = override[1].find(([k]: any) => k === "description");
|
|
const recurrenceId = override[1].find(
|
|
([k]: any) => k === "recurrence-id"
|
|
);
|
|
// Summary should be updated
|
|
expect(summary[3]).toBe("New title");
|
|
// Override-specific time should be preserved
|
|
expect(dtstart[3]).toBe("2024-02-02T14:00:00Z");
|
|
expect(dtend[3]).toBe("2024-02-02T15:00:00Z");
|
|
// Custom field should be preserved
|
|
expect(description[3]).toBe("custom-value");
|
|
// Recurrence-id should be preserved
|
|
expect(recurrenceId[3]).toBe("2024-02-02T10:00:00Z");
|
|
});
|
|
|
|
const recurringWithAlarmICS = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Old
|
|
BEGIN:VALARM
|
|
ACTION:DISPLAY
|
|
TRIGGER:-PT15M
|
|
END:VALARM
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T100000Z
|
|
SUMMARY:Old
|
|
BEGIN:VALARM
|
|
ACTION:DISPLAY
|
|
TRIGGER:-PT15M
|
|
END:VALARM
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`;
|
|
|
|
it("replaces VALARM on overrides when alarm changes", async () => {
|
|
(api.get as jest.Mock).mockResolvedValueOnce({
|
|
text: jest.fn().mockResolvedValue(recurringWithAlarmICS),
|
|
});
|
|
|
|
await updateSeries(
|
|
{ ...mockEvent, alarm: { action: "EMAIL", trigger: "-PT30M" } } as any,
|
|
undefined,
|
|
false
|
|
);
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body);
|
|
const vevents = body[2].filter(([n]: any) => n === "vevent");
|
|
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === "recurrence-id")
|
|
);
|
|
|
|
const alarms = override[2].filter(([n]: any) => n === "valarm");
|
|
|
|
expect(alarms).toHaveLength(1);
|
|
const action = alarms[0][1].find(([k]: any) => k === "action");
|
|
const trigger = alarms[0][1].find(([k]: any) => k === "trigger");
|
|
expect(action[3]).toBe("EMAIL");
|
|
expect(trigger[3]).toBe("-PT30M");
|
|
});
|
|
});
|
|
});
|