// wire-registration.test.js — a registration becomes a served actor.
//
// The join between the two halves: the registry knows a name and a feed key; the holder can hold a
// feed; PerUser can build a source. This is what connects them, and what makes a registration
// survive a restart.
import test from 'brittle'
import { Users } from '../lib/users.js'
import { FavelaSource } from '../lib/favela-source.js'
import { PerUser } from '../lib/per-user.js'
import { MemoryFollowerStore } from '../lib/followers.js'
const heldFeed = () => ({
async reach () { return { networks: ['fediverse'], credential: null } },
async ownPosts () { return { feed: 'f'.repeat(64), rows: [] } },
async call () { return null }
})
test('★★ a user can be added after construction — registration is a runtime act', async (t) => {
const users = new Users([{ username: 'primary' }], 'ap.7rnx.net')
t.is(users.size, 1)
users.add({ username: 'ashatron', root: 'a'.repeat(64) })
t.is(users.size, 2)
t.is(users.get('ashatron').username, 'ashatron')
t.is(users.primary.username, 'primary', '⛔ and it did NOT become primary — the flat paths do not move')
})
test('⛔ a duplicate is refused, not silently overwritten', async (t) => {
const users = new Users([{ username: 'primary' }], 'ap.7rnx.net')
users.add({ username: 'ashatron' })
t.exception(() => users.add({ username: 'ashatron' }), /duplicate/i)
})
// ⛔ A RE-CLAIM IS NOT A DUPLICATE. The same holder claiming its own name again (same root, same feed) is how a
// peer asks for what a bridge now returns at claim time (its observation feed, #82 I1 S3). It used to throw here,
// and admit.js turned that into "202 pending" for a user who was fully served.
test('★ re-adding the SAME registration (same root and feed) is idempotent; any other is still refused', async (t) => {
const users = new Users([{ username: 'primary' }], 'ap.7rnx.net')
const first = users.add({ username: 'fleeky', root: 'a'.repeat(64), feedKey: 'f'.repeat(64) })
const again = users.add({ username: 'fleeky', root: 'a'.repeat(64), feedKey: 'f'.repeat(64) })
t.is(again, first, 'the same user, not a second one')
t.is(users.size, 2)
t.exception(() => users.add({ username: 'fleeky', root: 'b'.repeat(64), feedKey: 'f'.repeat(64) }), /duplicate/i, 'another root')
t.exception(() => users.add({ username: 'fleeky', root: 'a'.repeat(64), feedKey: 'e'.repeat(64) }), /duplicate/i, 'another feed')
})
test('⛔ a bad username is refused at add time, as at construction', async (t) => {
const users = new Users([{ username: 'primary' }], 'ap.7rnx.net')
t.exception(() => users.add({ username: 'not a name' }))
})
// ⛔ THE POINT. A registered peer is on another machine, so its source must be a HELD FEED and not a
// socket. FavelaSource takes the peer it is given rather than building one from a path.
test('★★ FavelaSource accepts an injected peer — no socket required', async (t) => {
const src = new FavelaSource({
peer: heldFeed(),
host: 'ap.7rnx.net',
actorId: 'https://ap.7rnx.net/ap/users/ashatron/actor',
followersId: 'https://ap.7rnx.net/ap/users/ashatron/followers'
})
const { notes, reason } = await src.notes()
t.absent(reason, 'it read reach off the held feed')
t.alike(notes, [])
})
test('★★ PerUser builds a source from a held feed', async (t) => {
const users = new Users([{ username: 'primary' }], 'ap.7rnx.net')
users.add({ username: 'ashatron', peer: heldFeed() })
const pu = new PerUser({ users, dataDir: '/tmp', shared: { records: null, followers: new MemoryFollowerStore(), signer: null } })
const src = pu.records(users.get('ashatron'))
t.ok(src, 'a registered user gets a source')
t.absent(reason(await src.notes()), 'and it works without a socket')
function reason (r) { return r.reason }
})
// ⛔ WEBFINGER IS HOW AN ACTOR IS FOUND AT ALL. A fediverse client resolves acct:name@host through
// it before it ever fetches the actor URL — so an actor whose /ap/users/<name>/actor serves 200 but
// whose webfinger 404s does not exist as far as any client is concerned. Measured on the live
// bridge: ashatron's actor returned 200 while acct:ashatron@ap.7rnx.net returned "no such resource".
import { handleRequest } from '../lib/router.js'
const wf = (name, ctx) => handleRequest(
{ method: 'GET', path: '/.well-known/webfinger', query: { resource: 'acct:' + name + '@ap.7rnx.net' }, headers: {} },
ctx
)
test('★★ webfinger resolves a REGISTERED user, not only the primary', async (t) => {
const users = new Users([{ username: 'p2p', primary: true }], 'ap.7rnx.net')
users.add({ username: 'ashatron' })
const ctx = { config: { host: 'ap.7rnx.net', username: 'p2p' }, users, log: () => {} }
const primary = await wf('p2p', ctx)
t.is(primary.status, 200, 'the incumbent still resolves')
const reg = await wf('ashatron', ctx)
t.is(reg.status, 200, 'and so does a registered name')
const doc = JSON.parse(reg.body)
t.is(doc.subject, 'acct:ashatron@ap.7rnx.net')
const self = doc.links.find((l) => l.rel === 'self')
t.is(self.href, 'https://ap.7rnx.net/ap/users/ashatron/actor', 'pointing at THEIR actor, not the primary\'s')
})
test('⛔ an unregistered name is still 404 — never a fallback to the primary', async (t) => {
const users = new Users([{ username: 'p2p', primary: true }], 'ap.7rnx.net')
const res = await wf('nobody', { config: { host: 'ap.7rnx.net', username: 'p2p' }, users, log: () => {} })
t.is(res.status, 404, 'answering for one person under another name is the failure this prevents')
})
// ⛔ A REGISTERED USER'S BLOBS ARE IN THE OVERLAY, not a PDS. createBlobFetcher fetches by CID from
// config.pdsUrl — the PRIMARY's atproto store. Ashatron's avatar is a favela oid on their held
// feed, so the route asked the wrong store and correctly got nothing: the actor advertised an icon
// that 404d, which renders as no avatar at all.
import { blobFetcherFor } from '../lib/blobs.js'
test('★★ a held user\'s blob comes from their overlay, not the PDS', async (t) => {
const OID = 'bb02a15a7f755740174f4ae6b9b4a9724cd53d4159878844db79e3ce9f94b580'
const files = { async get (oid) { return oid === OID ? { path: '/x', mimeType: 'image/png', size: 3 } : null } }
const fetch = blobFetcherFor({ files, readFile: async () => Buffer.from('png') })
const b = await fetch(OID)
t.is(b.contentType, 'image/png', 'typed from the BYTES, as files.js sniffs it')
t.is(String(b.body), 'png')
t.is(await fetch('c'.repeat(64)), null, 'and an oid nobody holds is null, not a guess')
})
// ⛔ THE SHARED KEYS ARE A CONTRACT, and getting one wrong fails SILENTLY. PerUser reads
// shared.records / shared.followerStore / shared.fetchBlob / shared.signer. index.js passed
// `followers` and omitted `fetchBlob`, so `_inherits` never matched for the primary: the incumbent
// and a registered user ended up reading the SAME follower list, and the per-user publisher found
// no recipients because its own store was empty.
//
// Nothing threw. The only symptom was two actors sharing followers.
test('⛔⛔ PerUser reads exactly the keys index.js supplies', async (t) => {
const users = new Users([{ username: 'p2p', primary: true }], 'ap.7rnx.net')
users.add({ username: 'ashatron' })
const sharedFollowers = new MemoryFollowerStore(['https://prsm.space/users/fleeky'])
const pu = new PerUser({
users,
dataDir: '/tmp/pu-' + Date.now(),
shared: { records: null, followerStore: sharedFollowers, fetchBlob: async () => null, signer: null }
})
t.is(pu.followers(users.primary), sharedFollowers,
'the primary INHERITS the existing list — a fresh store would stop delivering to real people')
t.not(pu.followers(users.get('ashatron')), sharedFollowers,
'and a registered user gets its OWN, never the incumbent\'s')
})