mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 04:53:00 +08:00
106 lines
6.6 KiB
JavaScript
106 lines
6.6 KiB
JavaScript
'use strict';
|
|
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { spawnSync, spawn } = require('node:child_process');
|
|
const { root } = require('./helpers/runtime');
|
|
const mac = 'AA:BB:CC:11:22:33';
|
|
|
|
function fixture(t, command, prefix) {
|
|
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'device-manager-single-ping-'));
|
|
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
|
const leases = path.join(directory, 'leases'); fs.writeFileSync(leases, '');
|
|
const source = fs.readFileSync(path.join(root, 'root/usr/libexec/rpcd/luci.device-manager'), 'utf8')
|
|
.replaceAll('. /usr/share/libubox/jshn.sh', '. "$AUDIT_JSHN"')
|
|
.replaceAll('/tmp/luci-device-manager-single-ping', path.join(directory, 'single-ping'))
|
|
.replaceAll('/tmp/dhcp.leases', leases).replaceAll('/var/dhcp.leases', leases);
|
|
const wrapper = `
|
|
ip() { printf '%s\\n' "$AUDIT_NEIGH"; return "$AUDIT_IP_EXIT"; }
|
|
ping() { printf '%s\\n' "$@" >> "$AUDIT_PING_LOG"; sleep "$AUDIT_DELAY"; printf '%s\\n' "$AUDIT_OUTPUT"; return "$AUDIT_PING_EXIT"; }
|
|
ping6() { ping "$@"; }
|
|
` + source;
|
|
let sequence = 0;
|
|
function options(extra) {
|
|
const id = sequence++;
|
|
const log = path.join(directory, 'pings-' + id);
|
|
return { log, options: { encoding: 'utf8', timeout: 10000, env: Object.assign({}, process.env, {
|
|
AUDIT_JSHN: process.env.TEST_JSHN_PATH || path.join(root, 'test/helpers/jshn.sh'), AUDIT_NODE: process.execPath,
|
|
AUDIT_JSON_ENCODER: path.join(root, 'test/helpers/json-encoder.js'),
|
|
AUDIT_JSON_LOG: path.join(directory, 'json-' + id), AUDIT_PING_LOG: log,
|
|
AUDIT_NEIGH: '192.168.1.20 dev br-lan lladdr aa:bb:cc:11:22:33 REACHABLE',
|
|
AUDIT_IP_EXIT: '0', AUDIT_PING_EXIT: '0', AUDIT_DELAY: '0', AUDIT_OUTPUT: '1 packet received, time=0.5 ms'
|
|
}, extra) } };
|
|
}
|
|
return {
|
|
directory, leases,
|
|
run(request = { mac }, extra = {}) {
|
|
const cfg = options(extra);
|
|
const result = spawnSync(command, [...prefix, '-c', wrapper, 'ping-test', 'call', 'ping_device'],
|
|
Object.assign({}, cfg.options, { input: JSON.stringify(request) }));
|
|
assert.equal(result.status, 0, result.stderr);
|
|
return { reply: JSON.parse(result.stdout), args: fs.existsSync(cfg.log) ? fs.readFileSync(cfg.log, 'utf8').trim().split('\n') : [] };
|
|
},
|
|
start(extra = {}) {
|
|
const cfg = options(extra);
|
|
const child = spawn(command, [...prefix, '-c', wrapper, 'ping-test', 'call', 'ping_device'], cfg.options);
|
|
child.stdin.end(JSON.stringify({ mac }));
|
|
let stdout = '', stderr = '';
|
|
child.stdout.on('data', data => { stdout += data; }); child.stderr.on('data', data => { stderr += data; });
|
|
const done = new Promise((resolve, reject) => {
|
|
child.on('error', reject);
|
|
child.on('close', code => code === 0 ? resolve(JSON.parse(stdout)) : reject(new Error(stderr)));
|
|
});
|
|
return { done, log: cfg.log };
|
|
}
|
|
};
|
|
}
|
|
|
|
for (const [command, prefix] of [['sh', []], ['busybox', ['sh']]]) {
|
|
const available = spawnSync(command, [...prefix, '-c', 'command -v flock >/dev/null']).status === 0;
|
|
test(`${command}: single-device Ping resolves the MAC, ignores caller targets and reports reply versus timeout`, { skip: !available }, t => {
|
|
const f = fixture(t, command, prefix);
|
|
const result = f.run({ mac, ip: '8.8.8.8', command: 'touch /tmp/unsafe' });
|
|
assert.equal(result.reply.reachable, true); assert.equal(result.reply.ip, '192.168.1.20');
|
|
assert.equal(result.args.at(-1), '192.168.1.20'); assert.ok(result.args.includes('br-lan'));
|
|
assert.deepEqual(result.args.slice(0, 6), ['-c', '1', '-w', '3', '-W', '2']);
|
|
const timeout = f.run({ mac }, { AUDIT_PING_EXIT: '1' });
|
|
assert.equal(timeout.reply.ok, true); assert.equal(timeout.reply.reachable, false);
|
|
const error = f.run({ mac }, { AUDIT_PING_EXIT: '2', AUDIT_OUTPUT: 'invalid option' });
|
|
assert.equal(error.reply.ok, false); assert.equal(error.reply.error, 'Ping could not be completed');
|
|
});
|
|
test(`${command}: invalid and unknown MACs never launch Ping`, { skip: !available }, t => {
|
|
const f = fixture(t, command, prefix);
|
|
for (const value of ['', null, 'aa:bb:cc:11:22:33; touch /tmp/unsafe', 'AA:BB:CC:11:22:33\ninvalid', "AA:BB:CC:11:22:33'$(touch /tmp/unsafe)'", 'FF:FF:FF:FF:FF:FF', '00:00:00:00:00:00', '11:22:33:44:55:66']) {
|
|
const result = f.run({ mac: value });
|
|
assert.equal(result.reply.ok, false); assert.deepEqual(result.args, []);
|
|
}
|
|
assert.deepEqual(f.run({ mac }, { AUDIT_IP_EXIT: '127' }).args, []);
|
|
});
|
|
test(`${command}: DHCP fallback and IPv6-only link-local neighbors use a single numeric destination`, { skip: !available }, t => {
|
|
const f = fixture(t, command, prefix);
|
|
fs.writeFileSync(f.leases, '123 aa:bb:cc:11:22:33 192.168.1.50 device *\n');
|
|
const lease = f.run({ mac }, { AUDIT_NEIGH: '' });
|
|
assert.equal(lease.reply.ip, '192.168.1.50'); assert.equal(lease.reply.interface, '');
|
|
const reassigned = f.run({ mac }, { AUDIT_NEIGH: '192.168.1.50 dev br-lan lladdr aa:bb:cc:44:55:66 REACHABLE' });
|
|
assert.equal(reassigned.reply.ok, false); assert.deepEqual(reassigned.args, []);
|
|
fs.writeFileSync(f.leases, '');
|
|
const v6 = f.run({ mac }, { AUDIT_NEIGH: 'fe80::1234 dev br-lan lladdr aa:bb:cc:11:22:33 STALE' });
|
|
assert.equal(v6.reply.ip, 'fe80::1234'); assert.equal(v6.reply.interface, 'br-lan');
|
|
assert.equal(v6.args.at(-1), 'fe80::1234'); assert.ok(v6.args.includes('-I'));
|
|
fs.writeFileSync(f.leases, '123 aa:bb:cc:11:22:33 999.1.2.3 invalid *\n');
|
|
assert.equal(f.run({ mac }, { AUDIT_NEIGH: '' }).reply.ok, false);
|
|
});
|
|
test(`${command}: a global nonblocking lock prevents concurrent detail probes`, { skip: !available }, async t => {
|
|
const f = fixture(t, command, prefix);
|
|
const first = f.start({ AUDIT_DELAY: '0.3' });
|
|
for (let attempt = 0; attempt < 100 && !fs.existsSync(first.log); attempt++) await new Promise(resolve => setTimeout(resolve, 10));
|
|
assert.ok(fs.existsSync(first.log));
|
|
const second = f.run();
|
|
assert.equal(second.reply.ok, false); assert.match(second.reply.error, /Another device/); assert.deepEqual(second.args, []);
|
|
await first.done;
|
|
assert.equal(f.run().reply.reachable, true, 'lock must be released after the probe');
|
|
});
|
|
}
|