Files
webmail/lib/__tests__/contact-wire.test.ts
T
Linus Rath 73d971dbf9 fix: save contacts with calendar links and cleared fields correctly
Stalwart rejects a whole ContactCard over one property it does not
know, and several reached it:

- the form sent `calendarUri`, `schedulingUri` and `freeBusyUri`,
  so any contact with a calendar, scheduling or free/busy link failed
  to save ("Invalid property.")
- vCard import sent `source`, and Pro mode left `localAccountId` on
  the card
- vCard import wrote addresses as flat `street`/`locality` fields
  instead of RFC 9553 `components`

The card now passes through `lib/jmap/contact-wire.ts` on its way to
the server: client-only fields are dropped, the links are written as
`calendars` / `schedulingAddresses` (and SOURCE as a `directories`
entry), flat addresses become components, and reads map the links back
for the form and detail view.

Clearing a field (nickname, notes, a link) never reached the server:
the form set it to `undefined`, which JSON drops, and an omitted
property keeps its value. Updates now send `null` for cleared keys.

Deleting an address book that still held contacts always failed with
`addressBookHasContents`. The delete now passes
`onDestroyRemoveContents`, which the confirm dialog already promised.
2026-09-22 20:37:16 +02:00

78 lines
3.1 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { contactFromWire, contactToWire } from '../jmap/contact-wire';
import type { ContactCard } from '../jmap/types';
describe('contactToWire', () => {
it('drops client-only fields, which ContactCard/set rejects', () => {
const wire = contactToWire({
name: { full: 'A' },
originalId: 'x', accountId: 'a', accountName: 'n', isShared: true, localAccountId: 'l',
}, 'create');
expect(wire).toEqual({ name: { full: 'A' } });
});
it('writes the calendar URIs as RFC 9553 calendars and schedulingAddresses', () => {
const wire = contactToWire({
calendarUri: 'https://x.test/cal',
freeBusyUri: 'https://x.test/fb',
schedulingUri: 'mailto:a@x.test',
}, 'create');
expect(wire).toEqual({
calendars: {
cal: { '@type': 'Calendar', kind: 'calendar', uri: 'https://x.test/cal' },
fb: { '@type': 'Calendar', kind: 'freeBusy', uri: 'https://x.test/fb' },
},
schedulingAddresses: { sched: { '@type': 'SchedulingAddress', uri: 'mailto:a@x.test' } },
});
});
it('sends null for cleared fields on update, but omits them on create', () => {
const cleared: Partial<ContactCard> = { nicknames: undefined, calendarUri: undefined, schedulingUri: undefined };
expect(contactToWire(cleared, 'update')).toEqual({ nicknames: null, calendars: null, schedulingAddresses: null });
expect(contactToWire(cleared, 'create')).toEqual({});
});
it('leaves calendar links alone when the update does not mention them', () => {
expect(contactToWire({ keywords: { vip: true } }, 'update')).toEqual({ keywords: { vip: true } });
});
it('converts flat vCard addresses to components and SOURCE to a directory entry', () => {
const wire = contactToWire({
addresses: { a0: { street: 'Main St 1', locality: 'Town', country: 'DE', fullAddress: 'Main St 1, Town', contexts: { work: true } } },
source: 'https://x.test/card.vcf',
}, 'create');
expect(wire.addresses).toEqual({
a0: {
components: [
{ kind: 'name', value: 'Main St 1' },
{ kind: 'locality', value: 'Town' },
{ kind: 'country', value: 'DE' },
],
isOrdered: true,
defaultSeparator: ', ',
full: 'Main St 1, Town',
contexts: { work: true },
},
});
expect(wire.directories).toEqual({ source: { '@type': 'Directory', kind: 'entry', uri: 'https://x.test/card.vcf' } });
});
});
describe('contactFromWire', () => {
it('fills the flat URI fields from the server card', () => {
const card = contactFromWire({
id: 'c1',
addressBookIds: {},
calendars: { k: { kind: 'calendar', uri: 'https://x.test/cal' }, f: { kind: 'freeBusy', uri: 'https://x.test/fb' } },
schedulingAddresses: { s: { uri: 'mailto:a@x.test' } },
directories: { d: { kind: 'entry', uri: 'https://x.test/card.vcf' } },
});
expect(card).toMatchObject({
calendarUri: 'https://x.test/cal',
freeBusyUri: 'https://x.test/fb',
schedulingUri: 'mailto:a@x.test',
source: 'https://x.test/card.vcf',
});
});
});