mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
Stalwart 1.0 returns recurrence overrides as RFC 8984 PatchObjects, with keys like participants/p1/participationStatus and nulls that remove. Expanding a series on the device spread them onto the occurrence, so a declined occurrence stayed accepted, a changed room stayed the same and the pointer keys became junk properties. Moved or resized occurrences also kept the time and length the rule gave them, and were placed in the range by their original date. - apply overrides as patches: pointer paths decoded per RFC 6901, null deletes, shorter paths first, a whole participants map completed from the series - time an occurrence from its patched start, time zone and duration, and range-check a moved occurrence by where it is now - fall back to the JSCalendar duration when the master's utcStart or utcEnd don't parse (0.16.23 returned the year 32548 for a counted series), and never keep the master's instants on another occurrence - merge a change into an existing override without keys that are prefixes of one another, and match an occurrence listed twice against the members its override patches The device expands series on 0.16.6 to 0.16.19, after a created series and when a range can't be expanded on the server. 0.16 returns whole override values, which apply the same way.
81 lines
4.0 KiB
TypeScript
81 lines
4.0 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { applyOverridePatch } from '../recurrence-instances';
|
|
import type { CalendarEvent } from '../../api/types';
|
|
|
|
// A recurrence override is an RFC 8984 PatchObject: its keys are JSON
|
|
// Pointers into the occurrence, and null removes what one points at. Stalwart
|
|
// 1.0 returns minimal patches (jmap/src/calendar_event/get.rs, conformance
|
|
// test "Figures 2-6"); 0.16 returned whole top-level values.
|
|
|
|
const series = {
|
|
id: 'b',
|
|
title: 'Team sync',
|
|
description: 'Agenda',
|
|
locations: { l1: { '@type': 'Location', name: 'Room A' } },
|
|
participants: {
|
|
org: { '@type': 'Participant', calendarAddress: 'mailto:a@example.org', participationStatus: 'accepted' },
|
|
tom: { '@type': 'Participant', calendarAddress: 'mailto:b@example.org', participationStatus: 'needs-action' },
|
|
'a/b': { '@type': 'Participant', calendarAddress: 'mailto:c@example.org', participationStatus: 'needs-action' },
|
|
'x~y': { '@type': 'Participant', calendarAddress: 'mailto:d@example.org', participationStatus: 'needs-action' },
|
|
},
|
|
} as unknown as CalendarEvent;
|
|
|
|
describe('applyOverridePatch', () => {
|
|
it('sets what a pointer names on Stalwart 1.0, leaving the rest and the series untouched', () => {
|
|
const before = JSON.stringify(series);
|
|
const occurrence = applyOverridePatch(series, { 'participants/tom/participationStatus': 'declined' });
|
|
|
|
expect(occurrence.participants?.tom.participationStatus).toBe('declined');
|
|
expect(occurrence.participants?.org).toBe(series.participants?.org);
|
|
expect(occurrence).not.toHaveProperty(['participants/tom/participationStatus']);
|
|
expect(JSON.stringify(series)).toBe(before);
|
|
});
|
|
|
|
it('reads escaped tokens: ~1 is a slash, ~0 a tilde', () => {
|
|
const occurrence = applyOverridePatch(series, {
|
|
'participants/a~1b/participationStatus': 'accepted',
|
|
'participants/x~0y/participationStatus': 'tentative',
|
|
});
|
|
expect(occurrence.participants?.['a/b'].participationStatus).toBe('accepted');
|
|
expect(occurrence.participants?.['x~y'].participationStatus).toBe('tentative');
|
|
});
|
|
|
|
it('removes what null points at, at the top and further down', () => {
|
|
const occurrence = applyOverridePatch(series, { description: null, 'participants/tom': null, 'links/none': null });
|
|
expect(occurrence).not.toHaveProperty('description');
|
|
expect(Object.keys(occurrence.participants ?? {})).toEqual(['org', 'a/b', 'x~y']);
|
|
// Nothing to remove under a member that is not there.
|
|
expect(occurrence).not.toHaveProperty('links');
|
|
});
|
|
|
|
it('creates the members a pointer needs', () => {
|
|
const occurrence = applyOverridePatch(series, { 'keywords/$flagged': true });
|
|
expect((occurrence as unknown as { keywords: unknown }).keywords).toEqual({ $flagged: true });
|
|
});
|
|
|
|
it('applies whole values the way 0.16 returns them, completing participants from the series', () => {
|
|
const occurrence = applyOverridePatch(series, {
|
|
title: 'Moved sync',
|
|
start: '2026-09-08T14:00:00',
|
|
participants: {
|
|
tom: { '@type': 'Participant', calendarAddress: 'mailto:b@example.org', participationStatus: 'declined' },
|
|
n1: { '@type': 'Participant', calendarAddress: 'mailto:a@example.org' },
|
|
},
|
|
});
|
|
expect(occurrence.title).toBe('Moved sync');
|
|
expect(occurrence.start).toBe('2026-09-08T14:00:00');
|
|
expect(occurrence.participants).toEqual({
|
|
tom: { '@type': 'Participant', calendarAddress: 'mailto:b@example.org', participationStatus: 'declined' },
|
|
org: { '@type': 'Participant', calendarAddress: 'mailto:a@example.org', participationStatus: 'accepted' },
|
|
});
|
|
});
|
|
|
|
it('applies shorter paths first, so a pointer refines a value set whole in the same patch', () => {
|
|
const occurrence = applyOverridePatch(series, {
|
|
'locations/l1/name': 'Room B',
|
|
locations: { l1: { '@type': 'Location', name: 'Room C', description: 'Second floor' } },
|
|
});
|
|
expect(occurrence.locations).toEqual({ l1: { '@type': 'Location', name: 'Room B', description: 'Second floor' } });
|
|
});
|
|
});
|