ctl.lua 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. --[[
  2. tests/ctl.lua — Unit tests for ctl.lua
  3. Pure helpers (build_request, validation, rendering) + file-IPC round trip
  4. in a temp dir. No daemon required.
  5. ]]
  6. package.path = package.path .. ";./src/?.lua"
  7. local ctl = require("balancerlite.ctl")
  8. local os_tmp = os.tmpname()
  9. local passed = 0
  10. local failed = 0
  11. local function check(name, ok, detail)
  12. if ok then
  13. passed = passed + 1
  14. print(" PASS " .. name)
  15. else
  16. failed = failed + 1
  17. print(" FAIL " .. name .. (detail and (" " .. detail) or ""))
  18. end
  19. end
  20. -- Make a temp state dir for IPC tests
  21. local function mkdtemp()
  22. local p = "/tmp/bltest_" .. tostring(os.time()) .. "_" ..
  23. tostring(math.random(1000000))
  24. os.execute("mkdir -p " .. p)
  25. return p
  26. end
  27. -- -----------------------------------------------------------------
  28. -- TEST 1: build_request
  29. -- -----------------------------------------------------------------
  30. print("\n[1] build_request")
  31. do
  32. local r = ctl.build_request("switch", { target = "wan-b" })
  33. check("kind set", r.kind == "switch")
  34. check("target carried", r.target == "wan-b")
  35. check("ts is a number", type(r.ts) == "number" and r.ts > 0)
  36. end
  37. -- -----------------------------------------------------------------
  38. -- TEST 2: validate_switch_target
  39. -- -----------------------------------------------------------------
  40. print("\n[2] validate_switch_target")
  41. do
  42. local ok, _ = ctl.validate_switch_target("wan-a")
  43. check("wan-a accepted", ok == true)
  44. ok, _ = ctl.validate_switch_target("wan-b")
  45. check("wan-b accepted", ok == true)
  46. ok, err = ctl.validate_switch_target("wan-z")
  47. check("wan-z rejected", ok == false and type(err) == "string" and err:match("wan.z"))
  48. ok, _ = ctl.validate_switch_target(nil)
  49. check("nil rejected", ok == false)
  50. end
  51. -- -----------------------------------------------------------------
  52. -- TEST 3: validate_days
  53. -- -----------------------------------------------------------------
  54. print("\n[3] validate_days")
  55. do
  56. local n, err = ctl.validate_days(nil)
  57. check("nil -> default 30", n == 30 and err == nil)
  58. n, err = ctl.validate_days("")
  59. check("empty -> default 30", n == 30)
  60. n, err = ctl.validate_days("14")
  61. check("14 -> 14", n == 14)
  62. n, err = ctl.validate_days("14.7")
  63. check("14.7 -> 14 (floor)", n == 14)
  64. n, err = ctl.validate_days("0")
  65. check("0 rejected", n == nil and type(err) == "string")
  66. n, err = ctl.validate_days("notanumber")
  67. check("garbage rejected", n == nil)
  68. n, err = ctl.validate_days("99999")
  69. check("99999 rejected (>3650)", n == nil)
  70. end
  71. -- -----------------------------------------------------------------
  72. -- TEST 4: render_status with empty status
  73. -- -----------------------------------------------------------------
  74. print("\n[4] render_status empty")
  75. do
  76. local out = ctl.render_status(nil)
  77. check("contains 'no status file'", type(out) == "string" and out:match("no status file"))
  78. end
  79. -- -----------------------------------------------------------------
  80. -- TEST 5: render_status with real status
  81. -- -----------------------------------------------------------------
  82. print("\n[5] render_status happy path")
  83. do
  84. local st = { daemon=true, state="WAN_A_PRIMARY", active_wan="wan-a",
  85. host="rtr1", cycles=42, last_event_ts=os.time()-60, updated_ts=os.time() }
  86. local out = ctl.render_status(st)
  87. check("contains 'alive'", out:match("alive"))
  88. check("contains WAN_A_PRIMARY", out:match("WAN_A_PRIMARY"))
  89. check("contains active_wan=wan-a", out:match("wan%-a"))
  90. check("contains cycles 42", out:match("42"))
  91. end
  92. -- -----------------------------------------------------------------
  93. -- TEST 6: render_event
  94. -- -----------------------------------------------------------------
  95. print("\n[6] render_event")
  96. do
  97. local ev = { ts=os.time(), type="failover.switch",
  98. from_state="WAN_A_PRIMARY", to_state="WAN_B_PRIMARY",
  99. active_wan="wan-b", reason="primary down" }
  100. local out = ctl.render_event(ev)
  101. check("contains type", out:match("failover%.switch"))
  102. check("contains WAN_A -> WAN_B", out:match("WAN_A_PRIMARY") and out:match("WAN_B_PRIMARY"))
  103. check("contains active=wan-b", out:match("wan%-b"))
  104. check("contains reason", out:match("primary down"))
  105. end
  106. -- -----------------------------------------------------------------
  107. -- TEST 7: write_control / read_control round trip
  108. -- -----------------------------------------------------------------
  109. print("\n[7] write/read control round trip")
  110. do
  111. local dir = mkdtemp()
  112. local req = ctl.build_request("switch", { target = "wan-a" })
  113. local ok = ctl.write_control(dir, req)
  114. check("write ok", ok == true)
  115. check("control file exists",
  116. io.open(dir .. "/control.json", "r") ~= nil)
  117. local got = ctl.read_control(dir)
  118. check("read returns table", type(got) == "table")
  119. check("kind preserved", got.kind == "switch")
  120. check("target preserved", got.target == "wan-a")
  121. ctl.clear_control(dir)
  122. check("clear removes file",
  123. io.open(dir .. "/control.json", "r") == nil)
  124. os.execute("rm -rf " .. dir)
  125. end
  126. -- -----------------------------------------------------------------
  127. -- TEST 8: read_status on missing file returns nil
  128. -- -----------------------------------------------------------------
  129. print("\n[8] read_status missing")
  130. do
  131. local dir = mkdtemp()
  132. check("returns nil on missing status", ctl.read_status(dir) == nil)
  133. os.execute("rm -rf " .. dir)
  134. end
  135. -- -----------------------------------------------------------------
  136. -- TEST 9: cmd_switch writes valid JSON request
  137. -- -----------------------------------------------------------------
  138. print("\n[9] cmd_switch writes valid request")
  139. do
  140. local dir = mkdtemp()
  141. local rc = ctl.cmd_switch(dir, "wan-b")
  142. check("rc=0", rc == 0)
  143. local got = ctl.read_control(dir)
  144. check("kind=switch", got and got.kind == "switch")
  145. check("target=wan-b", got and got.target == "wan-b")
  146. ctl.clear_control(dir)
  147. os.execute("rm -rf " .. dir)
  148. end
  149. -- -----------------------------------------------------------------
  150. -- TEST 10: cmd_switch bad target exits 1
  151. -- -----------------------------------------------------------------
  152. print("\n[10] cmd_switch bad target")
  153. do
  154. local dir = mkdtemp()
  155. local rc = ctl.cmd_switch(dir, "wan-z")
  156. check("rc=1", rc == 1)
  157. check("no control file written",
  158. io.open(dir .. "/control.json", "r") == nil)
  159. os.execute("rm -rf " .. dir)
  160. end
  161. -- -----------------------------------------------------------------
  162. -- TEST 11: cmd_compact
  163. -- -----------------------------------------------------------------
  164. print("\n[11] cmd_compact")
  165. do
  166. local dir = mkdtemp()
  167. local rc = ctl.cmd_compact(dir, "7")
  168. check("rc=0", rc == 0)
  169. local got = ctl.read_control(dir)
  170. check("kind=compact", got and got.kind == "compact")
  171. check("days=7", got and got.days == 7)
  172. ctl.clear_control(dir)
  173. local rc2 = ctl.cmd_compact(dir, "garbage")
  174. check("bad days rc=1", rc2 == 1)
  175. os.execute("rm -rf " .. dir)
  176. end
  177. -- -----------------------------------------------------------------
  178. -- TEST 12: cmd_verify
  179. -- -----------------------------------------------------------------
  180. print("\n[12] cmd_verify")
  181. do
  182. local dir = mkdtemp()
  183. local rc = ctl.cmd_verify(dir)
  184. check("rc=0", rc == 0)
  185. local got = ctl.read_control(dir)
  186. check("kind=verify", got and got.kind == "verify")
  187. ctl.clear_control(dir)
  188. os.execute("rm -rf " .. dir)
  189. end
  190. -- -----------------------------------------------------------------
  191. -- TEST 13: cmd_status with no daemon returns rc=1
  192. -- -----------------------------------------------------------------
  193. print("\n[13] cmd_status no daemon")
  194. do
  195. local dir = mkdtemp()
  196. local rc = ctl.cmd_status(dir)
  197. check("rc=1 when no status file", rc == 1)
  198. os.execute("rm -rf " .. dir)
  199. end
  200. -- -----------------------------------------------------------------
  201. -- Done
  202. -- -----------------------------------------------------------------
  203. print(string.format("\n=== ctl: %d/%d passed ===", passed, passed + failed))
  204. if failed > 0 then os.exit(1) end