Files
webmail/lib/__tests__/contact-wire.test.ts
T
Linus Rath 606771fee6 fix: keep contact UIDs on update and export MEMBER URIs unchanged
`contactToWire` sends a key that is present but undefined as null, so a
caller passing a card back without `uid`, `id` or `addressBookIds` (the
plugin API does) asked the server to clear them. Stalwart 1.0 refuses a
null UID and 0.16 silently deletes it; a null `id` or `addressBookIds`
fails the update on both. Update mode now leaves those keys out.

vCard export prefixed every group member with `urn:uuid:`, but keys are
already URIs for groups made in Bulwark (members are keyed by uid) and
for groups from CardDAV clients, which gave
`MEMBER:urn:uuid:urn:uuid:…`. Keys with a URI scheme are written as they
are.
2026-09-25 18:40:22 +02:00

101 lines
4.2 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('never clears id, uid or addressBookIds on update', () => {
const wire = contactToWire({
id: undefined, uid: undefined, addressBookIds: undefined, nicknames: undefined,
} as Partial<ContactCard>, 'update');
expect(wire).toEqual({ nicknames: null });
const nulls = contactToWire({ id: null, uid: null, addressBookIds: null } as unknown as Partial<ContactCard>, 'update');
expect(nulls).toEqual({});
});
it('still sends a uid or addressBookIds that has a value', () => {
expect(contactToWire({ uid: 'urn:uuid:1', addressBookIds: { ab: true } }, 'update'))
.toEqual({ uid: 'urn:uuid:1', addressBookIds: { ab: true } });
});
it('leaves calendar links alone when the update does not mention them', () => {
expect(contactToWire({ keywords: { vip: true } }, 'update')).toEqual({ keywords: { vip: true } });
});
it('leaves media out of an update that does not mention it, which keeps the photos on both versions', () => {
// An omitted property keeps its value; `media: null` would delete every
// photo, logo and sound (1.0 contact/set.rs:609-611, same on 0.16).
const wire = contactToWire({ phones: { p0: { number: '+33612345678' } } }, 'update');
expect(wire).toEqual({ phones: { p0: { number: '+33612345678' } } });
expect('media' in wire).toBe(false);
});
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',
});
});
});