--[[ tests/probes_config.lua — regression tests for the three testbed bugs: 1. probes.all() must accept per-WAN tcp targets (tables or "host:port" strings) instead of dereferencing a nil global. 2. config.load() must find UCI wan sections named wan_a / wan_b (hyphens are not valid UCI identifiers) and map them onto the internal wan-a / wan-b ids. 3. main.lua --dry-run: covered by the daemon dry-run smoke check ("(DRY RUN)" banner + "exited after 1 cycles"). Pure-Lua, no root, no network (exec is not invoked by these paths except via injected stubs / disabled probes). ]] package.path = package.path .. ";./src/?.lua" local probes = require("balancerlite.probes") local config = require("balancerlite.config") local passed = 0 local failed = 0 local function check(name, ok, detail) if ok then passed = passed + 1 print(" PASS " .. name) else failed = failed + 1 print(" FAIL " .. name .. (detail and (" " .. detail) or "")) end end -- ----------------------------------------------------------------- -- TEST 1: probes.all with per-WAN tcp targets (the drive_cycle crash) -- ----------------------------------------------------------------- print("\n[1] probes.all per-WAN tcp targets") do -- All probe types disabled except TCP; use a dead local port so -- nc fails fast (connect refused, no network dependency). local P = probes.new({ tcp_targets = {}, -- global list intentionally empty tcp_enabled = true, icmp_enabled = false, dns_enabled = false, wg_enabled = false, tcp_timeout = 1, }) -- Table form local r = probes.all(P, "lo", { { host = "127.0.0.1", port = 1 } }, nil) check("returns 1 result", #r == 1, "got " .. tostring(#r)) check("probe is tcp", r[1] and r[1].name == "tcp") check("no crash on empty global list", true) -- String form local P2 = probes.new({ tcp_targets = {}, tcp_enabled = true, icmp_enabled = false, dns_enabled = false, wg_enabled = false, tcp_timeout = 1, }) local r2 = probes.all(P2, "lo", { "127.0.0.1:1" }, nil) check("string form returns 1 result", #r2 == 1, "got " .. tostring(#r2)) check("string form probe is tcp", r2[1] and r2[1].name == "tcp") -- nil override → falls back to global list local P3 = probes.new({ tcp_targets = { { host = "127.0.0.1", port = 1 } }, tcp_enabled = true, icmp_enabled = false, dns_enabled = false, wg_enabled = false, tcp_timeout = 1, }) local r3 = probes.all(P3, "lo", nil, nil) check("nil override uses global", #r3 == 1, "got " .. tostring(#r3)) -- Empty per-WAN list → no tcp probes at all local P4 = probes.new({ tcp_targets = { { host = "127.0.0.1", port = 1 } }, tcp_enabled = true, icmp_enabled = false, dns_enabled = false, wg_enabled = false, }) local r4 = probes.all(P4, "lo", {}, nil) check("empty override yields no results", #r4 == 0, "got " .. tostring(#r4)) end -- ----------------------------------------------------------------- -- TEST 2: config.load maps UCI wan_a/wan_b sections to wan-a/wan-b -- ----------------------------------------------------------------- print("\n[2] config.load wan section naming") do local dir = "/tmp/bl_pcfg_" .. tostring(os.time()) .. "_" .. tostring(math.random(1000000)) os.execute("mkdir -p " .. dir) local p = dir .. "/balancerlite" local f = io.open(p, "w") f:write([[ config general 'general' option host 'unit-test' config health 'health' option probe_interval '1s' config wan 'wan_a' option interface 'eth10' option address '192.0.2.10/24' option gateway '192.0.2.1' option preference '90' config wan 'wan_b' option interface 'eth11' option address '198.51.100.10/24' option gateway '198.51.100.1' option preference '10' ]]) f:close() local cfg, err = config.load(p) check("config loads", cfg ~= nil, tostring(err)) check("two wans parsed", cfg and #cfg.wans == 2, cfg and tostring(#cfg.wans)) check("wan-a interface from wan_a section", cfg and cfg.wans[1].interface == "eth10", cfg and tostring(cfg.wans[1].interface)) check("wan-a address from wan_a section", cfg and cfg.wans[1].address == "192.0.2.10/24", cfg and tostring(cfg.wans[1].address)) check("wan-a gateway from wan_a section", cfg and cfg.wans[1].gateway == "192.0.2.1", cfg and tostring(cfg.wans[1].gateway)) check("wan-a preference from wan_a section", cfg and cfg.wans[1].preference == 90, cfg and tostring(cfg.wans[1].preference)) check("wan-b interface from wan_b section", cfg and cfg.wans[2].interface == "eth11", cfg and tostring(cfg.wans[2].interface)) check("wan-b gateway from wan_b section", cfg and cfg.wans[2].gateway == "198.51.100.1", cfg and tostring(cfg.wans[2].gateway)) os.execute("rm -rf " .. dir) end -- ----------------------------------------------------------------- -- TEST 3: config.load still accepts hyphenated wan-a/wan-b sections -- (back-compat) -- ----------------------------------------------------------------- print("\n[3] config.load hyphenated sections (back-compat)") do local dir = "/tmp/bl_pcfg2_" .. tostring(os.time()) .. "_" .. tostring(math.random(1000000)) os.execute("mkdir -p " .. dir) local p = dir .. "/balancerlite" local f = io.open(p, "w") f:write([[ config wan 'wan-a' option interface 'eth20' option gateway '10.20.0.1' config wan 'wan-b' option interface 'eth21' option gateway '10.21.0.1' ]]) f:close() local cfg, err = config.load(p) check("config loads", cfg ~= nil, tostring(err)) check("wan-a interface from wan-a section", cfg and cfg.wans[1].interface == "eth20", cfg and tostring(cfg.wans[1].interface)) check("wan-b interface from wan-b section", cfg and cfg.wans[2].interface == "eth21", cfg and tostring(cfg.wans[2].interface)) os.execute("rm -rf " .. dir) end -- ----------------------------------------------------------------- -- TEST 4: no wan sections at all → defaults, no crash -- ----------------------------------------------------------------- print("\n[4] config.load zero wan sections") do local dir = "/tmp/bl_pcfg3_" .. tostring(os.time()) .. "_" .. tostring(math.random(1000000)) os.execute("mkdir -p " .. dir) local p = dir .. "/balancerlite" local f = io.open(p, "w") f:write([[ config general 'general' option host 'nowan' ]]) f:close() local cfg, err = config.load(p) check("config loads", cfg ~= nil, tostring(err)) check("two wans defaulted", cfg and #cfg.wans == 2) check("default iface eth0", cfg and cfg.wans[1].interface == "eth0", cfg and tostring(cfg.wans[1].interface)) os.execute("rm -rf " .. dir) end -- ----------------------------------------------------------------- -- Done -- ----------------------------------------------------------------- print(string.format("\n=== probes_config: %d/%d passed ===", passed, passed + failed)) if failed > 0 then os.exit(1) end