fix: keep the organizer and list occurrences once in the rest of a split series

Two faults in the new series "This and following events" makes, found
on 0.16.23 (1.0 does not show them):

- The new series named its organizer only when the editor wrote one,
  which it does only for an event that had none. Created without it,
  0.16.23 makes the user who creates the event its organizer, whatever
  the owner participant says: a sharee who split the owner's meeting
  became the organizer of the rest (live: the sharee's address,
  isOrigin false), and a series organized under an alias would have
  moved to the user's main address. 1.0 takes the owner participant.
  The new series now carries the series' organizer address whenever it
  has participants.
- The overrides the new series takes over kept the old series'
  SEQUENCE while the new series starts at its own, and 0.16.23 listed
  each such occurrence twice, as changed and as the series has it
  (live). The overrides now leave the old number behind.
This commit is contained in:
Linus Rath
2026-09-26 04:47:03 +02:00
parent 27f57864cf
commit a4b5ed9c8f
2 changed files with 91 additions and 3 deletions
@@ -1173,6 +1173,85 @@ describe('the new series from this and the following occurrences', () => {
});
});
// The rest of a split series is a new event, and 0.16.23 makes whoever
// creates it the organizer unless it names one: a sharee who split the
// owner's meeting became its organizer (live: the sharee's address, isOrigin
// false), and so would the user's main address for a series organized under
// an alias. 1.0 takes the owner participant.
describe.each<[string, Version]>([['1.0', '1.0'], ['0.16', '0.16']])('the organizer of the rest of a split series on Stalwart %s', (_label, version) => {
it('is the series\' own when a sharee splits the owner\'s meeting', async () => {
const occurrence = serverOccurrence(version);
const deps = actions({ ...series(version), isOrigin: true, isShared: true, accountId: 'f' });
await saveOccurrenceEdit('this_and_future', {
event: occurrence, updates: editorUpdates(occurrence, { title: 'Planning, room 2' }), editor: editorOn(occurrence),
sendScheduling: true, userEmails: [SHAREE],
}, deps);
expect(deps.createEvent.mock.calls[0][1].organizerCalendarAddress).toBe(`mailto:${ORGANIZER}`);
});
it('is the exact address a series organized under an alias has', async () => {
const alias = 'planning@example.org';
const participants = { org: participant(version, alias, true, 'accepted'), b: participant(version, ATTENDEE, false, 'accepted') };
const master = { ...series(version), organizerCalendarAddress: `mailto:${alias}`, participants, recurrenceOverrides: null } as CalendarEvent;
const occurrence = { ...serverOccurrence(version), organizerCalendarAddress: `mailto:${alias}`, participants } as CalendarEvent;
const deps = actions(master);
await saveOccurrenceEdit('this_and_future', {
event: occurrence, updates: editorUpdates(occurrence, { title: 'Planning, room 2' }), editor: editorOn(occurrence),
sendScheduling: true, userEmails: [ORGANIZER, alias],
}, deps);
expect(deps.createEvent.mock.calls[0][1].organizerCalendarAddress).toBe(`mailto:${alias}`);
});
it('is left out with the participants when the user removed every attendee', async () => {
const occurrence = serverOccurrence(version);
const deps = actions(series(version));
await saveOccurrenceEdit('this_and_future', {
event: occurrence, updates: editorUpdates(occurrence, { participants: null, organizerCalendarAddress: null }),
editor: editorOn(occurrence, { attendees: [] }), sendScheduling: false, userEmails: [ORGANIZER],
}, deps);
const created = deps.createEvent.mock.calls[0][1];
expect(created.participants).toBeNull();
expect(created.organizerCalendarAddress).toBeNull();
});
});
// The new series of a split starts at a SEQUENCE of its own. An override
// that brought the old series' number along made 0.16.23 list its
// occurrence twice in the new series - changed, and as the series has it
// (live).
describe('the overrides a split gives the new series on Stalwart 0.16', () => {
it('leave the old series\' SEQUENCE behind', async () => {
const participants = series('0.16').participants;
const master = {
...series('0.16'), sequence: 3, start: '2026-12-07T10:00:00', utcStart: '2026-12-07T09:00:00Z', utcEnd: '2026-12-07T10:00:00Z',
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'weekly', count: 6 }],
recurrenceOverrides: {
'2027-01-04T10:00:00': { updated: '2026-11-20T09:00:00Z', start: '2027-01-04T10:00:00', duration: 'PT1H', sequence: 3, title: 'Planning, special', participants },
},
} as unknown as CalendarEvent;
const occurrence = {
...master, id: 'maaaaae', baseEventId: 'b', start: '2026-12-21T10:00:00', recurrenceId: '2026-12-21T10:00:00',
recurrenceIdTimeZone: 'Europe/Berlin', utcStart: berlinUtc('2026-12-21T10:00:00'), utcEnd: berlinUtc('2026-12-21T10:00:00', 1),
} as CalendarEvent;
const deps = actions(master);
await saveOccurrenceEdit('this_and_future', {
event: occurrence, updates: editorUpdates(occurrence), editor: editorOn(occurrence), sendScheduling: true, userEmails: [ORGANIZER],
}, deps);
const created = deps.createEvent.mock.calls[0][1];
expect(created).not.toHaveProperty('sequence');
expect(created.recurrenceOverrides!['2027-01-04T10:00:00']).toMatchObject({ title: 'Planning, special' });
expect(created.recurrenceOverrides!['2027-01-04T10:00:00']).not.toHaveProperty('sequence');
});
});
// A first change to one occurrence goes out in two steps when the user
// organizes the event (see the calendar store's updateEvent).
describe.each(occurrences)('saving one occurrence on Stalwart %s', (_label, version, make) => {
+12 -3
View File
@@ -816,7 +816,10 @@ function followingOverrides(
const target = targets.get(key);
if (!target) continue;
const opened = key === splitKey;
const { updated: _updated, ...rest } = override as OverrideEntry;
// The new series starts at a SEQUENCE of its own. The old series' number
// came along, and 0.16.23 listed that occurrence twice in the new series,
// changed and as the series has it (live, organizer and attendee alike).
const { updated: _updated, sequence: _sequence, ...rest } = override as OverrideEntry;
let entry = withParticipantsOf(rest, to.participants);
if (!entry.excluded) {
if (turned) {
@@ -911,12 +914,18 @@ export function followingSeriesData(
delete data.recurrenceId;
delete data.recurrenceOverrides;
if (data.participants) {
// The series' organizer, by the exact address it is organized under: the
// editor writes one only for an event that had none. Created without it,
// 0.16.23 made the user who created the rest its organizer, whatever the
// owner participant says - a sharee who split the owner's meeting, the
// main address for a series organized under an alias. 1.0 takes the
// owner participant (live).
data.organizerCalendarAddress ??= master.organizerCalendarAddress;
data.participants = participantsAskedAgain({
participants: data.participants,
organizerCalendarAddress: data.organizerCalendarAddress ?? master.organizerCalendarAddress,
organizerCalendarAddress: data.organizerCalendarAddress,
}) ?? data.participants;
}
const overrides = followingOverrides(
options.overrides, splitKey,
{ start: master.start, timeZone: master.timeZone, showWithoutTime: !!master.showWithoutTime, recurrenceRules: originalRules },