// 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()
})