Files
op-packages/luci-app-homeproxy-pro/tests/frontend-node-refs.js
T

144 lines
5.4 KiB
JavaScript

#!/usr/bin/env node
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* `hp.repairNodeRefs()` - the repair that keeps a deleted node from becoming a
* permanent generation failure.
*
* Deleting a node from the grid stages a plain `uci.remove`; nothing rewrites
* `config.main_node` / `main_udp_node` / the urltest member lists. The
* generator then dies on every later run:
*
* cannot build main-out: the node it refers to does not exist (it may have
* been deleted, or the reference is stale).
*
* Measured on the device: generation exit 254 with exactly that message, the
* reload aborted, and the UI still reported the save as applied. The repair
* runs before every save of the node map (including the footer's Save & Apply,
* which calls map.save() itself).
*
* The helper takes the cursor as an argument, so this drives it with a fake
* one - no form, no browser. `nil` and `urltest` are not section names and a
* reference that still resolves must be left alone: both are asserted, because
* a repair that "fixes" a working configuration is worse than none.
*
* Usage: node tests/frontend-node-refs.js <repo-root>
*/
'use strict';
const path = require('path');
const { loadLuciModule } = require('./lib/luci-module.js');
const root = path.resolve(process.argv[2] || '.');
const hp = loadLuciModule(
path.join(root, 'htdocs/luci-static/resources/homeproxy-pro.js'),
{
baseclass: { extend: (o) => o },
form: { DynamicList: { extend: (o) => o } },
fs: {}, rpc: {}, uci: {}, ui: {}
});
let checks = 0, failures = 0;
function check(what, ok, detail) {
checks++;
if (ok)
return;
failures++;
console.error(`FAIL ${what}${detail ? ': ' + detail : ''}`);
}
/* A cursor over a fixed set of sections, recording the writes. */
function fake_uci(sections, options) {
const state = Object.assign({}, options);
const writes = [];
return {
writes: writes,
state: state,
get: function(cfg, a, b) {
if (b !== undefined)
return state[a + '.' + b];
return Object.prototype.hasOwnProperty.call(sections, a) ? sections[a] : null;
},
set: function(cfg, section, option, value) {
writes.push({ section: section, option: option, value: value });
state[section + '.' + option] = value;
}
};
}
if (typeof hp.repairNodeRefs !== 'function') {
console.error('FAIL homeproxy-pro.js does not export repairNodeRefs()');
process.exit(1);
}
/* 1. A dangling main_node is cleared. */
{
const uci = fake_uci({ cfgOTHER: 'node' }, { 'config.main_node': 'cfgGONE' });
const repaired = hp.repairNodeRefs(uci, 'homeproxy-pro');
check('a dangling main_node is reported', repaired.includes('main_node'), JSON.stringify(repaired));
check('a dangling main_node becomes nil', uci.state['config.main_node'] === 'nil',
String(uci.state['config.main_node']));
}
/* 2. A main_node that resolves is untouched. */
{
const uci = fake_uci({ cfgALIVE: 'node' }, { 'config.main_node': 'cfgALIVE' });
const repaired = hp.repairNodeRefs(uci, 'homeproxy-pro');
check('a resolving main_node is not repaired', repaired.length === 0, JSON.stringify(repaired));
check('a resolving main_node keeps its value', uci.state['config.main_node'] === 'cfgALIVE');
check('a resolving main_node is not written', uci.writes.length === 0, JSON.stringify(uci.writes));
}
/* 3. The two sentinels are not section names. */
for (const sentinel of [ 'nil', 'urltest' ]) {
const uci = fake_uci({}, { 'config.main_node': sentinel });
const repaired = hp.repairNodeRefs(uci, 'homeproxy-pro');
check(`main_node='${sentinel}' is left alone`, repaired.length === 0 && uci.state['config.main_node'] === sentinel,
JSON.stringify([ repaired, uci.state['config.main_node'] ]));
}
/* 4. main_udp_node is repaired the same way (the UDP node is a separate
* reference and used to be forgotten by the subscription cleanup). */
{
const uci = fake_uci({}, { 'config.main_udp_node': 'cfgGONE' });
const repaired = hp.repairNodeRefs(uci, 'homeproxy-pro');
check('a dangling main_udp_node is reported', repaired.includes('main_udp_node'), JSON.stringify(repaired));
check('a dangling main_udp_node becomes nil', uci.state['config.main_udp_node'] === 'nil');
}
/* 5. urltest member lists drop only the missing members. */
{
const uci = fake_uci({ cfgA: 'node', cfgB: 'node' }, {
'config.main_urltest_nodes': [ 'cfgA', 'cfgGONE', 'cfgB' ]
});
const repaired = hp.repairNodeRefs(uci, 'homeproxy-pro');
check('a stale urltest member is reported', repaired.includes('main_urltest_nodes'), JSON.stringify(repaired));
check('a stale urltest member is dropped and the rest kept',
JSON.stringify(uci.state['config.main_urltest_nodes']) === JSON.stringify([ 'cfgA', 'cfgB' ]),
JSON.stringify(uci.state['config.main_urltest_nodes']));
}
/* 6. A fully valid urltest list is untouched. */
{
const uci = fake_uci({ cfgA: 'node', cfgB: 'node' }, {
'config.main_urltest_nodes': [ 'cfgA', 'cfgB' ]
});
const repaired = hp.repairNodeRefs(uci, 'homeproxy-pro');
check('a valid urltest list is not repaired', repaired.length === 0 && uci.writes.length === 0,
JSON.stringify([ repaired, uci.writes ]));
}
/* 7. An empty or absent list is not a repair either. */
{
const uci = fake_uci({}, {});
const repaired = hp.repairNodeRefs(uci, 'homeproxy-pro');
check('absent references are not repaired', repaired.length === 0 && uci.writes.length === 0);
}
console.log(`frontend node references: ${checks} checks, ${failures} failures`);
process.exit(failures ? 1 : 0);