Files
op-packages/smart-srun/tests/lua/bridge_probe.lua
T

516 lines
28 KiB
Lua

-- Exercises the LuCI bridge's pure mapping against the Go configuration shape.
--
-- Run by tests/test_luci_rpc_bridge.py with the repository's own Lua tree on
-- package.path and nixio/luci.jsonc stubbed: nothing here opens a socket, and
-- the functions under test are the ones that decide what a save sends.
local failures = 0
local function check(name, condition, detail)
if not condition then
failures = failures + 1
io.stderr:write(string.format("FAIL %s: %s\n", name, tostring(detail or "")))
end
end
local function equal(name, got, want)
check(name, got == want, string.format("got %s, want %s", tostring(got), tostring(want)))
end
local bridge = require "luci.smart_srun.bridge"
local rpc = require "luci.smart_srun.rpc"
-- rpc calls sock:writeall/readall, which only exist once nixio.util has been
-- loaded. It must load that itself: in the page nixio.fs happened to do it,
-- and a caller that did not load nixio.fs first failed on its first request.
check("rpc.loads_nixio_util", package.loaded["nixio.util"] ~= nil,
"luci.smart_srun.rpc did not require nixio.util")
-- One configuration in exactly the shape config.get answers with.
local CONFIG = {
schema_version = 2,
revision = 7,
enabled = true,
multi_wan_enabled = false,
school = "default",
sta_iface = "",
login_defaults = { n = "200", type = "1", enc = "srun_bx1" },
selection = {
active_campus_id = "c1", default_campus_id = "c2",
active_hotspot_id = "h1", default_hotspot_id = "h1",
},
quiet = { enabled = true, start = "00:00", ["end"] = "06:00", force_logout = true },
preset_updates = { enabled = true, time = "09:00" },
retry = { enabled = true, max_retries = 4, initial_seconds = 10, max_seconds = 60 },
checks = {
interval_seconds = 60, mode = "internet", switch_timeout_seconds = 30,
terminal_attempts = 5, terminal_interval_seconds = 2,
},
failover = { enabled = true, hotspot_failback_enabled = true },
log = { level = "INFO", file_omit_info = true },
campus_accounts = {
{
id = "c1", label = "宿舍有线", user_id = "2021001", password = "",
operator = "中国电信", operator_suffix = "telecom",
access_mode = "wired", wired_iface = "wan", auth_enabled = true,
base_url = "http://10.0.0.55", ac_id = "1",
login = { n = "200", type = "1", info_prefix = "SRBX1", double_stack = false },
},
{
id = "c2", label = "教学楼 Wi-Fi", user_id = "2021002",
operator_suffix = "", access_mode = "wifi", ssid = "JXNU",
encryption = "none", ap_selection = "fixed", bssid = "02:11:22:33:44:55",
base_url = "http://10.0.0.55", ac_id = "8", login = {},
},
},
hotspot_profiles = {
{ id = "h1", label = "手机热点", ssid = "iPhone", encryption = "psk2", key = "", radio = "radio0" },
},
school_extra = {},
}
-- flatten: the page's flat, string-valued view of a typed configuration.
local flat = bridge.flatten(CONFIG)
equal("flatten.enabled", flat.enabled, "1")
equal("flatten.multi_wan", flat.multi_wan_enabled, "0")
equal("flatten.quiet_enabled", flat.quiet_hours_enabled, "1")
equal("flatten.quiet_start", flat.quiet_start, "00:00")
equal("flatten.quiet_end", flat.quiet_end, "06:00")
equal("flatten.preset_auto_update", flat.preset_auto_update_enabled, "1")
equal("flatten.preset_time", flat.preset_update_time, "09:00")
local preset_patch = bridge.settings_patch(
{ preset_auto_update_enabled = "0", preset_update_time = "23:00" },
{ preset_auto_update_enabled = true, preset_update_time = true })
equal("patch.preset_disabled", preset_patch.preset_updates.enabled, false)
equal("patch.preset_time", preset_patch.preset_updates.time, "23:00")
equal("flatten.interval", flat.interval, "60")
equal("flatten.retry_cooldown", flat.retry_cooldown_seconds, "10")
equal("flatten.max_retries", flat.backoff_max_retries, "4")
equal("flatten.log_level", flat.log_level, "INFO")
equal("flatten.log_file_omit_info", flat.log_file_omit_info, "1")
local omit_off = bridge.settings_patch({ log_file_omit_info = "0", log_level = "WARN" }, { log_file_omit_info = true })
equal("patch.log_file_omit_info_off", omit_off.log.file_omit_info, false)
check("patch.log_file_omit_info_keeps_level", omit_off.log.level == nil, "the level must not travel with the flag")
local omit_on = bridge.settings_patch({ log_file_omit_info = "1" }, { log_file_omit_info = true })
equal("patch.log_file_omit_info_on", omit_on.log.file_omit_info, true)
equal("flatten.login_default_n", flat.n, "200")
equal("flatten.active_campus", flat.active_campus_id, "c1")
equal("flatten.default_campus", flat.default_campus_id, "c2")
equal("flatten.revision", flat.revision, 7)
equal("flatten.accounts", #flat.campus_accounts, 2)
equal("flatten.account_auth_enabled", flat.campus_accounts[1].auth_enabled, "1")
equal("flatten.account_double_stack", flat.campus_accounts[1].double_stack, "0")
equal("flatten.account_unset_double_stack", flat.campus_accounts[2].double_stack, "")
equal("flatten.account_mode", flat.campus_accounts[2].access_mode, "wifi")
-- A redacted configuration must stay redacted on its way to the page.
equal("flatten.no_password", flat.campus_accounts[1].password, "")
equal("flatten.no_hotspot_key", flat.hotspot_profiles[1].key, "")
-- settings_patch: only what the form touched.
local patch = bridge.settings_patch(
{ quiet_hours_enabled = "0", interval = "90", log_level = "DEBUG", enabled = "1" },
{ quiet_hours_enabled = true, interval = true })
equal("patch.quiet_enabled", patch.quiet.enabled, false)
equal("patch.interval", patch.checks.interval_seconds, 90)
check("patch.log_untouched", patch.log == nil, "log level must not travel unless edited")
check("patch.enabled_untouched", patch.enabled == nil, "enabled must not travel unless edited")
check("patch.nothing_dirty", bridge.settings_patch(flat, {}) == nil, "an untouched form sends nothing")
-- A number field that is not a number keeps its stored value rather than
-- becoming zero, which the daemon would accept as a valid setting.
local bad = bridge.settings_patch({ interval = "abc" }, { interval = true })
check("patch.rejects_non_numeric", bad == nil, "non-numeric interval must not be sent")
-- An emptied field is "not submitted", not "set to nothing": a clock with no
-- time or an enum with no choice would fail the whole save, including the
-- fields the user did change.
local cleared = bridge.settings_patch(
{ quiet_start = "", log_level = "", school = "", interval = "90" },
{ quiet_start = true, log_level = true, school = true, interval = true })
check("patch.skips_empty_clock", cleared.quiet == nil, "an empty clock must not be sent")
check("patch.skips_empty_enum", cleared.log == nil, "an empty enum must not be sent")
check("patch.skips_empty_required", cleared.school == nil, "an empty school must not be sent")
equal("patch.keeps_the_edited_field", cleared.checks.interval_seconds, 90)
-- The one field that may genuinely be empty still can be.
local sta = bridge.settings_patch({ sta_iface = "" }, { sta_iface = true })
equal("patch.allows_empty_sta_iface", sta.sta_iface, "")
-- Seconds stay fractional: the baseline parsed them with float().
local fraction = bridge.settings_patch({ retry_cooldown_seconds = "0.5" }, { retry_cooldown_seconds = true })
equal("patch.fractional_seconds", fraction.retry.initial_seconds, 0.5)
-- campus_patch: presence decides what a save overwrites.
local edit = bridge.campus_patch({
id = "c1", label = "", user_id = "2021001", operator_suffix = "telecom",
password = "", access_mode = "wired", wired_iface = "", network_interface = "wan.v2",
auth_enabled = "1", base_url = "http://10.0.0.55", ac_id = "1",
ssid = "leftover", bssid = "02:11:22:33:44:55", ap_selection = "fixed",
n = "200", type = "1", enc = "srun_bx1", info_prefix = "SRBX1",
double_stack = "", login_os = "Windows 10", login_name = "Windows",
}, { creating = false })
equal("campus.id", edit.id, "c1")
check("campus.keeps_password", edit.password == nil,
"an empty password box on an edit must not clear the stored credential")
equal("campus.wired_iface_alias", edit.wired_iface, "wan.v2")
equal("campus.auth_enabled", edit.auth_enabled, true)
equal("campus.clears_ssid", edit.ssid, "")
equal("campus.clears_bssid", edit.bssid, "")
equal("campus.wired_ap_selection", edit.ap_selection, "auto")
equal("campus.double_stack_cleared", edit.login.double_stack, rpc.NULL)
local created = bridge.campus_patch({
label = "", user_id = "2021003", operator_suffix = "", password = "",
access_mode = "wifi", ssid = "JXNU", bssid = "", ap_selection = "",
double_stack = "1",
}, { creating = true })
check("campus.new_id_absent", created.id == nil, "a new account must not choose its own id")
equal("campus.new_password_stored", created.password, "")
equal("campus.wifi_default_encryption", created.encryption, "none")
equal("campus.ap_selection_auto", created.ap_selection, "auto")
equal("campus.double_stack_true", created.login.double_stack, true)
local pinned = bridge.campus_patch({
access_mode = "wifi", ssid = "JXNU", bssid = "02:11:22:33:44:55",
ap_selection = "", double_stack = "0",
}, { creating = true })
equal("campus.ap_selection_from_bssid", pinned.ap_selection, "fixed")
equal("campus.double_stack_false", pinned.login.double_stack, false)
local hotspot = bridge.hotspot_patch(
{ id = "h1", label = "手机热点", ssid = "iPhone", encryption = "psk2", key = "", radio = "" },
{ creating = false })
check("hotspot.keeps_key", hotspot.key == nil, "an empty key box on an edit must not clear the stored key")
equal("hotspot.id", hotspot.id, "h1")
equal("label.with_suffix", bridge.default_label("2021001", "telecom", "未命名账号"), "2021001@telecom")
equal("label.plain", bridge.default_label("2021001", "", "未命名账号"), "2021001")
equal("label.fallback", bridge.default_label("", "", "未命名账号"), "未命名账号")
-- utc_seconds: known instants, and a difference that survives any local zone.
equal("time.epoch", bridge.utc_seconds("1970-01-01T00:00:00Z"), 0)
equal("time.known", bridge.utc_seconds("2026-09-18T12:00:00Z"), 1789732800)
equal("time.leap_day", bridge.utc_seconds("2024-02-29T00:00:00Z"), 1709164800)
check("time.rejects_garbage", bridge.utc_seconds("not a time") == nil, "a malformed timestamp is not a time")
-- status_view: what the page polls, built from a snapshot plus the config.
local snapshot = {
service = "running", enabled = true, config_revision = 7,
written_at = "2026-09-18T12:00:30Z",
accounts = {
{ account_id = "c1", link = "Ready", auth = "VerifiedSelf",
connectivity = "InternetReachable", identity = "2021001@telecom",
line = { iface = "wan", device = "eth0.2", address = "10.0.0.77" } },
},
actions = {
{ id = "a1", kind = "manual_login", state = "succeeded", message = "认证完成",
ended_at = "2026-09-18T12:00:00Z" },
},
}
local view = bridge.status_view(snapshot, CONFIG, 1000)
equal("status.text", view.status, "已认证")
equal("status.connectivity", view.connectivity, "互联网可达")
equal("status.level", view.connectivity_level, "online")
equal("status.mode_label", view.mode_label, "校园网模式(有线)")
equal("status.access_mode", view.current_campus_access_mode, "wired")
equal("status.account_label", view.campus_account_label, "宿舍有线")
equal("status.identity", view.online_account_label, "2021001@telecom")
-- The line as observed, not as configured: the account says "wan", the device
-- that carried the attempt was eth0.2, and the address is one nobody could
-- have guessed from the configuration.
equal("status.iface", view.current_iface, "wan")
equal("status.device", view.current_device, "eth0.2")
equal("status.ip", view.current_ip, "10.0.0.77")
equal("status.not_paused", view.paused, false)
equal("status.not_in_quiet", view.in_quiet, false)
equal("status.hotspot_label", view.hotspot_profile_label, "手机热点")
equal("status.last_action", view.last_action, "manual_login")
equal("status.result", view.action_result, "ok")
equal("status.message", view.last_action_message, "认证完成")
-- The action finished 30 seconds before the snapshot was written, so it reads
-- as 30 seconds ago on this clock, whatever zone either machine is in.
equal("status.last_ts", view.last_action_ts, 970)
equal("status.pending", view.pending_action, "")
-- A managed wired account gets a session row; an unmanaged one does not.
check("status.no_sessions_without_multiwan", next(view.wired_auth_sessions) == nil,
"multi_wan_enabled is off, so no account is managed")
local managed = {}
for key, value in pairs(CONFIG) do managed[key] = value end
managed.multi_wan_enabled = true
local managed_view = bridge.status_view(snapshot, managed, 1000)
check("status.session_present", managed_view.wired_auth_sessions.c1 ~= nil, "c1 opted in and the switch is on")
equal("status.session_online", managed_view.wired_auth_sessions.c1.online, true)
-- A queued action is what the progress dialog waits on.
local busy = {
service = "running", enabled = true, written_at = "2026-09-18T12:00:30Z",
accounts = {}, actions = {
{ id = "a2", kind = "switch_campus", state = "running" },
{ id = "a1", kind = "manual_login", state = "succeeded", ended_at = "2026-09-18T12:00:00Z" },
},
}
local busy_view = bridge.status_view(busy, CONFIG, 1000)
equal("status.pending_kind", busy_view.pending_action, "switch_campus")
equal("status.pending_result", busy_view.action_result, "pending")
equal("status.pending_clears_last_action", busy_view.last_action, "")
equal("status.pending_clears_message", busy_view.last_action_message, "")
equal("status.pending_clears_timestamp", busy_view.last_action_ts, 0)
-- Background work and wizard jobs have their own results. They must not hide
-- the manual terminal the frozen progress dialog matches by kind and time.
for _, state in ipairs({ "succeeded", "failed", "cancelled", "interrupted" }) do
for _, kind in ipairs({ "presets_refresh", "maintain", "forced_logout",
"quiet_hotspot", "quiet_campus", "detect_acid", "detect_verify", "wifi_setup_start" }) do
local mixed = {
service = "running", written_at = "2026-09-18T12:00:30Z", actions = {
{ kind = "manual_logout", state = state, message = "manual result",
ended_at = "2026-09-18T12:00:10Z" },
{ kind = kind, state = "succeeded", message = "unrelated result",
ended_at = "2026-09-18T12:00:20Z" },
{ kind = "maintain", state = "running" },
},
}
local result = bridge.status_view(mixed, CONFIG, 1000)
local name = "status.manual_" .. state .. "_after_" .. kind
equal(name .. ".kind", result.last_action, "manual_logout")
equal(name .. ".message", result.last_action_message, "manual result")
equal(name .. ".timestamp", result.last_action_ts, 980)
equal(name .. ".result", result.action_result,
state == "succeeded" and "ok" or state == "failed" and "error" or "forced")
equal(name .. ".background_visible", result.pending_action, "maintain")
equal(name .. ".no_manual_start", result.action_started_at, 0)
end
end
local background_only = bridge.status_view({ actions = {
{ kind = "presets_refresh", state = "succeeded", message = "refreshed",
ended_at = "2026-09-18T12:00:00Z" },
{ kind = "maintain", state = "queued" },
} }, CONFIG, 1000)
equal("status.background_has_no_manual_result", background_only.action_result, "")
equal("status.background_has_no_manual_message", background_only.last_action_message, "")
for _, kind in ipairs({ "manual_login", "manual_logout", "relogin", "switch_campus", "switch_hotspot" }) do
local queued = bridge.status_view({ actions = {
{ kind = "maintain", state = "running" },
{ kind = kind, state = "queued" },
{ kind = "manual_login", state = "succeeded", message = "old result",
ended_at = "2026-09-18T12:00:00Z" },
} }, CONFIG, 1000)
equal("status.queued_" .. kind, queued.action_result, "pending")
equal("status.queued_clears_" .. kind, queued.last_action_message, "")
end
-- Mode follows the switch that actually succeeded, not the one requested.
local switched = {
service = "running", enabled = true, written_at = "2026-09-18T12:10:00Z", accounts = {},
actions = {
{ id = "a3", kind = "switch_hotspot", state = "succeeded", ended_at = "2026-09-18T12:05:00Z" },
{ id = "a4", kind = "switch_campus", state = "failed", ended_at = "2026-09-18T12:09:00Z" },
},
}
local switched_view = bridge.status_view(switched, CONFIG, 1000)
equal("status.mode_after_switch", switched_view.current_mode, "hotspot")
equal("status.mode_label_hotspot", switched_view.mode_label, "热点模式")
equal("status.failed_switch_result", switched_view.action_result, "error")
-- Hotspot connectivity never inherits a campus identity or a campus probe.
-- The observed profile also survives service restart/action-history expiry.
local hotspot_snapshot = { service = "running", enabled = false, accounts = snapshot.accounts,
actions = {}, wireless = { state = "associated", hotspot_id = "h1", radio = "radio0",
ssid = "iPhone", iface = "wwan", device = "phy0-sta0", address = "192.0.2.2" } }
for _, case in ipairs({
{ "InternetReachable", "热点已联网", "online", "互联网可达" },
{ "Limited", "热点联网受限", "limited", "已连接但受限" },
{ "Offline", "热点联网检测失败", "offline", "互联网探测未通过" },
{ "Unknown", "热点已连接", "offline", "等待联网检测" },
}) do
hotspot_snapshot.wireless.connectivity = case[1]
local result = bridge.status_view(hotspot_snapshot, CONFIG, 1000)
equal("hotspot.mode", result.mode, "hotspot")
equal("hotspot.status_" .. case[1], result.status, case[2])
equal("hotspot.level_" .. case[1], result.connectivity_level, case[3])
equal("hotspot.connectivity_" .. case[1], result.connectivity, case[4])
equal("hotspot.no_campus_identity", result.online_account_label, "")
equal("hotspot.own_interface", result.current_iface, "wwan")
equal("hotspot.own_address", result.current_ip, "192.0.2.2")
end
hotspot_snapshot.accounts = {}
hotspot_snapshot.wireless.address = ""
equal("hotspot.awaiting_lease", bridge.status_view(hotspot_snapshot, CONFIG, 1000).status, "热点等待 IP 地址")
hotspot_snapshot.actions = switched.actions
hotspot_snapshot.wireless = { state = "disconnected" }
local disconnected_hotspot = bridge.status_view(hotspot_snapshot, CONFIG, 1000)
equal("hotspot.disconnected", disconnected_hotspot.status, "热点未连接")
equal("hotspot.no_old_ip", disconnected_hotspot.current_ip, "")
hotspot_snapshot.wireless = nil
equal("hotspot.expired", bridge.status_view(hotspot_snapshot, CONFIG, 1000).status, "正在读取热点状态")
hotspot_snapshot.wireless = {state = "associated", hotspot_id = "h1", address = "192.0.2.2", connectivity = "InternetReachable"}
hotspot_snapshot.service = "stopped"
equal("hotspot.stopped", bridge.status_view(hotspot_snapshot, CONFIG, 1000).connectivity_level, "offline")
-- A cancelled action is "forced", which is what the dialog's stop button means.
local cancelled = {
service = "running", enabled = true, written_at = "2026-09-18T12:00:10Z", accounts = {},
actions = { { id = "a5", kind = "manual_login", state = "cancelled", ended_at = "2026-09-18T12:00:00Z" } },
}
equal("status.cancelled", bridge.status_view(cancelled, CONFIG, 1000).action_result, "forced")
-- Quiet hours suspend a service that is still switched on. The page has to be
-- able to say that without contradicting the switch beside it.
local quiet = {
service = "running", enabled = true, pause = { "QuietHours" },
written_at = "2026-09-18T12:00:00Z", accounts = {}, actions = {},
}
local quiet_view = bridge.status_view(quiet, CONFIG, 1000)
equal("status.quiet_paused", quiet_view.paused, true)
equal("status.quiet_flag", quiet_view.in_quiet, true)
equal("status.quiet_keeps_the_switch", quiet_view.enabled, true)
local disabled = {
service = "running", enabled = false, pause = { "UserDisabled" },
written_at = "2026-09-18T12:00:00Z", accounts = {}, actions = {},
}
equal("status.disabled_is_not_quiet", bridge.status_view(disabled, CONFIG, 1000).in_quiet, false)
equal("status.disabled_is_paused", bridge.status_view(disabled, CONFIG, 1000).paused, true)
-- A line nobody has observed yet leaves the address empty rather than
-- repeating the configuration.
local unobserved = {
service = "running", enabled = true, written_at = "2026-09-18T12:00:00Z",
accounts = { { account_id = "c1", link = "Ready", auth = "Unknown",
connectivity = "Unknown" } },
actions = {},
}
local unobserved_view = bridge.status_view(unobserved, CONFIG, 1000)
equal("status.unobserved_ip", unobserved_view.current_ip, "")
equal("status.unobserved_device", unobserved_view.current_device, "")
-- The configured interface still stands in while the link is ready, which is
-- the first moment after a save and before the first attempt.
equal("status.unobserved_iface_falls_back", unobserved_view.current_iface, "wan")
-- A stopped service is a state, not a page error.
local stopped = bridge.status_view({ service = "stopped", enabled = false }, CONFIG, 1000)
equal("status.stopped", stopped.status, "认证服务已停止")
equal("status.stopped_level", stopped.connectivity_level, "offline")
equal("offline.status", bridge.offline_view("认证服务未在运行", 1000).status, "认证服务未在运行")
equal("offline.ts", bridge.offline_view(nil, 1000).ts, 1000)
-- A link that is not ready is reported as the link problem it is, not as an
-- authentication result the gateway never gave.
local pending_link = {
service = "running", enabled = true, written_at = "2026-09-18T12:00:00Z",
accounts = { { account_id = "c1", link = "AddressPending", auth = "Unknown", connectivity = "Unknown" } },
actions = {},
}
equal("status.link_problem", bridge.status_view(pending_link, CONFIG, 1000).status, "等待 IPv4 地址")
equal("status.link_iface_unknown", bridge.status_view(pending_link, CONFIG, 1000).current_iface, "")
-- Actual association data must win over a configured fixed BSSID. No radio
-- observation may borrow the old address or invent a current AP from config.
local wifi_config = { selection = {active_campus_id = "wifi"}, campus_accounts = {
{id = "wifi", access_mode = "wifi", ssid = "campus", bssid = "02:00:00:00:00:01", ap_selection = "fixed"}
} }
local wifi_state = {service = "running", accounts = {{account_id = "wifi", link = "Ready"}}, actions = {},
wireless = {state = "associated", device = "phy1-sta0", iface = "wwan", address = "192.0.2.9",
ssid = "campus", bssid = "02:00:00:00:00:02", signal = -53, channel = 44}}
local wifi_view = bridge.status_view(wifi_state, wifi_config, 1000)
equal("wifi.real_ap", wifi_view.current_bssid, "02:00:00:00:00:02")
equal("wifi.device", wifi_view.current_wireless_ifname, "phy1-sta0")
equal("wifi.signal", wifi_view.current_signal, -53)
equal("wifi.channel", wifi_view.current_channel, 44)
equal("wifi.policy", wifi_view.ap_selection_policy, "fixed")
equal("wifi.mismatch", wifi_view.ap_selection_reason, "当前接入点与固定 BSSID 不同")
wifi_state.wireless = nil
wifi_view = bridge.status_view(wifi_state, wifi_config, 1000)
equal("wifi.no_invented_ap", wifi_view.current_bssid, "")
equal("wifi.no_invented_ssid", wifi_view.current_ssid, "")
-- A failed manual login offers the school's page again, as 1.6.1 did.
--
-- The page has always had the link (renderPortalGuidance). The 2.0 bridge sent
-- "" in both places that feed it, so the path could never fire. The address is
-- the configured one for the account that failed, reduced to an origin.
local function failed(kind, account_id, state)
return { service = "running", written_at = "2026-09-18T12:00:30Z", actions = {
{ kind = kind, account_id = account_id, state = state or "failed",
message = "认证失败", ended_at = "2026-09-18T12:00:10Z" },
} }
end
equal("portal.failed_login", bridge.status_view(failed("manual_login", "c1"), CONFIG, 1000)
.last_action_portal_url, "http://10.0.0.55")
equal("portal.succeeded_login", bridge.status_view(failed("manual_login", "c1", "succeeded"), CONFIG, 1000)
.last_action_portal_url, "")
equal("portal.cancelled_login", bridge.status_view(failed("manual_login", "c1", "cancelled"), CONFIG, 1000)
.last_action_portal_url, "")
for _, kind in ipairs({ "manual_logout", "relogin", "switch_campus", "switch_hotspot" }) do
equal("portal.not_after_" .. kind, bridge.status_view(failed(kind, "c1"), CONFIG, 1000)
.last_action_portal_url, "")
end
-- The account the login was for, not the active one: c2 is not active here.
local elsewhere = {}
for key, value in pairs(CONFIG) do elsewhere[key] = value end
elsewhere.campus_accounts = {
CONFIG.campus_accounts[1],
{ id = "c2", base_url = "https://portal.example.edu:8443/srun_portal_pc?ac_id=8" },
}
equal("portal.uses_the_failed_account", bridge.status_view(failed("manual_login", "c2"), elsewhere, 1000)
.last_action_portal_url, "https://portal.example.edu:8443")
equal("portal.unknown_account", bridge.status_view(failed("manual_login", "gone"), CONFIG, 1000)
.last_action_portal_url, "")
-- Only a bare origin survives. Everything refused here is something a browser
-- would resolve differently from what the user sees, or would carry
-- credentials or control characters into a link.
for value, want in pairs({
["http://10.0.0.55"] = "http://10.0.0.55",
["http://10.0.0.55/"] = "http://10.0.0.55",
[" HTTP://gw.example.edu:801/path "] = "http://gw.example.edu:801",
["http://[fe80::1]:8080/x"] = "http://[fe80::1]:8080",
["http://user:pw@10.0.0.55"] = "",
["http://10.0.0.55\\@evil.example"] = "",
["javascript:alert(1)"] = "",
["ftp://10.0.0.55"] = "",
["http://"] = "",
["http://gw example"] = "",
["http://gw.example:port"] = "",
["http://gw.example:123456"] = "",
["10.0.0.55"] = "",
[""] = "",
}) do
equal("portal.origin[" .. value .. "]", bridge.portal_origin(value), want)
end
-- The progress dialog reads its own receipt through action_feedback.
local real_call = rpc.call
local answers = {}
rpc.call = function(method) return answers[method] end
answers["config.get"] = CONFIG
answers["action.get"] = { id = "a1", kind = "manual_login", account_id = "c1", state = "failed", message = "认证失败" }
equal("feedback.failed_login_portal", bridge.action_feedback("a1").last_action_portal_url, "http://10.0.0.55")
answers["action.get"] = { id = "a1", kind = "manual_login", account_id = "c1", state = "running" }
answers["config.get"] = nil
equal("feedback.running_reads_no_config", bridge.action_feedback("a1").last_action_portal_url, "")
answers["action.get"] = { id = "a1", kind = "manual_logout", account_id = "c1", state = "failed", message = "x" }
answers["config.get"] = CONFIG
equal("feedback.logout_has_no_portal", bridge.action_feedback("a1").last_action_portal_url, "")
answers["action.get"] = { id = "a1", kind = "manual_login", account_id = "c1", state = "failed", message = "x" }
answers["config.get"] = nil
equal("feedback.config_unreadable", bridge.action_feedback("a1").last_action_portal_url, "")
-- The form's default comes from the daemon's schema, so a page rendered with
-- a configuration that lacks the key still shows the flag checked.
answers["schema.get"] = { global = {
{ path = "log.level", kind = "enum", default = "INFO" },
{ path = "log.file_omit_info", kind = "bool", default = true },
} }
local defaults = bridge.defaults()
equal("defaults.log_file_omit_info", defaults.log_file_omit_info, "1")
equal("defaults.log_level", defaults.log_level, "INFO")
rpc.call = real_call
if failures > 0 then
io.stderr:write(string.format("%d check(s) failed\n", failures))
os.exit(1)
end
print("bridge probe: all checks passed")