Remove client-side isLoggedIn value
[ProtonMail-WebClient.git] / packages / shared / test / contacts / import.spec.ts
blobb3eee63296d97248bb61fd4fff424359bda5c049
1 import { parseISO } from 'date-fns';
3 import { ImportContactError } from '../../lib/contacts/errors/ImportContactError';
4 import {
5     extractContactImportCategories,
6     getContactId,
7     getImportCategories,
8     getSupportedContact,
9     naiveExtractPropertyValue,
10 } from '../../lib/contacts/helpers/import';
11 import { fromVCardProperties, getVCardProperties } from '../../lib/contacts/properties';
12 import { extractVcards } from '../../lib/contacts/vcard';
13 import { toCRLF } from '../../lib/helpers/string';
14 import type { ContactMetadata, EncryptedContact, ImportedContact } from '../../lib/interfaces/contacts';
15 import type { VCardContact, VCardProperty } from '../../lib/interfaces/contacts/VCard';
17 const excludeUids = (contact: VCardContact | ImportContactError) => {
18     if (contact instanceof ImportContactError) {
19         return undefined;
20     }
21     const properties = getVCardProperties(contact).map(({ uid, ...property }) => property);
22     return fromVCardProperties(properties as VCardProperty[]);
25 describe('import', () => {
26     describe('extract vcards', () => {
27         it('should keep the line separator used in the vcard', () => {
28             const vcardsPlain = `BEGIN:VCARD
29 VERSION:4.0
30 FN:One
31 END:VCARD
32 BEGIN:VCARD
33 VERSION:4.0
34 FN:Two
35 END:VCARD`;
36             const vcardsCRLF = toCRLF(vcardsPlain);
37             expect(extractVcards(vcardsPlain)).toEqual([
38                 'BEGIN:VCARD\nVERSION:4.0\nFN:One\nEND:VCARD',
39                 'BEGIN:VCARD\nVERSION:4.0\nFN:Two\nEND:VCARD',
40             ]);
41             expect(extractVcards(vcardsCRLF)).toEqual([
42                 'BEGIN:VCARD\r\nVERSION:4.0\r\nFN:One\r\nEND:VCARD',
43                 'BEGIN:VCARD\r\nVERSION:4.0\r\nFN:Two\r\nEND:VCARD',
44             ]);
45         });
47         it('extracts vcards separated by empty lines', () => {
48             const vcardsPlain = `BEGIN:VCARD
49 VERSION:4.0
50 FN:One
51 END:VCARD
53 BEGIN:VCARD
54 VERSION:4.0
55 FN:Two
56 END:VCARD
58             expect(extractVcards(vcardsPlain)).toEqual([
59                 'BEGIN:VCARD\nVERSION:4.0\nFN:One\nEND:VCARD',
60                 'BEGIN:VCARD\nVERSION:4.0\nFN:Two\nEND:VCARD',
61             ]);
62         });
63     });
65     describe('naiveExtractPropertyValue', () => {
66         const vcard = `BEGIN:VCARD
67 VERSION:4.0
68 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
69 FN;PID=1.1:J. Doe
70 N:Doe;J.;;;
71 BDAY:
72 EMAIL;PID=1.1:jdoe@example.com
73 CLIENTPIDMAP:1;urn:uuid:53e374d9-337e-4727-8803-a1e9c14e0556
74 NOTE:This is a long descrip
75  tion that exists o
76  n a long line.
77 END:VCARD`;
79         it('should return undefined when the property is not present', () => {
80             expect(naiveExtractPropertyValue(vcard, 'CATEGORIES')).toBeUndefined();
81         });
83         it('should return the empty string when the property has no value', () => {
84             expect(naiveExtractPropertyValue(vcard, 'BDAY')).toEqual('');
85         });
87         it('should extract values as expected with and without parameters', () => {
88             expect(naiveExtractPropertyValue(vcard, 'UID')).toEqual('urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1');
89             expect(naiveExtractPropertyValue(vcard, 'email')).toEqual('jdoe@example.com');
90             expect(naiveExtractPropertyValue(vcard, 'version')).toEqual('4.0');
91         });
93         it('should extract folded values', () => {
94             expect(naiveExtractPropertyValue(vcard, 'NOTE')).toEqual(
95                 'This is a long description that exists on a long line.'
96             );
97         });
98     });
100     describe('getContactId', () => {
101         it('should retrieve FN value whenever present', () => {
102             expect(
103                 getContactId(`BEGIN:VCARD
104 VERSION:4.0
105 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
106 FN;PID=1.1:J. Doe
107 N:Doe;J.;;;
108 EMAIL;PID=1.1:jdoe@example.com
109 CLIENTPIDMAP:1;urn:uuid:53e374d9-337e-4727-8803-a1e9c14e0556
110 END:VCARD`)
111             ).toEqual('J. Doe');
112             expect(
113                 getContactId(`BEGIN:VCARD
114 VERSION:4.0
115 FN:Simon Perreault
116 N:Perreault;Simon;;;ing. jr,M.Sc.
117 BDAY:--0203
118 ANNIVERSARY:20090808T1430-0500
119 GENDER:M
120 LANG;PREF=1:fr
121 LANG;PREF=2:en
122 ORG;TYPE=work:Viagenie
123 ADR;TYPE=work:;Suite D2-630;2875 Laurier;
124 Quebec;QC;G1V 2M2;Canada
125 TEL;VALUE=uri;TYPE="work,voice";PREF=1:tel:+1-418-656-9254;ext=102
126 TEL;VALUE=uri;TYPE="work,cell,voice,video,text":tel:+1-418-262-6501
127 EMAIL;TYPE=work:simon.perreault@viagenie.ca
128 GEO;TYPE=work:geo:46.772673,-71.282945
129 KEY;TYPE=work;VALUE=uri:
130 http://www.viagenie.ca/simon.perreault/simon.asc
131 TZ:-0500
132 URL;TYPE=home:http://nomis80.org
133 END:VCARD`)
134             ).toEqual('Simon Perreault');
135         });
137         it('should retrieve FN when multiple lines are present', () => {
138             expect(
139                 getContactId(`BEGIN:VCARD
140 VERSION:4.0
141 UID:urn:uuid:4fbe8971-0bc3
142  -424c-9c26-36
143  c3e1eff6b1
144 FN;PID=1.1:Joh
145  nnie
146   Do
148 N:Doe;J.;;;
149 EMAIL;PID=1.1:jdoe@example.com
150 CLIENTPIDMAP:1;urn:uuid:53e374d9-337e-4727-8803-a1e9c14e0556
151 END:VCARD`)
152             ).toEqual('Johnnie Doe');
153         });
155         it('should crop FN when too long', () => {
156             expect(
157                 getContactId(`BEGIN:VCARD
158 VERSION:4.0
159 UID:urn:uuid:4fbe8971-0bc3
160  -424c-9c26-36
161  c3e1eff6b1
162 FN;PID=1.1:This contact has a very loo
163  ong name, but that's a pity since no
164  one will remember such a long one
165 N:Doe;J.;;;
166 EMAIL;PID=1.1:jdoe@example.com
167 CLIENTPIDMAP:1;urn:uuid:53e374d9-337e-4727-8803-a1e9c14e0556
168 END:VCARD`)
169             ).toEqual('This contact has a very looong name, bu…');
170         });
171     });
173     describe('extractCategories', () => {
174         it('should pick the right contactEmail ids for a contact with categories', () => {
175             const contact = {
176                 ID: 'contact',
177                 ContactEmails: [
178                     {
179                         ID: 'one',
180                         Email: 'one@email.test',
181                     },
182                     {
183                         ID: 'two',
184                         Email: 'two@email.test',
185                     },
186                     {
187                         ID: 'three',
188                         Email: 'three@email.test',
189                     },
190                 ],
191             } as ContactMetadata;
192             const encryptedContact = {
193                 contactEmails: [
194                     { email: 'one@email.test', group: 'item1' },
195                     { email: 'two@email.test', group: 'item2' },
196                     { email: 'three@email.test', group: 'item3' },
197                 ],
198                 categories: [
199                     { name: 'dogs', group: 'item1' },
200                     { name: 'cats', group: 'item2' },
201                     { name: 'cats', group: 'item3' },
202                     { name: 'pets', group: 'item2' },
203                     { name: 'all' },
204                 ],
205             } as EncryptedContact;
206             const result = [
207                 { name: 'dogs', contactEmailIDs: ['one'] },
208                 { name: 'cats', contactEmailIDs: ['two', 'three'] },
209                 { name: 'pets', contactEmailIDs: ['two'] },
210                 { name: 'all', contactEmailIDs: ['one', 'two', 'three'] },
211             ];
212             expect(extractContactImportCategories(contact, encryptedContact)).toEqual(result);
213         });
214     });
216     it('should pick the right contactEmail ids for a contact with categories', () => {
217         const contact = {
218             ID: 'contact',
219             ContactEmails: [
220                 {
221                     ID: 'one',
222                     Email: 'one@email.test',
223                 },
224                 {
225                     ID: 'two',
226                     Email: 'two@email.test',
227                 },
228                 {
229                     ID: 'three',
230                     Email: 'three@email.test',
231                 },
232             ],
233         } as ContactMetadata;
234         const encryptedContact = {
235             contactEmails: [
236                 { email: 'one@email.test', group: 'item1' },
237                 { email: 'two@email.test', group: 'item2' },
238                 { email: 'three@email.test', group: 'item3' },
239             ],
240             categories: [],
241         } as unknown as EncryptedContact;
242         expect(extractContactImportCategories(contact, encryptedContact)).toEqual([]);
243     });
245     describe('getImportCategories', () => {
246         it('should combine contact email ids and contact ids from different contacts', () => {
247             const contacts: ImportedContact[] = [
248                 {
249                     contactID: 'contact1',
250                     contactEmailIDs: ['contactemail1-1', 'contactemail1-2'],
251                     categories: [
252                         { name: 'cats', contactEmailIDs: ['contactemail1-1', 'contactemail1-2'] },
253                         { name: 'dogs', contactEmailIDs: ['contactemail1-1'] },
254                         { name: 'pets' },
255                     ],
256                 },
257                 {
258                     contactID: 'contact2',
259                     contactEmailIDs: [],
260                     categories: [{ name: 'dogs' }, { name: 'birds' }],
261                 },
262                 {
263                     contactID: 'contact3',
264                     contactEmailIDs: ['contactemail3-1', 'contactemail3-2'],
265                     categories: [
266                         { name: 'all' },
267                         { name: 'dogs', contactEmailIDs: ['contactemail3-1'] },
268                         { name: 'pets', contactEmailIDs: ['contactemail3-2'] },
269                     ],
270                 },
271             ];
272             const result = [
273                 {
274                     name: 'cats',
275                     contactEmailIDs: [],
276                     contactIDs: ['contact1'],
277                     totalContacts: 1,
278                 },
279                 {
280                     name: 'dogs',
281                     contactEmailIDs: ['contactemail1-1', 'contactemail3-1'],
282                     contactIDs: [],
283                     totalContacts: 2,
284                 },
285                 { name: 'pets', contactEmailIDs: ['contactemail3-2'], contactIDs: [], totalContacts: 1 },
286             ];
287             expect(getImportCategories(contacts)).toEqual(result);
288         });
289     });
291     describe('getSupportedContacts', () => {
292         const getExpectedProperties = (withLineBreaks = false): VCardContact => {
293             return {
294                 fn: [{ field: 'fn', value: 'Name', params: { pref: '1' }, uid: '' }],
295                 version: { field: 'version', value: '4.0', uid: '' },
296                 adr: [
297                     {
298                         field: 'adr',
299                         value: {
300                             postOfficeBox: '',
301                             extendedAddress: '',
302                             streetAddress: withLineBreaks ? 'street with line breaks' : 'street',
303                             locality: 'city',
304                             region: '',
305                             postalCode: '00000',
306                             country: 'FR',
307                         },
308                         uid: '',
309                     },
310                 ],
311                 org: [{ field: 'org', value: { organizationalName: 'company' }, uid: '' }],
312                 bday: { field: 'bday', value: { date: parseISO('1999-01-01') }, uid: '' },
313                 note: [{ field: 'note', value: 'Notes', uid: '' }],
314                 email: [
315                     { field: 'email', value: 'email1@protonmail.com', params: { pref: '1' }, group: 'item1', uid: '' },
316                 ],
317                 tel: [{ field: 'tel', value: '00 00000000', params: { pref: '1' }, uid: '' }],
318                 title: [{ field: 'title', value: 'title', uid: '' }],
319             };
320         };
322         it('should import normal vCard correctly', () => {
323             const vCard = `BEGIN:VCARD
324 VERSION:4.0
325 ADR:;;street;city;;00000;FR
326 ORG:company
327 BDAY:19990101
328 NOTE:Notes
329 TEL;PREF=1:00 00000000
330 TITLE:title
331 FN;PREF=1:Name
332 ITEM1.EMAIL;PREF=1:email1@protonmail.com
333 END:VCARD`;
335             const expected = getExpectedProperties();
337             const contact = getSupportedContact(vCard);
339             expect(excludeUids(contact)).toEqual(excludeUids(expected));
340         });
342         it('should import vCard with address containing \\r\\n correctly', () => {
343             const vCard = `BEGIN:VCARD
344 VERSION:4.0
345 ADR:;;street\\r\\nwith\\r\\nline\\r\\nbreaks;city;;00000;FR
346 ORG:company
347 BDAY:19990101
348 NOTE:Notes
349 TEL;PREF=1:00 00000000
350 TITLE:title
351 FN;PREF=1:Name
352 ITEM1.EMAIL;PREF=1:email1@protonmail.com
353 END:VCARD`;
355             const expected = getExpectedProperties(true);
357             const contact = getSupportedContact(vCard);
359             expect(excludeUids(contact)).toEqual(excludeUids(expected));
360         });
362         it('should import vCard with address containing \\\\n correctly', () => {
363             const vCard = `BEGIN:VCARD
364 VERSION:4.0
365 ADR:;;street\\nwith\\nline\\nbreaks;city;;00000;FR
366 ORG:company
367 BDAY:19990101
368 NOTE:Notes
369 TEL;PREF=1:00 00000000
370 TITLE:title
371 FN;PREF=1:Name
372 ITEM1.EMAIL;PREF=1:email1@protonmail.com
373 END:VCARD`;
375             const expected = getExpectedProperties(true);
377             const contact = getSupportedContact(vCard);
379             expect(excludeUids(contact)).toEqual(excludeUids(expected));
380         });
381     });
383     it('should import BDAY and ANNIVERSARY', () => {
384         const vCard = `BEGIN:VCARD
385 VERSION:4.0
386 FN:Name
387 BDAY:19990101
388 ANNIVERSARY:19990101
389 END:VCARD`;
391         const expected: VCardContact = {
392             fn: [{ field: 'fn', value: 'Name', uid: '' }],
393             version: { field: 'version', value: '4.0', uid: '' },
394             bday: { field: 'bday', value: { date: parseISO('1999-01-01') }, uid: '' },
395             anniversary: { field: 'anniversary', value: { date: parseISO('1999-01-01') }, uid: '' },
396         };
398         const contact = getSupportedContact(vCard);
400         expect(excludeUids(contact)).toEqual(excludeUids(expected));
401     });
403     it('should import BDAY and ANNIVERSARY with text format', () => {
404         const vCard = `BEGIN:VCARD
405 VERSION:4.0
406 FN:Name
407 BDAY;VALUE=text:bidet
408 ANNIVERSARY;VALUE=text:annie
409 END:VCARD`;
411         const expected: VCardContact = {
412             fn: [{ field: 'fn', value: 'Name', uid: '' }],
413             version: { field: 'version', value: '4.0', uid: '' },
414             bday: { field: 'bday', value: { text: 'bidet' }, params: { type: 'text' }, uid: '' },
415             anniversary: { field: 'anniversary', value: { text: 'annie' }, params: { type: 'text' }, uid: '' },
416         };
418         const contact = getSupportedContact(vCard);
420         expect(excludeUids(contact)).toEqual(excludeUids(expected));
421     });
423     it('should import the contact using email as FN when no FN specified', () => {
424         const vCard = `BEGIN:VCARD
425 VERSION:4.0
426 ITEM1.EMAIL;PREF=1:email1@protonmail.com
427 END:VCARD`;
429         const expected: VCardContact = {
430             fn: [{ field: 'fn', value: 'email1@protonmail.com', uid: '' }],
431             version: { field: 'version', value: '4.0', uid: '' },
432             email: [{ field: 'email', value: 'email1@protonmail.com', params: { pref: '1' }, group: 'item1', uid: '' }],
433         };
435         const contact = getSupportedContact(vCard);
437         expect(excludeUids(contact)).toEqual(excludeUids(expected));
438     });
440     it(`should not import a contact with a missing FN for which we can't find an alternative`, () => {
441         const vCard = `BEGIN:VCARD
442 VERSION:4.0
443 CATEGORIES:MISSING_FN,MISSING_EMAIL
444 BDAY:20000505
445 END:VCARD`;
447         expect(() => getSupportedContact(vCard)).toThrowError('Missing FN property');
448     });
450     it(`should not import a contact with version 2.1`, () => {
451         const vCard = `BEGIN:VCARD
452 VERSION:2.1
453 N:Gump;Forrest;;Mr.
454 FN:Forrest Gump
455 ORG:Bubba Gump Shrimp Co.
456 TITLE:Shrimp Man
457 TEL;WORK;VOICE:(111) 555-1212
458 TEL;HOME;VOICE:(404) 555-1212
459 ADR;WORK;PREF:;;100 Waters Edge;Baytown;LA;30314;United States of America
460 LABEL;WORK;PREF;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:100 Waters Edge=0D=
461  =0ABaytown\\, LA 30314=0D=0AUnited States of America
462 ADR;HOME:;;42 Plantation St.;Baytown;LA;30314;United States of America
463 LABEL;HOME;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:42 Plantation St.=0D=0A=
464  Baytown, LA 30314=0D=0AUnited States of America
465 PHOTO;GIF:http://www.example.com/dir_photos/my_photo.gif
466 EMAIL:forrestgump@example.com
467 REV:20080424T195243Z
468 END:VCARD`;
469         expect(() => getSupportedContact(vCard)).toThrowError('vCard versions < 3.0 not supported');
470     });