OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-bridge / pds / test / bare / start-membrane.test.js

95 lines · javascript ref:
// The membrane as a module: started and stopped inside one process, with no exit.
//
// ⛔ WHY. The favela gateway (hyper-social #80) runs the membrane beside ap-bridge in ONE process.
// pds-serve.js did everything at module top level — bound the port, installed SIGINT/SIGTERM handlers
// and called process.exit — so the only way to stop it was to end the process. A role that cannot be
// stopped cannot share a process: an OTA of the gateway, or a second role failing, would have no way
// to release this one's port and store locks.
//
// So: startMembrane(cfg) returns the running membrane plus its stop steps, and running those steps
// leaves the process alive with the port and the front-store free for the next start.

import '../../lib/bare-globals.js' // FIRST: cborg touches TextEncoder at module load
import test from 'brittle'
import fs from 'bare-fs'
import os from 'bare-os'
import path from 'bare-path'
import http from 'bare-http1'
import { startMembrane } from '../../lib/membrane.js'

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

// bootstrap [] keeps the swarm off the public DHT: start/stop is what this tests, not discovery.
function cfgFor (storage, port = 0) {
  return {
    serverDid: 'did:web:pds.test',
    userDomain: '.test',
    frontedDids: [],
    feedRegistries: [],
    crawlRelay: null,
    crawlHostname: null,
    port,
    bind: '127.0.0.1',
    storage
  }
}

function get (port, p) {
  return new Promise((resolve, reject) => {
    const req = http.request({ host: '127.0.0.1', port, path: p, method: 'GET' }, (res) => {
      let body = ''
      res.on('data', (c) => { body += c })
      res.on('end', () => resolve({ status: res.statusCode, body }))
    })
    req.on('error', reject)
    req.end()
  })
}

async function stop (m) {
  for (const [, fn] of m.steps) await fn()
}

test('★ start → serve → stop → start again on the same port and storage, in one process', async (t) => {
  const storage = tmp()
  const one = await startMembrane(cfgFor(storage), { bootstrap: [], log: () => {} })
  t.ok(one.port > 0, 'bound a port')
  t.is((await get(one.port, '/xrpc/com.atproto.server.describeServer')).status, 200, 'serving')
  const key = one.swarmKey

  await stop(one)
  // Same port and same storage: the port is released and the front-store's lock is let go.
  const two = await startMembrane(cfgFor(storage, one.port), { bootstrap: [], log: () => {} })
  t.teardown(() => stop(two))
  t.is(two.port, one.port, 'the same port binds again')
  t.is((await get(two.port, '/xrpc/com.atproto.server.describeServer')).status, 200, 'and serves')
  t.is(two.swarmKey, key, 'the persistent swarm identity is the same one')
})

test('the stop steps are named, so a shutdown can time and report each', async (t) => {
  const m = await startMembrane(cfgFor(tmp()), { bootstrap: [], log: () => {} })
  t.teardown(() => stop(m))
  const names = m.steps.map(([n]) => n)
  t.alike(names.slice(-2), ['server', 'frontStore'], 'the store fd-lock goes last, after the server')
  t.ok(names.includes('swarm') && names.includes('peers') && names.includes('relay'))
})

test('the control verbs are exposed as plain handlers, for whichever socket serves them', async (t) => {
  const m = await startMembrane(cfgFor(tmp()), { bootstrap: [], log: () => {} })
  t.teardown(() => stop(m))
  for (const verb of ['status', 'list-repos', 'list-fronted', 'diagnose', 'set-seq-base', 'emit-sync', 'probe-front', 'drop-front', 'allow-front', 'forget-did', 'resolve-handle']) {
    t.is(typeof m.handlers[verb], 'function', verb)
  }
  t.alike((await m.handlers['list-fronted']({})).fronted, [], 'answers')
})

test('★ metrics: per-DID peer state, the relay seq and the guard, with no network call', async (t) => {
  const m = await startMembrane(cfgFor(tmp()), { bootstrap: [], log: () => {} })
  t.teardown(() => stop(m))
  const x = m.metrics()
  t.alike(x.dids, [], 'nothing fronted, nothing listed')
  t.alike(x.relay, { emitted: 0, seqBase: 0 }, 'the relay seq bsky\'s cursor is compared against')
  t.is(x.guard.configured, false, 'no crawlRelay configured: the guard is off, and says so')
  t.is(x.guard.heals, 0)
})