fix: remove an event created in a read-only calendar and say so #34

An event created on the phone in a calendar the user may only read was
meant to be removed, since the server takes no changes there, but the
claim step ignored that answer: the event stayed on the phone, was
never sent or reported, and turning sync off always warned that a
change had not reached the server.

The claim step now applies it: the row goes, the report lists it as
read-only, and nothing is left waiting.
This commit is contained in:
Linus Rath
2026-09-27 08:44:58 +02:00
parent 1a348792c0
commit 1c75da8733
2 changed files with 43 additions and 0 deletions
@@ -310,6 +310,44 @@ describe('calendar device edits through the engine', () => {
});
});
it('removes an event created in a read-only calendar and reports it', async () => {
const h = real({ accounts: 'withShared' });
const team = h.server.addCalendar('team', {
name: 'Team events',
myRights: { mayReadFreeBusy: true, mayReadItems: true, mayWriteAll: false, mayWriteOwn: false, mayUpdatePrivate: false, mayRSVP: false, mayShare: false, mayDelete: false },
});
h.prefs.calendarSelection[`team/${team}`] = true;
await h.run(CALENDAR_AUTHORITY);
const teamRow = Number(h.device.rows('calendars').find((c) => c._sync_id === `team/${team}`)!._id);
const id = h.device.user.insertEvent(teamRow, {
[Events.TITLE]: 'Mine',
[Events.DTSTART]: Date.UTC(2026, 9, 14, 13),
[Events.DTEND]: Date.UTC(2026, 9, 14, 14),
[Events.EVENT_TIMEZONE]: 'Europe/Berlin',
});
const report = await h.run(CALENDAR_AUTHORITY);
expect(report).toMatchObject({ outcome: 'ok', stats: { uploaded: { created: 0 }, skipped: 1 } });
expect(report.itemErrors).toEqual([expect.objectContaining({ ref: `row:${id}`, side: 'upload', type: 'readOnly' })]);
expect(h.events().find((e) => Number(e._id) === id)).toBeUndefined();
expect(h.server.all('CalendarEvent', 'team')).toEqual([]);
// Nothing is left waiting: the next run has nothing to say about it.
expect((await h.run(CALENDAR_AUTHORITY)).itemErrors).toEqual([]);
});
it('leaves nothing waiting for turning sync off after an event was created in a read-only calendar', async () => {
const h = real({ accounts: 'withShared' });
const team = h.server.addCalendar('team', { name: 'Team events', myRights: { mayReadItems: true, mayWriteAll: false, mayWriteOwn: false, mayRSVP: false } });
h.prefs.calendarSelection[`team/${team}`] = true;
await h.run(CALENDAR_AUTHORITY);
const teamRow = Number(h.device.rows('calendars').find((c) => c._sync_id === `team/${team}`)!._id);
h.device.user.insertEvent(teamRow, { [Events.TITLE]: 'Mine', [Events.DTSTART]: Date.UTC(2026, 9, 14, 13), [Events.DTEND]: Date.UTC(2026, 9, 14, 14), [Events.EVENT_TIMEZONE]: 'Europe/Berlin' });
expect(await h.teardown(CALENDAR_AUTHORITY)).toEqual({ pending: 0 });
expect(h.server.all('CalendarEvent', 'team')).toEqual([]);
});
it('keeps a description and location cleared on one occurrence cleared', async () => {
const h = real();
const id = h.server.addEvent('a', {
+5
View File
@@ -912,6 +912,11 @@ export abstract class ItemSync {
if (plan.kind === 'skip') {
this.env.report.stats.skipped++;
this.env.report.itemError({ ref, side: 'upload', type: plan.reason });
} else if (plan.kind === 'revert') {
// It may not be created where it was made (a read-only calendar): the row goes, and the report says so.
this.env.report.stats.skipped++;
this.env.report.itemError({ ref, side: 'upload', type: 'readOnly' });
works.push(this.planWork(acct, held, 'revert', plan.ops));
} else if (plan.kind === 'claim' || plan.kind === 'purge' || plan.kind === 'clean') {
works.push(this.planWork(acct, held, plan.kind, plan.ops));
}