OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / ssh-resync-trigger.test.js

130 lines · javascript ref:
// Unit: #74 Phase 1 — the replication-triggered SSH resync.
//
// The add/remove handlers resync only the daemon that took the write. A key
// enrolled on a PEER lands in the replicated IdentityRegistry view but stays
// blind in this daemon's authorized_keys until its next boot (#73). The
// autobase 'update' event (local append AND remote replication) closes that
// gap. These tests pin: (1) it debounces a burst into one rebuild; (2) an
// 'update' materializes a peer-enrolled key end-to-end via the REAL
// svc._resyncAuthorizedKeys; (3) detach() stops further triggers.

import test from 'brittle'
import { mkdtempSync, readFileSync, rmSync } from '#node:fs'
import os from '#node:os'
import { join } from '#node:path'
import { attachSshResyncTrigger } from '../lib/auth/ssh-resync-trigger.js'
import { sshKeyHandlers } from '../lib/handlers/ssh-keys.js'
import { readManagedFingerprints } from '../lib/auth/authorized-keys.js'

const KDIR = mkdtempSync(join(os.tmpdir(), 'pgv3-sshresync-'))
const PUBKEY = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxYcab1fod7CwpYISeVVOs7wG7EoYTC7AGJSUEOD/W1 contributor@test'
const delay = (ms) => new Promise((r) => setTimeout(r, ms))

// Minimal autobase-shaped event source — exercises the real on/off wiring
// without pulling in a runtime EventEmitter.
function fakeBase () {
  const listeners = new Map()
  return {
    on (ev, fn) { if (!listeners.has(ev)) listeners.set(ev, new Set()); listeners.get(ev).add(fn) },
    off (ev, fn) { const s = listeners.get(ev); if (s) s.delete(fn) },
    emit (ev) { const s = listeners.get(ev); if (s) for (const fn of Array.from(s)) fn() }
  }
}

test('debounces an update burst into a single resync, refires on a later update', async (t) => {
  const base = fakeBase()
  let calls = 0
  const detach = attachSshResyncTrigger({
    base,
    resync: async () => { calls++ },
    debounceMs: 25
  })

  base.emit('update'); base.emit('update'); base.emit('update') // burst
  await delay(60)
  t.is(calls, 1, 'three rapid updates coalesced into one resync')

  base.emit('update')
  await delay(60)
  t.is(calls, 2, 'a later update fires again')

  detach()
})

test('detach removes the listener and cancels a pending timer', async (t) => {
  const base = fakeBase()
  let calls = 0
  const detach = attachSshResyncTrigger({
    base,
    resync: async () => { calls++ },
    debounceMs: 25
  })

  base.emit('update') // schedules a resync 25ms out…
  detach()            // …cancelled before it fires
  await delay(60)
  t.is(calls, 0, 'pending resync cancelled by detach')

  base.emit('update') // listener is gone
  await delay(60)
  t.is(calls, 0, 'no resync after detach')
})

test('no-op for an unusable base or resync (safe to wire unconditionally)', (t) => {
  t.is(typeof attachSshResyncTrigger({ base: null, resync: () => {} }), 'function', 'null base → empty detach')
  t.is(typeof attachSshResyncTrigger({ base: fakeBase(), resync: null }), 'function', 'null resync → empty detach')
  // empty detach must be callable without throwing
  attachSshResyncTrigger({ base: null, resync: null })()
  t.pass('empty detach is callable')
})

// End-to-end: a peer enrolls a key (present in the replicated view, ABSENT
// from the local-db enumerator). An autobase 'update' must materialize it
// into THIS daemon's authorized_keys via the real resync — the #73 fix.
test('update materializes a peer-enrolled key into authorized_keys (#73)', async (t) => {
  const remote = {
    identityId: 'ssh:SHA256:peer',
    method: 'ssh-key',
    publicKey: PUBKEY,
    credentials: { fingerprint: 'SHA256:peer', type: 'ssh-ed25519' }
  }
  let present = false // becomes true when the peer's write replicates in
  const reg = {
    async listAllIdentities () { return [] }, // local-db enumerator stays empty on a mirror
    async listUsers () { return present ? [{ userId: 'peer', identities: ['ssh:SHA256:peer'] }] : [] },
    async getUserWithIdentities (uid) {
      return (present && uid === 'peer') ? { user: { userId: 'peer' }, identities: [remote] } : null
    },
    async isRevoked () { return false }
  }

  const akPath = join(KDIR, 'authorized_keys_e2e')
  const svc = { identityRegistry: reg, _sync: { repos: new Set(['demo']) } }
  for (const { name, fn } of sshKeyHandlers(svc, { authorizedKeysPath: akPath, wrapperPath: '/opt/pgv3-git-shell' })) {
    void name; void fn // handlers register svc._resyncAuthorizedKeys as a side effect
  }

  const base = fakeBase()
  const detach = attachSshResyncTrigger({
    base,
    resync: () => svc._resyncAuthorizedKeys(),
    debounceMs: 25
  })

  // Before replication: an update with nothing new writes an empty block.
  base.emit('update')
  await delay(60)
  t.alike(readManagedFingerprints(readFileSync(akPath, 'utf8')), [], 'no peer key yet')

  // Peer's write replicates in; the autobase fires 'update'.
  present = true
  base.emit('update')
  await delay(60)
  t.alike(readManagedFingerprints(readFileSync(akPath, 'utf8')), ['SHA256:peer'],
    'peer-enrolled key materialized on replication update, no reboot')

  detach()
})

test.hook('cleanup', () => { rmSync(KDIR, { recursive: true, force: true }) })