OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / api-service-management.test.js

249 lines · javascript ref:
import test from 'brittle'
import {
  resolveServiceManagementRoute,
  runServiceManagementAction
} from 'p2p-hiverelay/core/relay-node/api-service-management.js'

function makeRegistry () {
  return {
    services: new Map([
      ['identity', { status: 'running', running: true }],
      ['vrf', { status: 'running', running: true }],
      ['ai', { status: 'running', restartCount: 2 }]
    ]),
    unregistered: [],
    async unregister (name) {
      this.unregistered.push(name)
      this.services.delete(name)
      return true
    }
  }
}

test('api service management: route helper maps exact live service management endpoint', (t) => {
  t.alike(resolveServiceManagementRoute('POST', '/api/manage/services'), {
    kind: 'service-management'
  })

  t.is(resolveServiceManagementRoute('GET', '/api/manage/services'), null)
  t.is(resolveServiceManagementRoute('POST', '/api/manage/services/config'), null)
  t.is(resolveServiceManagementRoute('POST', '/api/manage/services/restart'), null)
})

test('api service management: validates service registry and action payload', async (t) => {
  t.alike(await runServiceManagementAction({ body: {}, registry: null, config: {} }), {
    ok: false,
    kind: 'unavailable',
    status: 503,
    payload: { error: 'Services not enabled' }
  })

  t.alike(await runServiceManagementAction({ body: {}, registry: makeRegistry(), config: {} }), {
    ok: false,
    kind: 'bad-request',
    status: 400,
    payload: { error: 'action and service required (action: disable|restart)' }
  })

  t.alike(await runServiceManagementAction({
    body: { action: 'unknown', service: 'identity' },
    registry: makeRegistry(),
    config: {}
  }), {
    ok: false,
    kind: 'bad-request',
    status: 400,
    payload: { error: 'Unknown action: unknown (use: disable, restart)' }
  })
})

test('api service management: RelayKernel lock rejects service actions before registry or persistence', async (t) => {
  let persisted = false
  const out = await runServiceManagementAction({
    body: { action: 'disable', service: 'ai' },
    registry: null,
    config: { productProfile: 'relaykernel', enableServices: true, plugins: ['ai'] },
    persistConfig: async () => { persisted = true },
    serviceConfigPayload: () => ({
      enabled: false,
      plugins: [],
      active: [],
      locked: true,
      lockReason: 'relaykernel-profile'
    })
  })

  t.is(out.ok, false)
  t.is(out.kind, 'locked')
  t.is(out.status, 409)
  t.is(out.payload.error, 'Services are locked off by the RelayKernel profile')
  t.is(out.payload.errorCode, 'relaykernel-services-locked')
  t.alike(out.payload.config, {
    enabled: false,
    plugins: [],
    active: [],
    locked: true,
    lockReason: 'relaykernel-profile'
  })
  t.is(persisted, false)
})

test('api service management: disable persists configured plugin removal before unregistering', async (t) => {
  const registry = makeRegistry()
  const config = { enableServices: true, plugins: ['identity', 'vrf'] }
  const calls = []

  const out = await runServiceManagementAction({
    body: { action: 'disable', service: 'identity' },
    registry,
    config,
    persistConfig: async () => { calls.push('persist:' + config.plugins.join(',')) },
    serviceConfigPayload: () => ({ plugins: config.plugins, active: Array.from(registry.services.keys()) })
  })

  t.is(out.ok, true)
  t.alike(calls, ['persist:vrf'])
  t.alike(registry.unregistered, ['identity'])
  t.alike(config.plugins, ['vrf'])
  t.is(config.enableServices, true)
  t.alike(out.payload, {
    ok: true,
    action: 'disabled',
    service: 'identity',
    persistent: true,
    config: { plugins: ['vrf'], active: ['vrf', 'ai'] }
  })
})

test('api service management: disable turns off service flag when last configured plugin is removed', async (t) => {
  const registry = makeRegistry()
  const config = { enableServices: true, plugins: ['identity'] }

  const out = await runServiceManagementAction({
    body: { action: 'disable', service: 'identity' },
    registry,
    config,
    persistConfig: async () => {}
  })

  t.is(out.ok, true)
  t.alike(config.plugins, [])
  t.is(config.enableServices, false)
})

test('api service management: disable rolls back config when persistence fails', async (t) => {
  const registry = makeRegistry()
  const config = { enableServices: true, plugins: ['identity', 'vrf'] }

  const out = await runServiceManagementAction({
    body: { action: 'disable', service: 'identity' },
    registry,
    config,
    persistConfig: async () => { throw new Error('readonly config') }
  })

  t.is(out.ok, false)
  t.is(out.kind, 'config-persist')
  t.alike(config.plugins, ['identity', 'vrf'])
  t.is(config.enableServices, true)
  t.alike(registry.unregistered, [])
  t.ok(registry.services.has('identity'))
})

test('api service management: disable rejects bundle dependencies before persistence', async (t) => {
  const registry = makeRegistry()
  const config = { enableServices: true, plugins: ['poker', 'vrf', 'arbitration', 'zk'] }
  let persisted = false

  const out = await runServiceManagementAction({
    body: { action: 'disable', service: 'vrf' },
    registry,
    config,
    persistConfig: async () => { persisted = true }
  })

  t.is(out.ok, false)
  t.is(out.status, 409)
  t.is(out.kind, 'bundle-required')
  t.ok(out.payload.error.includes("Service 'vrf' is required by bundle: poker"))
  t.alike(out.payload.bundles, ['poker'])
  t.is(persisted, false)
  t.alike(registry.unregistered, [])
})

test('api service management: disable handles unconfigured live services', async (t) => {
  const registry = makeRegistry()
  const config = { enableServices: true, plugins: ['identity'] }
  let persisted = false

  const out = await runServiceManagementAction({
    body: { action: 'disable', service: 'vrf' },
    registry,
    config,
    persistConfig: async () => { persisted = true }
  })

  t.is(out.ok, true)
  t.is(out.payload.persistent, false)
  t.is(persisted, false)
  t.alike(config.plugins, ['identity'])
  t.alike(registry.unregistered, ['vrf'])
})

test('api service management: restart delegates to registry restart with node context', async (t) => {
  const registry = makeRegistry()
  const node = { id: 'node' }
  const store = { id: 'store' }
  const config = { plugins: ['ai'] }
  registry.restart = async (service, ctx) => {
    t.is(service, 'ai')
    t.alike(ctx, { node, store, config })
    return { status: 'running', restartCount: 3 }
  }

  const out = await runServiceManagementAction({
    body: { action: 'restart', service: 'ai' },
    registry,
    config,
    node,
    store
  })

  t.alike(out, {
    ok: true,
    payload: {
      ok: true,
      action: 'restarted',
      service: 'ai',
      status: 'running',
      restartCount: 3
    }
  })
})

test('api service management: provider errors and missing services stay typed', async (t) => {
  const registry = makeRegistry()

  let out = await runServiceManagementAction({
    body: { action: 'disable', service: 'missing' },
    registry,
    config: {}
  })
  t.is(out.ok, false)
  t.is(out.kind, 'not-found')
  t.is(out.status, 404)

  registry.restart = async () => { throw new Error('restart failed') }
  out = await runServiceManagementAction({
    body: { action: 'restart', service: 'ai' },
    registry,
    config: {}
  })
  t.alike(out, {
    ok: false,
    kind: 'provider-error',
    status: 500,
    payload: { error: 'restart failed' }
  })
})