diff --git a/src/api/__tests__/calendar.test.ts b/src/api/__tests__/calendar.test.ts index ea16e878..9f376758 100644 --- a/src/api/__tests__/calendar.test.ts +++ b/src/api/__tests__/calendar.test.ts @@ -15,6 +15,7 @@ import { queryEvents, getEvents, createEvent, + createCalendar, updateEvent, deleteEvents, batchCreateEvents, @@ -557,6 +558,35 @@ describe('calendar operations', () => { }); }); + describe('createCalendar', () => { + it('returns the new calendar with the name and colour sent when the server echoes only its id', async () => { + mockRequest.mockResolvedValue({ + methodResponses: [ + ['Calendar/set', { created: { 'new-cal': { id: 'c9', color: null, myRights: { mayDelete: true } } } }, '0'], + ['error', { type: 'serverFail' }, '1'], + ], + }); + const cal = await createCalendar('Work', '#123456'); + expect(cal).toMatchObject({ id: 'c9', name: 'Work', color: '#123456', isVisible: true, myRights: { mayDelete: true } }); + }); + + it('takes the read-back calendar with its rights', async () => { + const rights = { mayReadItems: true, mayWriteAll: true, mayDelete: true, mayShare: true }; + mockRequest.mockResolvedValue({ + methodResponses: [ + ['Calendar/set', { created: { 'new-cal': { id: 'c9' } } }, '0'], + ['Calendar/get', { list: [{ id: 'c9', name: 'Work', color: '#123456', myRights: rights }] }, '1'], + ], + }); + const cal = await createCalendar('Work', '#123456'); + expect(cal).toMatchObject({ id: 'c9', name: 'Work', color: '#123456', myRights: rights }); + const calls = mockRequest.mock.calls[0][0]; + expect(calls[1][0]).toBe('Calendar/get'); + // The creation id: 0.16 refuses a result reference into `created`. + expect(calls[1][1].ids).toEqual(['#new-cal']); + }); + }); + describe('refusals for the participant limit (Stalwart 1.0)', () => { // 1.0 enforces maxParticipantsPerEvent on every create and update, an // answer included; 0.16 advertises the limit without enforcing it. diff --git a/src/api/__tests__/live-stalwart-calendar.test.ts b/src/api/__tests__/live-stalwart-calendar.test.ts index e5b382e8..546507cd 100644 --- a/src/api/__tests__/live-stalwart-calendar.test.ts +++ b/src/api/__tests__/live-stalwart-calendar.test.ts @@ -700,6 +700,27 @@ live('live Stalwart calendar', () => { }, 60_000); }); + describe('a new calendar', () => { + let newCalendarId = ''; + + afterAll(async () => { + if (newCalendarId) { + await raw(client, [['Calendar/set', { accountId: client.accountId, destroy: [newCalendarId], onDestroyRemoveEvents: true }, '0']]).catch(() => undefined); + } + }, 30_000); + + it('has its name, colour and rights in the store right after it is made', async () => { + await connectSingleton(); + await useCalendarStore.getState().fetchCalendars(); + const made = await useCalendarStore.getState().createCalendar(`${RUN} new`, '#0f766e'); + newCalendarId = made.id; + const inStore = useCalendarStore.getState().calendars.find((c) => c.id === made.id); + expect(inStore).toMatchObject({ name: `${RUN} new`, color: '#0f766e' }); + expect(inStore?.myRights?.mayDelete).toBe(true); + expect(inStore?.myRights?.mayWriteAll).toBe(true); + }, 30_000); + }); + describe('a calendar\'s default reminders (#1209)', () => { let defaultsCalendarId = ''; let otherCalendarId = ''; diff --git a/src/api/calendar.ts b/src/api/calendar.ts index d66e7471..a21b3ea5 100644 --- a/src/api/calendar.ts +++ b/src/api/calendar.ts @@ -1250,23 +1250,43 @@ export async function createCalendar( const accountId = targetAccountId || jmapClient.accountId; const props: Record = { name, color, isVisible: true, isSubscribed: true }; if (description?.trim()) props.description = description.trim(); + // Stalwart's create answer never carries the name sent, and 1.0 answers + // `color: null` for one it stored. A get in the same request reads the + // calendar back with its rights, as the webmail re-reads it. The creation + // id stands for the new id: 0.16 refuses a result reference into + // `created`, both take `#new-cal`. const res = await jmapClient.request( - [['Calendar/set', { - accountId, - create: { 'new-cal': props }, - }, '0']], + [ + ['Calendar/set', { + accountId, + create: { 'new-cal': props }, + }, '0'], + ['Calendar/get', { + accountId, + ids: ['#new-cal'], + properties: CALENDAR_PROPERTIES, + }, '1'], + ], USING, ); const result = methodResult<{ created?: Record; notCreated?: Record; - }>(res); + }>(res, '0'); const created = result.created?.['new-cal']; if (!created) { const err = result.notCreated?.['new-cal']; throw new Error(err?.description || err?.type || 'Failed to create calendar'); } - return created; + let stored: Calendar | undefined; + try { + stored = methodResult<{ list?: Calendar[] }>(res, '1').list?.find((c) => c.id === created.id); + } catch { + // The calendar exists; what was sent stands in for the read-back. + } + if (stored) return { ...props, ...created, ...stored } as Calendar; + const echoed = Object.fromEntries(Object.entries(created).filter(([, v]) => v !== null)); + return { ...props, ...echoed } as Calendar; } /**