OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web / test / ap-followers.test.mjs

88 lines · javascript ref:
// The ActivityPub half of a DID's follower graph. Run: node test/ap-followers.test.mjs
//
// The load-bearing property is ATTRIBUTION: an actor's followers may only be shown on the profile of
// the DID that actor CLAIMS to be. Everything else here is degradation behaviour, because this reaches
// another host over HTTP on a page render.
import { createApFollowers, actorHandle } from '../lib/ap-followers.js'

let pass = 0
const ok = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log('  ok -', m); pass++ }
const eq = (a, b, m) => ok(JSON.stringify(a) === JSON.stringify(b), `${m} (got ${JSON.stringify(a)})`)

const DID = 'did:plc:wj62ebbusln6gxo5azo6ogzo'
const ACTOR = {
  id: 'https://ap.7rnx.net/ap/actor',
  type: 'Person',
  alsoKnownAs: [DID],
  followers: 'https://ap.7rnx.net/ap/followers'
}
const COLL = { type: 'OrderedCollection', totalItems: 1, orderedItems: ['https://prsm.space/users/fleeky'] }

const fetcher = (map, log = []) => async (url) => {
  log.push(url)
  if (!(url in map)) return { ok: false, status: 404, json: async () => null }
  const v = map[url]
  if (v === 'throw') throw new Error('network down')
  return { ok: true, status: 200, json: async () => v }
}

// address form
eq(actorHandle('https://prsm.space/users/fleeky'), '@fleeky@prsm.space', 'actor URL renders as a fediverse address')
eq(actorHandle('not a url'), null, 'a non-URL yields nothing rather than garbage')

const MAP = { 'https://ap.7rnx.net/ap/actor': ACTOR, 'https://ap.7rnx.net/ap/followers': COLL }

// ── the whole point: attribution ────────────────────────────────────────────────────────────────
{
  const ap = createApFollowers({ base: 'https://ap.7rnx.net', doFetch: fetcher(MAP) })
  const rows = await ap.followersOf(DID)
  ok(rows.length === 1, 'followers of the claimed DID are returned')
  eq(rows[0].source, 'fediverse', 'and are LABELLED, never blended in as ours')
  eq(rows[0].did, null, 'with no DID — we hold no atproto identity for them')
  eq(rows[0].url, 'https://prsm.space/users/fleeky', 'and a link to their own server')
  eq(rows[0].displayName, '@fleeky@prsm.space', 'shown as a fediverse address')

  // THE failure that would matter: attributing one identity's followers to a different profile.
  eq(await ap.followersOf('did:plc:someoneelse00000000000'), [], 'a DIFFERENT DID gets nothing — never mis-attributed')
  eq(await ap.followersOf(null), [], 'and neither does a missing DID')
}

// ── degradation: this is an optional source over a network hop ───────────────────────────────────
{
  const ap = createApFollowers({ base: 'https://ap.7rnx.net', doFetch: fetcher({ 'https://ap.7rnx.net/ap/actor': 'throw' }) })
  eq(await ap.followersOf(DID), [], 'a throwing bridge yields an empty list, never an exception')
}
{
  const ap = createApFollowers({ base: 'https://ap.7rnx.net', doFetch: fetcher({}) })
  eq(await ap.followersOf(DID), [], 'a 404 actor yields an empty list')
}
{
  const noAka = { ...ACTOR, alsoKnownAs: [] }
  const ap = createApFollowers({ base: 'https://ap.7rnx.net', doFetch: fetcher({ 'https://ap.7rnx.net/ap/actor': noAka }) })
  eq(await ap.followersOf(DID), [], 'an actor claiming NO DID is attributed to nobody')
}
{
  const ap = createApFollowers({ base: null, doFetch: null })
  ok(ap.enabled === false, 'unconfigured is disabled')
  eq(await ap.followersOf(DID), [], 'and returns nothing rather than reaching out')
}

// ── hostile collection contents ──────────────────────────────────────────────────────────────────
{
  const nasty = { orderedItems: ['javascript:alert(1)', 'http://insecure.example/u/x', { id: 'https://ok.example/u/y' }, null, 42] }
  const ap = createApFollowers({ base: 'https://ap.7rnx.net', doFetch: fetcher({ 'https://ap.7rnx.net/ap/actor': ACTOR, 'https://ap.7rnx.net/ap/followers': nasty }) })
  const rows = await ap.followersOf(DID)
  eq(rows.map((r) => r.url), ['https://ok.example/u/y'], 'only https items survive — no javascript:, no http:, no junk')
}

// ── caching: a follower page render must not re-fetch per row ────────────────────────────────────
{
  const log = []
  const ap = createApFollowers({ base: 'https://ap.7rnx.net', doFetch: fetcher(MAP, log) })
  await ap.followersOf(DID)
  await ap.followersOf(DID)
  ok(log.length === 2, `two calls hit the network once each, then cache (got ${log.length} requests)`)
}

console.log(`\nAP-FOLLOWERS OK: ${pass} assertions`)