OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge / test / observation-feeds.test.js

126 lines · javascript ref:
// observation-feeds.test.js — the gateway writes what it OBSERVES for a user into a feed it owns, and the user's
// node holds it (hyper-social #82, I1 slice S1).
//
// ⛔ REAL feeds, real replication over a pipe, the same harness as feed-holder.test.js: a second corestore
// opening the observation feed BY KEY must read what the gateway wrote, and must be able to check that the
// gateway founded it — the node trusts the observer root it was given at registration, nothing else.

import test from 'brittle'
import fs from 'bare-fs'
import os from 'bare-os'
import { join } from 'bare-path'
import Corestore from 'corestore'
import { Feed } from 'favela-peer/lib/feed-base.js'
import { RecordStore } from 'favela-peer/lib/record-store.js'
import { ObservationFeeds, observationName } from '../lib/observation-feeds.js'

const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'obs-'))

function pipe (a, b) {
  const s1 = a.replicate(true)
  const s2 = b.replicate(false)
  s1.pipe(s2).pipe(s1)
  return () => { s1.destroy(); s2.destroy() }
}

async function until (fn, { timeout = 8000 } = {}) {
  const deadline = Date.now() + timeout
  while (Date.now() < deadline) {
    const v = await fn()
    if (v) return v
    await new Promise((r) => setTimeout(r, 25))
  }
  return null
}

const LIKE = {
  kind: 'like',
  actor: 'https://prsm.space/users/alice',
  subject: 'https://ap.7rnx.net/ap/users/fleeky/objects/01k2post',
  target: { feed: 'aa'.repeat(32), collection: 'post', name: '01k2post' },
  at: 1792000000000,
  activity: 'https://prsm.space/users/alice#likes/1'
}

test('★ the gateway mints its identity ONCE, and keeps it across restarts', async (t) => {
  const storage = tmp()
  const a = new ObservationFeeds({ corestore: new Corestore(join(storage, 'store')), storage })
  await a.ready()
  const root = a.root
  t.ok(/^[0-9a-f]{64}$/.test(root), 'an identity root')
  for (const f of ['vault.json', 'device.json']) {
    const mode = fs.statSync(join(storage, 'observer', f)).mode & 0o777
    t.is(mode, 0o600, f + ' is 0600 (it holds a secret)')
  }
  await a.close()

  const b = new ObservationFeeds({ corestore: new Corestore(join(storage, 'store')), storage })
  await b.ready()
  t.is(b.root, root, 'a restart reuses the same identity — it is never re-minted')
  await b.close()
})

test('★★ one feed per user, founded by the gateway root, readable by a node that holds it by key', async (t) => {
  const storage = tmp()
  const obs = new ObservationFeeds({ corestore: new Corestore(join(storage, 'store')), storage })
  await obs.ready()

  const fleeky = await obs.open('fleeky', 1)
  const ashatron = await obs.open('ashatron', 1)
  t.not(fleeky.key, ashatron.key, 'a feed per user')
  t.is((await obs.open('fleeky', 1)).key, fleeky.key, 'opening again is idempotent')
  t.not((await obs.open('fleeky', 2)).key, fleeky.key, 'a new generation (a re-claimed name) is a new feed')

  const name = await obs.note('fleeky', 1, { ...LIKE, body: '' })
  t.is(name, observationName(LIKE.activity), 'named by a hash of the activity id')

  // The user's node: a SECOND corestore holding the observation feed by key, reading bytes from the same
  // records dir (which stands in for the overlay, as in feed-holder.test.js).
  const node = new Feed(new Corestore(tmp()), Buffer.from(fleeky.key, 'hex'), {
    store: new RecordStore(join(storage, 'records', 'obs-fleeky-1')),
    optimistic: true
  })
  await node.ready()
  t.teardown(() => Promise.all([node.close(), obs.close()]))
  t.teardown(pipe(obs.corestore, node.base.store))

  const rec = await until(async () => { await node.update(); return node.get('observation', name) })
  t.ok(rec, 'the node reads the observation')
  t.alike({ kind: rec.frontmatter.kind, actor: rec.frontmatter.actor, subject: rec.frontmatter.subject, activity: rec.frontmatter.activity },
    { kind: 'like', actor: LIKE.actor, subject: LIKE.subject, activity: LIKE.activity })
  t.alike(rec.frontmatter.target, LIKE.target, 'with the subject mapped to the user\'s own record')

  const founder = await node.founder()
  t.is(founder && founder.identityRoot, obs.root, 'the node can check the gateway founded it')
})

test('a reply carries its text; an Undo removes the observation', async (t) => {
  const storage = tmp()
  const obs = new ObservationFeeds({ corestore: new Corestore(join(storage, 'store')), storage })
  await obs.ready()
  t.teardown(() => obs.close())
  const { feed } = await obs.open('fleeky', 1)

  const reply = { ...LIKE, kind: 'reply', activity: 'https://prsm.space/users/alice/statuses/9/activity', body: 'nice post!' }
  const name = await obs.note('fleeky', 1, reply)
  await feed.update()
  const rec = await feed.get('observation', name)
  t.is(rec.frontmatter.kind, 'reply')
  t.is(rec.body.trim(), 'nice post!', 'the reply text is the record body')

  t.is(await obs.remove('fleeky', 1, reply.activity), true, 'removed')
  await feed.update()
  t.absent(await feed.get('observation', name), 'gone after the Undo')
  t.is(await obs.remove('fleeky', 1, reply.activity), false, 'removing again is a no-op')
})

test('note refuses what it cannot attribute', async (t) => {
  const storage = tmp()
  const obs = new ObservationFeeds({ corestore: new Corestore(join(storage, 'store')), storage })
  await obs.ready()
  t.teardown(() => obs.close())
  await t.exception(() => obs.note('fleeky', 1, { ...LIKE, actor: '' }), /actor/)
  await t.exception(() => obs.note('fleeky', 1, { ...LIKE, activity: '' }), /activity/)
  await t.exception(() => obs.note('fleeky', 1, { ...LIKE, kind: 'dm' }), /kind/)
})