OverviewFilesCommitsIssuesDocsReleasesPeers

p2pbuilders / test / m5.js

175 lines · javascript ref:
'use strict'

// M5: end-to-end HTTP + WebSocket RPC smoke test.

const assert = require('assert/strict')
const http = require('http')
const fs = require('fs')
const os = require('os')
const path = require('path')
const WebSocket = require('ws')
const { start } = require('../src/backend/server')

const tests = []
function test (name, fn) { tests.push({ name, fn }) }

function fetchText (url) {
  return new Promise((resolve, reject) => {
    http.get(url, (res) => {
      let body = ''
      res.on('data', (c) => { body += c })
      res.on('end', () => resolve({ status: res.statusCode, body, headers: res.headers }))
      res.on('error', reject)
    }).on('error', reject)
  })
}

function rpcClient (url) {
  const ws = new WebSocket(url)
  const pending = new Map()
  let id = 1
  const ready = new Promise((resolve, reject) => {
    ws.on('open', resolve)
    ws.on('error', reject)
  })
  ws.on('message', (data) => {
    const msg = JSON.parse(data.toString())
    const p = pending.get(msg.id)
    if (!p) return
    pending.delete(msg.id)
    if (msg.error) p.reject(new Error(msg.error))
    else p.resolve(msg.result)
  })
  return {
    ready,
    call (method, params) {
      const reqId = id++
      return new Promise((resolve, reject) => {
        pending.set(reqId, { resolve, reject })
        ws.send(JSON.stringify({ id: reqId, method, params }))
      })
    },
    close () { ws.close() }
  }
}

async function waitFor (pred, { timeoutMs = 5000, intervalMs = 20 } = {}) {
  const t0 = Date.now()
  while (Date.now() - t0 < timeoutMs) {
    if (await pred()) return
    await new Promise(r => setTimeout(r, intervalMs))
  }
  throw new Error('waitFor timeout')
}

test('server boots, serves index.html and static assets', async () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'p2pbuilders-m5-'))
  const app = await start({ dir, port: 0, swarm: null })
  const port = app.server.address().port

  const root = await fetchText(`http://localhost:${port}/`)
  assert.equal(root.status, 200)
  assert.ok(root.body.includes('p2pbuilders'))

  const css = await fetchText(`http://localhost:${port}/styles.css`)
  assert.equal(css.status, 200)
  assert.ok(css.body.includes('--accent'))

  const js = await fetchText(`http://localhost:${port}/app.js`)
  assert.equal(js.status, 200)
  assert.ok(js.body.includes('WebSocket'))

  // SPA fallback: arbitrary route returns index.html
  const fallback = await fetchText(`http://localhost:${port}/b/anything`)
  assert.equal(fallback.status, 200)
  assert.ok(fallback.body.includes('<title>p2pbuilders</title>'))

  await app.close()
  fs.rmSync(dir, { recursive: true, force: true })
})

test('WebSocket RPC: createBoard → createPost → listPosts → vote → comment', async () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'p2pbuilders-m5-'))
  const app = await start({ dir, port: 0, swarm: null })
  const port = app.server.address().port
  const client = rpcClient(`ws://localhost:${port}/rpc`)
  await client.ready

  const me = await client.call('me')
  assert.ok(me.pubkey && me.pubkey.length === 64)

  await client.call('createBoard', { name: 'meta', description: 'about p2pbuilders itself' })

  // createBoard is fire-and-indexed; let the indexer catch up
  await waitFor(async () => {
    const boards = await client.call('listBoards')
    return boards.includes('meta')
  })

  const b = await client.call('getBoard', { name: 'meta' })
  assert.equal(b.description, 'about p2pbuilders itself')

  const { opId } = await client.call('createPost', {
    board: 'meta',
    title: 'hello',
    body: 'this is the first post',
    link: null
  })
  assert.ok(opId && opId.length === 80)

  // Let indexer see the post
  await waitFor(async () => {
    const posts = await client.call('listPosts', { board: 'meta' })
    return posts.length === 1
  })

  const posts = await client.call('listPosts', { board: 'meta' })
  assert.equal(posts[0].title, 'hello')
  assert.equal(posts[0].body, 'this is the first post')
  assert.equal(posts[0].commentCount, 0)

  // Vote on it
  await client.call('vote', { targetOpId: opId, dir: 1 })
  await waitFor(async () => {
    const t = await client.call('getVoteTotals', { opId })
    return t.up === 1
  })

  // Comment on it
  const { opId: cId } = await client.call('createComment', {
    parentOpId: opId,
    body: 'nice'
  })
  assert.ok(cId)

  await waitFor(async () => {
    const cs = await client.call('listComments', { parentOpId: opId })
    return cs.length === 1
  })

  const comments = await client.call('listComments', { parentOpId: opId })
  assert.equal(comments[0].body, 'nice')

  client.close()
  await app.close()
  fs.rmSync(dir, { recursive: true, force: true })
})

// runner
;(async () => {
  let pass = 0, fail = 0
  for (const { name, fn } of tests) {
    try {
      await fn()
      console.log(`  ✓ ${name}`)
      pass++
    } catch (err) {
      console.log(`  ✗ ${name}`)
      console.log(`    ${err.stack || err.message}`)
      fail++
    }
  }
  console.log(`\n${pass}/${pass + fail} passed`)
  process.exit(fail ? 1 : 0)
})()