OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / scripts / test-credits.js

332 lines · javascript ref:
#!/usr/bin/env node

/**
 * Credit & Payment Pipeline Integration Test Suite
 *
 * Tests the full credit, metering, pricing, and billing system:
 *   1. Wallet creation — welcome credits auto-granted
 *   2. Balance & transactions — check, top-up, deduct, history
 *   3. Pricing engine — rate card, cost estimation, comparison
 *   4. Free tier quotas — enforcement and tier promotion
 *   5. Metering — dispatch calls increment meters correctly
 *   6. Freeze/unfreeze — admin wallet controls
 *   7. Credit grants — operator grants free credits
 *   8. Invoice lifecycle — create, status, settle (mock)
 *   9. Cross-relay consistency — credits are per-relay (not shared)
 *
 * Usage: node scripts/test-credits.js
 */

import http from 'http'
import crypto from 'crypto'

const API_KEY = process.env.HIVERELAY_API_KEY || 'hiverelay-secret'
const RELAYS = [
  { name: 'Utah', host: process.env.UTAH_IP || '144.172.101.215', port: 9100 },
  { name: 'Utah-US', host: process.env.UTAH_US_IP || '144.172.91.26', port: 9100 },
  { name: 'Singapore', host: process.env.SINGAPORE_IP || '104.194.153.179', port: 9100 }
]

function request (relay, method, path, body = null, opts = {}) {
  return new Promise((resolve, reject) => {
    const headers = { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' }
    if (opts.headers) Object.assign(headers, opts.headers)
    const payload = body ? JSON.stringify(body) : null
    if (payload) headers['Content-Length'] = Buffer.byteLength(payload)
    const start = process.hrtime.bigint()
    const req = http.request({
      method, hostname: relay.host, port: relay.port, path, headers, timeout: opts.timeout || 15000
    }, (res) => {
      let data = ''
      res.setEncoding('utf8')
      res.on('data', chunk => { data += chunk })
      res.on('end', () => {
        const ms = Math.round(Number(process.hrtime.bigint() - start) / 1e6)
        try { resolve({ status: res.statusCode, data: JSON.parse(data), ms }) } catch { resolve({ status: res.statusCode, data, ms }) }
      })
    })
    req.on('error', reject)
    req.on('timeout', () => { req.destroy(); reject(new Error('TIMEOUT')) })
    if (payload) req.write(payload)
    req.end()
  })
}

function dispatch (relay, route, params = {}) {
  return request(relay, 'POST', '/api/v1/dispatch', { route, params })
}

let passed = 0; let failed = 0
const results = []
function log (icon, msg) { console.log(`  ${icon} ${msg}`) }
async function test (name, fn) {
  const start = Date.now()
  try { await fn(); log('✅', `${name} (${Date.now() - start}ms)`); passed++; results.push({ name, status: 'pass' }) } catch (err) { log('❌', `${name} — ${err.message}`); failed++; results.push({ name, status: 'fail', error: err.message }) }
}
function assert (cond, msg) { if (!cond) throw new Error(msg) }

const TEST_APP_KEY = crypto.randomBytes(32).toString('hex')
const relay = RELAYS[1] // Utah-US for all credit tests

async function runTests () {
  console.log('\n╔═══════════════════════════════════════════════════╗')
  console.log('║    HiveRelay Credits & Payments Integration Tests  ║')
  console.log('╚═══════════════════════════════════════════════════╝\n')

  // ──────────────────────────────────────────
  // 1. Pricing Engine
  // ──────────────────────────────────────────
  console.log('── Test Group 1: Pricing Engine ──')

  await test('Rate card available', async () => {
    const res = await request(relay, 'GET', '/api/v1/credits/pricing')
    assert(res.status === 200, `HTTP ${res.status}`)
    assert(res.data, 'Empty pricing data')
    const routes = Object.keys(res.data)
    assert(routes.length > 0, 'No routes in rate card')
    log('ℹ️', `Rate card has ${routes.length} routes: ${routes.slice(0, 5).join(', ')}...`)
  })

  await test('Price comparison available', async () => {
    const res = await request(relay, 'GET', '/api/v1/credits/pricing/compare')
    assert(res.status === 200, `HTTP ${res.status}`)
    log('ℹ️', `Comparison: ${JSON.stringify(res.data).slice(0, 120)}`)
  })

  await test('Cost estimation works', async () => {
    const res = await request(relay, 'GET', '/api/v1/credits/estimate?route=ai.infer&inputTokens=1000&outputTokens=500')
    assert(res.status === 200, `HTTP ${res.status}`)
    assert(res.data.cost !== undefined || res.data.total !== undefined, 'No cost in estimate')
    log('ℹ️', `Estimate for ai.infer (1K in, 500 out): ${JSON.stringify(res.data)}`)
  })

  // ──────────────────────────────────────────
  // 2. Wallet & Welcome Credits
  // ──────────────────────────────────────────
  console.log('\n── Test Group 2: Wallet & Welcome Credits ──')

  await test('Check balance for new app key (triggers welcome credits)', async () => {
    const res = await request(relay, 'GET', `/api/v1/credits/balance/${TEST_APP_KEY}`)
    assert(res.status === 200, `HTTP ${res.status}`)
    // New wallet may have 0 or welcome credits depending on auto-create behavior
    log('ℹ️', `New app balance: ${JSON.stringify(res.data)}`)
  })

  await test('Top up credits for test app', async () => {
    const res = await request(relay, 'POST', '/api/v1/credits/topup', {
      appKey: TEST_APP_KEY,
      amount: 5000
    })
    assert(res.status === 200, `HTTP ${res.status}: ${JSON.stringify(res.data)}`)
    log('ℹ️', `Topped up: ${JSON.stringify(res.data)}`)
  })

  await test('Verify balance after top-up', async () => {
    const res = await request(relay, 'GET', `/api/v1/credits/balance/${TEST_APP_KEY}`)
    assert(res.status === 200, `HTTP ${res.status}`)
    const balance = res.data.balance ?? res.data
    assert(balance >= 5000, `Expected ≥5000 balance, got ${balance}`)
    log('ℹ️', `Balance: ${JSON.stringify(res.data).slice(0, 100)}`)
  })

  await test('Transaction history has entries', async () => {
    const res = await request(relay, 'GET', `/api/v1/credits/transactions/${TEST_APP_KEY}?limit=10`)
    assert(res.status === 200, `HTTP ${res.status}`)
    const txs = res.data.transactions || res.data
    assert(Array.isArray(txs), 'Expected transaction array')
    assert(txs.length > 0, 'No transactions found')
    log('ℹ️', `${txs.length} transactions for test app`)
  })

  // ──────────────────────────────────────────
  // 3. Billing & Metering
  // ──────────────────────────────────────────
  console.log('\n── Test Group 3: Billing & Metering ──')

  await test('Billing stats accessible', async () => {
    const res = await request(relay, 'GET', '/api/v1/billing/stats')
    assert(res.status === 200, `HTTP ${res.status}`)
    assert(res.data.totalCalls !== undefined, 'Missing totalCalls')
    log('ℹ️', `Total calls: ${res.data.totalCalls}, revenue: ${res.data.totalRevenue}`)
  })

  await test('Dispatch calls increment metering counter', async () => {
    const before = await request(relay, 'GET', '/api/v1/billing/stats')
    const callsBefore = before.data.totalCalls

    // Make 3 dispatch calls
    await dispatch(relay, 'schema.list')
    await dispatch(relay, 'sla.stats')
    await dispatch(relay, 'identity.whoami')

    const after = await request(relay, 'GET', '/api/v1/billing/stats')
    assert(after.data.totalCalls >= callsBefore + 3,
      `Expected ≥${callsBefore + 3}, got ${after.data.totalCalls}`)
    log('ℹ️', `Metering: ${callsBefore} → ${after.data.totalCalls}`)
  })

  // ──────────────────────────────────────────
  // 4. Free Tier Quotas
  // ──────────────────────────────────────────
  console.log('\n── Test Group 4: Free Tier Quotas ──')

  await test('Quota check for test app', async () => {
    const res = await request(relay, 'GET', `/api/v1/billing/quota/${TEST_APP_KEY}`)
    assert(res.status === 200, `HTTP ${res.status}`)
    log('ℹ️', `Quota: ${JSON.stringify(res.data).slice(0, 150)}`)
  })

  await test('Usage tracking for test app', async () => {
    const res = await request(relay, 'GET', `/api/v1/billing/usage/${TEST_APP_KEY}`)
    assert(res.status === 200, `HTTP ${res.status}`)
    log('ℹ️', `Usage: ${JSON.stringify(res.data).slice(0, 150)}`)
  })

  await test('Set app to standard tier', async () => {
    const res = await request(relay, 'POST', '/api/v1/billing/tier', {
      appKey: TEST_APP_KEY,
      tier: 'standard'
    })
    assert(res.status === 200, `HTTP ${res.status}`)
    assert(res.data.ok || res.data.tier === 'standard', `Unexpected: ${JSON.stringify(res.data)}`)
  })

  await test('Add app to unlimited whitelist', async () => {
    const res = await request(relay, 'POST', '/api/v1/billing/whitelist', {
      appKey: TEST_APP_KEY
    })
    assert(res.status === 200, `HTTP ${res.status}`)
  })

  // ──────────────────────────────────────────
  // 5. Freeze / Unfreeze
  // ──────────────────────────────────────────
  console.log('\n── Test Group 5: Wallet Admin Controls ──')

  await test('Freeze wallet', async () => {
    const res = await request(relay, 'POST', '/api/v1/credits/freeze', {
      appKey: TEST_APP_KEY,
      reason: 'integration-test'
    })
    assert(res.status === 200, `HTTP ${res.status}: ${JSON.stringify(res.data)}`)
  })

  await test('Frozen wallet shows frozen status', async () => {
    const res = await request(relay, 'GET', `/api/v1/credits/balance/${TEST_APP_KEY}`)
    assert(res.status === 200, `HTTP ${res.status}`)
    // Check if frozen flag is set
    if (res.data.frozen !== undefined) {
      assert(res.data.frozen === true, `Expected frozen=true, got ${res.data.frozen}`)
    }
    log('ℹ️', `Wallet status: ${JSON.stringify(res.data).slice(0, 100)}`)
  })

  await test('Unfreeze wallet', async () => {
    const res = await request(relay, 'POST', '/api/v1/credits/unfreeze', {
      appKey: TEST_APP_KEY
    })
    assert(res.status === 200, `HTTP ${res.status}: ${JSON.stringify(res.data)}`)
  })

  // ──────────────────────────────────────────
  // 6. Credit Grants
  // ──────────────────────────────────────────
  console.log('\n── Test Group 6: Credit Grants ──')

  await test('Grant free credits', async () => {
    const balanceBefore = await request(relay, 'GET', `/api/v1/credits/balance/${TEST_APP_KEY}`)
    const before = balanceBefore.data.balance || 0

    const res = await request(relay, 'POST', '/api/v1/credits/grant', {
      appKey: TEST_APP_KEY,
      amount: 2000,
      reason: 'integration-test-grant'
    })
    assert(res.status === 200, `HTTP ${res.status}: ${JSON.stringify(res.data)}`)

    const balanceAfter = await request(relay, 'GET', `/api/v1/credits/balance/${TEST_APP_KEY}`)
    const after = balanceAfter.data.balance || 0
    assert(after >= before + 2000, `Expected balance ≥${before + 2000}, got ${after}`)
    log('ℹ️', `Balance: ${before} → ${after} (granted 2000)`)
  })

  // ──────────────────────────────────────────
  // 7. Invoice System
  // ──────────────────────────────────────────
  console.log('\n── Test Group 7: Invoice System ──')

  await test('Create Lightning invoice', async () => {
    const res = await request(relay, 'POST', '/api/v1/credits/invoice', {
      appKey: TEST_APP_KEY,
      amount: 1000,
      memo: 'Integration test invoice'
    })
    // May fail if Lightning not configured (expected in test environment)
    if (res.status === 200) {
      assert(res.data.id, 'Missing invoice ID')
      log('ℹ️', `Invoice created: ${res.data.id}, amount: ${res.data.amountSats || res.data.amount}`)
    } else {
      log('ℹ️', `Invoice creation returned ${res.status} (Lightning may not be configured)`)
    }
  })

  await test('List app invoices', async () => {
    const res = await request(relay, 'GET', `/api/v1/credits/invoices/${TEST_APP_KEY}`)
    if (res.status === 200) {
      const invoices = res.data.invoices || res.data
      log('ℹ️', `${Array.isArray(invoices) ? invoices.length : 0} invoices for test app`)
    } else {
      log('ℹ️', `Invoices endpoint returned ${res.status}`)
    }
  })

  // ──────────────────────────────────────────
  // 8. Aggregate Stats
  // ──────────────────────────────────────────
  console.log('\n── Test Group 8: Aggregate Stats ──')

  await test('Credit system stats', async () => {
    const res = await request(relay, 'GET', '/api/v1/credits/stats')
    assert(res.status === 200, `HTTP ${res.status}`)
    log('ℹ️', `Credit stats: ${JSON.stringify(res.data).slice(0, 150)}`)
  })

  await test('Overview includes credit, metering, and invoice data', async () => {
    const res = await request(relay, 'GET', '/api/overview')
    assert(res.status === 200, `HTTP ${res.status}`)
    assert(res.data.credits, 'Missing credits in overview')
    assert(res.data.metering, 'Missing metering in overview')
    log('ℹ️', `Credits: ${JSON.stringify(res.data.credits).slice(0, 80)}`)
    log('ℹ️', `Metering: ${JSON.stringify(res.data.metering).slice(0, 80)}`)
  })

  // ──────────────────────────────────────────
  // 9. Cross-Relay Independence
  // ──────────────────────────────────────────
  console.log('\n── Test Group 9: Cross-Relay Independence ──')

  await test('Credits are per-relay (not shared)', async () => {
    // The test app was topped up on relay[1] — check relay[0] has different state
    const res0 = await request(RELAYS[0], 'GET', `/api/v1/credits/balance/${TEST_APP_KEY}`)
    const res1 = await request(relay, 'GET', `/api/v1/credits/balance/${TEST_APP_KEY}`)
    // relay[0] should have 0 or just welcome credits (different from relay[1])
    log('ℹ️', `${RELAYS[0].name}: ${JSON.stringify(res0.data).slice(0, 60)}`)
    log('ℹ️', `${relay.name}: ${JSON.stringify(res1.data).slice(0, 60)}`)
  })

  // ──────────────────────────────────────────
  // Summary
  // ──────────────────────────────────────────
  console.log('\n╔═══════════════════════════════════════════════════╗')
  console.log(`║  Results: ${passed} passed, ${failed} failed`)
  console.log('╚═══════════════════════════════════════════════════╝')
  if (failed > 0) {
    console.log('\nFailed tests:')
    for (const r of results.filter(r => r.status === 'fail')) console.log(`  ❌ ${r.name}: ${r.error}`)
  }
  console.log()
  process.exit(failed > 0 ? 1 : 0)
}

runTests().catch(err => { console.error('Fatal:', err); process.exit(1) })