OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / api-custody-status.test.js

294 lines · javascript ref:
import test from 'brittle'
import {
  buildCustodyStatusPayload,
  buildCustodyStatusRoutePayload,
  custodyStatusIntentId,
  detailedCustodyStatus,
  isCustodyStatusRoute,
  isDetailedCustodyStatusQuery,
  redactCustodyReceipt,
  redactCustodyStatus,
  resolveCustodyStatusRoute
} from '../../packages/core/core/relay-node/api-custody-status.js'

const INTENT_ID = 'a'.repeat(64)

function makeUrl (path) {
  return new URL(path, 'http://localhost')
}

function makeRegistry (status = {}) {
  const calls = []
  return {
    calls,
    getCustodyStatus (intentId) {
      calls.push(intentId)
      return { intentId, ...status }
    }
  }
}

test('api custody status: route helper validates registry and intent id before lookup', (t) => {
  t.alike(buildCustodyStatusRoutePayload({
    path: `/api/custody/${INTENT_ID}/status`,
    url: makeUrl(`/api/custody/${INTENT_ID}/status`),
    registry: null
  }), {
    status: 503,
    payload: { error: 'Registry not running' }
  })

  const registry = makeRegistry()
  t.alike(buildCustodyStatusRoutePayload({
    path: '/api/custody/not-hex/status',
    url: makeUrl('/api/custody/not-hex/status'),
    registry
  }), {
    status: 400,
    payload: { error: 'intentId must be 64 hex characters' }
  })
  t.alike(registry.calls, [])
})

test('api custody status: disabled profile rejects before registry lookup', (t) => {
  const result = buildCustodyStatusRoutePayload({
    path: `/api/custody/${INTENT_ID}/status`,
    url: makeUrl(`/api/custody/${INTENT_ID}/status`),
    disabled: true,
    registry: {
      getCustodyStatus () {
        t.fail('disabled custody status must not reach registry')
      }
    }
  })

  t.is(result.ok, false)
  t.is(result.kind, 'disabled-profile')
  t.is(result.status, 409)
  t.ok(result.payload.error.startsWith('not-enabled: '), 'status read returns formatted disabled-profile error')
})

test('api custody status: route helper builds public and detailed payloads', (t) => {
  const registry = makeRegistry({
    receipts: [{ relayPubkey: 'b'.repeat(64), shareIndex: 1, relayRegion: 'secret-region' }],
    pvss: { shareScheme: 'pvss-secp256k1-v1' }
  })

  const publicResult = buildCustodyStatusRoutePayload({
    path: `/api/custody/${INTENT_ID}/status`,
    url: makeUrl(`/api/custody/${INTENT_ID}/status`),
    registry
  })
  t.absent(publicResult.requiresAuth)
  t.is(publicResult.intentId, INTENT_ID)
  t.is(publicResult.status || 200, 200)
  t.absent(Object.prototype.hasOwnProperty.call(publicResult.payload, 'pvss'))
  t.absent(Object.prototype.hasOwnProperty.call(publicResult.payload.receipts[0], 'relayRegion'))

  const detailedResult = buildCustodyStatusRoutePayload({
    path: `/api/custody/${INTENT_ID}/status`,
    url: makeUrl(`/api/custody/${INTENT_ID}/status?detailed=true`),
    registry
  })
  t.is(detailedResult.requiresAuth, true)
  t.is(detailedResult.authMessage, 'Unauthorized — API key required for detailed custody status')
  t.is(detailedResult.payload.pvss.shareScheme, 'pvss-secp256k1-v1')
  t.alike(registry.calls, [INTENT_ID, INTENT_ID])
})

test('api custody status: route query helpers stay narrow', (t) => {
  t.alike(resolveCustodyStatusRoute('GET', `/api/custody/${INTENT_ID}/status`), {
    kind: 'custody-status'
  })
  t.is(resolveCustodyStatusRoute('POST', `/api/custody/${INTENT_ID}/status`), null)
  t.is(resolveCustodyStatusRoute('GET', '/api/custody/not-hex/commit'), null)
  t.is(isCustodyStatusRoute(`/api/custody/${INTENT_ID}/status`), true)
  t.is(isCustodyStatusRoute('/api/custody/not-hex/status'), true)
  t.is(isCustodyStatusRoute('/api/custody/not-hex/commit'), false)
  t.is(isCustodyStatusRoute('/api/v1/custody/not-hex/status'), false)
  t.is(custodyStatusIntentId(`/api/custody/${INTENT_ID}/status`), INTENT_ID)
  t.is(custodyStatusIntentId('/api/custody/not-hex/commit'), '')
  t.is(isDetailedCustodyStatusQuery(makeUrl('/api/custody/x/status?detailed=1')), true)
  t.is(isDetailedCustodyStatusQuery(makeUrl('/api/custody/x/status?detailed=true')), true)
  t.is(isDetailedCustodyStatusQuery(makeUrl('/api/custody/x/status?detailed=yes')), false)
})

test('api custody status: redacted receipts expose only public attestation fields', (t) => {
  const receipt = redactCustodyReceipt({
    relayPubkey: 'b'.repeat(64),
    shareIndex: 2,
    shareVerified: true,
    anchored: true,
    addressKey: '9'.repeat(64),
    blindContentId: 'c'.repeat(64),
    ciphertextRoot: '8'.repeat(64),
    relayRegion: 'us-test',
    signature: '7'.repeat(128)
  })

  t.alike(receipt, {
    relayPubkey: 'b'.repeat(64),
    shareIndex: 2,
    shareVerified: true,
    anchored: true
  })
  t.alike(Object.keys(receipt).sort(), ['anchored', 'relayPubkey', 'shareIndex', 'shareVerified'])
  t.absent(JSON.stringify(receipt).includes('9'.repeat(64)))
  t.absent(JSON.stringify(receipt).includes('8'.repeat(64)))
  t.absent(JSON.stringify(receipt).includes('7'.repeat(128)))
  t.absent(JSON.stringify(receipt).includes('us-test'))
})

test('api custody status: redacted status omits detailed intent proof and witness bodies', (t) => {
  const status = redactCustodyStatus({
    intentId: 'a'.repeat(64),
    blindContentId: 'c'.repeat(64),
    custodyMode: 'blind',
    requiredReplicas: 3,
    receiptCount: 1,
    quorumReached: true,
    receiptRoot: 'd'.repeat(64),
    relayQuorum: ['b'.repeat(64)],
    receipts: [{
      relayPubkey: 'b'.repeat(64),
      shareIndex: 1,
      shareVerified: true,
      anchored: true,
      addressKey: '9'.repeat(64),
      ciphertextRoot: '8'.repeat(64),
      signature: '7'.repeat(128)
    }],
    committed: true,
    sourceRetired: true,
    proofCount: 2,
    passingProofs: 1,
    nonServingProofCount: 1,
    nonServingRelays: ['b'.repeat(64)],
    expiryWitnessCount: 1,
    validExpiryWitnessCount: 1,
    expiryWitnessRelays: ['b'.repeat(64)],
    intent: { shareBundleKey: 'f'.repeat(64) },
    commit: { publisherSignature: 'e'.repeat(128) },
    proofs: [{ nonce: 'proof-secret' }],
    nonServingProofs: [{ signature: 'non-serving-secret' }],
    expiryWitnesses: [{ signature: 'witness-secret' }]
  })

  t.is(status.intentId, 'a'.repeat(64))
  t.is(status.receipts.length, 1)
  t.alike(Object.keys(status.receipts[0]).sort(), ['anchored', 'relayPubkey', 'shareIndex', 'shareVerified'])
  t.absent(Object.prototype.hasOwnProperty.call(status, 'intent'))
  t.absent(Object.prototype.hasOwnProperty.call(status, 'commit'))
  t.absent(Object.prototype.hasOwnProperty.call(status, 'proofs'))
  t.absent(Object.prototype.hasOwnProperty.call(status, 'nonServingProofs'))
  t.absent(Object.prototype.hasOwnProperty.call(status, 'expiryWitnesses'))
  t.absent(JSON.stringify(status).includes('shareBundleKey'))
  t.absent(JSON.stringify(status).includes('publisherSignature'))
  t.absent(JSON.stringify(status).includes('proof-secret'))
  t.absent(JSON.stringify(status).includes('non-serving-secret'))
  t.absent(JSON.stringify(status).includes('witness-secret'))
  t.absent(JSON.stringify(status).includes('7'.repeat(128)))
})

test('api custody status: detailed status preserves diagnostics without raw proof bodies', (t) => {
  const status = detailedCustodyStatus({
    intentId: 'a'.repeat(64),
    blindContentId: 'c'.repeat(64),
    custodyMode: 'blind',
    requiredReplicas: 3,
    receiptCount: 1,
    quorumReached: true,
    receiptRoot: 'd'.repeat(64),
    relayQuorum: ['b'.repeat(64)],
    receipts: [{
      relayPubkey: 'b'.repeat(64),
      shareIndex: 1,
      shareVerified: true,
      anchored: true,
      relayRegion: 'eu-west',
      receivedAt: 10,
      attestedAt: 20,
      addressKey: '9'.repeat(64),
      ciphertextRoot: '8'.repeat(64),
      signature: '7'.repeat(128)
    }],
    pvss: {
      shareScheme: 'pvss-secp256k1-v1',
      shareThreshold: 2,
      commitmentRoot: 'E'.repeat(64),
      shareIndices: [2, 1, 'bad']
    },
    committed: true,
    sourceRetired: false,
    commitPendingReason: 'waiting for receipts',
    sourceRetirementPendingReason: 'bad\nreason',
    intent: { shareBundleKey: 'f'.repeat(64) },
    commit: { publisherSignature: 'e'.repeat(128) },
    proofs: [{ nonce: 'proof-secret' }],
    nonServingProofs: [{ signature: 'non-serving-secret' }],
    expiryWitnesses: [{ signature: 'witness-secret' }]
  })

  t.is(status.committed, true)
  t.is(status.sourceRetired, false)
  t.is(status.commitPendingReason, 'waiting for receipts')
  t.is(status.sourceRetirementPendingReason, null)
  t.alike(status.pvss, {
    shareScheme: 'pvss-secp256k1-v1',
    shareThreshold: 2,
    commitmentRoot: 'e'.repeat(64),
    shareIndices: [2, 1]
  })
  t.alike(status.receipts[0], {
    relayPubkey: 'b'.repeat(64),
    shareIndex: 1,
    shareVerified: true,
    anchored: true,
    relayRegion: 'eu-west',
    receivedAt: 10,
    attestedAt: 20
  })
  t.absent(Object.prototype.hasOwnProperty.call(status, 'intent'))
  t.absent(Object.prototype.hasOwnProperty.call(status, 'commit'))
  t.absent(Object.prototype.hasOwnProperty.call(status, 'proofs'))
  t.absent(Object.prototype.hasOwnProperty.call(status, 'nonServingProofs'))
  t.absent(Object.prototype.hasOwnProperty.call(status, 'expiryWitnesses'))
  const json = JSON.stringify(status)
  for (const hidden of ['shareBundleKey', 'publisherSignature', 'proof-secret', 'non-serving-secret', 'witness-secret', '7'.repeat(128), '9'.repeat(64), '8'.repeat(64)]) {
    t.absent(json.includes(hidden), hidden + ' omitted from detailed custody status')
  }
})

test('api custody status: missing or malformed fields normalize to stable defaults', (t) => {
  t.alike(buildCustodyStatusPayload({ receipts: [] }), redactCustodyStatus({ receipts: [] }))
  t.alike(buildCustodyStatusPayload({ receipts: [] }, { detailed: true }), detailedCustodyStatus({ receipts: [] }))

  t.alike(redactCustodyReceipt({ shareIndex: '2' }), {
    relayPubkey: null,
    shareIndex: null,
    shareVerified: false,
    anchored: false
  })

  t.alike(redactCustodyStatus({ receipts: 'not-array', relayQuorum: 'not-array' }), {
    intentId: null,
    blindContentId: null,
    custodyMode: 'blind',
    requiredReplicas: 0,
    receiptCount: 0,
    quorumReached: false,
    receiptRoot: null,
    relayQuorum: [],
    receipts: [],
    committed: false,
    sourceRetired: false,
    proofCount: 0,
    passingProofs: 0,
    nonServingProofCount: 0,
    nonServingRelays: [],
    expiryWitnessCount: 0,
    validExpiryWitnessCount: 0,
    expiryWitnessRelays: []
  })
})