mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-30 23:25:13 +08:00
157 lines
5.4 KiB
JavaScript
157 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/*
|
|
* SPDX-License-Identifier: GPL-2.0-only
|
|
*
|
|
* The certificate upload buttons: which staging path do they actually write?
|
|
*
|
|
* `hp.uploadCertificate` derives its staging path from the *second* argument
|
|
* the call site binds - the UCI option name:
|
|
*
|
|
* const tmpPath = '/tmp/homeproxy_cert_' + filename + '.tmp';
|
|
*
|
|
* L.bind() prepends the bound arguments, and LuCI's form.Button calls
|
|
* `onclick(ev, section_id)` (form.js renderWidget), so the event lands on the
|
|
* handler's *first* parameter. A handler that declares a leading parameter no
|
|
* caller passes therefore shifts everything by one: filename becomes the event
|
|
* object, the path becomes /tmp/homeproxy_cert_[object Event].tmp, and both the
|
|
* ACL whitelist (acl.d/luci-app-homeproxy-pro.json) and certificate_write
|
|
* (luci.homeproxy-pro, which reads /tmp/homeproxy_cert_${filename}.tmp) stop
|
|
* matching. Every upload fails and the UI only shows a generic error.
|
|
*
|
|
* The unit under test is the *argument alignment*, not the handler body, so
|
|
* this test wires each real call site's arguments exactly as the view does and
|
|
* checks the path and the RPC filename that come out.
|
|
*
|
|
* Usage: node tests/frontend-certificate-upload.js <repo-root>
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { loadLuciModule } = require('./lib/luci-module.js');
|
|
|
|
const root = path.resolve(process.argv[2] || '.');
|
|
|
|
let checks = 0, failures = 0;
|
|
function check(what, ok, detail) {
|
|
checks++;
|
|
if (ok) return;
|
|
failures++;
|
|
console.error(`FAIL ${what}${detail ? ': ' + detail : ''}`);
|
|
}
|
|
|
|
/* --- the call sites, read from the views --------------------------------- *
|
|
* Parsed rather than hardcoded so a renamed button, a changed UCI option name
|
|
* or a different binding shape is exercised as it is today. */
|
|
const VIEWS = [ 'node.js', 'server.js' ];
|
|
const calls = [];
|
|
for (const view of VIEWS) {
|
|
const file = path.join(root, 'htdocs/luci-static/resources/view/homeproxy-pro', view);
|
|
const src = fs.readFileSync(file, 'utf8');
|
|
const re = /L\.bind\(hp\.uploadCertificate[^;]*\)/g;
|
|
let m;
|
|
while ((m = re.exec(src)) !== null)
|
|
calls.push({ view, text: m[0] });
|
|
}
|
|
|
|
check('the certificate upload buttons were found', calls.length === 4,
|
|
`expected 4 L.bind(hp.uploadCertificate ...) call sites, found ${calls.length}`);
|
|
|
|
/* Turn `L.bind(hp.uploadCertificate, hp, _('certificate'), 'client_ca')` into
|
|
* ['certificate', 'client_ca']: the arguments the view binds, in order. */
|
|
function boundArgs(text) {
|
|
const inner = text.replace(/^L\.bind\(hp\.uploadCertificate\s*,\s*hp\s*,?/, '').replace(/\)$/, '');
|
|
const args = [];
|
|
const re = /_\(\s*'([^']*)'\s*\)|'([^']*)'|"([^"]*)"|([A-Za-z_$][\w$]*)|(\d+)/g;
|
|
let m;
|
|
while ((m = re.exec(inner)) !== null) {
|
|
if (m[1] !== undefined || m[2] !== undefined || m[3] !== undefined)
|
|
args.push(m[1] ?? m[2] ?? m[3]);
|
|
else
|
|
args.push(m[4] !== undefined ? { identifier: m[4] } : Number(m[5]));
|
|
}
|
|
return args;
|
|
}
|
|
|
|
const parsed = calls.map((c) => ({ ...c, args: boundArgs(c.text) }));
|
|
|
|
/* --- load homeproxy-pro.js the way LuCI does ---------------------------------- */
|
|
const uploads = [];
|
|
const rpcCalls = [];
|
|
|
|
const L = {
|
|
bind(fn, self, ...bound) {
|
|
return fn.bind(self, ...bound);
|
|
}
|
|
};
|
|
|
|
/* The recording ui stub goes in the deps record: that is the object the
|
|
* module's own `'require ui'` resolves to (the loader's runtime argument only
|
|
* supplies the _ / E / L globals). */
|
|
const uiStub = {
|
|
addNotification: () => {},
|
|
showModal: () => {},
|
|
hideModal: () => {},
|
|
uploadFile: (target, fileEl) => {
|
|
uploads.push({ target, fileEl });
|
|
return Promise.resolve({ size: 1 });
|
|
},
|
|
createHandlerFn: (self, fn) => fn
|
|
};
|
|
|
|
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: uiStub
|
|
},
|
|
{ L, E: () => null }
|
|
);
|
|
|
|
check('homeproxy-pro.js exposes uploadCertificate', typeof hp.uploadCertificate === 'function',
|
|
typeof hp.uploadCertificate);
|
|
|
|
/* --- drive each call site the way form.js does ---------------------------- */
|
|
async function run() {
|
|
for (const call of parsed) {
|
|
const label = `${call.view}: ${call.text}`;
|
|
const handler = L.bind(hp.uploadCertificate, Object.assign({}, hp, {
|
|
rpcCall: (method, params) => {
|
|
rpcCalls.push({ method, params });
|
|
return Promise.resolve({ result: true });
|
|
}
|
|
}), ...call.args);
|
|
|
|
uploads.length = 0;
|
|
rpcCalls.length = 0;
|
|
|
|
/* form.js: ui.createHandlerFn(this, (section_id, ev) => this.onclick(ev, section_id), section_id)
|
|
* L.bind prepends, so the handler is invoked with (event, section_id). */
|
|
const fakeFile = { name: 'cert.pem' };
|
|
const ev = { target: { files: [ fakeFile ], value: 'C:\\fakepath\\cert.pem' } };
|
|
await handler(ev, 'cfg01');
|
|
|
|
const want = call.args[1];
|
|
const got = uploads.length === 1 ? uploads[0].target : null;
|
|
check(`${label}: uploads to /tmp/homeproxy_cert_${want}.tmp`,
|
|
got === `/tmp/homeproxy_cert_${want}.tmp`, String(got));
|
|
|
|
const rpc = rpcCalls.length === 1 ? rpcCalls[0] : null;
|
|
check(`${label}: certificate_write receives the same name`,
|
|
rpc && rpc.method === 'certificate_write' && rpc.params && rpc.params[0] === want,
|
|
rpc ? JSON.stringify(rpc) : 'no rpcCall');
|
|
}
|
|
}
|
|
|
|
run().then(() => {
|
|
console.log(`${checks - failures}/${checks} checks passed`);
|
|
if (failures) {
|
|
console.error('FRONTEND CERTIFICATE UPLOAD TEST FAILED');
|
|
process.exit(1);
|
|
}
|
|
console.log('FRONTEND CERTIFICATE UPLOAD TEST PASSED');
|
|
process.exit(0);
|
|
});
|