Files
webmail/lib/__tests__/recurrence-overrides.test.ts
T
Stefan Hildebrandt b5e71e7b84 fix(calendar): edit a single recurring occurrence via a one-shot override patch
The "This event only" edit path built nested JMAP patch pointers
(recurrenceOverrides/<recurrenceId>/<field>). On the first edit of an
occurrence the override object doesn't exist yet, and JMAP pointers can't
create a missing intermediate object, so Stalwart returned notUpdated and
the edit failed with two error toasts.

Build the override as a single pointer recurrenceOverrides/<recurrenceId>
whose value is the occurrence's field set (dropping identity/series keys),
mirroring the working "delete this occurrence" path. Extracted as the pure,
tested helper buildRecurrenceOverridePatch.

Closes #774.
2026-08-16 21:24:52 +02:00

60 lines
2.3 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { buildRecurrenceOverridePatch } from '@/lib/recurrence-overrides';
import type { CalendarEvent } from '@/lib/jmap/types';
const RID = '2026-08-15T10:00:00';
describe('buildRecurrenceOverridePatch (#774 "This event only")', () => {
it('sets the whole override at a single pointer, not nested per-field pointers', () => {
const patch = buildRecurrenceOverridePatch(
{ title: 'Moved standup', start: '2026-08-15T11:00:00' } as Partial<CalendarEvent>,
RID,
);
// Exactly one top-level pointer, at the override object itself.
expect(Object.keys(patch)).toEqual([`recurrenceOverrides/${RID}`]);
// No nested `recurrenceOverrides/<id>/<field>` pointers (the shape Stalwart
// rejected because it can't create the missing override object).
expect(Object.keys(patch).some((k) => k.startsWith(`recurrenceOverrides/${RID}/`))).toBe(false);
});
it('carries the per-occurrence fields into the override object', () => {
const patch = buildRecurrenceOverridePatch(
{
title: 'Moved standup',
start: '2026-08-15T11:00:00',
duration: 'PT30M',
description: 'one-off',
} as Partial<CalendarEvent>,
RID,
);
expect(patch[`recurrenceOverrides/${RID}` as keyof typeof patch]).toEqual({
title: 'Moved standup',
start: '2026-08-15T11:00:00',
duration: 'PT30M',
description: 'one-off',
});
});
it('drops identity and series-level keys that must not vary per occurrence', () => {
const patch = buildRecurrenceOverridePatch(
{
id: 'evt1',
uid: 'uid-1',
'@type': 'Event',
calendarIds: { cal1: true },
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'daily' }],
recurrenceOverrides: {},
excludedRecurrenceRules: [],
title: 'Only this',
} as unknown as Partial<CalendarEvent>,
RID,
);
expect(patch[`recurrenceOverrides/${RID}` as keyof typeof patch]).toEqual({ title: 'Only this' });
});
it('produces an empty override object when nothing overridable is supplied', () => {
const patch = buildRecurrenceOverridePatch({ id: 'evt1', uid: 'uid-1' } as unknown as Partial<CalendarEvent>, RID);
expect(patch[`recurrenceOverrides/${RID}` as keyof typeof patch]).toEqual({});
});
});