OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web / test / bookmarks.test.mjs

59 lines · javascript ref:
// Bookmarks — private, node-local, per viewer. Run: node test/bookmarks.test.mjs
//
// The property that matters most is a NEGATIVE one: this writes no record. There is no interoperable
// lexicon for a bookmark that we should be writing, and inventing one would publish a user's reading
// list to the network forever in signed records, to solve an entirely local problem.
import { createBookmarkStore } from '../lib/bookmarks.js'
import Hyperbee from 'hyperbee'
import Corestore from 'corestore'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'

let pass = 0
const ok = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log('  ok -', m); pass++ }

const dir = mkdtempSync(join(tmpdir(), 'favela-bm-'))
const store = new Corestore(dir)
const bee = new Hyperbee(store.get({ name: 'bm' }), { keyEncoding: 'utf-8', valueEncoding: 'json' })
await bee.ready()
const bm = createBookmarkStore(bee)

const A = 'did:plc:alice'; const B = 'did:plc:bob'
const U1 = 'at://did:plc:x/app.bsky.feed.post/one'
const U2 = 'at://did:plc:x/app.bsky.feed.post/two'

ok((await bm.has(A, U1)) === false, 'nothing saved to begin with')
ok((await bm.add(A, U1)) === true, 'a post can be saved')
ok((await bm.has(A, U1)) === true, 'and reads back as saved')

// PER VIEWER — alice saving a post must not save it for bob.
ok((await bm.has(B, U1)) === false, "alice's bookmark is not bob's")

// Removal leaves nothing behind: it is private state, and a tombstone would be a record of what you
// once saved.
await bm.remove(A, U1)
ok((await bm.has(A, U1)) === false, 'a removal clears it')
ok((await bm.uris(A)).size === 0, 'and leaves nothing behind')

// Newest-SAVED first, NOT newest-posted: a list ordered by the post's date would bury something you
// saved today under an old post you just found — the opposite of what the feature is for.
await bm.add(A, U1)
await new Promise((r) => setTimeout(r, 5))
await bm.add(A, U2)
const list = await bm.list(A)
ok(list.length === 2, 'both are listed')
ok(list[0].uri === U2, 'and the most recently SAVED comes first')

// Bulk read for the render path — one scan, not a lookup per post.
const set = await bm.uris(A)
ok(set.has(U1) && set.has(U2), 'uris() returns every saved uri in one scan')

// Garbage is refused rather than stored.
ok((await bm.add(A, 'https://not-an-at-uri')) === false, 'a non-at:// uri is refused')
ok((await bm.add(null, U1)) === false, 'no viewer → refused')

await bee.close(); await store.close()
try { rmSync(dir, { recursive: true, force: true }) } catch {}
console.log(`\nBOOKMARKS OK: ${pass} assertions`)