OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / manifest-store.test.js

225 lines · javascript ref:
import test from 'brittle'
import { tmpdir } from 'os'
import { join } from 'path'
import { mkdtemp, rm } from 'fs/promises'
import b4a from 'b4a'
import sodium from 'sodium-universal'
import { ManifestStore } from 'p2p-hiverelay/core/manifest-store.js'
import { createSeedingManifest } from 'p2p-hiverelay/core/seeding-manifest.js'

function makeKeyPair () {
  const publicKey = b4a.alloc(32)
  const secretKey = b4a.alloc(64)
  sodium.crypto_sign_keypair(publicKey, secretKey)
  return { publicKey, secretKey }
}

function validHex (n) {
  return Array.from({ length: n }).map((_, i) => (i % 16).toString(16)).join('')
}

const DRIVE_KEY = validHex(64)

async function setup (t, storeOpts = {}) {
  const dir = await mkdtemp(join(tmpdir(), 'hr-manifest-store-'))
  t.teardown(async () => { try { await rm(dir, { recursive: true, force: true }) } catch (_) {} })
  return new ManifestStore({ storagePath: join(dir, 'manifests.json'), ...storeOpts })
}

function mkManifest (keyPair, opts = {}) {
  return createSeedingManifest({
    keyPair,
    relays: [{ url: 'hyperswarm://a', role: 'primary' }],
    drives: [{ driveKey: DRIVE_KEY }],
    timestamp: opts.timestamp
  })
}

test('put + get stores and retrieves a valid manifest', async (t) => {
  const store = await setup(t)
  const kp = makeKeyPair()
  const m = mkManifest(kp)
  const result = store.put(m)
  t.ok(result.ok)
  t.is(result.replaced, false)
  const pubkeyHex = b4a.toString(kp.publicKey, 'hex')
  const fetched = store.get(pubkeyHex)
  t.is(fetched.signature, m.signature)
})

test('put rejects unsigned / invalid manifests', async (t) => {
  const store = await setup(t)
  const result = store.put({ type: 'not/the/right/type' })
  t.absent(result.ok)
  t.is(store.size(), 0)
})

test('put replaces older manifest with newer one', async (t) => {
  const store = await setup(t)
  const kp = makeKeyPair()
  const older = mkManifest(kp, { timestamp: 1000 })
  const newer = mkManifest(kp, { timestamp: 2000 })

  t.is(store.put(older).ok, true)
  const r = store.put(newer)
  t.ok(r.ok)
  t.is(r.replaced, true)
  t.is(store.get(b4a.toString(kp.publicKey, 'hex')).timestamp, 2000)
})

test('put rejects older manifest when newer exists', async (t) => {
  const store = await setup(t)
  const kp = makeKeyPair()
  const newer = mkManifest(kp, { timestamp: 2000 })
  const older = mkManifest(kp, { timestamp: 1000 })

  t.is(store.put(newer).ok, true)
  const r = store.put(older)
  t.absent(r.ok)
  t.ok(/stale/.test(r.reason))
  // Still holds newer.
  t.is(store.get(b4a.toString(kp.publicKey, 'hex')).timestamp, 2000)
})

test('get is case-insensitive on pubkey', async (t) => {
  const store = await setup(t)
  const kp = makeKeyPair()
  const m = mkManifest(kp)
  store.put(m)
  const hex = b4a.toString(kp.publicKey, 'hex')
  t.ok(store.get(hex.toUpperCase()))
  t.ok(store.get(hex.toLowerCase()))
})

test('save + load persists manifests across instances', async (t) => {
  const dir = await mkdtemp(join(tmpdir(), 'hr-manifest-store-'))
  t.teardown(async () => { try { await rm(dir, { recursive: true, force: true }) } catch (_) {} })
  const path = join(dir, 'manifests.json')

  const a = new ManifestStore({ storagePath: path })
  const kp = makeKeyPair()
  const m = mkManifest(kp)
  a.put(m)
  await a.save()

  const b = new ManifestStore({ storagePath: path })
  await b.load()
  t.is(b.size(), 1)
  t.is(b.get(b4a.toString(kp.publicKey, 'hex')).signature, m.signature)
})

test('load drops invalid manifests but keeps valid ones', async (t) => {
  const dir = await mkdtemp(join(tmpdir(), 'hr-manifest-store-'))
  t.teardown(async () => { try { await rm(dir, { recursive: true, force: true }) } catch (_) {} })
  const path = join(dir, 'manifests.json')

  const kp = makeKeyPair()
  const good = mkManifest(kp)
  // Seed the file manually with a mix of valid/invalid entries.
  const { writeFile } = await import('fs/promises')
  await writeFile(path, JSON.stringify({
    version: 1,
    entries: [
      { manifest: good, storedAt: Date.now() },
      { manifest: { type: 'garbage', version: 1 }, storedAt: Date.now() },
      { manifest: null, storedAt: 0 }
    ]
  }), 'utf8')

  const store = new ManifestStore({ storagePath: path })
  let rejected = 0
  store.on('load-rejected', () => { rejected++ })
  await store.load()
  t.is(store.size(), 1, 'only the valid manifest survived')
  t.ok(rejected >= 1, 'at least one rejection was emitted')
})

test('enforces maxAuthors cap via oldest-first eviction', async (t) => {
  const store = await setup(t, { maxAuthors: 2 })
  const kpA = makeKeyPair()
  const kpB = makeKeyPair()
  const kpC = makeKeyPair()

  const mA = mkManifest(kpA)
  const mB = mkManifest(kpB)
  const mC = mkManifest(kpC)

  let evicted = 0
  store.on('evicted', () => { evicted++ })

  store.put(mA)
  // Stamp storedAt spread out so ordering is deterministic.
  await new Promise(resolve => setTimeout(resolve, 5))
  store.put(mB)
  await new Promise(resolve => setTimeout(resolve, 5))
  store.put(mC)

  t.is(store.size(), 2)
  t.is(evicted, 1, 'oldest evicted')
  // A was oldest, so A should be gone.
  t.absent(store.get(b4a.toString(kpA.publicKey, 'hex')))
  t.ok(store.get(b4a.toString(kpB.publicKey, 'hex')))
  t.ok(store.get(b4a.toString(kpC.publicKey, 'hex')))
})

test('delete removes a manifest', async (t) => {
  const store = await setup(t)
  const kp = makeKeyPair()
  store.put(mkManifest(kp))
  const hex = b4a.toString(kp.publicKey, 'hex')
  t.ok(store.delete(hex))
  t.absent(store.get(hex))
})

test('snapshot + restore rolls back replacements and cap evictions', async (t) => {
  const store = await setup(t, { maxAuthors: 2 })
  const kpA = makeKeyPair()
  const kpB = makeKeyPair()
  const kpC = makeKeyPair()
  const olderA = mkManifest(kpA, { timestamp: 1000 })
  const newerA = mkManifest(kpA, { timestamp: 2000 })
  const manifestB = mkManifest(kpB)
  const manifestC = mkManifest(kpC)
  const keyA = b4a.toString(kpA.publicKey, 'hex')
  const keyB = b4a.toString(kpB.publicKey, 'hex')
  const keyC = b4a.toString(kpC.publicKey, 'hex')

  store.put(olderA)
  store.put(manifestB)
  const snapshot = store.snapshot()
  store.put(newerA)
  store.put(manifestC)
  t.is(store.size(), 2)
  t.ok(store.get(keyC), 'new manifest changed live store before rollback')

  store.restoreSnapshot(snapshot)
  t.is(store.size(), 2)
  t.is(store.get(keyA).timestamp, 1000, 'replacement rolled back')
  t.ok(store.get(keyB), 'evicted manifest restored')
  t.absent(store.get(keyC), 'new manifest removed')
})

test('list returns snapshot of all manifests', async (t) => {
  const store = await setup(t)
  const kpA = makeKeyPair()
  const kpB = makeKeyPair()
  store.put(mkManifest(kpA))
  store.put(mkManifest(kpB))

  const items = store.list()
  t.is(items.length, 2)
  for (const item of items) {
    t.ok(item.pubkey)
    t.ok(item.manifest)
    t.is(typeof item.storedAt, 'number')
  }
})

test('load is no-op when no storagePath (runtime-only store)', async (t) => {
  const store = new ManifestStore({})
  await store.load()
  t.is(store.size(), 0)
  await store.save() // no-op
  t.pass('save + load are silent when storagePath is null')
})