Files
selfphoneng/apps/api/src/contacts/vcard.test.ts
T
iamdoubz a754974a90 contacts: my book, the company book, vCard/CSV import, sharing with Accept/Deny
C1 (spec 11): /api/me/contacts and /api/contacts (admin), our own vCard 2.1/3.0/4.0 and CSV
readers with a preview, contact_shares (migration 0009) for offers, and contact names in call
history, voicemail, Home, the softphone and the call report email. Admin → Contacts.
2026-10-23 14:53:21 -05:00

158 lines
4.3 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
// C1: vCard and CSV imports read what phones and address books export (11 §3.1).
import { describe, expect, it } from 'vitest';
import { parseCsv } from './csv.ts';
import { parseVcards } from './vcard.ts';
const IPHONE = [
'BEGIN:VCARD',
'VERSION:3.0',
'PRODID:-//Apple Inc.//iPhone OS 17.0//EN',
'N:Doe;Jane;;;',
'FN:Jane Doe',
'ORG:Acme Corp;',
'EMAIL;type=INTERNET;type=HOME;type=pref:jane@example.com',
'TEL;type=CELL;type=VOICE;type=pref:(312) 555-1234',
'item1.TEL:+1 312 555 0000',
'item1.X-ABLabel:_$!<Main>!$_',
'NOTE:Met at the fair\\, 2026\\nCall after 5',
'PHOTO;ENCODING=b;TYPE=JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD',
' /2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJ',
'END:VCARD',
].join('\r\n');
const GOOGLE = `BEGIN:VCARD
VERSION:3.0
FN:Bob Smith
N:Smith;Bob;;;
TEL;TYPE=WORK:+13125559999
TEL;TYPE=HOME:3125558888
EMAIL;TYPE=INTERNET:bob@example.com
ORG:Widgets Inc.
item1.URL:http\\://example.com
END:VCARD
BEGIN:VCARD
VERSION:3.0
N:;;;;
TEL;TYPE=CELL:3125550101
END:VCARD
`;
const V21 = `BEGIN:VCARD
VERSION:2.1
N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:M=C3=BCller;J=C3=BCrgen;;Dr.;
TEL;CELL:+49301234567
TEL;WORK;FAX:+49301234500
NOTE;ENCODING=QUOTED-PRINTABLE:Line one=0D=0ALine tw=
o
END:VCARD`;
const V40 = `BEGIN:VCARD
VERSION:4.0
FN:Carol Lee
TEL;VALUE=uri;TYPE="voice,cell":tel:+1-312-555-7777
END:VCARD
BEGIN:VCARD
VERSION:4.0
NOTE:nothing to call
END:VCARD`;
describe('parseVcards', () => {
it('reads an iPhone export: names, company, labels, note; skips the photo', () => {
expect(parseVcards(IPHONE)).toEqual({
contacts: [
{
name: 'Jane Doe',
company: 'Acme Corp',
email: 'jane@example.com',
numbers: [
{ label: 'mobile', number: '(312) 555-1234' },
{ label: 'main', number: '+1 312 555 0000' },
],
notes: 'Met at the fair, 2026\nCall after 5',
},
],
errors: [],
});
});
it('reads a Google export, and names a card with only a number after it', () => {
const { contacts } = parseVcards(GOOGLE);
expect(contacts.map((c) => [c.name, c.company, c.numbers])).toEqual([
[
'Bob Smith',
'Widgets Inc.',
[
{ label: 'work', number: '+13125559999' },
{ label: 'home', number: '3125558888' },
],
],
['3125550101', null, [{ label: 'mobile', number: '3125550101' }]],
]);
});
it('reads vCard 2.1: quoted-printable UTF-8, bare types, soft line breaks', () => {
const [c] = parseVcards(V21).contacts;
expect(c).toMatchObject({
name: 'Dr. Jürgen Müller',
numbers: [
{ label: 'mobile', number: '+49301234567' },
{ label: 'fax', number: '+49301234500' },
],
notes: 'Line one\r\nLine two',
});
});
it('reads vCard 4.0 tel: URIs, and reports a card with nothing to call', () => {
expect(parseVcards(V40)).toEqual({
contacts: [
{
name: 'Carol Lee',
company: null,
email: null,
numbers: [{ label: 'mobile', number: '+1-312-555-7777' }],
notes: null,
},
],
errors: ['Card 2: no name or number, skipped'],
});
});
});
describe('parseCsv', () => {
it('reads the 04 §7.4 columns in any order, with quotes and line breaks', () => {
const csv =
'Number1,Name,label1,company,notes,number2,label2\r\n' +
'312-555-1234,"Doe, Jane",mobile,Acme,"two\nlines",,\r\n' +
',,,,,,\r\n' +
'3125550000,,work,,,,\r\n' +
'+44 20 7946 0958,"Bob ""B"" Smith",,,,101,desk\r\n';
expect(parseCsv(csv)).toEqual({
contacts: [
{
name: 'Doe, Jane',
company: 'Acme',
email: null,
numbers: [{ label: 'mobile', number: '312-555-1234' }],
notes: 'two\nlines',
},
{
name: 'Bob "B" Smith',
company: null,
email: null,
numbers: [
{ label: '', number: '+44 20 7946 0958' },
{ label: 'desk', number: '101' },
],
notes: null,
},
],
errors: ['Row 4: no name, skipped'],
});
});
it('needs a header with a name column', () => {
expect(parseCsv('Jane,3125551234\n').errors[0]).toMatch(/"name" column/);
});
});