'use strict'; // Temperature is the one real sensor reading this platform exposes, and it arrives from a promise // on the player. Two things have to hold at the server end, and neither is provable from a unit // test of the guard alone — this drives a real socket into a real server: // // 1. Adding the column must not break the insert for every OTHER player. The telemetry INSERT is // on the heartbeat path, which every device hits every 15 seconds; getting its arity wrong // would take the whole fleet's telemetry down, not just BrightSign's. // 2. A panel with no sensor sends nothing, and a flaky one can send NaN or Infinity. Both must // land as "no reading" rather than as a number, because the dashboard renders this value and // null is the only honest way to say "this hardware has no thermometer". const { test, before, after } = require('node:test'); const assert = require('node:assert/strict'); const path = require('node:path'); const os = require('node:os'); const { spawn } = require('node:child_process'); const fs = require('node:fs'); const crypto = require('node:crypto'); const Database = require('better-sqlite3'); const ioClient = require('../node_modules/socket.io-client'); const { freePort } = require('./helpers/free-port'); let PORT, BASE, proc, db; const DATA_DIR = path.join(os.tmpdir(), 'st-temp-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-temp-' + crypto.randomBytes(4).toString('hex') + '.log'); // Exactly what a browser/Android player sends: no temperature key at all. const BASE_TELEMETRY = { battery_level: null, battery_charging: false, storage_free_mb: null, storage_total_mb: null, ram_free_mb: null, ram_total_mb: null, cpu_usage: null, wifi_ssid: 'Web Player', wifi_rssi: null, uptime_seconds: 42, }; before(async () => { PORT = await freePort(); BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), env: { ...process.env, DATA_DIR, SELF_HOSTED: 'true', PORT: String(PORT), NODE_ENV: 'test' }, stdio: ['ignore', logFd, logFd], }); let up = false; for (let i = 0; i < 80; i++) { try { const r = await fetch(BASE + '/api/status'); if (r.ok) { up = true; break; } } catch { /* */ } await new Promise((r) => setTimeout(r, 250)); } if (!up) throw new Error('server did not boot:\n' + fs.readFileSync(LOG, 'utf8').slice(-2000)); db = new Database(path.join(DATA_DIR, 'db', 'remote_display.db')); }); after(() => { try { db && db.close(); } catch { /* */ } try { proc.kill('SIGKILL'); } catch { /* */ } }); function makeDevice() { const id = crypto.randomUUID(); const token = crypto.randomBytes(32).toString('hex'); db.prepare(`INSERT INTO devices (id, name, status, device_token, created_at) VALUES (?, 'TEMP', 'online', ?, strftime('%s','now'))`).run(id, token); return { id, token }; } function connect(dev) { return new Promise((resolve, reject) => { const s = ioClient(BASE + '/device', { transports: ['websocket'], reconnection: false }); s.on('connect', () => s.emit('device:register', { device_id: dev.id, device_token: dev.token })); s.on('device:registered', () => resolve(s)); s.on('device:auth-error', (e) => reject(new Error(e && e.error))); setTimeout(() => reject(new Error('register timeout')), 10000); }); } const wait = (ms) => new Promise((r) => setTimeout(r, ms)); const lastTemp = (id) => db.prepare( 'SELECT temperature_c FROM device_telemetry WHERE device_id = ? ORDER BY reported_at DESC, id DESC LIMIT 1' ).get(id); test('a reading is stored', async () => { const dev = makeDevice(); const s = await connect(dev); s.emit('device:heartbeat', { device_id: dev.id, telemetry: { ...BASE_TELEMETRY, temperature_c: 47.3 } }); await wait(400); assert.equal(lastTemp(dev.id).temperature_c, 47.3); s.close(); }); test('EVERY OTHER PLAYER still records telemetry — the insert arity did not change under them', async () => { const dev = makeDevice(); const s = await connect(dev); s.emit('device:heartbeat', { device_id: dev.id, telemetry: BASE_TELEMETRY }); await wait(400); const row = db.prepare( 'SELECT * FROM device_telemetry WHERE device_id = ? ORDER BY reported_at DESC, id DESC LIMIT 1' ).get(dev.id); assert.ok(row, 'a player that sends no temperature must still get a telemetry row'); assert.equal(row.temperature_c, null); assert.equal(row.uptime_seconds, 42, 'the rest of the payload is unaffected'); s.close(); }); test('a flaky sensor cannot poison the column', async () => { const dev = makeDevice(); const s = await connect(dev); for (const bad of [NaN, Infinity, -Infinity, 'hot', {}, true]) { s.emit('device:heartbeat', { device_id: dev.id, telemetry: { ...BASE_TELEMETRY, temperature_c: bad } }); await wait(220); assert.equal(lastTemp(dev.id).temperature_c, null, `accepted ${String(bad)} as a reading`); } s.close(); }); test('a sub-zero reading is a real reading, not a falsy one', async () => { // 0 and negatives are legitimate temperatures; a truthiness check here would drop them. const dev = makeDevice(); const s = await connect(dev); s.emit('device:heartbeat', { device_id: dev.id, telemetry: { ...BASE_TELEMETRY, temperature_c: 0 } }); await wait(400); assert.equal(lastTemp(dev.id).temperature_c, 0); s.emit('device:heartbeat', { device_id: dev.id, telemetry: { ...BASE_TELEMETRY, temperature_c: -5.5 } }); await wait(400); assert.equal(lastTemp(dev.id).temperature_c, -5.5); s.close(); });