OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer / test / registry.test.js

123 lines · javascript ref:
// The registry — a feed whose records are listings of other feeds. No new primitive.
//
// ★ Consent is STRUCTURAL, not conventional. A listing may only be written for a feed that a live,
// PROVEN knock in this same registry named, and the knock proves which identity root wrote it
// (feed.js applyKnock; knock-identity.test.js). So "asking to be listed" and "being listed" are two
// acts by two parties, and apply enforces it rather than a convention someone has to remember. A
// reader then checks the listed feed's founder against the granted root (registry-peer.test.js).
//
// Two honesty limits, both from hyper-social #61 §8 and neither fixable here:
//   Delisting stops propagation; it does not retract. The listing already replicated.
//   Unlisted is not private. A feed announces on a topic derived from its key, so anyone holding
//   the key finds it regardless of any directory. A registry is a phone book, not a gate.

import test from 'brittle'
import { proofsFor } from './helpers.js'
import { applyOp, isRosterKey, KNOCKS, LISTING_ROOT } from '../lib/feed.js'

const kv = () => {
  const m = new Map()
  return {
    m,
    async set (k, v) { m.set(k, v) },
    async get (k) { return m.has(k) ? m.get(k) : null },
    entries () { return [...m.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) }
  }
}

const OWNER = 'd1'.repeat(32)
const OTHER = 'd2'.repeat(32)
const FEED_A = 'aa'.repeat(32)
const FEED_B = 'bb'.repeat(32)

const run = async (ops, view = kv()) => {
  for (const [device, op] of ops) {
    const genesis = !view.entries().some(([k]) => isRosterKey(k))
    await applyOp(view, op, { device, genesis })
  }
  return view
}

// One owner's devices. Minted once so every call site stays synchronous.
const PROOF = await proofsFor([OWNER, OTHER, FEED_A, FEED_B])
const addDevice = (d) => ({ t: 'add-device', device: d, proof: PROOF[d] })
// OTHER is the knocking writer core; its proof chains to ROOT, so the knock is keyed by ROOT.
const ROOT = PROOF.__root
const knock = (feed) => ({ t: 'knock', feed, proof: PROOF[OTHER] })
const listFeed = (feed) => ({ t: 'list-feed', root: ROOT, feed, $oid: 'oid-' + feed, size: 10 })
const delist = (feed) => ({ t: 'delist', feed })

test('a knock then a listing — two parties, two acts', async (t) => {
  const v = await run([[OWNER, addDevice(OWNER)], [OTHER, knock(FEED_A)], [OWNER, listFeed(FEED_A)]])
  t.alike(await v.get('listing/' + FEED_A), { $oid: 'oid-' + FEED_A, size: 10 },
    'the listing is an ordinary pointer, in an ordinary collection')
  t.alike(await v.get(LISTING_ROOT + '/' + FEED_A), { root: ROOT }, 'and the root it was granted to sits beside it')
})

test('⛔ a listing with NO knock is refused — consent is not optional', async (t) => {
  const v = await run([[OWNER, addDevice(OWNER)], [OWNER, listFeed(FEED_A)]])
  t.is(await v.get('listing/' + FEED_A), null, 'the registry cannot list a feed that never asked')
})

test('⛔ a knock alone does not list you', async (t) => {
  const v = await run([[OWNER, addDevice(OWNER)], [OTHER, knock(FEED_A)]])
  t.is(await v.get('listing/' + FEED_A), null, 'asking is not being listed')
  t.ok(await v.get(KNOCKS + '/' + ROOT), 'the request is waiting, keyed by the identity that asked')
})

test('⛔ a stranger cannot list itself even after knocking', async (t) => {
  const v = await run([[OWNER, addDevice(OWNER)], [OTHER, knock(FEED_A)], [OTHER, listFeed(FEED_A)]])
  t.is(await v.get('listing/' + FEED_A), null, 'writing the listing needs an admitted device')
})

test('a dismissed knock cannot be used to list', async (t) => {
  const v = await run([
    [OWNER, addDevice(OWNER)],
    [OTHER, knock(FEED_A)],
    [OWNER, { t: 'dismiss-knock', root: ROOT }],
    [OWNER, listFeed(FEED_A)]
  ])
  t.is(await v.get('listing/' + FEED_A), null, 'consent withdrawn before use is consent refused')
})

test('delisting tombstones the listing', async (t) => {
  const v = await run([
    [OWNER, addDevice(OWNER)],
    [OTHER, knock(FEED_A)],
    [OWNER, listFeed(FEED_A)],
    [OWNER, delist(FEED_A)]
  ])
  t.alike(await v.get('listing/' + FEED_A), { $deleted: true },
    'positive evidence — it stops propagating, it does not retract')
})

test('⛔ a stranger cannot delist', async (t) => {
  const v = await run([
    [OWNER, addDevice(OWNER)],
    [OTHER, knock(FEED_A)],
    [OWNER, listFeed(FEED_A)],
    [OTHER, delist(FEED_A)]
  ])
  t.alike(await v.get('listing/' + FEED_A), { $oid: 'oid-' + FEED_A, size: 10 }, 'still listed')
})

test('a registry holds many listings, each independent', async (t) => {
  const v = await run([
    [OWNER, addDevice(OWNER)],
    [OTHER, knock(FEED_A)], [OWNER, listFeed(FEED_A)],
    [OTHER, knock(FEED_B)], [OWNER, listFeed(FEED_B)],
    [OWNER, delist(FEED_A)]
  ])
  t.alike(await v.get('listing/' + FEED_A), { $deleted: true })
  t.alike(await v.get('listing/' + FEED_B), { $oid: 'oid-' + FEED_B, size: 10 })
})

test('a malformed listing writes nothing and does not throw', async (t) => {
  const v = await run([[OWNER, addDevice(OWNER)], [OTHER, knock(FEED_A)],
    // A valid root on each, so these test the missing pointer / feed they were written for.
    [OWNER, { t: 'list-feed', root: ROOT, feed: FEED_A }],
    [OWNER, { t: 'list-feed', root: ROOT, $oid: 'x', size: 1 }],
    [OWNER, { t: 'list-feed', root: ROOT, feed: 'not/a/segment', $oid: 'x', size: 1 }]])
  t.is(await v.get('listing/' + FEED_A), null, 'a listing without a pointer is not a listing')
})