OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / api-dispatch.test.js

128 lines · javascript ref:
import test from 'brittle'
import {
  DISPATCH_AUTH_MESSAGE,
  resolveDispatchRoute,
  runDispatchAction
} from '../../packages/core/core/relay-node/api-dispatch.js'

function makeRouter (opts = {}) {
  const calls = []
  return {
    calls,
    getRouteAccess: opts.getRouteAccess || (() => null),
    async dispatch (route, params, ctx) {
      calls.push({ route, params, ctx })
      if (opts.error) throw opts.error
      return opts.result || { route, params, role: ctx.role }
    }
  }
}

test('api dispatch: route resolver matches only the exact dispatch POST route', (t) => {
  t.alike(resolveDispatchRoute('POST', '/api/v1/dispatch'), {
    kind: 'dispatch',
    authMessage: DISPATCH_AUTH_MESSAGE
  })
  t.is(resolveDispatchRoute('GET', '/api/v1/dispatch'), null, 'wrong method falls through')
  t.is(resolveDispatchRoute('POST', '/api/v1/dispatch/extra'), null, 'extra path segment falls through')
  t.is(resolveDispatchRoute('POST', '/api/v1/router'), null, 'adjacent v1 route falls through')
})

test('api dispatch: readiness and route validation happen before dispatch', async (t) => {
  let out = await runDispatchAction({
    body: {},
    router: null
  })
  t.is(out.status, 503)
  t.alike(out.payload, { error: 'Router not enabled' })

  const router = makeRouter()
  out = await runDispatchAction({
    body: {},
    router
  })
  t.is(out.status, 400)
  t.alike(out.payload, { error: 'route required (e.g. "ai.infer", "zk.commit")' })
  t.is(router.calls.length, 0, 'missing route does not reach router')
})

test('api dispatch: remote callers cannot use local-only identity routes', async (t) => {
  const router = makeRouter()

  const out = await runDispatchAction({
    body: { route: 'identity.sign', params: { message: 'secret' } },
    router,
    isLocalRequest: false
  })

  t.is(out.status, 403)
  t.alike(out.payload, { error: 'ACCESS_DENIED: identity.sign is local-only' })
  t.is(router.calls.length, 0, 'local-only denial happens before dispatch')
})

test('api dispatch: rejects non-object params before dispatch', async (t) => {
  const router = makeRouter()

  for (const params of [null, [], 'value', 1, true]) {
    const out = await runDispatchAction({
      body: { route: 'storage.get', params },
      router,
      isLocalRequest: false
    })
    t.is(out.status, 400)
    t.alike(out.payload, { error: 'params must be an object' })
  }

  t.is(router.calls.length, 0, 'malformed params do not reach router')
})

test('api dispatch: role mapping preserves HTTP remote caller contract', async (t) => {
  const router = makeRouter({
    getRouteAccess (route) {
      return route === 'secure.admin' ? 'relay-admin' : 'authenticated-user'
    }
  })

  let out = await runDispatchAction({
    body: { route: 'secure.admin', params: { value: 1 } },
    router,
    isLocalRequest: false
  })
  t.alike(out.payload, { ok: true, result: { route: 'secure.admin', params: { value: 1 }, role: 'relay-admin' } })

  out = await runDispatchAction({
    body: { route: 'storage.get' },
    router,
    isLocalRequest: false
  })
  t.is(out.payload.result.role, 'authenticated-user')

  out = await runDispatchAction({
    body: { route: 'identity.verify', params: { signed: true } },
    router,
    isLocalRequest: true
  })
  t.is(out.payload.result.role, 'local')

  t.alike(router.calls.map(call => call.ctx), [
    { transport: 'http', caller: 'remote', role: 'relay-admin', authenticated: true },
    { transport: 'http', caller: 'remote', role: 'authenticated-user', authenticated: true },
    { transport: 'http', caller: 'remote', role: 'local', authenticated: true }
  ])
  t.alike(router.calls[1].params, {}, 'missing params default to empty object')
})

test('api dispatch: router errors stay mapped as bad requests', async (t) => {
  const router = makeRouter({ error: new Error('ROUTE_NOT_FOUND') })

  const out = await runDispatchAction({
    body: { route: 'missing.route' },
    router,
    isLocalRequest: false
  })

  t.is(out.status, 400)
  t.alike(out.payload, { error: 'ROUTE_NOT_FOUND' })
  t.is(router.calls.length, 1)
})