mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-13 20:08:05 +08:00
129 lines
6.5 KiB
JavaScript
129 lines
6.5 KiB
JavaScript
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const vm = require('node:vm');
|
|
|
|
const root = path.resolve(__dirname, '..');
|
|
|
|
function pageScript(file) {
|
|
const source = fs.readFileSync(path.join(root, file), 'utf8');
|
|
const start = source.indexOf('<script type="text/javascript">');
|
|
const end = source.indexOf('</script>', start);
|
|
return source.slice(start + '<script type="text/javascript">'.length, end).replace(/\]\] \.\. (\w+) \.\. \[\[/g, '$1');
|
|
}
|
|
|
|
function fakePage(script, { storageBlocked = false } = {}) {
|
|
const elements = new Map();
|
|
const element = id => {
|
|
if (!elements.has(id)) {
|
|
elements.set(id, {
|
|
id, style: { setProperty() {} }, className: '', textContent: '', innerHTML: '', value: '',
|
|
disabled: false, checked: false, hidden: false, children: [],
|
|
setAttribute() {}, getAttribute() { return ''; }, focus() {}, appendChild() {}, insertBefore() {},
|
|
querySelector(selector) { return selector === 'table' && this.innerHTML.includes('<table') ? {} : null; },
|
|
querySelectorAll() { return []; }, contains() { return false; }
|
|
});
|
|
}
|
|
return elements.get(id);
|
|
};
|
|
const listeners = {};
|
|
const requests = [];
|
|
const intervals = [];
|
|
const document = {
|
|
hidden: false, body: { style: {} }, activeElement: null,
|
|
getElementById: element, querySelector: () => null, querySelectorAll: () => [],
|
|
addEventListener: (name, handler) => { listeners[name] = handler; }
|
|
};
|
|
const request = method => (url, data, callback) => { requests.push({ method, url, data, callback }); };
|
|
const context = vm.createContext({
|
|
document, location: { reload() {} }, getComputedStyle: () => ({ getPropertyValue: () => '' }),
|
|
localStorage: storageBlocked
|
|
? { getItem() { throw new Error('storage blocked'); }, setItem() { throw new Error('storage blocked'); } }
|
|
: { getItem() { return null; }, setItem() {} },
|
|
setTimeout: callback => { callback(); return 0; }, setInterval: callback => { intervals.push(callback); return intervals.length; },
|
|
clearInterval() {}, confirm: () => true, alert() {},
|
|
XHR: { get: request('GET'), post: request('POST') }
|
|
});
|
|
vm.runInContext(script, context);
|
|
return { context, document, element, listeners, requests, intervals };
|
|
}
|
|
|
|
const overviewScript = pageScript('luasrc/model/cbi/minigate/general.lua');
|
|
const loginGuardScript = pageScript('luasrc/model/cbi/minigate/login_guard.lua');
|
|
const take = (page, url) => {
|
|
const index = page.requests.findIndex(item => item.url === url);
|
|
assert.ok(index >= 0, `missing request ${url}`);
|
|
return page.requests.splice(index, 1)[0];
|
|
};
|
|
|
|
test('overview loads even when browser storage is blocked', () => {
|
|
const page = fakePage(overviewScript, { storageBlocked: true });
|
|
assert.deepEqual(page.requests.map(item => item.url).sort(), ['au', 'su', 'xu']);
|
|
});
|
|
|
|
test('overview treats an empty visitor object as no records and keeps errors distinct', () => {
|
|
const page = fakePage(overviewScript);
|
|
take(page, 'au').callback(null, { visitors: {} });
|
|
assert.match(page.element('mg-v-list').innerHTML, /暂无访问记录/);
|
|
assert.equal(page.element('mg-v-count').textContent, '');
|
|
|
|
const failed = fakePage(overviewScript);
|
|
take(failed, 'au').callback(null, null);
|
|
assert.match(failed.element('mg-v-list').innerHTML, /读取失败/);
|
|
take(failed, 'su').callback(null, null);
|
|
assert.match(failed.element('mg-d1').innerHTML, /读取失败/);
|
|
});
|
|
|
|
test('overview shows cached locations immediately instead of flashing a lookup state', () => {
|
|
const page = fakePage(overviewScript);
|
|
const visitors = { visitors: [{ ip: '198.51.100.7', last_time: '2026-10-14T08:00:00+08:00', domain: 'a.example.com', count: 2, online: true }] };
|
|
take(page, 'au').callback(null, visitors);
|
|
take(page, 'gu').callback(null, { geo: '四川成都 电信' });
|
|
assert.match(page.element('geo-0').innerHTML, /四川成都 电信/);
|
|
|
|
page.context.loadVisitors();
|
|
take(page, 'au').callback(null, visitors);
|
|
assert.match(page.element('mg-v-list').innerHTML, /四川成都 电信/);
|
|
assert.doesNotMatch(page.element('mg-v-list').innerHTML, /查询中/);
|
|
assert.equal(page.requests.some(item => item.url === 'gu'), false);
|
|
});
|
|
|
|
test('overview polling pauses in a hidden tab and refreshes when visible again', () => {
|
|
const page = fakePage(overviewScript);
|
|
take(page, 'su').callback(null, { ddns_list: {}, proxy_rules: {}, acme: {} });
|
|
take(page, 'au').callback(null, { visitors: [] });
|
|
page.requests.length = 0;
|
|
page.document.hidden = true;
|
|
page.intervals.forEach(callback => callback());
|
|
assert.equal(page.requests.length, 0);
|
|
page.document.hidden = false;
|
|
page.listeners.visibilitychange();
|
|
assert.deepEqual(page.requests.map(item => item.url).sort(), ['au', 'su']);
|
|
});
|
|
|
|
test('login guard renders empty JSON objects as empty lists and uses POST for changes', () => {
|
|
const page = fakePage(loginGuardScript);
|
|
take(page, 'lu').callback(null, { enabled: '1', running: true, banned: {}, banned_total: 0, watching: {}, watching_total: 0, threshold: 3 });
|
|
assert.match(page.element('lg-banned-list').innerHTML, /暂无封禁/);
|
|
assert.match(page.element('lg-watching-list').innerHTML, /无/);
|
|
assert.doesNotMatch(page.element('lg-banned-list').innerHTML, /<table/);
|
|
|
|
page.element('lg-ban-input').value = '198.51.100.9';
|
|
page.context.lgManualBan();
|
|
page.context.lgFlushAll();
|
|
assert.deepEqual(page.requests.filter(item => item.url !== 'lu').map(item => `${item.method} ${item.url}`), ['POST bu', 'POST fu']);
|
|
});
|
|
|
|
test('state-changing LuCI endpoints require POST with the CSRF token', () => {
|
|
const controller = fs.readFileSync(path.join(root, 'luasrc/controller/minigate.lua'), 'utf8');
|
|
for (const name of ['acme_install', 'acme_issue', 'ddns_sync', 'lg_ban', 'lg_unban', 'lg_flush', 'update_apply', 'uninstall']) {
|
|
assert.match(controller, new RegExp(`"${name}"\\}, post\\("action_${name}"\\)\\)`), name);
|
|
}
|
|
const ddns = fs.readFileSync(path.join(root, 'luasrc/model/cbi/minigate/ddns.lua'), 'utf8');
|
|
const acme = fs.readFileSync(path.join(root, 'luasrc/model/cbi/minigate/acme.lua'), 'utf8');
|
|
assert.match(ddns, /XHR\.post\("\]\]\.\.url\.\.\[\[",\{section:s\}/);
|
|
assert.doesNotMatch(acme, /XHR\.get\(/);
|
|
assert.match(controller, /sec:match\("\^\[%w_\]\+\$"\)/);
|
|
});
|