OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge / test / admin.test.js

106 lines · javascript ref:
// The operator's read-out. Loopback, read-mostly, and it mints nothing.
//
// ⛔ WHAT THIS PINS. The bridge is keyless for atproto and holds only its own AP signing key. An admin
// screen that created identities would make it a key holder for other people and reverse the peer/bridge
// split the whole architecture rests on. So every action here is a flow that ALREADY EXISTS — issue an
// invite, list who registered, release a name whose namespace the operator owns.

import test from 'brittle'
import { adminRoutes, renderOverview } from '../lib/admin.js'
import { MemoryRegistrations } from '../lib/registrations.js'

const ROOT_A = 'a'.repeat(64)
const ROOT_B = 'b'.repeat(64)
const config = { host: 'ap.7rnx.net', registration: 'open', inboundCap: 500 }

const get = (path) => ({ method: 'GET', path })
const post = (path, form) => ({ method: 'POST', path, form })

test('★★ the overview lists who holds a name on this bridge', async (t) => {
  const registrations = new MemoryRegistrations({ policy: 'open' })
  await registrations.claim({ username: 'fleeky', root: ROOT_A, feedKey: 'd'.repeat(64) })
  await registrations.claim({ username: 'ashatron', root: ROOT_B })

  const res = await adminRoutes(get('/admin'), { registrations, inbound: null, config })
  t.is(res.status, 200)
  t.ok(/fleeky@ap\.7rnx\.net/.test(res.body), 'the name as a person actually types it')
  t.ok(/ashatron@ap\.7rnx\.net/.test(res.body))
  t.ok(/Registration policy.*open/s.test(res.body), 'and the policy, which decides who may join')
})

test('★★★ a released name is shown, not hidden', async (t) => {
  // ⛔ A burned name is NOT a free name: the generation counter makes a reused one a different actor so
  // it cannot inherit the previous holder's audience. An operator seeing only the live list would read
  // a burned name as merely absent.
  const registrations = new MemoryRegistrations({ policy: 'open' })
  await registrations.claim({ username: 'gone', root: ROOT_A })
  await registrations.release('gone', { root: ROOT_A })

  const res = await adminRoutes(get('/admin'), { registrations, inbound: null, config })
  t.ok(/Released names/.test(res.body))
  t.ok(/>gone</.test(res.body), 'the tombstone is visible')
})

test('★★★ the operator may release a name, and it burns', async (t) => {
  const registrations = new MemoryRegistrations({ policy: 'open' })
  await registrations.claim({ username: 'squatted', root: ROOT_A })

  const res = await adminRoutes(post('/admin/release', { username: 'squatted' }), { registrations, inbound: null, config })
  t.is(res.status, 303, 'back to the overview')
  t.absent(await registrations.get('squatted'), 'the name is no longer held')

  // ⚠ Burned, not freed — so this is not a way to hand somebody else's name to a third party.
  const dead = await registrations.released('squatted')
  t.ok(dead, 'a tombstone remains')
  t.is(dead.generation, 1)
})

test('★★ issuing an invite is the existing flow, surfaced', async (t) => {
  const registrations = new MemoryRegistrations({ policy: 'invite' })
  const res = await adminRoutes(post('/admin/invite', {}), { registrations, inbound: null, config })
  t.is(res.status, 200)
  t.ok(/single-use/.test(res.body), 'and it says what an invite is')
})

test('★★★ the cap is stated, and so is the fact that it cannot be backfilled', (t) => {
  // atproto inbound can be rebuilt from Constellation — a backlink index answering "what links to this
  // subject?" for any point in the past. ActivityPub is push-only delivery and has no such index, so an
  // eviction here may destroy the only copy. An operator choosing a cap has to be told which of the two
  // they are choosing.
  const body = renderOverview({ host: 'ap.7rnx.net', cap: 500, inbound: [] })
  t.ok(/Keeping the newest/.test(body))
  t.ok(/no backlink index/.test(body), 'the fediverse asymmetry is said out loud')
  t.ok(/peer holds the durable copy/.test(body), 'and that the cap is a buffer, not the memory')
})

test('★★ nothing here mints an identity', async (t) => {
  const registrations = new MemoryRegistrations({ policy: 'open' })
  const res = await adminRoutes(get('/admin'), { registrations, inbound: null, config })
  t.absent(/mint/i.test(res.body), 'no minting offered — the bridge holds no key for anybody else')
  const missing = await adminRoutes(post('/admin/mint', {}), { registrations, inbound: null, config })
  t.is(missing.status, 404, 'and no such route exists')
})

// ── pages another role of the process adds (the favela gateway's status page, hyper-social #80) ──────
test('★ an added page is served behind the same gate, even when registration is off', async (t) => {
  const pages = new Map([['/admin/status', async () => ({ title: 'Gateway status', body: '<p id="x">all roles up</p>' })]])
  const res = await adminRoutes(get('/admin/status'), { registrations: null, inbound: null, config, pages })
  t.is(res.status, 200)
  t.ok(res.body.includes('<p id="x">all roles up</p>'), 'the page body, as rendered by its owner')
  t.ok(res.body.includes('<title>Gateway status</title>'), 'inside the admin chrome')
})

test('the overview links every added page', async (t) => {
  const registrations = new MemoryRegistrations({ policy: 'open' })
  const pages = new Map([['/admin/status', async () => ({ title: 'Gateway status', body: '' })]])
  const res = await adminRoutes(get('/admin'), { registrations, inbound: null, config, pages })
  t.ok(res.body.includes('href="/admin/status"') && res.body.includes('Gateway status'))
})

test('an added page that throws answers 500 with the reason, not a dead admin', async (t) => {
  const pages = new Map([['/admin/status', async () => { throw new Error('metrics unavailable') }]])
  const res = await adminRoutes(get('/admin/status'), { registrations: null, inbound: null, config, pages })
  t.is(res.status, 500)
  t.ok(res.body.includes('metrics unavailable'))
})