144 lines
6.2 KiB
JavaScript
144 lines
6.2 KiB
JavaScript
// The settings form, the upgrade script and the cue refresh (0.1.5).
|
|||
|
|
//
|
||
|
|
// Seen at North Attleboro: the connection's settings couldn't be saved at all while
|
||
|
|
// it wasn't connected -- the saved decoder showed as "??" (not in the list) and a
|
||
|
|
// cue refresh that had never been saved showed as 0 (below its minimum), and either
|
||
|
|
// one blocks Save, so the login that would have fixed it couldn't be changed.
|
||
|
|
// Runs the real module code against a fake Resi; touches nothing real.
|
||
|
|
//
|
||
|
|
// node config_test.js (defaults to dist/)
|
||
|
|
// node config_test.js path/to/dist
|
||
|
|
|
||
|
|
'use strict';
|
||
|
|
const Module = require('module');
|
||
|
|
const path = require('path');
|
||
|
|
const assert = require('assert');
|
||
|
|
|
||
|
|
const dist = path.resolve(process.argv[2] || path.join(__dirname, 'dist'));
|
||
|
|
const stubs = {
|
||
|
|
'@companion-module/base': {
|
||
|
|
InstanceBase: class { updateStatus() {} log() {} setVariableValues() {} },
|
||
|
|
InstanceStatus: { Ok: 'ok', Connecting: 'c', ConnectionFailure: 'f', BadConfig: 'b' },
|
||
|
|
runEntrypoint() {}, combineRgb() { return 0 }, Regex: {},
|
||
|
|
},
|
||
|
|
'./variables.js': { UpdateVariableDefinitions() {} },
|
||
|
|
'./actions.js': { UpdateActions() {} },
|
||
|
|
'./feedbacks.js': { UpdateFeedbacks() {} },
|
||
|
|
'./presets.js': { UpdatePresets() {} },
|
||
|
|
};
|
||
|
|
const realLoad = Module._load;
|
||
|
|
Module._load = function (req, parent, isMain) {
|
||
|
|
if (stubs[req]) return stubs[req];
|
||
|
|
return realLoad.apply(this, arguments);
|
||
|
|
};
|
||
|
|
|
||
|
|
let fetches = [];
|
||
|
|
let cues = [{ uuid: 'a', name: 'Welcome', position: '00:05:00' }, { uuid: 'b', name: 'Message', position: '00:30:00' }];
|
||
|
|
let cueStatus = 200;
|
||
|
|
const reply = (status, body) => ({
|
||
|
|
ok: status >= 200 && status < 300, status,
|
||
|
|
headers: { get: () => null, getSetCookie: () => [] },
|
||
|
|
json: async () => body, text: async () => JSON.stringify(body),
|
||
|
|
});
|
||
|
|
global.fetch = async (url) => {
|
||
|
|
const p = String(url).replace('https://central.resi.io', '').split('?')[0];
|
||
|
|
fetches.push(p);
|
||
|
|
if (/\/events\/[^/]+$/.test(p)) return reply(200, { eventProfileId: 'P1', startTime: '2026-10-07T13:30:00Z' });
|
||
|
|
if (p.endsWith('/cues')) return reply(cueStatus, cues);
|
||
|
|
return reply(404, {});
|
||
|
|
};
|
||
|
|
|
||
|
|
const { ModuleInstance } = require(path.join(dist, 'main.js'));
|
||
|
|
const { UpgradeScripts } = require(path.join(dist, 'upgrades.js'));
|
||
|
|
|
||
|
|
function fresh() {
|
||
|
|
const inst = Object.create(ModuleInstance.prototype);
|
||
|
|
inst.config = {};
|
||
|
|
inst.secrets = { password: 'pw' };
|
||
|
|
inst.venues = [];
|
||
|
|
inst.playersByVenueUuid = {};
|
||
|
|
inst.updateStatus = () => {};
|
||
|
|
inst.setVariableValues = () => {};
|
||
|
|
inst.infos = [];
|
||
|
|
inst.warns = [];
|
||
|
|
inst.log = (lvl, m) => { (lvl === 'info' ? inst.infos : inst.warns).push(m); };
|
||
|
|
return inst;
|
||
|
|
}
|
||
|
|
|
||
|
|
let passed = 0;
|
||
|
|
async function test(name, fn) { await fn(); passed++; console.log(` ok ${name}`); }
|
||
|
|
|
||
|
|
(async () => {
|
||
|
|
await test('the saved decoder is a valid choice even before Resi answers', () => {
|
||
|
|
const inst = fresh();
|
||
|
|
inst.config = { defaultPlayerHardwareId: 'DEC1' };
|
||
|
|
const field = inst.getConfigFields().find((f) => f.id === 'defaultPlayerHardwareId');
|
||
|
|
const ids = field.choices.map((c) => c.id);
|
||
|
|
assert.ok(ids.includes('DEC1'), `choices were ${JSON.stringify(ids)}`);
|
||
|
|
assert.match(field.choices.find((c) => c.id === 'DEC1').label, /not connected/);
|
||
|
|
});
|
||
|
|
|
||
|
|
await test('once connected, the decoder is listed once, under its real name', () => {
|
||
|
|
const inst = fresh();
|
||
|
|
inst.config = { defaultPlayerHardwareId: 'DEC1' };
|
||
|
|
inst.venues = [{ uuid: 'V1', userName: 'North Attleboro' }];
|
||
|
|
inst.playersByVenueUuid = { V1: [{ playerHardwareId: 'DEC1', userName: 'North Attleboro' }] };
|
||
|
|
const field = inst.getConfigFields().find((f) => f.id === 'defaultPlayerHardwareId');
|
||
|
|
const dec = field.choices.filter((c) => c.id === 'DEC1');
|
||
|
|
assert.strictEqual(dec.length, 1);
|
||
|
|
assert.match(dec[0].label, /North Attleboro/);
|
||
|
|
});
|
||
|
|
|
||
|
|
await test('the upgrade fills settings an older connection never saved', () => {
|
||
|
|
const r = UpgradeScripts[0]({}, { config: { username: 'x', playerPollMs: 3000 }, actions: [], feedbacks: [] });
|
||
|
|
assert.ok(r.updatedConfig, 'config should be updated');
|
||
|
|
assert.strictEqual(r.updatedConfig.cuePollMs, 5000);
|
||
|
|
assert.strictEqual(r.updatedConfig.listPollMs, 120000);
|
||
|
|
assert.strictEqual(r.updatedConfig.positionTrimMs, 0);
|
||
|
|
assert.strictEqual(r.updatedConfig.playerPollMs, 3000, 'a saved value is kept');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test('the upgrade leaves a complete connection alone', () => {
|
||
|
|
const r = UpgradeScripts[0]({}, {
|
||
|
|
config: { playerPollMs: 3000, positionTrimMs: 0, cuePollMs: 5000, listPollMs: 120000 }, actions: [], feedbacks: [],
|
||
|
|
});
|
||
|
|
assert.strictEqual(r.updatedConfig, null);
|
||
|
|
});
|
||
|
|
|
||
|
|
await test('event details are read once per event, and the cue log only speaks on a change', async () => {
|
||
|
|
const inst = fresh();
|
||
|
|
inst.customerId = 'C';
|
||
|
|
inst.currentEventId = 'EV1';
|
||
|
|
inst.cookieHeader = 'livingas1=T';
|
||
|
|
inst.lastLoginMs = Date.now();
|
||
|
|
fetches = [];
|
||
|
|
for (let i = 0; i < 5; i++) await inst.refreshEventProfileAndCues();
|
||
|
|
assert.strictEqual(fetches.filter((p) => p.endsWith('/events/EV1')).length, 1, 'event detail once');
|
||
|
|
assert.strictEqual(fetches.filter((p) => p.endsWith('/cues')).length, 5, 'cues every refresh');
|
||
|
|
assert.strictEqual(inst.infos.filter((m) => /Loaded/.test(m)).length, 1, `logged ${inst.infos.length} times`);
|
||
|
|
|
||
|
|
cues = cues.concat({ uuid: 'c', name: 'Closing', position: '00:45:00' });
|
||
|
|
await inst.refreshEventProfileAndCues();
|
||
|
|
assert.strictEqual(inst.infos.filter((m) => /Loaded/.test(m)).length, 2, 'a changed list is logged');
|
||
|
|
|
||
|
|
inst.currentEventId = 'EV2';
|
||
|
|
await inst.refreshEventProfileAndCues();
|
||
|
|
assert.strictEqual(fetches.filter((p) => p.endsWith('/events/EV2')).length, 1, 'a new event is read');
|
||
|
|
});
|
||
|
|
|
||
|
|
await test('a rate-limited cue request holds off every Resi call', async () => {
|
||
|
|
const inst = fresh();
|
||
|
|
inst.customerId = 'C';
|
||
|
|
inst.currentEventId = 'EV1';
|
||
|
|
inst.cookieHeader = 'livingas1=T';
|
||
|
|
inst.lastLoginMs = Date.now();
|
||
|
|
inst.config = { username: 'a@b.c' };
|
||
|
|
cueStatus = 429;
|
||
|
|
await assert.rejects(inst.refreshEventProfileAndCues(), /cues failed \(429\)/);
|
||
|
|
await assert.rejects(inst.ensureAuthenticated(false), /rate-limiting.*retrying in \d+s/);
|
||
|
|
cueStatus = 200;
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log(`\n${passed} tests passed`);
|
||
|
|
})().catch((e) => { console.error(e); process.exit(1); });
|