OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge / test / profile-update.test.js

92 lines · javascript ref:
// ⛔ A Mastodon Update WITHOUT an `updated` timestamp is silently discarded — 2xx, no edit, no
// media re-fetch. Recorded in the ap-bridge skill, and the profile Update did not set one: note
// Updates do (publisher.js:234), the profile path did not. So every profile change ever sent from
// this bridge was accepted and ignored, which is why a stale display name never corrected itself.
import test from 'brittle'
import { Publisher } from '../lib/publisher.js'
import { MemoryState } from '../lib/state.js'
import { MemoryFollowerStore } from '../lib/followers.js'

test('★★ a profile Update carries an `updated` timestamp', async (t) => {
  const sent = []
  const p = new Publisher({
    records: {
      async getProfile () { return { name: 'ashes' } },
      async listNotes () { return { notes: [], cursor: null } },
      async listInteractions () { return { interactions: [] } }
    },
    followers: new MemoryFollowerStore(['https://prsm.space/users/fleeky']),
    deliver: async (d) => { sent.push(d) },
    actorId: 'https://ap.7rnx.net/ap/users/ashatron/actor',
    newId: () => 'id1',
    log: () => {},
    state: new MemoryState([], true),
    buildActorDoc: async () => ({ id: 'https://ap.7rnx.net/ap/users/ashatron/actor', type: 'Person', name: 'ashes' })
  })

  await p.refreshProfile()

  t.is(sent.length, 1, 'delivered to the follower')
  t.is(sent[0].activity.type, 'Update')
  t.ok(sent[0].activity.object.updated, '⛔ without this Mastodon accepts it and changes nothing')
  t.ok(!isNaN(Date.parse(sent[0].activity.object.updated)), 'and it is a real timestamp')
})

// ⛔ THE POISONED FINGERPRINT. refreshProfile stored the fingerprint BEFORE checking recipients, so
// a run with no followers recorded "already sent this" — and every later run short-circuited. The
// profile was then permanently un-sendable, and the only symptom was silence.
test('⛔⛔ a run with no followers does not mark the profile as sent', async (t) => {
  const sent = []
  const followers = new MemoryFollowerStore([])
  const state = new MemoryState([], true)
  const mk = () => new Publisher({
    records: { async getProfile () { return { name: 'ashes' } }, async listNotes () { return { notes: [], cursor: null } } },
    followers,
    deliver: async (d) => { sent.push(d) },
    actorId: 'https://ap.7rnx.net/ap/users/ashatron/actor',
    newId: () => 'id1',
    log: () => {},
    state,
    buildActorDoc: async () => ({ id: 'https://ap.7rnx.net/ap/users/ashatron/actor', type: 'Person', name: 'ashes' })
  })

  await mk().refreshProfile()
  t.is(sent.length, 0, 'nobody to send to')

  await followers.add('https://prsm.space/users/fleeky')
  await mk().refreshProfile()
  t.is(sent.length, 1, 'and when a follower arrives, it IS sent — not swallowed by a stale fingerprint')
})

// ⛔⛔ THE SAME DEFECT ONE STEP LATER. The no-followers guard above fixed "recorded as told when
// nobody was told". But the fingerprint is still stored BEFORE the deliveries, and every delivery
// failure is caught and logged rather than propagated — so a run where every recipient failed also
// records "already sent this", and the profile becomes un-sendable without `force`.
//
// This is the shape ashatron's un-refreshed avatar has: the actor advertises an icon that serves,
// and no follower ever re-fetched it.
test('⛔⛔ a run where every delivery FAILS does not mark the profile as sent', async (t) => {
  let attempts = 0
  const followers = new MemoryFollowerStore(['https://prsm.space/users/fleeky'])
  const state = new MemoryState([], true)
  const mk = (fail) => new Publisher({
    records: { async getProfile () { return { name: 'ashes' } }, async listNotes () { return { notes: [], cursor: null } } },
    followers,
    deliver: async () => { attempts++; if (fail) throw new Error('connect ECONNREFUSED') },
    actorId: 'https://ap.7rnx.net/ap/users/ashatron/actor',
    newId: () => 'id1',
    log: () => {},
    state,
    buildActorDoc: async () => ({ id: 'https://ap.7rnx.net/ap/users/ashatron/actor', type: 'Person', name: 'ashes' })
  })

  await mk(true).refreshProfile()
  t.is(attempts, 1, 'it tried')
  t.absent(await state.getProfileFingerprint(), '⛔ nothing landed, so nothing may be recorded as sent')

  // The retry is the point: a profile that failed to reach anybody must still be sendable.
  await mk(false).refreshProfile()
  t.is(attempts, 2, 'a later run tries again rather than short-circuiting')
  t.ok(await state.getProfileFingerprint(), 'and records it once somebody was actually told')
})