OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / api-config-update.test.js

233 lines · javascript ref:
import test from 'brittle'
import {
  resolveConfigUpdateRoute,
  runConfigUpdateAction
} from '../../packages/core/core/relay-node/api-config-update.js'
import { getStorageCapProvenance } from '../../packages/core/config/storage-cap.js'

test('api config update: route helper maps exact config update route', (t) => {
  t.alike(resolveConfigUpdateRoute('POST', '/api/manage/config'), {
    kind: 'config-update'
  })

  t.is(resolveConfigUpdateRoute('GET', '/api/manage/config'), null)
  t.is(resolveConfigUpdateRoute('POST', '/api/manage/config/extra'), null)
  t.is(resolveConfigUpdateRoute('POST', '/api/manage/configuration'), null)
})

test('api config update: rejects malformed integers and rolls back earlier fields', async (t) => {
  const config = {
    maxConnections: 7,
    maxStorageBytes: 2 * 1048576
  }
  let persisted = false

  const result = await runConfigUpdateAction({
    body: {
      maxConnections: '12',
      maxStorageBytes: '1e9'
    },
    config,
    persistConfig: async () => { persisted = true }
  })

  t.is(result.ok, false)
  t.is(result.status, 400)
  t.alike(result.payload, { error: 'maxStorageBytes must be a valid integer' })
  t.is(config.maxConnections, 7)
  t.is(config.maxStorageBytes, 2 * 1048576)
  t.is(persisted, false)
})

test('api config update: rejects malformed decimals and rolls back earlier fields', async (t) => {
  const config = {
    maxConnections: 7,
    maxRelayBandwidthMbps: 25
  }

  const result = await runConfigUpdateAction({
    body: {
      maxConnections: '12',
      maxRelayBandwidthMbps: '1e3'
    },
    config
  })

  t.is(result.ok, false)
  t.is(result.status, 400)
  t.alike(result.payload, { error: 'maxRelayBandwidthMbps must be a valid number' })
  t.is(config.maxConnections, 7)
  t.is(config.maxRelayBandwidthMbps, 25)
})

test('api config update: applies booleans arrays objects and persists before success', async (t) => {
  const config = {
    discovery: { mdns: true, bootstrap: ['old'] },
    access: { privateMode: false },
    pairing: { enabled: false }
  }
  const persisted = []

  const result = await runConfigUpdateAction({
    body: {
      registryAutoAccept: false,
      replicationRepairEnabled: false,
      gatewayPublicOnlyPrivacyTier: true,
      strictSeedingPrivacy: false,
      enableDistributedDriveBridge: true,
      requireSignedCatalog: true,
      regions: ['NA', 'EU'],
      discovery: { bootstrap: ['new'] },
      access: { privateMode: true },
      pairing: { enabled: true }
    },
    config,
    persistConfig: async () => {
      persisted.push(JSON.parse(JSON.stringify(config)))
    },
    safeConfigPayload: () => ({ safe: true, config })
  })

  t.is(result.ok, true)
  t.alike(result.payload.config, { safe: true, config })
  t.is(config.registryAutoAccept, false)
  t.is(config.replicationRepairEnabled, false)
  t.is(config.gatewayPublicOnlyPrivacyTier, true)
  t.is(config.strictSeedingPrivacy, false)
  t.is(config.enableDistributedDriveBridge, true)
  t.is(config.requireSignedCatalog, true)
  t.alike(config.regions, ['NA', 'EU'])
  t.alike(config.discovery, { mdns: true, bootstrap: ['new'] })
  t.alike(config.access, { privateMode: true })
  t.alike(config.pairing, { enabled: true })
  t.is(persisted.length, 1)
  t.alike(persisted[0].regions, ['NA', 'EU'])
})

test('api config update: rejects malformed regions and nested object fields without mutation', async (t) => {
  const config = {
    maxConnections: 7,
    regions: ['NA'],
    discovery: { mdns: true },
    access: { privateMode: false },
    pairing: { enabled: false }
  }
  let persisted = false

  let result = await runConfigUpdateAction({
    body: {
      maxConnections: '12',
      regions: 'EU'
    },
    config,
    persistConfig: async () => { persisted = true }
  })
  t.is(result.status, 400)
  t.alike(result.payload, { error: 'regions must be an array of strings' })
  t.is(config.maxConnections, 7)
  t.alike(config.regions, ['NA'])
  t.is(persisted, false)

  result = await runConfigUpdateAction({
    body: {
      maxConnections: '12',
      discovery: []
    },
    config,
    persistConfig: async () => { persisted = true }
  })
  t.is(result.status, 400)
  t.alike(result.payload, { error: 'discovery must be an object' })
  t.is(config.maxConnections, 7)
  t.alike(config.discovery, { mdns: true })

  result = await runConfigUpdateAction({
    body: {
      access: null
    },
    config,
    persistConfig: async () => { persisted = true }
  })
  t.is(result.status, 400)
  t.alike(result.payload, { error: 'access must be an object' })
  t.alike(config.access, { privateMode: false })

  result = await runConfigUpdateAction({
    body: {
      pairing: 'enabled'
    },
    config,
    persistConfig: async () => { persisted = true }
  })
  t.is(result.status, 400)
  t.alike(result.payload, { error: 'pairing must be an object' })
  t.alike(config.pairing, { enabled: false })

  result = await runConfigUpdateAction({
    body: {
      maxConnections: '12',
      registryAutoAccept: 'false'
    },
    config,
    persistConfig: async () => { persisted = true }
  })
  t.is(result.status, 400)
  t.alike(result.payload, { error: 'registryAutoAccept must be a boolean' })
  t.is(config.maxConnections, 7)
  t.absent(config.registryAutoAccept)

  result = await runConfigUpdateAction({
    body: {
      requireSignedCatalog: { enabled: true }
    },
    config,
    persistConfig: async () => { persisted = true }
  })
  t.is(result.status, 400)
  t.alike(result.payload, { error: 'requireSignedCatalog must be a boolean' })
  t.absent(config.requireSignedCatalog)
})

test('api config update: persistence failure rolls back all applied fields', async (t) => {
  const previousDiscovery = { mdns: true }
  const config = {
    maxConnections: 7,
    discovery: previousDiscovery
  }

  const result = await runConfigUpdateAction({
    body: {
      maxConnections: '12',
      registryAutoAccept: false,
      discovery: { bootstrap: ['new'] }
    },
    config,
    persistConfig: async () => { throw new Error('readonly config') }
  })

  t.is(result.ok, false)
  t.is(result.kind, 'config-persist')
  t.is(result.error.message, 'readonly config')
  t.is(config.maxConnections, 7)
  t.absent(config.registryAutoAccept)
  t.is(config.discovery, previousDiscovery)
})

test('api config update: maxStorageBytes equal to 50 GiB is marked explicit before persistence', async (t) => {
  const config = { maxStorageBytes: 10 * 1024 ** 3 }
  let persistedProvenance = null

  const result = await runConfigUpdateAction({
    body: { maxStorageBytes: 50 * 1024 ** 3 },
    config,
    persistConfig: async () => {
      persistedProvenance = getStorageCapProvenance(config)
    }
  })

  t.is(result.ok, true)
  t.is(config.maxStorageBytes, 50 * 1024 ** 3)
  t.is(persistedProvenance.explicit, true)
  t.is(persistedProvenance.source, 'management-api')
})