--[[ tests/routing.lua — Unit tests for routing.lua Pure helpers + injected-exec integration (no root needed). ]] package.path = package.path .. ";./src/?.lua" local routing = require("balancerlite.routing") 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: q() safe-quoting -- ----------------------------------------------------------------- print("\n[1] q() safe-quoting") do local q = routing.q check("plain host accepted", q("1.2.3.4") == "'1.2.3.4'", tostring(q("1.2.3.4"))) check("iface accepted", q("eth0") == "'eth0'", tostring(q("eth0"))) check("colon-in-port accepted", q("8.8.8.8:53") == "'8.8.8.8:53'", tostring(q("8.8.8.8:53"))) check("slash accepted", q("/dev/null") == "'/dev/null'", tostring(q("/dev/null"))) check("unsafe spaces rejected", q("a b") == nil, tostring(q("a b"))) check("unsafe shell-meta rejected", q("a;rm") == nil, tostring(q("a;rm"))) check("nil rejected", q(nil) == nil, "nil returned non-nil") check("number rejected", q(42) == nil, "number returned non-nil") end -- ----------------------------------------------------------------- -- TEST 2: build_switch_cmds happy path -- ----------------------------------------------------------------- print("\n[2] build_switch_cmds") do local rcfg = { table_a = 100, table_b = 101, mark_a = 0x1, mark_b = 0x2 } local wan = { id = "wan-a", interface = "eth0", gateway = "1.2.3.1", routing_table = 100, mark = 0x1, } local r = routing.build_switch_cmds(rcfg, wan, 254) check("ok", r.ok, r.err) check("2 cmds emitted", r.cmds and #r.cmds == 2, "got " .. tostring(#r.cmds)) check("main-table replace present", r.cmds[1]:match("table 254") and r.cmds[1]:match("via '1.2.3.1'") and r.cmds[1]:match("dev 'eth0'"), "cmd: " .. tostring(r.cmds[1])) check("per-WAN table replace present", r.cmds[2]:match("table 100") and r.cmds[2]:match("via '1.2.3.1'"), "cmd: " .. tostring(r.cmds[2])) end -- ----------------------------------------------------------------- -- TEST 3: build_switch_cmds rejects unsafe iface -- ----------------------------------------------------------------- print("\n[3] build_switch_cmds unsafe input") do local r = routing.build_switch_cmds({}, { interface = "eth0; rm", gateway = "1.2.3.1" }, 254) check("rejected", r.ok == false) check("err set", type(r.err) == "string" and r.err:match("unsafe")) end -- ----------------------------------------------------------------- -- TEST 4: build_init_cmds covers both WANs + main -- ----------------------------------------------------------------- print("\n[4] build_init_cmds") do local rcfg = { table_a = 100, table_b = 101, mark_a = 0x1, mark_b = 0x2 } local wans = { { id = "wan-a", interface = "eth0", gateway = "1.2.3.1", routing_table = 100, mark = 0x1 }, { id = "wan-b", interface = "eth1", gateway = "4.5.6.1", routing_table = 101, mark = 0x2 }, } local r = routing.build_init_cmds(rcfg, wans, 254) check("ok", r.ok, r.err) check("4 cmds emitted (2 tables + 2 del/add rules per-WAN + 1 main)", r.cmds and #r.cmds == 7, "got " .. tostring(#r.cmds)) -- main table command is the last one local last = r.cmds[#r.cmds] check("main-table default via preferred WAN", last:match("table 254") and last:match("via '1.2.3.1'"), "cmd: " .. tostring(last)) end -- ----------------------------------------------------------------- -- TEST 5: parse_default_gw -- ----------------------------------------------------------------- print("\n[5] parse_default_gw") do local p = routing.parse_default_gw check("happy path", p("default via 1.2.3.1 dev eth0 proto static metric 100") == "1.2.3.1") check("returns first default only", p("default via 1.2.3.1 dev eth0\ndefault via 4.5.6.1 dev eth1") == "1.2.3.1") check("nil on empty", p("") == nil) check("nil on nil", p(nil) == nil) check("nil on no default", p("1.2.3.0/24 dev eth0") == nil) end -- ----------------------------------------------------------------- -- TEST 6: switch_to with injectable exec -- ----------------------------------------------------------------- print("\n[6] switch_to with exec injection") do local cmds_run = {} local function fake_exec(cmd) cmds_run[#cmds_run+1] = cmd; return 0 end local rcfg = { table_a = 100, table_b = 101, mark_a = 0x1, mark_b = 0x2 } local wans = { { id = "wan-a", interface = "eth0", gateway = "1.2.3.1", routing_table = 100, mark = 0x1 }, { id = "wan-b", interface = "eth1", gateway = "4.5.6.1", routing_table = 101, mark = 0x2 }, } local R = routing.new(rcfg, wans, { exec = fake_exec }) local r = R:switch_to("wan-b") check("ok", r.ok, r.err) check("2 cmds executed", #cmds_run == 2, "got " .. tostring(#cmds_run)) check("main-table cmd is wan-b's gateway", cmds_run[1]:match("via '4.5.6.1'") and cmds_run[1]:match("table 254")) check("wan-b table cmd", cmds_run[2]:match("via '4.5.6.1'") and cmds_run[2]:match("table 101")) end -- ----------------------------------------------------------------- -- TEST 7: switch_to returns failure when exec non-zero -- ----------------------------------------------------------------- print("\n[7] switch_to failure handling") do local function fake_exec(cmd) return 1 end -- ip failure local rcfg = { table_a = 100, table_b = 101 } local wans = { { id = "wan-a", interface = "eth0", gateway = "1.2.3.1", routing_table = 100 }, { id = "wan-b", interface = "eth1", gateway = "4.5.6.1", routing_table = 101 }, } local R = routing.new(rcfg, wans, { exec = fake_exec }) local r = R:switch_to("wan-a") check("ok=false", r.ok == false) check("err reports rc", type(r.err) == "string" and r.err:match("rc=1")) end -- ----------------------------------------------------------------- -- TEST 8: switch_to unknown wan -- ----------------------------------------------------------------- print("\n[8] switch_to unknown wan") do local R = routing.new({}, { { id = "wan-a", interface = "eth0", gateway = "1.2.3.1", routing_table = 100 }, }) local r = R:switch_to("wan-z") check("ok=false", r.ok == false) check("err mentions wan-z", type(r.err) == "string" and r.err:match("wan.z")) end -- ----------------------------------------------------------------- -- TEST 9: apply_default with dry-run -- ----------------------------------------------------------------- print("\n[9] apply_default dry-run") do local R = routing.new({}, { { id = "wan-a", interface = "eth0", gateway = "1.2.3.1", routing_table = 100, mark = 0x1 }, }, { dry_run = true }) local r = R:apply_default() check("ok", r.ok, r.err) check("dry=true", r.dry == true) end -- ----------------------------------------------------------------- -- Done -- ----------------------------------------------------------------- print(string.format("\n=== routing: %d/%d passed ===", passed, passed + failed)) if failed > 0 then os.exit(1) end