Basic CardDav UI (Contacts)
This implements a basic CardDav UI, titled "Contacts". It allows creating new contacts with a first name + last name, and editing their first and last names, main email, main phone, and notes. You can also import and export VCF (VCARD) files. It also allows editing the VCARD directly, for power users. Additionally, you can choose, create, or delete address books, and if there's no address book created yet in your CardDav server (first-time setup), it'll automatically create one, titled "Contacts". Finally, there are some dependency updates and a fix for the config not allowing disabling the `cardDav` or the `calDav` server. Related to #56
This commit is contained in:
238
lib/utils/contacts.ts
Normal file
238
lib/utils/contacts.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { Contact } from '/lib/models/contacts.ts';
|
||||
|
||||
export function getIdFromVCard(vCard: string): string {
|
||||
const lines = vCard.split('\n').map((line) => line.trim()).filter(Boolean);
|
||||
|
||||
// Loop through every line and find the UID line
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('UID:')) {
|
||||
const uid = line.replace('UID:', '');
|
||||
return uid.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
export function splitTextIntoVCards(text: string): string[] {
|
||||
const lines = text.split('\n').map((line) => line.trim()).filter(Boolean);
|
||||
const vCards: string[] = [];
|
||||
const currentVCard: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
currentVCard.push(line);
|
||||
|
||||
if (line.startsWith('END:VCARD')) {
|
||||
vCards.push(currentVCard.join('\n'));
|
||||
currentVCard.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return vCards;
|
||||
}
|
||||
|
||||
export function generateVCard(contactId: string, firstName: string, lastName?: string): string {
|
||||
const vCardText = `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
N:${getSafelyEscapedTextForVCard(lastName || '')};${getSafelyEscapedTextForVCard(firstName)};
|
||||
FN:${getSafelyEscapedTextForVCard(firstName)} ${getSafelyEscapedTextForVCard(lastName || '')}
|
||||
UID:${getSafelyEscapedTextForVCard(contactId)}
|
||||
END:VCARD`;
|
||||
|
||||
return vCardText;
|
||||
}
|
||||
|
||||
export function updateVCard(
|
||||
vCard: string,
|
||||
{ firstName, lastName, email, phone, notes }: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
notes?: string;
|
||||
},
|
||||
): string {
|
||||
const lines = vCard.split('\n').map((line) => line.trim()).filter(Boolean);
|
||||
|
||||
let replacedName = false;
|
||||
let replacedFormattedName = false;
|
||||
let replacedEmail = false;
|
||||
let replacedPhone = false;
|
||||
let replacedNotes = false;
|
||||
|
||||
const updatedVCardLines = lines.map((line) => {
|
||||
if (line.startsWith('N:') && firstName && !replacedName) {
|
||||
replacedName = true;
|
||||
return `N:${getSafelyEscapedTextForVCard(lastName || '')};${getSafelyEscapedTextForVCard(firstName)};`;
|
||||
}
|
||||
|
||||
if (line.startsWith('FN:') && firstName && !replacedFormattedName) {
|
||||
replacedFormattedName = true;
|
||||
return `FN:${getSafelyEscapedTextForVCard(firstName)} ${getSafelyEscapedTextForVCard(lastName || '')}`;
|
||||
}
|
||||
|
||||
if ((line.startsWith('EMAIL:') || line.startsWith('EMAIL;')) && email && !replacedEmail) {
|
||||
replacedEmail = true;
|
||||
return line.replace(line.split(':')[1], getSafelyEscapedTextForVCard(email));
|
||||
}
|
||||
|
||||
if ((line.startsWith('TEL:') || line.startsWith('TEL;')) && phone && !replacedPhone) {
|
||||
replacedPhone = true;
|
||||
return line.replace(line.split(':')[1], getSafelyEscapedTextForVCard(phone));
|
||||
}
|
||||
|
||||
if (line.startsWith('NOTE:') && notes && !replacedNotes) {
|
||||
replacedNotes = true;
|
||||
return line.replace(line.split(':')[1], getSafelyEscapedTextForVCard(notes.replaceAll('\r', '')));
|
||||
}
|
||||
|
||||
return line;
|
||||
});
|
||||
|
||||
// Remove last line with END:VCARD
|
||||
const endLineIndex = updatedVCardLines.findIndex((line) => line.startsWith('END:VCARD'));
|
||||
updatedVCardLines.splice(endLineIndex, 1);
|
||||
|
||||
if (!replacedEmail && email) {
|
||||
updatedVCardLines.push(`EMAIL;TYPE=HOME:${getSafelyEscapedTextForVCard(email)}`);
|
||||
}
|
||||
if (!replacedPhone && phone) {
|
||||
updatedVCardLines.push(`TEL;TYPE=HOME:${getSafelyEscapedTextForVCard(phone)}`);
|
||||
}
|
||||
if (!replacedNotes && notes) {
|
||||
updatedVCardLines.push(`NOTE:${getSafelyEscapedTextForVCard(notes.replaceAll('\r', ''))}`);
|
||||
}
|
||||
|
||||
updatedVCardLines.push('END:VCARD');
|
||||
|
||||
const updatedVCard = updatedVCardLines.map((line) => line.trim()).filter(Boolean).join('\n');
|
||||
|
||||
return updatedVCard;
|
||||
}
|
||||
|
||||
function getSafelyEscapedTextForVCard(text: string) {
|
||||
return text.replaceAll('\n', '\\n').replaceAll(',', '\\,');
|
||||
}
|
||||
|
||||
function getSafelyUnescapedTextFromVCard(text: string): string {
|
||||
return text.replaceAll('\\n', '\n').replaceAll('\\,', ',');
|
||||
}
|
||||
|
||||
type VCardVersion = '2.1' | '3.0' | '4.0';
|
||||
|
||||
export function parseVCard(text: string): Partial<Contact>[] {
|
||||
const lines = text.split('\n').map((line) => line.trim()).filter(Boolean);
|
||||
|
||||
const partialContacts: Partial<Contact>[] = [];
|
||||
|
||||
let partialContact: Partial<Contact> = {};
|
||||
let vCardVersion: VCardVersion = '2.1';
|
||||
|
||||
// Loop through every line
|
||||
for (const line of lines) {
|
||||
// Start new contact and vCard version
|
||||
if (line.startsWith('BEGIN:VCARD')) {
|
||||
partialContact = {};
|
||||
vCardVersion = '2.1';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Finish contact
|
||||
if (line.startsWith('END:VCARD')) {
|
||||
partialContacts.push(partialContact);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Select proper vCard version
|
||||
if (line.startsWith('VERSION:')) {
|
||||
if (line.startsWith('VERSION:2.1')) {
|
||||
vCardVersion = '2.1';
|
||||
} else if (line.startsWith('VERSION:3.0')) {
|
||||
vCardVersion = '3.0';
|
||||
} else if (line.startsWith('VERSION:4.0')) {
|
||||
vCardVersion = '4.0';
|
||||
} else {
|
||||
// Default to 2.1, log warning
|
||||
vCardVersion = '2.1';
|
||||
console.warn(`Invalid vCard version found: "${line}". Defaulting to 2.1 parser.`);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (vCardVersion !== '2.1' && vCardVersion !== '3.0' && vCardVersion !== '4.0') {
|
||||
console.warn(`Invalid vCard version found: "${vCardVersion}". Defaulting to 2.1 parser.`);
|
||||
vCardVersion = '2.1';
|
||||
}
|
||||
|
||||
if (line.startsWith('UID:')) {
|
||||
const uid = line.replace('UID:', '');
|
||||
|
||||
if (!uid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
partialContact.uid = uid;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('N:')) {
|
||||
const names = line.split('N:')[1].split(';');
|
||||
|
||||
const lastName = names[0] || '';
|
||||
const firstName = names[1] || '';
|
||||
const middleNames = names.slice(2, -1).filter(Boolean);
|
||||
const title = names.slice(-1).join(' ') || '';
|
||||
|
||||
if (!firstName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
partialContact.firstName = firstName;
|
||||
partialContact.lastName = lastName;
|
||||
partialContact.middleNames = middleNames;
|
||||
partialContact.title = title;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('NOTE:')) {
|
||||
const notes = getSafelyUnescapedTextFromVCard(line.split('NOTE:')[1] || '');
|
||||
|
||||
partialContact.notes = notes;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((line.includes('TEL;') || line.includes('TEL:')) && !partialContact.phone) {
|
||||
const phoneInfo = line.split('TEL;')[1] || line.split('TEL')[1] || '';
|
||||
const phoneNumber = phoneInfo.split(':')[1] || '';
|
||||
// const label = (phoneInfo.split(':')[0].split('TYPE=')[1] || 'home').replaceAll(';', '');
|
||||
|
||||
if (!phoneNumber) {
|
||||
continue;
|
||||
}
|
||||
|
||||
partialContact.phone = phoneNumber;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((line.includes('EMAIL;') || line.includes('EMAIL:')) && !partialContact.email) {
|
||||
const emailInfo = line.split('EMAIL;')[1] || line.split('EMAIL')[1] || '';
|
||||
const emailAddress = emailInfo.split(':')[1] || '';
|
||||
// const label = (emailInfo.split(':')[0].split('TYPE=')[1] || 'home').replaceAll(';', '');
|
||||
|
||||
if (!emailAddress) {
|
||||
continue;
|
||||
}
|
||||
|
||||
partialContact.email = emailAddress;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return partialContacts;
|
||||
}
|
||||
503
lib/utils/contacts_test.ts
Normal file
503
lib/utils/contacts_test.ts
Normal file
@@ -0,0 +1,503 @@
|
||||
import { assertEquals } from 'std/assert/assert_equals.ts';
|
||||
import { assertMatch } from 'std/assert/assert_match.ts';
|
||||
|
||||
import { generateVCard, getIdFromVCard, parseVCard, splitTextIntoVCards, updateVCard } from './contacts.ts';
|
||||
|
||||
Deno.test('that getIdFromVCard works', () => {
|
||||
const tests: { input: string; expected?: string; shouldBeUUID?: boolean }[] = [
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:12345-abcde-67890
|
||||
FN:John Doe
|
||||
END:VCARD`,
|
||||
expected: '12345-abcde-67890',
|
||||
},
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
FN:Jane Smith
|
||||
UID:jane-smith-uuid
|
||||
EMAIL:jane@example.com
|
||||
END:VCARD`,
|
||||
expected: 'jane-smith-uuid',
|
||||
},
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:No UID Contact
|
||||
EMAIL:nouid@example.com
|
||||
END:VCARD`,
|
||||
shouldBeUUID: true,
|
||||
},
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID: spaced-uid
|
||||
FN:Spaced UID
|
||||
END:VCARD`,
|
||||
expected: 'spaced-uid',
|
||||
},
|
||||
];
|
||||
|
||||
for (const test of tests) {
|
||||
const output = getIdFromVCard(test.input);
|
||||
if (test.expected) {
|
||||
assertEquals(output, test.expected);
|
||||
} else if (test.shouldBeUUID) {
|
||||
// Check that it's a valid UUID format
|
||||
assertMatch(output, /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test('that splitTextIntoVCards works', () => {
|
||||
const tests: { input: string; expected: string[] }[] = [
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:1
|
||||
FN:John Doe
|
||||
END:VCARD
|
||||
BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:2
|
||||
FN:Jane Smith
|
||||
END:VCARD`,
|
||||
expected: [
|
||||
`BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:1
|
||||
FN:John Doe
|
||||
END:VCARD`,
|
||||
`BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:2
|
||||
FN:Jane Smith
|
||||
END:VCARD`,
|
||||
],
|
||||
},
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
FN:Single Contact
|
||||
EMAIL:single@example.com
|
||||
END:VCARD`,
|
||||
expected: [
|
||||
`BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
FN:Single Contact
|
||||
EMAIL:single@example.com
|
||||
END:VCARD`,
|
||||
],
|
||||
},
|
||||
{
|
||||
input: '',
|
||||
expected: [],
|
||||
},
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:Incomplete Contact`,
|
||||
expected: [],
|
||||
},
|
||||
];
|
||||
|
||||
for (const test of tests) {
|
||||
const output = splitTextIntoVCards(test.input);
|
||||
assertEquals(output, test.expected);
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test('that generateVCard works', () => {
|
||||
const tests: { input: { contactId: string; firstName: string; lastName?: string }; expected: string }[] = [
|
||||
{
|
||||
input: { contactId: 'test-123', firstName: 'John', lastName: 'Doe' },
|
||||
expected: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
N:Doe;John;
|
||||
FN:John Doe
|
||||
UID:test-123
|
||||
END:VCARD`,
|
||||
},
|
||||
{
|
||||
input: { contactId: 'single-name', firstName: 'Madonna' },
|
||||
expected: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
N:;Madonna;
|
||||
FN:Madonna
|
||||
UID:single-name
|
||||
END:VCARD`,
|
||||
},
|
||||
{
|
||||
input: { contactId: 'special-chars', firstName: 'John,Test', lastName: 'Doe\nSmith' },
|
||||
expected: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
N:Doe\\nSmith;John\\,Test;
|
||||
FN:John\\,Test Doe\\nSmith
|
||||
UID:special-chars
|
||||
END:VCARD`,
|
||||
},
|
||||
];
|
||||
|
||||
for (const test of tests) {
|
||||
const output = generateVCard(test.input.contactId, test.input.firstName, test.input.lastName);
|
||||
assertEquals(output, test.expected);
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test('that updateVCard works', () => {
|
||||
const tests: {
|
||||
input: {
|
||||
vCard: string;
|
||||
updates: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
notes?: string;
|
||||
};
|
||||
};
|
||||
expected: string;
|
||||
}[] = [
|
||||
{
|
||||
input: {
|
||||
vCard: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:test-123
|
||||
N:Doe;John;
|
||||
FN:John Doe
|
||||
END:VCARD`,
|
||||
updates: { firstName: 'Jane', lastName: 'Smith' },
|
||||
},
|
||||
expected: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:test-123
|
||||
N:Smith;Jane;
|
||||
FN:Jane Smith
|
||||
END:VCARD`,
|
||||
},
|
||||
{
|
||||
input: {
|
||||
vCard: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:test-456
|
||||
N:Doe;John;
|
||||
FN:John Doe
|
||||
EMAIL:old@example.com
|
||||
TEL:+1234567890
|
||||
END:VCARD`,
|
||||
updates: { email: 'new@example.com', phone: '+9876543210' },
|
||||
},
|
||||
expected: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:test-456
|
||||
N:Doe;John;
|
||||
FN:John Doe
|
||||
EMAIL:new@example.com
|
||||
TEL:+9876543210
|
||||
END:VCARD`,
|
||||
},
|
||||
{
|
||||
input: {
|
||||
vCard: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:test-789
|
||||
N:Doe;John;
|
||||
FN:John Doe
|
||||
END:VCARD`,
|
||||
updates: { email: 'added@example.com', phone: '+1111111111', notes: 'Test notes' },
|
||||
},
|
||||
expected: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:test-789
|
||||
N:Doe;John;
|
||||
FN:John Doe
|
||||
EMAIL;TYPE=HOME:added@example.com
|
||||
TEL;TYPE=HOME:+1111111111
|
||||
NOTE:Test notes
|
||||
END:VCARD`,
|
||||
},
|
||||
{
|
||||
input: {
|
||||
vCard: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:test-special
|
||||
N:Doe;John;
|
||||
FN:John Doe
|
||||
NOTE:Old notes
|
||||
END:VCARD`,
|
||||
updates: { notes: 'New notes\nwith newlines, and commas' },
|
||||
},
|
||||
expected: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:test-special
|
||||
N:Doe;John;
|
||||
FN:John Doe
|
||||
NOTE:New notes\\nwith newlines\\, and commas
|
||||
END:VCARD`,
|
||||
},
|
||||
{
|
||||
input: {
|
||||
vCard: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:test-carriage
|
||||
N:Doe;John;
|
||||
FN:John Doe
|
||||
END:VCARD`,
|
||||
updates: { notes: 'Notes with\r\ncarriage returns' },
|
||||
},
|
||||
expected: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:test-carriage
|
||||
N:Doe;John;
|
||||
FN:John Doe
|
||||
NOTE:Notes with\\ncarriage returns
|
||||
END:VCARD`,
|
||||
},
|
||||
];
|
||||
|
||||
for (const test of tests) {
|
||||
const output = updateVCard(test.input.vCard, test.input.updates);
|
||||
assertEquals(output, test.expected);
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test('that parseVCard works', () => {
|
||||
const tests: {
|
||||
input: string;
|
||||
expected: Array<{
|
||||
uid?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
middleNames?: string[];
|
||||
title?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
notes?: string;
|
||||
}>;
|
||||
}[] = [
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:test-123
|
||||
N:Doe;John;Middle;Jr
|
||||
FN:John Middle Doe Jr
|
||||
EMAIL;TYPE=HOME:john@example.com
|
||||
TEL;TYPE=HOME:+1234567890
|
||||
NOTE:Test contact notes
|
||||
END:VCARD`,
|
||||
expected: [{
|
||||
uid: 'test-123',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
middleNames: ['Middle'],
|
||||
title: 'Jr',
|
||||
email: 'john@example.com',
|
||||
phone: '+1234567890',
|
||||
notes: 'Test contact notes',
|
||||
}],
|
||||
},
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
UID:test-456
|
||||
N:Smith;Jane;;
|
||||
FN:Jane Smith
|
||||
EMAIL:jane@example.com
|
||||
TEL:+9876543210
|
||||
END:VCARD`,
|
||||
expected: [{
|
||||
uid: 'test-456',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
middleNames: [],
|
||||
title: '',
|
||||
email: 'jane@example.com',
|
||||
phone: '+9876543210',
|
||||
}],
|
||||
},
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:multi-1
|
||||
N:Doe;John;
|
||||
FN:John Doe
|
||||
END:VCARD
|
||||
BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:multi-2
|
||||
N:Smith;Jane;
|
||||
FN:Jane Smith
|
||||
EMAIL;PREF=1:jane@example.com
|
||||
END:VCARD`,
|
||||
expected: [
|
||||
{
|
||||
uid: 'multi-1',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
middleNames: [],
|
||||
title: '',
|
||||
},
|
||||
{
|
||||
uid: 'multi-2',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
middleNames: [],
|
||||
title: '',
|
||||
email: 'jane@example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:escaped-contact
|
||||
N:Test;Contact;
|
||||
FN:Contact Test
|
||||
NOTE:Notes with\\nescaped newlines\\, and commas
|
||||
END:VCARD`,
|
||||
expected: [{
|
||||
uid: 'escaped-contact',
|
||||
firstName: 'Contact',
|
||||
lastName: 'Test',
|
||||
middleNames: [],
|
||||
title: '',
|
||||
notes: 'Notes with\nescaped newlines, and commas',
|
||||
}],
|
||||
},
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:2.1
|
||||
UID:version-21
|
||||
N:Old;Format;
|
||||
FN:Format Old
|
||||
EMAIL:old@example.com
|
||||
TEL:+1111111111
|
||||
END:VCARD`,
|
||||
expected: [{
|
||||
uid: 'version-21',
|
||||
firstName: 'Format',
|
||||
lastName: 'Old',
|
||||
middleNames: [],
|
||||
title: '',
|
||||
email: 'old@example.com',
|
||||
phone: '+1111111111',
|
||||
}],
|
||||
},
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:email-variations
|
||||
N:Test;Email;
|
||||
FN:Email Test
|
||||
EMAIL:direct@example.com
|
||||
EMAIL;TYPE=WORK:work@example.com
|
||||
TEL:+1234567890
|
||||
TEL;TYPE=WORK:+9876543210
|
||||
END:VCARD`,
|
||||
expected: [{
|
||||
uid: 'email-variations',
|
||||
firstName: 'Email',
|
||||
lastName: 'Test',
|
||||
middleNames: [],
|
||||
title: '',
|
||||
email: 'direct@example.com', // Only first email is captured
|
||||
phone: '+1234567890', // Only first phone is captured
|
||||
}],
|
||||
},
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:5.0
|
||||
UID:invalid-version
|
||||
N:Invalid;Version;
|
||||
FN:Version Invalid
|
||||
END:VCARD`,
|
||||
expected: [{
|
||||
uid: 'invalid-version',
|
||||
firstName: 'Version',
|
||||
lastName: 'Invalid',
|
||||
middleNames: [],
|
||||
title: '',
|
||||
}],
|
||||
},
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:no-first-name
|
||||
N:LastOnly;;
|
||||
FN:LastOnly
|
||||
END:VCARD`,
|
||||
expected: [{ uid: 'no-first-name' }],
|
||||
},
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:empty-uid
|
||||
N:Test;Empty;
|
||||
FN:Empty Test
|
||||
END:VCARD`,
|
||||
expected: [{
|
||||
firstName: 'Empty',
|
||||
lastName: 'Test',
|
||||
middleNames: [],
|
||||
title: '',
|
||||
uid: 'empty-uid',
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
for (const test of tests) {
|
||||
const output = parseVCard(test.input);
|
||||
assertEquals(output, test.expected);
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test('that parseVCard handles edge cases', () => {
|
||||
const edgeCases: { input: string; description: string; expected: any[] }[] = [
|
||||
{
|
||||
input: '',
|
||||
description: 'empty string',
|
||||
expected: [],
|
||||
},
|
||||
{
|
||||
input: 'Not a vCard at all',
|
||||
description: 'invalid format',
|
||||
expected: [],
|
||||
},
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:incomplete`,
|
||||
description: 'incomplete vCard without END',
|
||||
expected: [],
|
||||
},
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:missing-required
|
||||
FN:Missing Required Fields
|
||||
END:VCARD`,
|
||||
description: 'vCard without N field',
|
||||
expected: [{ uid: 'missing-required' }],
|
||||
},
|
||||
{
|
||||
input: `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:empty-fields
|
||||
N:;;;
|
||||
FN:Empty Fields
|
||||
EMAIL:
|
||||
TEL:
|
||||
NOTE:
|
||||
END:VCARD`,
|
||||
description: 'vCard with empty field values',
|
||||
expected: [{ uid: 'empty-fields', notes: '' }],
|
||||
},
|
||||
];
|
||||
|
||||
for (const test of edgeCases) {
|
||||
const output = parseVCard(test.input);
|
||||
assertEquals(output, test.expected, `Failed for: ${test.description}`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user