--[[ tests/ctl.lua — Unit tests for ctl.lua Pure helpers (build_request, validation, rendering) + file-IPC round trip in a temp dir. No daemon required. ]] package.path = package.path .. ";./src/?.lua" local ctl = require("balancerlite.ctl") local os_tmp = os.tmpname() 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 -- Make a temp state dir for IPC tests local function mkdtemp() local p = "/tmp/bltest_" .. tostring(os.time()) .. "_" .. tostring(math.random(1000000)) os.execute("mkdir -p " .. p) return p end -- ----------------------------------------------------------------- -- TEST 1: build_request -- ----------------------------------------------------------------- print("\n[1] build_request") do local r = ctl.build_request("switch", { target = "wan-b" }) check("kind set", r.kind == "switch") check("target carried", r.target == "wan-b") check("ts is a number", type(r.ts) == "number" and r.ts > 0) end -- ----------------------------------------------------------------- -- TEST 2: validate_switch_target -- ----------------------------------------------------------------- print("\n[2] validate_switch_target") do local ok, _ = ctl.validate_switch_target("wan-a") check("wan-a accepted", ok == true) ok, _ = ctl.validate_switch_target("wan-b") check("wan-b accepted", ok == true) ok, err = ctl.validate_switch_target("wan-z") check("wan-z rejected", ok == false and type(err) == "string" and err:match("wan.z")) ok, _ = ctl.validate_switch_target(nil) check("nil rejected", ok == false) end -- ----------------------------------------------------------------- -- TEST 3: validate_days -- ----------------------------------------------------------------- print("\n[3] validate_days") do local n, err = ctl.validate_days(nil) check("nil -> default 30", n == 30 and err == nil) n, err = ctl.validate_days("") check("empty -> default 30", n == 30) n, err = ctl.validate_days("14") check("14 -> 14", n == 14) n, err = ctl.validate_days("14.7") check("14.7 -> 14 (floor)", n == 14) n, err = ctl.validate_days("0") check("0 rejected", n == nil and type(err) == "string") n, err = ctl.validate_days("notanumber") check("garbage rejected", n == nil) n, err = ctl.validate_days("99999") check("99999 rejected (>3650)", n == nil) end -- ----------------------------------------------------------------- -- TEST 4: render_status with empty status -- ----------------------------------------------------------------- print("\n[4] render_status empty") do local out = ctl.render_status(nil) check("contains 'no status file'", type(out) == "string" and out:match("no status file")) end -- ----------------------------------------------------------------- -- TEST 5: render_status with real status -- ----------------------------------------------------------------- print("\n[5] render_status happy path") do local st = { daemon=true, state="WAN_A_PRIMARY", active_wan="wan-a", host="rtr1", cycles=42, last_event_ts=os.time()-60, updated_ts=os.time() } local out = ctl.render_status(st) check("contains 'alive'", out:match("alive")) check("contains WAN_A_PRIMARY", out:match("WAN_A_PRIMARY")) check("contains active_wan=wan-a", out:match("wan%-a")) check("contains cycles 42", out:match("42")) end -- ----------------------------------------------------------------- -- TEST 6: render_event -- ----------------------------------------------------------------- print("\n[6] render_event") do local ev = { ts=os.time(), type="failover.switch", from_state="WAN_A_PRIMARY", to_state="WAN_B_PRIMARY", active_wan="wan-b", reason="primary down" } local out = ctl.render_event(ev) check("contains type", out:match("failover%.switch")) check("contains WAN_A -> WAN_B", out:match("WAN_A_PRIMARY") and out:match("WAN_B_PRIMARY")) check("contains active=wan-b", out:match("wan%-b")) check("contains reason", out:match("primary down")) end -- ----------------------------------------------------------------- -- TEST 7: write_control / read_control round trip -- ----------------------------------------------------------------- print("\n[7] write/read control round trip") do local dir = mkdtemp() local req = ctl.build_request("switch", { target = "wan-a" }) local ok = ctl.write_control(dir, req) check("write ok", ok == true) check("control file exists", io.open(dir .. "/control.json", "r") ~= nil) local got = ctl.read_control(dir) check("read returns table", type(got) == "table") check("kind preserved", got.kind == "switch") check("target preserved", got.target == "wan-a") ctl.clear_control(dir) check("clear removes file", io.open(dir .. "/control.json", "r") == nil) os.execute("rm -rf " .. dir) end -- ----------------------------------------------------------------- -- TEST 8: read_status on missing file returns nil -- ----------------------------------------------------------------- print("\n[8] read_status missing") do local dir = mkdtemp() check("returns nil on missing status", ctl.read_status(dir) == nil) os.execute("rm -rf " .. dir) end -- ----------------------------------------------------------------- -- TEST 9: cmd_switch writes valid JSON request -- ----------------------------------------------------------------- print("\n[9] cmd_switch writes valid request") do local dir = mkdtemp() local rc = ctl.cmd_switch(dir, "wan-b") check("rc=0", rc == 0) local got = ctl.read_control(dir) check("kind=switch", got and got.kind == "switch") check("target=wan-b", got and got.target == "wan-b") ctl.clear_control(dir) os.execute("rm -rf " .. dir) end -- ----------------------------------------------------------------- -- TEST 10: cmd_switch bad target exits 1 -- ----------------------------------------------------------------- print("\n[10] cmd_switch bad target") do local dir = mkdtemp() local rc = ctl.cmd_switch(dir, "wan-z") check("rc=1", rc == 1) check("no control file written", io.open(dir .. "/control.json", "r") == nil) os.execute("rm -rf " .. dir) end -- ----------------------------------------------------------------- -- TEST 11: cmd_compact -- ----------------------------------------------------------------- print("\n[11] cmd_compact") do local dir = mkdtemp() local rc = ctl.cmd_compact(dir, "7") check("rc=0", rc == 0) local got = ctl.read_control(dir) check("kind=compact", got and got.kind == "compact") check("days=7", got and got.days == 7) ctl.clear_control(dir) local rc2 = ctl.cmd_compact(dir, "garbage") check("bad days rc=1", rc2 == 1) os.execute("rm -rf " .. dir) end -- ----------------------------------------------------------------- -- TEST 12: cmd_verify -- ----------------------------------------------------------------- print("\n[12] cmd_verify") do local dir = mkdtemp() local rc = ctl.cmd_verify(dir) check("rc=0", rc == 0) local got = ctl.read_control(dir) check("kind=verify", got and got.kind == "verify") ctl.clear_control(dir) os.execute("rm -rf " .. dir) end -- ----------------------------------------------------------------- -- TEST 13: cmd_status with no daemon returns rc=1 -- ----------------------------------------------------------------- print("\n[13] cmd_status no daemon") do local dir = mkdtemp() local rc = ctl.cmd_status(dir) check("rc=1 when no status file", rc == 1) os.execute("rm -rf " .. dir) end -- ----------------------------------------------------------------- -- Done -- ----------------------------------------------------------------- print(string.format("\n=== ctl: %d/%d passed ===", passed, passed + failed)) if failed > 0 then os.exit(1) end