OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / test / name-registry.test.js

214 lines · javascript ref:
/**
 * NameRegistry tests — TOFU app-name registry.
 */

import test from 'brittle'
import fs from '#node:fs'
import path from '#node:path'
import os from '#node:os'
import Corestore from 'corestore'

import { NameRegistry } from '../lib/name-registry.js'

function mktempStore () {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'name-reg-'))
  return { store: new Corestore(dir), dir }
}

function fakePublisher (overrides = {}) {
  return {
    devicePubkey: 'd'.repeat(64),
    registryKey: 'r'.repeat(64),
    serverKey: 'd'.repeat(64),
    identityRoot: 'i'.repeat(64),
    ...overrides
  }
}

// ── Lifecycle ───────────────────────────────────────────────

test('ready + close', async (t) => {
  const { store } = mktempStore()
  const reg = new NameRegistry(store)
  await reg.ready()
  t.ok(reg.keyHex && reg.keyHex.length === 64, 'registry key exposed')
  t.ok(reg.writable, 'fresh registry is writable by its creator')
  await reg.close()
  await store.close()
})

// ── Claim + resolve ─────────────────────────────────────────

test('claim a name, resolve it', async (t) => {
  const { store } = mktempStore()
  const reg = new NameRegistry(store)
  await reg.ready()

  const publisher = fakePublisher()
  await reg.claim({
    name: 'pear-git-v3',
    publisher,
    description: 'p2p git forge'
  })

  const entry = await reg.resolve('pear-git-v3')
  t.ok(entry, 'entry resolved')
  t.is(entry.name, 'pear-git-v3')
  t.is(entry.publisher.devicePubkey, publisher.devicePubkey)
  t.is(entry.publisher.registryKey, publisher.registryKey)
  t.is(entry.description, 'p2p git forge')
  t.ok(entry.claimedAt > 0)

  await reg.close()
  await store.close()
})

test('resolve unknown name returns null', async (t) => {
  const { store } = mktempStore()
  const reg = new NameRegistry(store)
  await reg.ready()

  const res = await reg.resolve('does-not-exist')
  t.is(res, null)

  await reg.close()
  await store.close()
})

// ── First-claim-wins ────────────────────────────────────────

test('duplicate claim is rejected (first-claim-wins)', async (t) => {
  const { store } = mktempStore()
  const reg = new NameRegistry(store)
  await reg.ready()

  const first = fakePublisher({ devicePubkey: '1'.repeat(64), identityRoot: 'a'.repeat(64) })
  const second = fakePublisher({ devicePubkey: '2'.repeat(64), identityRoot: 'b'.repeat(64) })

  await reg.claim({ name: 'squatter', publisher: first, description: 'first' })
  await reg.claim({ name: 'squatter', publisher: second, description: 'second' })

  const entry = await reg.resolve('squatter')
  t.is(entry.description, 'first', 'second claim did not overwrite')
  t.is(entry.publisher.identityRoot, 'a'.repeat(64), 'original publisher wins')

  await reg.close()
  await store.close()
})

// ── Update (publisher-only) ─────────────────────────────────

test('update: only publisher can rotate registryKey', async (t) => {
  const { store } = mktempStore()
  const reg = new NameRegistry(store)
  await reg.ready()

  const publisher = fakePublisher({ identityRoot: 'owner'.padEnd(64, '0') })
  await reg.claim({ name: 'rotatable', publisher })

  // Legit update (signedBy matches identityRoot)
  await reg.updateName('rotatable', { registryKey: 'NEW'.padEnd(64, '0') }, 'owner'.padEnd(64, '0'))
  const updated = await reg.resolve('rotatable')
  t.is(updated.publisher.registryKey, 'NEW'.padEnd(64, '0'), 'publisher update applied')

  // Hostile update (wrong signedBy) — must be ignored
  await reg.updateName('rotatable', { registryKey: 'HIJACK'.padEnd(64, '0') }, 'attacker'.padEnd(64, '0'))
  const afterHijackAttempt = await reg.resolve('rotatable')
  t.is(
    afterHijackAttempt.publisher.registryKey,
    'NEW'.padEnd(64, '0'),
    'hostile update rejected (signedBy mismatch)'
  )

  await reg.close()
  await store.close()
})

// ── Release + reclaim ───────────────────────────────────────

test('release tombstones the claim; resolve returns null; reclaim works', async (t) => {
  const { store } = mktempStore()
  const reg = new NameRegistry(store)
  await reg.ready()

  const first = fakePublisher({ identityRoot: 'first'.padEnd(64, '0'), devicePubkey: 'f'.repeat(64) })
  const second = fakePublisher({ identityRoot: 'second'.padEnd(64, '0'), devicePubkey: 's'.repeat(64) })

  await reg.claim({ name: 'transient', publisher: first })
  t.ok(await reg.resolve('transient'), 'claim visible')

  // Wrong releaser cannot tombstone
  await reg.release('transient', 'attacker'.padEnd(64, '0'))
  t.ok(await reg.resolve('transient'), 'release by non-publisher was rejected')

  // Publisher releases
  await reg.release('transient', 'first'.padEnd(64, '0'))
  t.is(await reg.resolve('transient'), null, 'tombstoned — resolve returns null')

  // Second publisher reclaims
  await reg.claim({ name: 'transient', publisher: second, description: 'reclaimed' })
  const reclaimed = await reg.resolve('transient')
  t.ok(reclaimed, 'reclaim succeeded')
  t.is(reclaimed.publisher.identityRoot, 'second'.padEnd(64, '0'))
  t.is(reclaimed.description, 'reclaimed')

  await reg.close()
  await store.close()
})

// ── List + by-publisher ─────────────────────────────────────

test('list returns all live claims; includeReleased surfaces tombstones', async (t) => {
  const { store } = mktempStore()
  const reg = new NameRegistry(store)
  await reg.ready()

  const pubA = fakePublisher({ identityRoot: 'A'.repeat(64), devicePubkey: '1'.repeat(64) })
  const pubB = fakePublisher({ identityRoot: 'B'.repeat(64), devicePubkey: '2'.repeat(64) })

  await reg.claim({ name: 'apple', publisher: pubA })
  await reg.claim({ name: 'banana', publisher: pubA })
  await reg.claim({ name: 'cherry', publisher: pubB })

  const all = await reg.list()
  t.is(all.length, 3, 'three live claims')

  const onlyA = await reg.listByPublisher('A'.repeat(64))
  t.is(onlyA.length, 2, 'A owns two names')
  t.alike(onlyA.map(e => e.name).sort(), ['apple', 'banana'])

  // Release banana — drops from live list, stays in includeReleased
  await reg.release('banana', 'A'.repeat(64))
  const liveAfterRelease = await reg.list()
  t.is(liveAfterRelease.length, 2, 'banana tombstoned, not listed')
  const withReleased = await reg.list({ includeReleased: true })
  t.is(withReleased.length, 3, 'includeReleased surfaces tombstone')

  await reg.close()
  await store.close()
})

// ── Persistence ─────────────────────────────────────────────

test('claims persist across close+reopen of the same store', async (t) => {
  const { store, dir } = mktempStore()
  const reg1 = new NameRegistry(store)
  await reg1.ready()
  await reg1.claim({ name: 'durable', publisher: fakePublisher() })
  const key = reg1.keyHex
  await reg1.close()
  await store.close()

  // Reopen
  const store2 = new Corestore(dir)
  const reg2 = new NameRegistry(store2)
  await reg2.ready()
  t.is(reg2.keyHex, key, 'same autobase key')
  const entry = await reg2.resolve('durable')
  t.ok(entry, 'claim survived')
  t.is(entry.name, 'durable')

  await reg2.close()
  await store2.close()
})