Files
op-packages/luci-app-substore/tests/controller_local_test.lua
T

254 lines
9.5 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- controller_local_test.lua — 控制器错误反馈回归测试(§18/§19)
-- 用法:lua5.1 tests/controller_local_test.lua
--
-- 不需要真实 LuCI:用最小 stub 提供 luci.http / luci.dispatcher / luci.util,
-- 把 substore.core 换成可控的假实现,然后直接调用控制器的 action_* 函数,
-- 断言「失败必须体现在 redirect 的 ?err= 上」,而不是静默跳回列表页。
package.path = "./root/usr/share/?.lua;" .. package.path
local passed, failed = 0, 0
local function check(name, cond)
if cond then passed = passed + 1; print("PASS " .. name)
else failed = failed + 1; print("FAIL " .. name) end
end
-- ---------- luci stub ----------
local FORM = {} -- formvalue 数据
local LAST_REDIRECT = nil
local URLENC = {}
_G.luci = {
http = {
formvalue = function(k) return FORM[k] end,
redirect = function(url) LAST_REDIRECT = url end,
},
dispatcher = {
build_url = function(...)
local t = { ... }
return "/" .. table.concat(t, "/")
end,
},
util = {
urlencode = function(s)
s = tostring(s or "")
URLENC[#URLENC + 1] = s
return (s:gsub("[^%w%-%._~]", function(c)
return string.format("%%%02X", string.byte(c))
end))
end,
pcdata = function(s) return tostring(s or "") end,
},
i18n = { translate = function(s) return s end },
}
_G.luci.controller = { admin = {} }
-- 控制器内部用的是 require("luci.http") 而非全局,因此同时登记进 package.loaded
package.loaded["luci.http"] = _G.luci.http
package.loaded["luci.dispatcher"] = _G.luci.dispatcher
package.loaded["luci.util"] = _G.luci.util
package.loaded["luci.i18n"] = _G.luci.i18n
-- ---------- substore.core stub ----------
local CORE_RESULT = { add_local = { "s00000001" }, save_meta = { true }, sync = { true } }
local SYNC_CALLS = 0
package.loaded["substore.core"] = {
add_local = function() return CORE_RESULT.add_local[1], CORE_RESULT.add_local[2] end,
save_meta = function() return CORE_RESULT.save_meta[1], CORE_RESULT.save_meta[2] end,
sync = function() SYNC_CALLS = SYNC_CALLS + 1; return CORE_RESULT.sync[1], CORE_RESULT.sync[2] end,
write_cron = function() end,
cron_time_valid = function() return true end,
-- read_rules_fields 会遍历它收集 proto_filter_*。这里只是 stub:真实清单的
-- 内容/规范性由 tests/rules_fields_test.lua 对着 core.lua 校验,本文件只保证
-- 控制器在「清单存在」的前提下能正常跑完(缺了它 read_rules_fields 会崩)。
RULE_PROTOS = { "vmess", "trojan" },
}
-- 载入控制器(module() 会在 package.loaded 里建表,不需要真实 luci 模块)
-- 路径可用 SUBSTORE_CONTROLLER 覆盖,便于对「修复前」的副本跑同一套断言
local CTL_PATH = os.getenv("SUBSTORE_CONTROLLER")
or "root/usr/lib/lua/luci/controller/admin/substore.lua"
local ok, err = pcall(dofile, CTL_PATH)
check("controller loads under stub", ok)
if not ok then print(" load error: " .. tostring(err)) end
local ctl = package.loaded["luci.controller.admin.substore"] or {}
local function reset()
FORM = {}
LAST_REDIRECT = nil
CORE_RESULT.add_local = { "s00000001" }
CORE_RESULT.save_meta = { true }
CORE_RESULT.sync = { true }
SYNC_CALLS = 0
end
local function has_err()
return type(LAST_REDIRECT) == "string" and LAST_REDIRECT:find("err=", 1, true) ~= nil
end
-- ---------- §19 空名称 / 空内容 ----------
reset()
FORM = { token = "t", name = "", content = "vmess://x" }
ctl.action_local_create()
check("local_create empty name redirects", type(LAST_REDIRECT) == "string")
check("local_create empty name reports error", has_err())
check("local_create empty name does not create", CORE_RESULT.add_local[1] == "s00000001" and SYNC_CALLS == 0)
reset()
FORM = { token = "t", name = "test", content = "" }
ctl.action_local_create()
check("local_create empty content reports error", has_err())
reset()
FORM = { token = "t", name = "", content = "" }
ctl.action_local_create()
check("local_create both empty reports error", has_err())
-- ---------- §18 core 失败必须传到 UI ----------
reset()
CORE_RESULT.add_local = { nil, "写入失败" }
FORM = { token = "t", name = "ok", content = "vmess://x" }
ctl.action_local_create()
check("local_create core failure redirects", type(LAST_REDIRECT) == "string")
check("local_create core failure reports error", has_err())
check("local_create core failure surfaces reason",
LAST_REDIRECT:find("err=", 1, true) ~= nil and URLENC[#URLENC] == "写入失败")
-- local_save: save_meta 失败
reset()
CORE_RESULT.save_meta = { nil, "订阅不存在" }
FORM = { token = "t", id = "s00000001", name = "ok", content = "vmess://x" }
ctl.action_local_save()
check("local_save save failure reports error", has_err())
check("local_save save failure surfaces reason", URLENC[#URLENC] == "订阅不存在")
check("local_save save failure skips sync", SYNC_CALLS == 0)
-- local_save: sync 失败(解析失败)
reset()
CORE_RESULT.sync = { nil, "无法识别的订阅格式" }
FORM = { token = "t", id = "s00000001", name = "ok", content = "garbage" }
ctl.action_local_save()
check("local_save sync failure reports error", has_err())
check("local_save sync failure surfaces reason", URLENC[#URLENC] == "无法识别的订阅格式")
check("local_save sync was attempted", SYNC_CALLS == 1)
-- local_save: 缺 id
reset()
FORM = { token = "t", id = "", name = "ok", content = "vmess://x" }
ctl.action_local_save()
check("local_save missing id reports error", has_err())
-- ---------- 成功路径:不应带 err ----------
reset()
FORM = { token = "t", name = "ok", content = "vmess://x" }
ctl.action_local_create()
check("local_create success redirects", type(LAST_REDIRECT) == "string")
check("local_create success has no err", not has_err())
reset()
FORM = { token = "t", id = "s00000001", name = "ok", content = "vmess://x" }
ctl.action_local_save()
check("local_save success has no err", not has_err())
check("local_save success ran sync", SYNC_CALLS == 1)
-- ---------- 无 CSRF token:拒绝执行,并明确回显(§18) ----------
-- 此前是「静默重定向」:表单没提交上来,页面却和成功一样跳回去,
-- 用户看不到任何原因。校验失败也是失败,必须让用户看见(M28)。
reset()
FORM = { name = "", content = "" }
ctl.action_local_create()
check("no token still redirects", type(LAST_REDIRECT) == "string")
check("no token reports error", has_err())
-- ---------- §18 单节点保存:解析/写入失败必须可见 ----------
-- 修复前 action_node_save 只在成功分支做事,其余情况一律静默重定向,
-- 用户提交了坏数据却看到「已保存」的样子。
local NODES = {}
local WRITE_CALLS = 0
local CORE_STUB = package.loaded["substore.core"]
CORE_STUB.read_nodes = function() return NODES end
CORE_STUB.write_nodes = function(_, ns)
WRITE_CALLS = WRITE_CALLS + 1
NODES = ns
return true
end
CORE_STUB.refresh_combos = function() end
-- 真实的 merge_form_node 在 substore.core 里(会碰 /etc/substore),这里只要
-- 「表单字段覆盖原节点」的语义即可
CORE_STUB.merge_form_node = function(orig, form)
local out = {}
for k, v in pairs(orig or {}) do out[k] = v end
for k, v in pairs(form or {}) do out[k] = v end
return out
end
local VALID_NODE = '[{"type":"vmess","server":"1.2.3.4","port":"443","uuid":"u1"}]'
reset()
NODES = { { name = "old", proto = "vmess", server = "1.2.3.4", port = 443 } }
WRITE_CALLS = 0
FORM = { token = "t", id = "s00000001", idx = "1", content = VALID_NODE }
ctl.action_node_save()
check("node_save valid writes", WRITE_CALLS == 1)
check("node_save valid has no err", not has_err())
-- 解析失败:必须报错且不写盘
reset()
NODES = { { name = "old", proto = "vmess", server = "1.2.3.4", port = 443 } }
WRITE_CALLS = 0
FORM = { token = "t", id = "s00000001", idx = "1", content = "not json at all" }
ctl.action_node_save()
check("node_save bad json reports error", has_err())
check("node_save bad json does not write", WRITE_CALLS == 0)
-- 下标无效
reset()
NODES = { { name = "old" } }
WRITE_CALLS = 0
FORM = { token = "t", id = "s00000001", idx = "9", content = VALID_NODE }
ctl.action_node_save()
check("node_save bad idx reports error", has_err())
check("node_save bad idx does not write", WRITE_CALLS == 0)
-- 缺 idx
reset()
NODES = { { name = "old" } }
WRITE_CALLS = 0
FORM = { token = "t", id = "s00000001", content = VALID_NODE }
ctl.action_node_save()
check("node_save missing idx reports error", has_err())
check("node_save missing idx does not write", WRITE_CALLS == 0)
-- 空内容
reset()
NODES = { { name = "old" } }
WRITE_CALLS = 0
FORM = { token = "t", id = "s00000001", idx = "1", content = "" }
ctl.action_node_save()
check("node_save empty content reports error", has_err())
check("node_save empty content does not write", WRITE_CALLS == 0)
-- 写入失败
reset()
NODES = { { name = "old" } }
WRITE_CALLS = 0
CORE_STUB.write_nodes = function() WRITE_CALLS = WRITE_CALLS + 1; return false end
FORM = { token = "t", id = "s00000001", idx = "1", content = VALID_NODE }
ctl.action_node_save()
check("node_save write failure reports error", has_err())
check("node_save write failure surfaced reason", URLENC[#URLENC] == "写入节点数据失败")
-- 无 token:拒绝执行,但**必须回显**(见上面 M28 的说明)
reset()
NODES = { { name = "old" } }
WRITE_CALLS = 0
FORM = { id = "s00000001", idx = "1", content = VALID_NODE }
ctl.action_node_save()
check("node_save no token redirects", type(LAST_REDIRECT) == "string")
check("node_save no token reports error", has_err())
check("node_save no token does not write", WRITE_CALLS == 0)
print(string.format("\n%d passed, %d failed", passed, failed))
os.exit(failed == 0 and 0 or 1)