OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / api-unseed-actions.test.js

115 lines · javascript ref:
import test from 'brittle'
import {
  OPERATOR_UNSEED_AUTH_MESSAGE,
  resolveUnseedRoute,
  runOperatorUnseedAction,
  runPublisherUnseedAction
} from 'p2p-hiverelay/core/relay-node/api-unseed-actions.js'

test('api unseed actions: route resolver maps only exact operator and publisher unseed routes', (t) => {
  t.alike(resolveUnseedRoute('POST', '/unseed'), {
    kind: 'operator-unseed',
    authMessage: OPERATOR_UNSEED_AUTH_MESSAGE
  })
  t.alike(resolveUnseedRoute('POST', '/api/v1/unseed'), {
    kind: 'publisher-unseed'
  })
  t.is(resolveUnseedRoute('GET', '/unseed'), null, 'wrong method falls through')
  t.is(resolveUnseedRoute('POST', '/unseed/extra'), null, 'operator subpath falls through')
  t.is(resolveUnseedRoute('POST', '/api/v1/unseed/extra'), null, 'publisher subpath falls through')
  t.is(resolveUnseedRoute('POST', '/api/v1/seed'), null, 'adjacent publisher route falls through')
})

test('api unseed actions: operator unseed validates app key before mutation', async (t) => {
  const calls = []
  const node = {
    async unseedApp (appKey) {
      calls.push(appKey)
    }
  }

  let out = await runOperatorUnseedAction({ body: {}, node })
  t.is(out.status, 400)
  t.alike(out.payload, { error: 'appKey required' })

  out = await runOperatorUnseedAction({ body: { appKey: 'not-hex' }, node })
  t.is(out.status, 400)
  t.alike(out.payload, { error: 'appKey must be 64 hex characters' })
  t.alike(calls, [], 'invalid operator unseed does not mutate')

  out = await runOperatorUnseedAction({ body: { appKey: 'a'.repeat(64) }, node })
  t.is(out.ok, true)
  t.alike(out.payload, { ok: true })
  t.alike(calls, ['a'.repeat(64)])
})

test('api unseed actions: publisher-signed unseed validates body before verifier and mutation', async (t) => {
  const calls = []
  const node = {
    verifyUnseedRequest (...args) {
      calls.push(['verify', ...args])
      return { ok: true }
    },
    async unseedApp (appKey) {
      calls.push(['unseed', appKey])
    },
    broadcastUnseed (...args) {
      calls.push(['broadcast', ...args])
    }
  }
  const validBody = {
    appKey: 'a'.repeat(64),
    publisherPubkey: 'b'.repeat(64),
    signature: 'c'.repeat(128),
    timestamp: 123456789
  }

  for (const timestamp of ['123456789', 1.5, -1, Number.MAX_SAFE_INTEGER + 1]) {
    const out = await runPublisherUnseedAction({
      body: { ...validBody, timestamp },
      node
    })
    t.is(out.status, 400, 'malformed timestamp rejected')
    t.alike(out.payload, { error: 'timestamp must be a positive safe integer' })
  }
  t.alike(calls, [], 'malformed signed unseed does not verify, mutate, or broadcast')
})

test('api unseed actions: publisher-signed unseed preserves verifier decision and broadcast order', async (t) => {
  const calls = []
  const node = {
    verifyUnseedRequest (...args) {
      calls.push(['verify', ...args])
      return { ok: true }
    },
    async unseedApp (appKey) {
      calls.push(['unseed', appKey])
    },
    broadcastUnseed (...args) {
      calls.push(['broadcast', ...args])
    }
  }
  const body = {
    appKey: 'd'.repeat(64),
    publisherPubkey: 'e'.repeat(64),
    signature: 'f'.repeat(128),
    timestamp: 987654321
  }

  const out = await runPublisherUnseedAction({ body, node })
  t.is(out.ok, true)
  t.alike(out.payload, { ok: true, message: 'App unseeded and unseed broadcast to network' })
  t.alike(calls, [
    ['verify', body.appKey, body.publisherPubkey, body.signature, body.timestamp],
    ['unseed', body.appKey],
    ['broadcast', body.appKey, body.publisherPubkey, body.signature, body.timestamp]
  ])

  node.verifyUnseedRequest = () => ({ ok: false, error: 'STALE_TIMESTAMP' })
  const beforeReject = calls.length
  const rejected = await runPublisherUnseedAction({ body, node })
  t.is(rejected.status, 403)
  t.alike(rejected.payload, { error: 'STALE_TIMESTAMP' })
  t.is(calls.length, beforeReject, 'verifier rejection does not mutate or broadcast')
})