Files
hub/tools/contacts-test.js
T
nmemmertandClaude Sonnet 5.5 176b1b8169
build-container / build (push) Successful in 7m21s
Open Library in the book store; podcast topic search and transcript search; Recipes (Mealie) with a shopping list synced to Tasks; Contacts linked to Mail and the Calendar
- Store: Open Library search, books fetched from the Internet Archive (free editions only)
- Podcast: Podcast Index discovery by name and topic (optional key); search what is said across transcripts and play from that moment
- Recipes: browse, import from a link, add ingredients to Mealie's shopping list, mirrored two ways with a Shopping list in Tasks
- Contacts: per-person address book, vCard import/export, recipient suggestions in Mail, birthdays in the Calendar

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 08:55:32 -04:00

35 lines
4.3 KiB
JavaScript

// Contacts: adding, editing, finding, vCard import/export (merging people already there), birthdays for the calendar, and each person's own book.
const http = require('http'), assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path');
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-ct-'));
const contacts = require('../contacts'), users = require('../users');
const people = { a: users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), b: users.create({ name: 'Bob', role: 'user', handle: 'Yg' }) };
const hub = http.createServer((req, res) => users.runAs(req.headers['x-user'] === 'b' ? people.b : people.a, () => contacts.handle(req, res))).listen(0, async () => {
const base = 'http://127.0.0.1:' + hub.address().port + '/api/contacts', post = (p, b, h = {}) => fetch(base + p, { method: 'POST', body: JSON.stringify(b), headers: h });
try {
let r = await post('/', { name: 'Zoe Zed', emails: [{ label: 'work', value: 'Zoe@Example.com' }, { value: 'not an email' }], phones: [{ value: '555 1234' }], birthday: '1990-04-15' }); let c = await r.json();
assert.equal(r.status, 200); assert.deepEqual(c.emails, [{ label: 'work', value: 'zoe@example.com' }], 'addresses are lower-cased and bad ones dropped');
assert.equal((await post('/', { name: '', emails: [] })).status, 400, 'a contact needs a name or an address');
assert.equal((await (await post('/', { emails: [{ value: 'amy@x.org' }] })).json()).name, 'amy', 'with no name the address stands in');
assert.equal((await (await post('/', { name: 'Bad Day', birthday: '1990-02-31' })).json()).birthday, '', 'a date that does not exist is dropped');
c = await (await post('/', { id: c.id, name: 'Zoe Zed', org: 'Acme', emails: c.emails, birthday: '--04-15' })).json(); assert.equal(c.org, 'Acme'); assert.equal((await (await fetch(base + '/')).json()).length, 3, 'editing does not add another');
assert.equal((await post('/', { id: 'nope', name: 'X' })).status, 404);
// vCard in and out
const vcf = 'BEGIN:VCARD\r\nVERSION:3.0\r\nFN:Zoe Zed\r\nEMAIL;TYPE=HOME:zoe@example.com\r\nEMAIL:zoe2@example.com\r\nTEL;TYPE=CELL:+1 555 0000\r\nEND:VCARD\r\nBEGIN:VCARD\r\nVERSION:4.0\r\nN:Smith;Jo;;;\r\nORG:Big\\, Co;Sales\r\nEMAIL;TYPE=work:jo@big.co\r\nBDAY:19801225\r\nNOTE:Line one\\nLine two\r\n continued\r\nADR;TYPE=home:;;1 Main St;Town;ST;12345;USA\r\nEND:VCARD\r\nBEGIN:VCARD\r\nEND:VCARD\r\n';
j = await (await post('/import', { vcf })).json(); assert.deepEqual(j, { added: 1, merged: 1, skipped: 1 }, 'Zoe is filled in, Jo is new, the empty card is skipped');
const all = await (await fetch(base + '/')).json(), zoe = all.find(x => x.name === 'Zoe Zed'), jo = all.find(x => x.name === 'Jo Smith');
assert.deepEqual(zoe.emails.map(e => e.value), ['zoe@example.com', 'zoe2@example.com']); assert.equal(zoe.phones.length, 1); assert.equal(jo.org, 'Big, Co'); assert.equal(jo.birthday, '1980-12-25'); assert.equal(jo.notes, 'Line one\nLine twocontinued', 'folded lines are joined'); assert.match(jo.address, /1 Main St/);
const out = await (await fetch(base + '/export')).text(); assert.match(out, /BEGIN:VCARD/); assert.match(out, /FN:Jo Smith/); assert.match(out, /BDAY:19801225/); assert.match(out, /BDAY:--0415/);
const back = contacts.parseVcf(out); assert.equal(back.length, all.length, 'what is exported can be read back');
// calendar birthdays
const ev = contacts.birthdayEvents({ fd: '2026-04-01', td: '2026-04-30' }, people.a.id); assert.equal(ev.length, 1); assert.equal(ev[0].start, '2026-04-15'); assert.equal(ev[0].end, '2026-04-16'); assert.equal(ev[0].title, "Zoe Zed's birthday");
const dec = contacts.birthdayEvents({ fd: '2026-12-01', td: '2027-01-10' }, people.a.id); assert.equal(dec[0].title, "Jo Smith's birthday (46)", 'the age is shown when the year is known');
assert.equal(contacts.birthdayEvents({ fd: '2026-06-01', td: '2026-06-30' }, people.a.id).length, 0);
// each person has their own book
assert.deepEqual(await (await fetch(base + '/', { headers: { 'x-user': 'b' } })).json(), []);
r = await post('/delete', { id: zoe.id }); assert.equal(r.status, 200); assert.equal((await (await fetch(base + '/')).json()).some(x => x.id === zoe.id), false);
console.log('contacts ok');
} catch (e) { console.error(e); process.exitCode = 1; }
hub.close(); setTimeout(() => process.exit(process.exitCode || 0), 50);
});
var j;