| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- --[[
- 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
|