OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / standalone / test.js

381 lines · javascript ref:
/**
 * Test Suite
 * ===========
 * Verifies all standalone block storage operations.
 * Self-contained — no external DHT needed.
 */

import Corestore from 'corestore'
import Hyperswarm from 'hyperswarm'
import ProtomuxRPC from 'protomux-rpc'
import createTestnet from '@hyperswarm/testnet'
import c from 'compact-encoding'
import b4a from 'b4a'
import crypto from 'crypto'
import { mkdirSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'

// ---------------------------------------------------------------------------
// Mini test runner (zero deps)
// ---------------------------------------------------------------------------

let passed = 0
let failed = 0
const tests = []

function test (name, fn) {
  tests.push({ name, fn })
}

function assert (condition, msg) {
  if (!condition) throw new Error(msg || 'Assertion failed')
}

assert.equal = (a, b, msg) => {
  if (a !== b) throw new Error(msg || `Expected ${a} to equal ${b}`)
}

assert.ok = (v, msg) => {
  if (!v) throw new Error(msg || `Expected truthy value, got ${v}`)
}

async function runTests () {
  console.log('╔═══════════════════════════════════════════════════╗')
  console.log('║  Standalone Block Storage — Test Suite            ║')
  console.log('╚═══════════════════════════════════════════════════╝\n')

  for (const t of tests) {
    try {
      await t.fn()
      console.log(`  ✓ ${t.name}`)
      passed++
    } catch (err) {
      console.log(`  ✗ ${t.name}`)
      console.log(`    ${err.message}`)
      failed++
    }
  }

  console.log(`\n  Results: ${passed} passed, ${failed} failed, ${tests.length} total\n`)

  if (failed > 0) process.exit(1)
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function tmpPath (name) {
  const dir = join(tmpdir(), `block-storage-test-${name}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`)
  mkdirSync(dir, { recursive: true })
  return dir
}

/**
 * Create a server + client pair connected via local Hyperswarm.
 * Returns { server, client, cleanup }
 */
async function createPair () {
  // Local testnet — instant connections, no public DHT
  const testnet = await createTestnet(3)

  // Server side
  const serverStore = new Corestore(tmpPath('server'))
  const core = serverStore.get({ name: 'block-storage' })
  await core.ready()

  const serverSwarm = new Hyperswarm(testnet)
  let serverRPC = null

  serverSwarm.on('connection', (conn) => {
    serverStore.replicate(conn)

    serverRPC = new ProtomuxRPC(conn, {
      id: b4a.from('block-storage-rpc'),
      valueEncoding: c.json
    })

    serverRPC.respond('store-block', async (req) => {
      const buf = b4a.from(req.data, 'base64')
      const result = await core.append(buf)
      return { seq: result.length - 1, length: core.length }
    })

    serverRPC.respond('get-block', async (req) => {
      if (req.seq < 0 || req.seq >= core.length) {
        return { error: `Block ${req.seq} out of range` }
      }
      const block = await core.get(req.seq)
      return { seq: req.seq, data: b4a.toString(block, 'base64'), length: block.length }
    })

    serverRPC.respond('get-info', async () => {
      return { key: b4a.toString(core.key, 'hex'), length: core.length, byteLength: core.byteLength }
    })
  })

  const serverDiscovery = serverSwarm.join(core.discoveryKey, { server: true, client: false })
  await serverDiscovery.flushed()

  // Client side
  const clientStore = new Corestore(tmpPath('client'))
  const clientCore = clientStore.get({ key: core.key })
  await clientCore.ready()

  const clientSwarm = new Hyperswarm(testnet)
  let clientRPC = null

  const connected = new Promise((resolve) => {
    clientSwarm.on('connection', (conn) => {
      clientStore.replicate(conn)
      clientRPC = new ProtomuxRPC(conn, {
        id: b4a.from('block-storage-rpc'),
        valueEncoding: c.json
      })
      resolve()
    })
  })

  const clientDiscovery = clientSwarm.join(clientCore.discoveryKey, { server: false, client: true })
  await clientDiscovery.flushed()
  await connected

  const cleanup = async () => {
    if (clientRPC) clientRPC.destroy()
    if (serverRPC) serverRPC.destroy()
    await clientSwarm.destroy()
    await serverSwarm.destroy()
    await clientStore.close()
    await serverStore.close()
    await testnet.destroy()
  }

  return {
    server: { store: serverStore, core, swarm: serverSwarm, rpc: () => serverRPC },
    client: { store: clientStore, core: clientCore, swarm: clientSwarm, rpc: () => clientRPC },
    cleanup
  }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

test('store a single block and retrieve it', async () => {
  const { client, cleanup } = await createPair()

  try {
    const data = 'Hello, Holepunch!'
    const storeResult = await client.rpc().request('store-block', {
      data: b4a.toString(b4a.from(data), 'base64')
    })

    assert.equal(storeResult.seq, 0, 'First block should be seq 0')
    assert.equal(storeResult.length, 1, 'Core length should be 1')

    const getResult = await client.rpc().request('get-block', { seq: 0 })
    const retrieved = b4a.from(getResult.data, 'base64').toString()
    assert.equal(retrieved, data, 'Retrieved data should match stored data')
  } finally {
    await cleanup()
  }
})

test('store multiple blocks sequentially', async () => {
  const { client, server, cleanup } = await createPair()

  try {
    const blocks = ['block-0', 'block-1', 'block-2', 'block-3', 'block-4']

    for (let i = 0; i < blocks.length; i++) {
      const result = await client.rpc().request('store-block', {
        data: b4a.toString(b4a.from(blocks[i]), 'base64')
      })
      assert.equal(result.seq, i, `Block ${i} should have seq ${i}`)
    }

    assert.equal(server.core.length, 5, 'Core should have 5 blocks')

    // Verify all blocks
    for (let i = 0; i < blocks.length; i++) {
      const result = await client.rpc().request('get-block', { seq: i })
      const data = b4a.from(result.data, 'base64').toString()
      assert.equal(data, blocks[i], `Block ${i} content should match`)
    }
  } finally {
    await cleanup()
  }
})

test('store binary data (random bytes)', async () => {
  const { client, cleanup } = await createPair()

  try {
    const original = crypto.randomBytes(1024)
    const result = await client.rpc().request('store-block', {
      data: b4a.toString(original, 'base64')
    })

    assert.equal(result.seq, 0)

    const getResult = await client.rpc().request('get-block', { seq: 0 })
    const retrieved = b4a.from(getResult.data, 'base64')

    assert.equal(retrieved.length, 1024, 'Length should match')
    assert.ok(b4a.equals(original, retrieved), 'Binary content should match exactly')
  } finally {
    await cleanup()
  }
})

test('store JSON-structured data', async () => {
  const { client, cleanup } = await createPair()

  try {
    const tx = {
      type: 'payment',
      from: 'alice',
      to: 'bob',
      amount: 50000,
      currency: 'sats',
      timestamp: Date.now()
    }

    const data = JSON.stringify(tx)
    await client.rpc().request('store-block', {
      data: b4a.toString(b4a.from(data), 'base64')
    })

    const result = await client.rpc().request('get-block', { seq: 0 })
    const retrieved = JSON.parse(b4a.from(result.data, 'base64').toString())

    assert.equal(retrieved.from, 'alice')
    assert.equal(retrieved.to, 'bob')
    assert.equal(retrieved.amount, 50000)
  } finally {
    await cleanup()
  }
})

test('get-block returns error for out-of-range index', async () => {
  const { client, cleanup } = await createPair()

  try {
    const result = await client.rpc().request('get-block', { seq: 99 })
    assert.ok(result.error, 'Should return an error for out-of-range index')
  } finally {
    await cleanup()
  }
})

test('get-info returns correct metadata', async () => {
  const { client, cleanup } = await createPair()

  try {
    // Store some blocks first
    for (let i = 0; i < 3; i++) {
      await client.rpc().request('store-block', {
        data: b4a.toString(b4a.from(`block-${i}`), 'base64')
      })
    }

    const info = await client.rpc().request('get-info', {})
    assert.equal(info.length, 3, 'Should report 3 blocks')
    assert.ok(info.key, 'Should include core key')
    assert.ok(info.byteLength > 0, 'Should report positive byteLength')
  } finally {
    await cleanup()
  }
})

test('large block (1MB)', async () => {
  const { client, cleanup } = await createPair()

  try {
    const large = crypto.randomBytes(1024 * 1024) // 1 MB
    const result = await client.rpc().request('store-block', {
      data: b4a.toString(large, 'base64')
    })

    assert.equal(result.seq, 0)

    const getResult = await client.rpc().request('get-block', { seq: 0 })
    const retrieved = b4a.from(getResult.data, 'base64')
    assert.equal(retrieved.length, large.length, '1MB block should round-trip correctly')
    assert.ok(b4a.equals(large, retrieved), 'Content should match')
  } finally {
    await cleanup()
  }
})

test('many blocks rapidly (100 writes)', async () => {
  const { client, server, cleanup } = await createPair()

  try {
    const start = Date.now()
    const count = 100

    for (let i = 0; i < count; i++) {
      await client.rpc().request('store-block', {
        data: b4a.toString(crypto.randomBytes(64), 'base64')
      })
    }

    const elapsed = Date.now() - start
    assert.equal(server.core.length, count, `Should have ${count} blocks`)
    assert.ok(elapsed < 30000, `Should complete in under 30s (took ${elapsed}ms)`)
  } finally {
    await cleanup()
  }
})

test('Hypercore replication — client can read locally after sync', async () => {
  const { client, cleanup } = await createPair()

  try {
    // Store a block via RPC
    const data = 'replicated-block'
    await client.rpc().request('store-block', {
      data: b4a.toString(b4a.from(data), 'base64')
    })

    // Wait for Hypercore replication
    await client.core.update()

    if (client.core.length > 0) {
      const localBlock = await client.core.get(0)
      assert.equal(localBlock.toString(), data, 'Locally replicated block should match')
    }
  } finally {
    await cleanup()
  }
})

test('empty block (zero bytes)', async () => {
  const { client, cleanup } = await createPair()

  try {
    const result = await client.rpc().request('store-block', {
      data: b4a.toString(b4a.from(''), 'base64')
    })

    assert.equal(result.seq, 0)

    const getResult = await client.rpc().request('get-block', { seq: 0 })
    const retrieved = b4a.from(getResult.data, 'base64')
    assert.equal(retrieved.length, 0, 'Empty block should have 0 length')
  } finally {
    await cleanup()
  }
})

// ---------------------------------------------------------------------------
// Run
// ---------------------------------------------------------------------------

runTests().catch((err) => {
  console.error('Test runner failed:', err)
  process.exit(1)
})