OverviewFilesCommitsIssuesDocsReleasesPeers

cthuloid / webui / test / server.test.js

117 lines · javascript ref:
// Drives the REAL routes over HTTP against a rig with a fake unit.
import test from 'brittle'
import http from 'bare-http1'
import EventEmitter from 'bare-events'
import { Rig } from '../lib/rig.js'
import { createServer } from '../lib/server.js'
import { FakeUnit, waitFor } from './helpers.js'

class FakeDiscovery extends EventEmitter {
  constructor () { super(); this.available = true; this.reason = '' }
  start () {}
  stop () {}
}

async function setup () {
  const fake = await new FakeUnit('u1').listen()
  const discovery = new FakeDiscovery()
  const store = { load: () => ({}), save: () => {} }
  const rig = new Rig({ store, discovery, readbackMs: 100, unitOpts: { backoffStartMs: 100 } })
  rig.start()
  discovery.emit('up', { name: 'u1', host: 'u1.local', addr: '127.0.0.1', port: fake.port, proto: 2 })
  const server = createServer(rig)
  await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
  const port = server.address().port
  const teardown = async () => {
    rig.stop()
    await new Promise((resolve) => server.close(resolve))
    await fake.close()
  }
  return { fake, rig, server, port, teardown }
}

function request (port, method, path, body, headers = {}) {
  return new Promise((resolve, reject) => {
    const req = http.request({ host: '127.0.0.1', port, method, path, headers }, (res) => {
      let data = ''
      res.on('data', (d) => { data += d })
      res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: data }))
    })
    req.on('error', reject)
    req.end(body)
  })
}

const post = (port, path, obj) => request(port, 'POST', path, JSON.stringify(obj || {}), {
  'content-type': 'application/json', 'x-cthuloid': '1'
})

test('page and assets are served', async (t) => {
  const s = await setup()
  const page = await request(s.port, 'GET', '/')
  t.is(page.status, 200)
  t.ok(page.body.includes('<title>'), 'html')
  t.is((await request(s.port, 'GET', '/app.js')).status, 200)
  t.is((await request(s.port, 'GET', '/app.css')).status, 200)
  t.is((await request(s.port, 'GET', '/color.js')).status, 200)
  t.is((await request(s.port, 'GET', '/picker.js')).status, 200)
  t.is((await request(s.port, 'GET', '/swatches.js')).status, 200)
  t.is((await request(s.port, 'GET', '/nope')).status, 404)
  await s.teardown()
})

test('state lists the discovered unit, disconnected', async (t) => {
  const s = await setup()
  const res = await request(s.port, 'GET', '/api/state')
  const st = JSON.parse(res.body)
  t.is(st.connected, false)
  t.is(st.units[0].id, 'u1')
  await s.teardown()
})

test('a POST without the x-cthuloid header is refused (cross-site guard)', async (t) => {
  const s = await setup()
  const res = await request(s.port, 'POST', '/api/connect', '{"on":true}', { 'content-type': 'application/json' })
  t.is(res.status, 403)
  t.is(s.rig.connected, false)
  await s.teardown()
})

test('tick, connect, set a slot: the unit receives it', async (t) => {
  const s = await setup()
  t.is((await post(s.port, '/api/units/u1', { enabled: true, fadeMs: 0 })).status, 200)
  t.is((await post(s.port, '/api/connect', { on: true })).status, 200)
  await waitFor(() => s.rig.snapshot().units[0].slots !== null)
  const res = await post(s.port, '/api/units/u1/slot', { index: 0, r: 0, g: 1, b: 0, level: 1 })
  t.is(res.status, 200)
  t.ok(await waitFor(() => s.fake.lines.includes('pwm 0 65535 0 0 0 0 0 0 0 0 0 0')), 'green on slot 1')
  await s.teardown()
})

test('bad input is a 400 with a reason, not a crash', async (t) => {
  const s = await setup()
  const res = await request(s.port, 'POST', '/api/units/u1', '{not json', { 'content-type': 'application/json', 'x-cthuloid': '1' })
  t.is(res.status, 400)
  const unknown = await post(s.port, '/api/units/zzz', { enabled: true })
  t.is(unknown.status, 400)
  t.ok(JSON.parse(unknown.body).error.includes('unknown unit'))
  await s.teardown()
})

test('events stream sends the state at once and on change', async (t) => {
  const s = await setup()
  const frames = []
  const req = http.request({ host: '127.0.0.1', port: s.port, path: '/api/events' }, (res) => {
    t.is(res.headers['content-type'], 'text/event-stream')
    res.on('data', (d) => frames.push(d.toString()))
  })
  req.end()
  t.ok(await waitFor(() => frames.join('').includes('"connected":false')), 'initial state')
  await post(s.port, '/api/connect', { on: true })
  t.ok(await waitFor(() => frames.join('').includes('"connected":true')), 'change pushed')
  t.is(s.rig.viewers, 1, 'one page watching')
  req.destroy()
  t.ok(await waitFor(() => s.rig.viewers === 0), 'viewer count drops when the page goes')
  await s.teardown()
})