* [#352] update exceptions on all update without reseting them * [#352] added tests
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { updateSeries } from "@/features/Events/api/updateSeries";
|
||||
import {
|
||||
deleteEvent,
|
||||
importEventFromFile,
|
||||
@@ -271,4 +272,511 @@ describe("eventApi", () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
|
||||
jest.mock("@/features/Events/EventApi");
|
||||
jest.mock("@/features/Calendars/CalendarApi");
|
||||
jest.mock("@/features/Events/api/updateSeries");
|
||||
|
||||
describe("EventUpdateModal - Recurring Event 'Edit All' Handling", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
|
||||
Reference in New Issue
Block a user