1 import { createContactPropertyUid } from '@proton/shared/lib/contacts/properties';
2 import { parseToVCard, serialize } from '@proton/shared/lib/contacts/vcard';
3 import type { VCardContact } from '@proton/shared/lib/interfaces/contacts/VCard';
5 describe('serialize', () => {
6 describe('produces the expected vcf', () => {
7 it('when there are all supported standard properties', () => {
8 const contact: VCardContact = {
9 version: { field: 'version', value: '4.0', uid: createContactPropertyUid() },
13 familyNames: ['Messi'],
14 givenNames: ['Lionel'],
15 additionalNames: ['Andrés'],
16 honorificPrefixes: [''],
17 honorificSuffixes: [''],
19 uid: createContactPropertyUid(),
21 fn: [{ field: 'fn', value: 'Leo Messi', uid: createContactPropertyUid() }],
22 title: [{ field: 'title', value: 'Football ambassador', uid: createContactPropertyUid() }],
24 { field: 'nickname', value: 'La Pulga', uid: createContactPropertyUid() },
25 { field: 'nickname', value: 'The Messiah', uid: createContactPropertyUid() },
30 value: 'leo@barca.cat',
31 params: { type: 'WORK', pref: '1' },
33 uid: createContactPropertyUid(),
42 streetAddress: "Carrer d'Elisabeth Eidenbenz",
43 locality: 'Barcelona',
48 uid: createContactPropertyUid(),
54 value: 'tel:+1-555-555-5555;ext=5555',
59 uid: createContactPropertyUid(),
65 date: new Date(Date.UTC(1999, 5, 9)),
67 uid: createContactPropertyUid(),
74 uid: createContactPropertyUid(),
79 value: 'xmpp:leo@barca.cat',
80 params: { pref: '1' },
81 uid: createContactPropertyUid(),
84 photo: [{ field: 'photo', value: 'bcspevahwseowngfs23dkl1lsxslfj', uid: createContactPropertyUid() }],
88 params: { type: 'home', pref: '1' },
90 uid: createContactPropertyUid(),
94 params: { type: 'work', pref: '1' },
96 uid: createContactPropertyUid(),
100 params: { type: 'work', pref: '2' },
102 uid: createContactPropertyUid(),
105 geo: [{ field: 'geo', value: 'geo:37.386013,-122.082932', uid: createContactPropertyUid() }],
106 role: [{ field: 'role', value: 'Fake 9', uid: createContactPropertyUid() }],
110 value: 'https://static.messi.com/wp-content/uploads/2019/10/messi-logo-01.png',
111 uid: createContactPropertyUid(),
117 value: { organizationalName: 'Barcelona FC', organizationalUnitNames: ['Training-center'] },
118 uid: createContactPropertyUid(),
122 value: { organizationalName: 'Leo Messi Foundation' },
123 uid: createContactPropertyUid(),
126 member: [{ field: 'member', value: 'tel:+1-418-555-5555', uid: createContactPropertyUid() }],
130 params: { type: 'spouse' },
131 value: 'Antonela Roccuzzo',
132 uid: createContactPropertyUid(),
136 params: { type: 'child' },
137 value: 'Matteo Messi Roccuzzo',
138 uid: createContactPropertyUid(),
142 params: { type: 'child' },
143 value: 'Thiago Messi Roccuzzo',
144 uid: createContactPropertyUid(),
148 params: { type: 'child' },
149 value: 'Ciro Messi Roccuzzo',
150 uid: createContactPropertyUid(),
153 note: [{ field: 'note', value: 'Way better than Cristiano Ronaldo!', uid: createContactPropertyUid() }],
154 url: [{ field: 'url', value: 'https://messi.com/', uid: createContactPropertyUid() }],
158 value: 'Tax-evading players',
159 uid: createContactPropertyUid(),
163 value: 'Best players of all time',
164 uid: createContactPropertyUid(),
172 `N:Messi;Lionel;Andrés;;`,
173 `TITLE:Football ambassador`,
175 `NICKNAME:The Messiah`,
176 `ITEM1.EMAIL;TYPE=WORK;PREF=1:leo@barca.cat`,
177 `ADR:;;Carrer d'Elisabeth Eidenbenz;Barcelona;Catalunya;08028;Catalonia`,
178 `TEL;VALUE=uri;TYPE=voice:tel:+1-555-555-5555;ext=5555`,
180 `ANNIVERSARY;VALUE=TEXT:ferragosto`,
181 `IMPP;PREF=1:xmpp:leo@barca.cat`,
182 `PHOTO:bcspevahwseowngfs23dkl1lsxslfj`,
183 `LANG;TYPE=home;PREF=1:es`,
184 `LANG;TYPE=work;PREF=1:es`,
185 `LANG;TYPE=work;PREF=2:en`,
186 `GEO;VALUE=FLOAT:geo:37.386013,-122.082932`,
188 `LOGO:https://static.messi.com/wp-content/uploads/2019/10/messi-logo-01.png`,
189 `ORG:Barcelona FC;Training-center`,
190 `ORG:Leo Messi Foundation`,
191 `MEMBER:tel:+1-418-555-5555`,
192 `RELATED;TYPE=spouse:Antonela Roccuzzo`,
193 `RELATED;TYPE=child:Matteo Messi Roccuzzo`,
194 `RELATED;TYPE=child:Thiago Messi Roccuzzo`,
195 `RELATED;TYPE=child:Ciro Messi Roccuzzo`,
196 `NOTE:Way better than Cristiano Ronaldo!`,
197 `URL:https://messi.com/`,
198 `CATEGORIES:Tax-evading players`,
199 `CATEGORIES:Best players of all time`,
203 expect(serialize(contact)).toEqual(vcf);
206 it('when there are both categories with vcard group and without', () => {
207 const contact: VCardContact = {
208 version: { field: 'version', value: '4.0', uid: createContactPropertyUid() },
209 fn: [{ field: 'fn', value: 'dummy', uid: createContactPropertyUid() }],
211 { field: 'categories', value: 'first', uid: createContactPropertyUid() },
216 uid: createContactPropertyUid(),
222 uid: createContactPropertyUid(),
231 `ITEM1.CATEGORIES:second`,
232 `ITEM1.CATEGORIES:third`,
236 expect(serialize(contact)).toEqual(vcf);
239 it('unexpectedly prints FLOAT for the GEO property', () => {
240 const contact: VCardContact = {
241 version: { field: 'version', value: '4.0', uid: createContactPropertyUid() },
242 fn: [{ field: 'fn', value: 'dummy', uid: createContactPropertyUid() }],
243 geo: [{ field: 'geo', value: 'geo:41,34', uid: createContactPropertyUid() }],
245 const vcf = [`BEGIN:VCARD`, `VERSION:4.0`, `FN:dummy`, `GEO;VALUE=FLOAT:geo:41,34`, `END:VCARD`].join(
249 expect(serialize(contact)).toEqual(vcf);
252 it('no value is passed to FN field', () => {
254 version: { field: 'version', value: '4.0', uid: createContactPropertyUid() },
255 fn: [{ field: 'fn', value: '', uid: createContactPropertyUid(), params: { pref: '1' } }],
258 expect(serialize(contact)).toEqual([`BEGIN:VCARD`, `VERSION:4.0`, `END:VCARD`].join('\r\n'));
262 describe('round trips with parse', () => {
263 it('for a simple vcard', () => {
267 // `FN;PID=1.1:J. Doe`,
268 // `UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1`,
270 // `EMAIL;PID=1.1:jdoe@example.com`,
271 // `EMAIL:jdoeeeee@example.com`,
272 // `CLIENTPIDMAP:1;urn:uuid:53e374d9-337e-4727-8803-a1e9c14e0556`,
273 // `CATEGORIES:TRAVEL AGENT`,
274 // `CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY`,
278 expect(serialize(parseToVCard(vcf))).toEqual(vcf);
281 it('always puts FN after version', () => {
285 `UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1`,
287 `EMAIL;PID=1.1:jdoe@example.com`,
288 `EMAIL:jdoeeeee@example.com`,
289 `CLIENTPIDMAP:1;urn:uuid:53e374d9-337e-4727-8803-a1e9c14e0556`,
291 `CATEGORIES:TRAVEL AGENT`,
292 `CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY`,
299 `UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1`,
301 `EMAIL;PID=1.1:jdoe@example.com`,
302 `EMAIL:jdoeeeee@example.com`,
303 `CLIENTPIDMAP:1;urn:uuid:53e374d9-337e-4727-8803-a1e9c14e0556`,
304 `CATEGORIES:TRAVEL AGENT`,
305 `CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY`,
309 expect(serialize(parseToVCard(vcf))).toEqual(expected);
314 describe('parseToVcard', () => {
315 describe('parses correctly the N property', () => {
316 it('when it contains all components and single values', () => {
317 const vcf = [`BEGIN:VCARD`, `VERSION:4.0`, `N:Public;John;Quinlan;Mr.;Esq.`, `END:VCARD`].join('\r\n');
319 const result = parseToVCard(vcf);
320 expect(result.n?.value).toEqual({
321 familyNames: ['Public'],
322 givenNames: ['John'],
323 additionalNames: ['Quinlan'],
324 honorificPrefixes: ['Mr.'],
325 honorificSuffixes: ['Esq.'],
329 it('when it contains all components and multiple values', () => {
333 `N:Stevenson;John;Philip,Paul;Dr.;Jr.,M.D.,A.C.P.`,
337 const result = parseToVCard(vcf);
338 expect(result.n?.value).toEqual({
339 familyNames: ['Stevenson'],
340 givenNames: ['John'],
341 additionalNames: ['Philip', 'Paul'],
342 honorificPrefixes: ['Dr.'],
343 honorificSuffixes: ['Jr.', 'M.D.', 'A.C.P.'],
347 it('when it contains all components and some are empty', () => {
348 const vcf = [`BEGIN:VCARD`, `VERSION:4.0`, `N:Yamada;Taro;;;`, `END:VCARD`].join('\r\n');
350 const result = parseToVCard(vcf);
351 expect(result.n?.value).toEqual({
352 familyNames: ['Yamada'],
353 givenNames: ['Taro'],
354 additionalNames: [''],
355 honorificPrefixes: [''],
356 honorificSuffixes: [''],
361 describe('can parse a vcard with an invalid N property', () => {
362 it('if the N property has no semicolon at all', () => {
363 const vcf = [`BEGIN:VCARD`, `VERSION:4.0`, `FN:Santa Claus`, `N:Santa Claus`, `END:VCARD`].join('\r\n');
365 const result = parseToVCard(vcf);
366 expect(result.fn[0].value).toBe('Santa Claus');
367 expect(result.n?.value).toEqual({
368 familyNames: ['Santa Claus'],
370 additionalNames: [''],
371 honorificPrefixes: [''],
372 honorificSuffixes: [''],
376 it('if the N property has an incorrect number of semicolons', () => {
377 const vcf = [`BEGIN:VCARD`, `VERSION:4.0`, `FN:Santa Claus`, `N:Claus;Santa`, `END:VCARD`].join('\r\n');
379 const result = parseToVCard(vcf);
380 expect(result.fn[0].value).toBe('Santa Claus');
381 expect(result.n?.value).toEqual({
382 familyNames: ['Claus'],
383 givenNames: ['Santa'],
384 additionalNames: [''],
385 honorificPrefixes: [''],
386 honorificSuffixes: [''],