mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
Stalwart 1.0 returns recurrence overrides as RFC 8984 PatchObjects, with keys like participants/p1/participationStatus and nulls that remove. Browser expansion spread them onto the occurrence, so a declined occurrence stayed accepted and the pointer keys became junk properties. Moved or resized occurrences also kept the time and length the rule gave them and were placed by their original date. Overrides are now applied as patches, occurrence times come from the patched start, time zone and duration, and new changes merge into an existing override without keys that are prefixes of one another.
81 lines
3.9 KiB
TypeScript
81 lines
3.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { applyOverridePatch } from '../recurrence-instances';
|
|
import type { CalendarEvent } from '@/lib/jmap/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.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' } });
|
|
});
|
|
});
|