// ⛔ THE INCUMBENT MUST NOT MOVE.
//
// ap.7rnx.net's actor is https://ap.7rnx.net/ap/actor, 22 objects are federated under
// /ap/objects/<rkey>, and a follower at prsm.space is following that exact id. An actor that changes
// its id is a stranger to every server that already knows it, and a moved object id is a dead link
// in somebody's timeline. Multi-user is ADDITIVE or it is a migration.
import test from 'brittle'
import { Users, usersFromConfig } from '../lib/users.js'
const HOST = 'ap.7rnx.net'
const two = () => new Users([
{ username: 'p2p', primary: true },
{ username: 'ashatron' }
], HOST)
test('★★ the primary keeps the FLAT paths, byte for byte', async (t) => {
const u = two()
const ids = u.ids(u.get('p2p'))
t.is(ids.actor, 'https://ap.7rnx.net/ap/actor')
t.is(ids.inbox, 'https://ap.7rnx.net/ap/inbox')
t.is(ids.followers, 'https://ap.7rnx.net/ap/followers')
t.is(ids.objects, 'https://ap.7rnx.net/ap/objects', 'the 22 federated objects keep resolving')
})
test('★ a second user gets their own namespace', async (t) => {
const ids = two().ids(two().get('ashatron'))
t.is(ids.actor, 'https://ap.7rnx.net/ap/users/ashatron/actor')
t.is(ids.objects, 'https://ap.7rnx.net/ap/users/ashatron/objects')
})
test('★★ routing: flat is the primary, /users/<name> is theirs', async (t) => {
const u = two()
t.is(u.route('/ap/actor').user.username, 'p2p')
t.is(u.route('/ap/actor').rest, '/actor')
t.is(u.route('/ap/objects/abc').rest, '/objects/abc', 'and the rest survives for the handler')
t.is(u.route('/ap/users/ashatron/outbox').user.username, 'ashatron')
t.is(u.route('/ap/users/ashatron/outbox').rest, '/outbox')
t.is(u.route('/ap/users/ashatron').rest, '/', 'a bare user path is the root of theirs')
})
test('⛔ an unknown user is nothing, not the primary', async (t) => {
// Falling back would answer for one person under another's name — the whole failure this exists
// to prevent, arriving quietly.
t.is(two().route('/ap/users/nobody/actor'), null)
t.is(two().route('/notap/actor'), null)
})
test('⛔ webfinger matches the HOST exactly', async (t) => {
const u = two()
t.is(u.fromResource('acct:p2p@ap.7rnx.net').username, 'p2p')
t.is(u.fromResource('acct:ashatron@ap.7rnx.net').username, 'ashatron')
// Answering for another host's account would claim somebody we do not serve.
t.is(u.fromResource('acct:p2p@evil.example'), null)
t.is(u.fromResource('acct:nobody@ap.7rnx.net'), null)
t.is(u.fromResource('https://ap.7rnx.net/ap/actor'), null, 'only acct: resources')
})
test('⛔ a duplicate username is REFUSED at construction', async (t) => {
// Otherwise one of them silently never serves, and which one depends on config order.
t.exception(() => new Users([{ username: 'a' }, { username: 'A' }], HOST), /duplicate/i)
t.exception(() => new Users([{ username: 'has space' }], HOST), /invalid username/i)
})
test('★★ a single-actor config produces IDENTICAL ids to having no registry', async (t) => {
// The compatibility story in one assertion: a node that never adds a second user cannot tell
// this refactor happened.
const u = usersFromConfig({ host: HOST, username: 'p2p', did: 'did:plc:x' })
t.is(u.size, 1)
t.is(u.ids(u.primary).actor, 'https://ap.7rnx.net/ap/actor')
t.is(u.route('/ap/outbox').user.username, 'p2p')
})