OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer / test / reach.test.js

139 lines · javascript ref:
// ⛔ WHICH NETWORKS THIS IDENTITY PUBLISHES TO — a preference, stored in its own feed.
//
// ⚠ NOT a bridge, and not knowledge of one. `favela-markdown/lib/reach.js` defines
// NETWORKS = ['favela', 'bsky', 'fediverse'] and favela-peer already depends on it, so these are
// favela's OWN network names. What the peer must never hold is the projection — the schemas, the
// signing, the wire format — and it holds none of that. `boundary.test.js` is the arbiter and it
// still passes.
//
// ⛔ IN THE FEED, not on disk. A person who turns on bsky publishing on their laptop has said
// something about their identity, not about that laptop, so it has to replicate to their phone the
// way a profile does. It is the same shape as setProfile for the same reason.
//
// ⛔ OFF BY DEFAULT, AND OFF MEANS OFF. An absent record must never read as "everything enabled":
// publication is the one setting where a missing value has to fail closed.

import test from 'brittle'
import fs from '#node:fs'
import os from '#node:os'
import { join } from '#node:path'
import Corestore from 'corestore'
import { RecordStore } from '../lib/record-store.js'
import { Feed } from '../lib/feed-base.js'
import { Peer } from '../lib/peer.js'
import { identity } from './helpers.js'

const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-reach-'))

async function mkPeer () {
  const id = await identity()
  const d = await id.device()
  const f = new Feed(new Corestore(tmp()), null, { store: new RecordStore(tmp()) })
  await f.ready()
  await f.addDevice(d.bind(f.deviceKey))
  await f.update()
  return new Peer(f).ready()
}

test('⛔ a node publishes NOWHERE until it is told to', async (t) => {
  const p = await mkPeer()
  t.teardown(() => p.close())
  const r = await p.reach()
  t.alike(r.networks, [], 'no record means no publication, not all publication')
})

test('★ turning a network on, and off again', async (t) => {
  const p = await mkPeer()
  t.teardown(() => p.close())

  await p.setReach({ networks: ['bsky'] })
  t.alike((await p.reach()).networks, ['bsky'])

  await p.setReach({ networks: ['bsky', 'fediverse'] })
  t.alike((await p.reach()).networks.sort(), ['bsky', 'fediverse'])

  // ⛔ An empty list is a real answer — "publish nowhere" — and must be storable. If it were treated
  // as "unset" there would be no way to turn everything off once anything was on.
  await p.setReach({ networks: [] })
  t.alike((await p.reach()).networks, [])
})

test('⛔ an unknown network is REFUSED, not stored', async (t) => {
  // Storing one would put a name in the feed that nothing can resolve to a bridge, and it would sit
  // there looking enabled.
  const p = await mkPeer()
  t.teardown(() => p.close())
  await t.exception(() => p.setReach({ networks: ['bsky', 'twitter'] }), /twitter|network/i)
  t.alike((await p.reach()).networks, [], 'and nothing was written')
})

test('⛔ favela cannot be switched off — it is the substrate, not a destination', async (t) => {
  // reach.js §5.4: "local is the substrate; there is no way to opt out of it". Accepting it here
  // would imply a post could exist without existing locally.
  const p = await mkPeer()
  t.teardown(() => p.close())
  await t.exception(() => p.setReach({ networks: ['favela'] }), /favela|substrate/i)
})

test('★★ the designated credential travels with it', async (t) => {
  // identity.md §5.1: a user has at most ONE credential marked for atproto, and with none there is
  // no projection. The peer stores WHICH identifier is designated; it does not know what the
  // identifier means or hold anything that can sign with it.
  const p = await mkPeer()
  t.teardown(() => p.close())
  await p.setReach({ networks: ['bsky'], credential: 'did:example:abc' })
  const r = await p.reach()
  t.is(r.credential, 'did:example:abc')

  // Clearing it is how you stop projecting under an identifier without forgetting the preference.
  await p.setReach({ credential: null })
  t.is((await p.reach()).credential, null)
  t.alike((await p.reach()).networks, ['bsky'], 'and the networks are untouched')
})

test('⚠ setReach merges — setting one field does not clear the other', async (t) => {
  const p = await mkPeer()
  t.teardown(() => p.close())
  await p.setReach({ networks: ['bsky'], credential: 'did:example:abc' })
  await p.setReach({ networks: ['fediverse'] })
  t.is((await p.reach()).credential, 'did:example:abc', 'the credential survived a networks-only write')
})

test('⛔ set-reach requires an ATTESTED device, like posting does', async (t) => {
  // Turning on a network publishes under this identity. That is an identity act, so it sits behind
  // the same gate as a post — not behind the one that lets a browser change a display name.
  const { dispatch } = await import('../lib/rpc.js')
  const src = fs.readFileSync(new URL('../lib/rpc.js', import.meta.url).pathname, 'utf8')
  const set = src.slice(src.indexOf('const MUTATIONS'), src.indexOf('async function requireLinked'))
  t.ok(set.includes("'set-reach'"), 'set-reach is a mutation')
  t.absent(set.includes("'set-profile'"), 'set-profile is not — a self-label asserts nothing')
  t.ok(typeof dispatch === 'function')
})

// ⛔ WHICH BRIDGE CARRIES THE NAME TRAVELS WITH THE IDENTITY (hyper-social #82, I1). bridges.json is per DEVICE, so a
// second device of the same identity — or any device after a registration that predates bridges.json — could not
// tell which bridge to ask for its observation feed, and needed a manual re-claim. The bridge key is a public swarm
// address, so it rides in reach beside the name it carries, and every device of the identity learns it.
test('★ the bridge that carries the fediverse name is stored beside it, and merges like every other field', async (t) => {
  const p = await mkPeer()
  t.teardown(() => p.close())
  const KEY = 'fe'.repeat(32)
  await p.setReach({ fediverse: 'fleeky@ap.7rnx.net', fediverseBridge: KEY })
  let r = await p.reach()
  t.is(r.fediverse, 'fleeky@ap.7rnx.net')
  t.is(r.fediverseBridge, KEY)
  await p.setReach({ networks: ['fediverse'] })
  t.is((await p.reach()).fediverseBridge, KEY, 'survives a networks-only write')
  await p.setReach({ fediverseBridge: null })
  r = await p.reach()
  t.is(r.fediverseBridge, null, 'and can be cleared')
  t.is(r.fediverse, 'fleeky@ap.7rnx.net', 'without touching the name')
})

test('⛔ a fediverseBridge that is not a 64-hex key is refused', async (t) => {
  const p = await mkPeer()
  t.teardown(() => p.close())
  await t.exception(() => p.setReach({ fediverseBridge: 'not-a-key' }), /fediverseBridge/)
  t.is((await p.reach()).fediverseBridge, null)
})