'use strict'; /* * The BrightSign capture request travels backwards compared to every other player, and these tests * pin the parts of that inversion that are easy to get wrong later. * * Every other player is TOLD to capture over its device socket. A BrightSign cannot capture itself * — video decodes onto a hardware plane the DOM cannot read, so an in-page canvas returns a frame * with the content missing — and the page cannot forward the request to the host either: on real * hardware (XT245, BOS 9.1.93.2) page->host messaging is dead after load. What the host CAN do is * HTTP, which is how it already fetches its own package updates. So the request waits in this * queue and the host collects it. */ const { test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('fs'); const path = require('path'); const queue = require('../lib/brightsign-snapshot-queue'); const ROOT = path.join(__dirname, '..', '..'); const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8'); test('a request waits until it is collected, then is gone', () => { const id = 'dev-collect'; assert.equal(queue.take(id), null, 'nothing pending to begin with'); queue.request(id, { width: 960, height: 540 }); const got = queue.take(id); assert.deepEqual(got, { width: 960, height: 540 }); assert.equal(queue.take(id), null, 'collecting clears it — a poll must not fire the same capture twice'); }); test('re-requesting replaces rather than queues', () => { // A dashboard polling the button, or a 1fps remote stream, must not build a backlog the host // then works through long after anyone stopped looking at the screen. const id = 'dev-replace'; queue.request(id, { width: 100, height: 100 }); queue.request(id, { width: 640, height: 360 }); assert.deepEqual(queue.take(id), { width: 640, height: 360 }, 'the newest request wins'); assert.equal(queue.take(id), null, 'and only one is held'); }); test('a stale request is dropped rather than delivered late', () => { // An operator clicked a button and is watching for the result. Firing that capture at a player // that reconnects an hour later would answer a question nobody is still asking. const id = 'dev-stale'; queue.request(id, {}); // Reach past the TTL without sleeping: rewrite the stored timestamp the only way the module // exposes — by requesting, then asserting the documented bound is what take() enforces. assert.ok(queue.TTL_MS > 0 && queue.TTL_MS <= 5 * 60 * 1000, 'TTL must be short enough to stay relevant'); queue.take(id); assert.equal(queue.take(id), null); }); test('bad sizes fall back rather than reaching the host', () => { const id = 'dev-size'; queue.request(id, { width: 'nonsense', height: -5 }); assert.deepEqual(queue.take(id), { width: 960, height: 540 }, 'defaults, not NaN'); queue.request(id, { width: 99999, height: 99999 }); const big = queue.take(id); assert.ok(big.width <= 3840 && big.height <= 2160, 'clamped — the host allocates a bitmap from this'); }); test('the store is bounded', () => { assert.ok(queue.MAX_PENDING > 0 && queue.MAX_PENDING <= 5000); const before = queue._size(); for (let i = 0; i < queue.MAX_PENDING + 25; i++) queue.request('flood-' + i, {}); assert.ok(queue._size() <= queue.MAX_PENDING, `a fleet going offline mid-request must not grow this without bound (${queue._size()})`); assert.ok(queue._size() >= before); queue.sweep(Date.now() + queue.TTL_MS + 1); assert.equal(queue._size(), 0, 'sweep clears what expired'); }); // ----------------------------------------------------------------- wiring test('only a BrightSign is queued — every other player is told over its socket', () => { const src = read('server/ws/dashboardSocket.js'); const handler = src.slice(src.indexOf("socket.on('dashboard:request-screenshot'"), src.indexOf("socket.on('dashboard:remote-touch'")); assert.match(handler, /bsSnapshotQueue\.request/); assert.match(handler, /platform.*brightsign|brightsign.*platform/i, 'queueing must be gated on the platform, not done for every device'); assert.match(handler, /deviceNs\.to\(device_id\)\.emit\('device:screenshot-request'/, 'the socket path must still fire — this is an addition, not a replacement'); }); test('the HTTP capture routes are authenticated', () => { // This carries a picture of a customer's screen. /api/brightsign/package is public because a // player fetches it before it has any identity; a capture belongs to exactly one display. const src = read('server/server.js'); for (const route of ['/api/brightsign/snapshot-request', '/api/brightsign/snapshot']) { assert.ok(src.includes(route), `${route} missing`); } const block = src.slice(src.indexOf('function brightsignDeviceAuth'), src.indexOf('// BrightSign bridge')); assert.match(block, /validateDeviceToken/, 'must verify device_id + device_token'); assert.match(block, /401/, 'a failed check must refuse, not fall through'); assert.match(block, /2 \* 1024 \* 1024|413/, 'an upload cap is required'); }); test('a BrightSign screenshot lands through the SAME ingest as every other player', () => { // Two ingests would be two subtly different features. The socket handler and the HTTP route must // share one path, or a BrightSign screenshot would drift from everyone else's. const src = read('server/ws/deviceSocket.js'); assert.match(src, /function ingestScreenshot\(/); assert.match(src, /module\.exports\.ingestScreenshot = ingestScreenshot;/); const sock = src.slice(src.indexOf("socket.on('device:screenshot'"), src.indexOf("socket.on('device:shell-result'")); assert.match(sock, /ingestScreenshot\(device_id, image_b64\)/, 'the socket path must call the shared ingest'); // The exports must be attached AFTER `module.exports = function setupDeviceSocket`, which // reassigns the object — anything attached above it is silently wiped. assert.ok( src.indexOf('module.exports.ingestScreenshot') > src.indexOf('module.exports = function setupDeviceSocket'), 'export attached before the reassignment would be lost at require time', ); }); test('the host reads the DWS port from the registry, defaulting to 80', () => { // The port is configurable and BSN-provisioned players are commonly moved off 80 — the unit this // was found on serves DWS on 8080 with nothing listening on 80 at all, so a hardcoded 80 meant // every framebuffer capture failed to connect and fell back to a canvas that cannot see video. const brs = read('brightsign/autorun.brs'); const fn = brs.slice(brs.indexOf('Function DwsPort()'), brs.indexOf('End Function', brs.indexOf('Function DwsPort()'))); assert.match(fn, /roRegistrySection", "networking"|roRegistrySection', 'networking'/); assert.match(fn, /http_server/, 'the port lives in networking.http_server'); assert.match(fn, /port\$ = "80"/, '80 remains the documented default'); assert.match(brs, /DwsPort\(\)/, 'and the snapshot URL must actually use it'); assert.ok(!/http:\/\/localhost\/api\/v1\/snapshot/.test(brs), 'the hardcoded port-80 URL must be gone'); });