mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-13 20:08:05 +08:00
🎁 Sync 2026-10-28 19:35:15
This commit is contained in:
1 parent
85209f310d
commit
390801c844
90 files changed
+5821
-1967
No files matched your search
@@ -0,0 +1,210 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const lua = process.env.MINIGATE_TEST_LUA;
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const controller = path.join(root, 'luasrc/controller/minigate.lua');
|
||||
|
||||
function runLua(source) {
|
||||
const result = spawnSync(lua, ['-'], { encoding: 'utf8', input: source });
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
function luaFiles(dir) {
|
||||
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
|
||||
const full = path.join(dir, entry.name);
|
||||
return entry.isDirectory() ? luaFiles(full) : (entry.name.endsWith('.lua') ? [full] : []);
|
||||
});
|
||||
}
|
||||
|
||||
test('every LuCI Lua file compiles', { skip: !lua }, () => {
|
||||
const files = luaFiles(path.join(root, 'luasrc'));
|
||||
assert.ok(files.length >= 6);
|
||||
const checks = files.map(file => `assert(loadfile(${JSON.stringify(file)}))`).join('\n');
|
||||
runLua(checks + '\nprint("ok")');
|
||||
});
|
||||
|
||||
// 加载控制器时只需要 luci.http;其他模块在函数内部按需 require
|
||||
const controllerPrelude = `
|
||||
local response, status_code, calls, form = nil, nil, {}, {}
|
||||
luci = {http={
|
||||
formvalue=function(name) return form[name] end,
|
||||
prepare_content=function() end,
|
||||
write_json=function(v) response=v end,
|
||||
status=function(code) status_code=code end
|
||||
}}
|
||||
package.preload['luci.sys'] = function() return {
|
||||
call=function(cmd) calls[#calls + 1] = cmd; return 0 end,
|
||||
exec=function() return '' end
|
||||
} end
|
||||
package.preload['luci.model.uci'] = function() return {cursor=function() return {
|
||||
get=function(_, _, sec) if sec == 'cfg01' then return 'ddns' end end,
|
||||
foreach=function() end
|
||||
} end} end
|
||||
dofile(${JSON.stringify(controller)})
|
||||
local c = luci.controller.minigate
|
||||
`;
|
||||
|
||||
test('certificate expiry parsing is timezone independent', { skip: !lua }, () => {
|
||||
const expected = Date.UTC(2026, 11, 16, 12, 0, 0) / 1000;
|
||||
const out = runLua(controllerPrelude + `
|
||||
print(c.parse_cert_time('Dec 16 12:00:00 2026 GMT'))
|
||||
print(c.parse_cert_time('Feb 1 00:00:00 2028 GMT'))
|
||||
print(tostring(c.parse_cert_time('garbage')))
|
||||
`);
|
||||
assert.deepEqual(out.split('\n'), [String(expected), String(Date.UTC(2028, 1, 1) / 1000), 'nil']);
|
||||
});
|
||||
|
||||
test('log view merges sources by time and keeps untimed lines with their entry', { skip: !lua }, () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'minigate-logs-'));
|
||||
fs.writeFileSync(path.join(dir, 'ddns.log'), '2026-10-28 10:00:01 [DDNS] a\n2026-10-28 10:00:05 [DDNS] c\n');
|
||||
fs.writeFileSync(path.join(dir, 'nginx.log'), '2026/10/28 10:00:03 [emerg] b\n');
|
||||
fs.writeFileSync(path.join(dir, 'update.log'), '2026-10-28 10:00:04 start\ncurl output without time\n');
|
||||
const out = runLua(controllerPrelude + `
|
||||
print(c.merge_logs({{'DDNS', ${JSON.stringify(path.join(dir, 'ddns.log'))}}, {'Nginx', ${JSON.stringify(path.join(dir, 'nginx.log'))}},
|
||||
{'更新', ${JSON.stringify(path.join(dir, 'update.log'))}}, {'缺失', '/missing.log'}}, 300))
|
||||
print('--')
|
||||
print(c.merge_logs({{'DDNS', ${JSON.stringify(path.join(dir, 'ddns.log'))}}}, 1))
|
||||
`);
|
||||
assert.equal(out, [
|
||||
'[DDNS] 2026-10-28 10:00:01 [DDNS] a',
|
||||
'[Nginx] 2026/10/28 10:00:03 [emerg] b',
|
||||
'[更新] 2026-10-28 10:00:04 start',
|
||||
'[更新] curl output without time',
|
||||
'[DDNS] 2026-10-28 10:00:05 [DDNS] c',
|
||||
'--',
|
||||
'[DDNS] 2026-10-28 10:00:05 [DDNS] c'
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
test('state files are read by the same domain key the shell scripts write', { skip: !lua }, () => {
|
||||
const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'minigate-state-')), 'proxy.status');
|
||||
fs.writeFileSync(file, 'result=ok\nmessage=配置检查通过\nwarning=one\nwarning=two=2\n');
|
||||
const out = runLua(controllerPrelude + `
|
||||
local st = c.read_state(${JSON.stringify(file)})
|
||||
print(st.result, st.message, #st.warnings, st.warnings[2])
|
||||
print(c.state_key('*.example.com'), c.state_key('home.example.com'))
|
||||
print(tostring(c.read_state('/missing/state')))
|
||||
`);
|
||||
assert.equal(out, 'ok\t配置检查通过\t2\ttwo=2\n__example_com\thome_example_com\nnil');
|
||||
});
|
||||
|
||||
test('login guard endpoints accept IPv6, quote the address and reject shell text', { skip: !lua }, () => {
|
||||
const out = runLua(controllerPrelude + `
|
||||
for _, ip in ipairs({'2001:db8::1', '198.51.100.7', "1.2.3.4'; reboot; '", 'abc'}) do
|
||||
form.ip = ip; calls = {}; response = nil
|
||||
c.action_lg_ban()
|
||||
print(response.success and calls[1] or 'rejected')
|
||||
end
|
||||
`);
|
||||
assert.equal(out, [
|
||||
"/bin/sh /usr/lib/minigate/login_guard.sh ban '2001:db8::1' >/dev/null 2>&1",
|
||||
"/bin/sh /usr/lib/minigate/login_guard.sh ban '198.51.100.7' >/dev/null 2>&1",
|
||||
'rejected',
|
||||
'rejected'
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
test('manual DDNS sync starts a background job only for a real DDNS section', { skip: !lua }, () => {
|
||||
const out = runLua(controllerPrelude + `
|
||||
form.section = 'cfg01'; c.action_ddns_sync()
|
||||
print(response.success, calls[1])
|
||||
form.section = 'cfg01;reboot'; calls = {}; c.action_ddns_sync()
|
||||
print(response.success, status_code, #calls)
|
||||
`);
|
||||
const [started, rejected] = out.split('\n');
|
||||
assert.match(started, /^true\t\( \/bin\/sh \/usr\/lib\/minigate\/job\.sh ddns-cfg01 \/bin\/sh \/usr\/lib\/minigate\/ddns\.sh single 'cfg01' \) <\/dev\/null >\/dev\/null 2>&1 &$/);
|
||||
assert.equal(rejected, 'false\t400\t0');
|
||||
});
|
||||
|
||||
// 用最小 CBI 桩加载每个页面,并执行表单渲染时会调用的 cfgvalue
|
||||
const cbiPrelude = `
|
||||
local options = {}
|
||||
local function node()
|
||||
local n = {}
|
||||
function n:option(kind, name, title)
|
||||
local o = {kind=kind, name=name, choices={}}
|
||||
function o:value(k, v) self.choices[k] = v or k end
|
||||
function o:depends() end
|
||||
options[name] = o
|
||||
return o
|
||||
end
|
||||
function n:tab() end
|
||||
function n:taboption(_, ...) return n:option(...) end
|
||||
return n
|
||||
end
|
||||
function Map(config, title)
|
||||
local m = node()
|
||||
m.title = title
|
||||
m.uci = {get=function(_, _, sec, opt) local v; if opt == 'domain' then v = 'home.example.com' end; return v end}
|
||||
function m:section() return node() end
|
||||
return m
|
||||
end
|
||||
for _, name in ipairs({'Flag','Value','DynamicList','ListValue','DummyValue','MultiValue','NamedSection','TypedSection','SimpleSection'}) do _G[name] = name end
|
||||
luci = {dispatcher={build_url=function(...) return '/cgi-bin/luci/' .. table.concat({...}, '/') end}}
|
||||
package.preload['luci.sys'] = function() return {exec=function() return '' end, call=function() return 0 end} end
|
||||
package.preload['nixio.fs'] = function() return {access=function() return false end, dir=function() return nil end, stat=function() return nil end} end
|
||||
package.preload['luci.model.uci'] = function() return {cursor=function() return {
|
||||
foreach=function(_, config, kind, fn) if kind == 'ddns' then fn({domain='home.example.com'}); fn({domain='*.example.com'}) end end,
|
||||
get=function(_, _, _, opt) local v; if opt == 'staging' then v = '1' end; return v end
|
||||
} end} end
|
||||
package.preload['luci.controller.minigate'] = function() return {
|
||||
ddns_state=function() return {status='error', message='IPv4 192.168.2.2 不是公网地址', ip='198.51.100.1'} end,
|
||||
cert_list=function() return {{domain='*.example.com', expires_date='2026-11-02', days_left=5, staging=true}} end
|
||||
} end
|
||||
package.preload['luci.cbi.datatypes'] = function() return {
|
||||
ip4addr=function(v) return v:match('^%d+%.%d+%.%d+%.%d+$') ~= nil end,
|
||||
ip6addr=function(v) return v:match('^[%x:]+$') ~= nil and v:find(':') ~= nil end
|
||||
} end
|
||||
local function page(name) return assert(dofile(${JSON.stringify(path.join(root, 'luasrc/model/cbi/minigate'))} .. '/' .. name .. '.lua')) end
|
||||
`;
|
||||
|
||||
test('every LuCI page loads and renders its status blocks', { skip: !lua }, () => {
|
||||
const out = runLua(cbiPrelude + `
|
||||
for _, name in ipairs({'general', 'ddns', 'acme', 'proxy', 'login_guard'}) do
|
||||
options = {}
|
||||
local m = page(name)
|
||||
assert(m.title:match('^MiniGate'), name .. ' title')
|
||||
for key, o in pairs(options) do
|
||||
if o.kind == 'DummyValue' and type(o.cfgvalue) == 'function' then
|
||||
assert(type(o.cfgvalue(o, 'cfg01')) == 'string', name .. '.' .. key)
|
||||
end
|
||||
end
|
||||
end
|
||||
options = {}; page('ddns')
|
||||
print(options._info.cfgvalue(options._info, 'cfg01'):match('不是公网地址') ~= nil)
|
||||
options = {}; page('acme')
|
||||
local certs = options._certs.cfgvalue(options._certs)
|
||||
print(certs:match('剩 5 天') ~= nil, certs:match('测试证书') ~= nil, certs:match('测试模式已开启') ~= nil)
|
||||
options = {}; page('general')
|
||||
print(options.enabled ~= nil, options.ipv6_listen == nil)
|
||||
`);
|
||||
assert.equal(out, 'true\ntrue\ttrue\ttrue\ntrue\ttrue');
|
||||
});
|
||||
|
||||
test('LuCI forms validate domains, Cloudflare IDs, intervals, thresholds and whitelists', { skip: !lua }, () => {
|
||||
const out = runLua(cbiPrelude + `
|
||||
local function ok(o, v) return (o.validate(o, v, 'cfg01')) ~= nil end
|
||||
options = {}; page('ddns')
|
||||
print(ok(options.domain, 'home.example.com'), ok(options.domain, '*.example.com'), ok(options.domain, 'bad domain.com'),
|
||||
ok(options.domain, '-a.example.com'), ok(options.domain, 'localhost'))
|
||||
print(ok(options.cf_zone_id, string.rep('a1', 16)), ok(options.cf_zone_id, 'zone'), ok(options.cf_api_token, 'abc_DEF-123'), ok(options.cf_api_token, 'abc def'))
|
||||
print(ok(options.check_interval, '300'), ok(options.check_interval, '60'))
|
||||
options = {}; page('proxy')
|
||||
print(ok(options.prefix, 'app'), ok(options.prefix, 'a.b'), ok(options.prefix, '-x'), options.target_addr.datatype)
|
||||
options = {}; page('login_guard')
|
||||
print(options.threshold.datatype, ok(options.whitelist, {'203.0.113.0/20', '2001:db8::/48', '8.8.8.8'}), ok(options.whitelist, {'1.2.3.0/255.255.255.0'}), ok(options.whitelist, '10.0.0.0/33'))
|
||||
`);
|
||||
assert.equal(out, [
|
||||
'true\ttrue\tfalse\tfalse\tfalse',
|
||||
'true\tfalse\ttrue\tfalse',
|
||||
'true\tfalse',
|
||||
'true\tfalse\tfalse\thost',
|
||||
'range(1,100)\ttrue\tfalse\tfalse'
|
||||
].join('\n'));
|
||||
});
|
||||
Reference in new issue
Block a user