Files
hub/tools/notes-test.js
T
nmemmertandClaude Sonnet 5.5 058f410a58
build-container / build (push) Successful in 4m14s
Shared Family calendar and task list; notes default to Family; phone chat and Family card fixes
- Built-in Family calendar everyone with Calendar can edit (data/family)
- Family task list shared by everyone with Tasks
- New notes and lists start shared with the family
- Phone chat sized above the bottom bar and mini player so the message box is visible
- Family page cards no longer overflow on long chat previews
- Tests added and updated

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 12:33:23 -04:00

31 lines
5.5 KiB
JavaScript

// Notes and lists: private, family and chosen-people sharing, checklist items changed one at a time, conflict detection, and who may change what.
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-notes-'));
const users = require('../users'), notes = require('../notes');
const P = { a: users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), b: users.create({ name: 'Bob', role: 'user', tabs: ['notes'], handle: 'Yg' }), c: users.create({ name: 'Cy', role: 'user', tabs: ['notes'], handle: 'Yw' }), d: users.create({ name: 'Di', role: 'user', tabs: ['music'], handle: 'ZA' }) };
const hub = http.createServer((req, res) => users.runAs(P[req.headers['x-user'] || 'a'], () => notes.handle(req, res))).listen(0, async () => {
const B = 'http://127.0.0.1:' + hub.address().port + '/api/notes', call = (p, o = {}, who) => fetch(B + p, { ...o, headers: { ...(o.headers || {}), ...(who ? { 'x-user': who } : {}) } }), post = (p, b, who) => call(p, { method: 'POST', body: JSON.stringify(b) }, who), j = r => r.json(), list = async who => (await j(await call('/', {}, who))).notes;
try {
const n1 = await j(await post('/', { type: 'note', title: ' Private thoughts ', body: 'secret', color: 'blue', share: 'me' }, 'b')); assert.equal(n1.title, 'Private thoughts'); assert.equal(n1.share, 'me'); assert.equal((await list('a')).length, 0, 'a private note is only for its owner (even an admin does not see it in the list)');
assert.equal((await post('/', { id: n1.id, title: 'Peek' }, 'a')).status, 404, 'and cannot be opened');
const gro = await j(await post('/', { type: 'list', title: 'Groceries', items: ['Milk', ' ', 'Eggs'], share: 'family' }, 'b')); assert.deepEqual(gro.items.map(x => x.text), ['Milk', 'Eggs'], 'blank items are dropped'); assert.equal((await list('c')).length, 1, 'a family note is seen by everyone with Notes'); assert.equal((await list('d')).length, 0, 'but not by someone without the Notes tab');
// checklist: items change one at a time, so two people do not overwrite each other
let g = await j(await post('/' + gro.id + '/items', { text: 'Bread' }, 'c')); assert.equal(g.items.length, 3); const milk = g.items[0].id, eggs = g.items[1].id;
await post('/' + gro.id + '/items', { toggle: milk, done: true }, 'c'); g = await j(await post('/' + gro.id + '/items', { toggle: eggs, done: true }, 'a')); assert.deepEqual(g.items.map(x => [x.text, x.done, x.byName]), [['Milk', true, 'Cy'], ['Eggs', true, 'Ann'], ['Bread', false, undefined]], 'two people ticking at once both count, and it says who');
g = await j(await post('/' + gro.id + '/items', { toggle: milk, done: false }, 'b')); assert.equal(g.items[0].done, false); assert.equal(g.items[0].byName, undefined, 'unticking forgets who ticked it');
g = await j(await post('/' + gro.id + '/items', { edit: eggs, text: 'Free-range eggs' }, 'b')); assert.equal(g.items[1].text, 'Free-range eggs'); g = await j(await post('/' + gro.id + '/items', { clearDone: true }, 'b')); assert.deepEqual(g.items.map(x => x.text), ['Milk', 'Bread'], 'clear done removes the ticked ones'); g = await j(await post('/' + gro.id + '/items', { remove: milk }, 'c')); assert.deepEqual(g.items.map(x => x.text), ['Bread']);
assert.equal((await post('/' + gro.id + '/items', { text: '' }, 'c')).status, 400); assert.equal((await post('/' + n1.id + '/items', { text: 'x' }, 'b')).status, 400, 'a plain note has no items'); assert.equal((await post('/' + gro.id + '/items', { text: 'x' }, 'd')).status, 404);
// editing text: a stale edit is refused, not lost
const n2 = await j(await post('/', { type: 'note', title: 'Plans', body: 'v1', share: 'family' }, 'a')); const e1 = await j(await post('/', { id: n2.id, body: 'v2 by Bob', rev: n2.rev }, 'b')); assert.equal(e1.body, 'v2 by Bob'); assert.equal(e1.updatedByName, 'Bob');
let r = await post('/', { id: n2.id, body: 'v2 by Ann', rev: n2.rev }, 'a'); assert.equal(r.status, 409); const conf = await j(r); assert.equal(conf.note.body, 'v2 by Bob', 'the other change comes back so nothing is lost'); assert.match(conf.error, /Someone else/);
// sharing: only the owner (or an admin) changes it; chosen people
assert.equal((await post('/', { id: n2.id, share: 'me' }, 'b')).status, 403); const sh = await j(await post('/', { id: n1.id, share: [P.c.id, P.d.id, P.b.id, 'nobody'] }, 'b')); assert.deepEqual(sh.share, [P.c.id], 'only other people with Notes, never yourself'); assert.equal((await list('c')).some(x => x.id === n1.id), true, 'a chosen person sees it'); assert.equal((await list('a')).some(x => x.id === n1.id), false);
assert.equal((await j(await post('/', { id: n1.id, pinned: true, color: 'red', archived: true }, 'c'))).color, 'red', 'a person it is shared with can pin, colour and archive'); assert.equal((await list('b'))[0].id, n1.id, 'pinned notes come first');
// delete
assert.equal((await call('/' + n1.id, { method: 'DELETE' }, 'c')).status, 403, 'only the owner or an admin deletes'); assert.equal((await call('/' + n1.id, { method: 'DELETE' }, 'b')).status, 200); assert.equal((await call('/' + n2.id, { method: 'DELETE' }, 'a')).status, 200); assert.equal((await list('b')).length, 1);
assert.equal(JSON.parse(fs.readFileSync(path.join(process.env.DATA_DIR, 'notes.json'), 'utf8')).notes.length, 1, 'it is kept on disk');
console.log('notes ok');
} catch (e) { console.error(e); process.exitCode = 1; }
hub.close(); setTimeout(() => process.exit(process.exitCode || 0), 50);
});