defmodule AprsmeWeb.Plugs.HealthCheckTest do # :health_status is an app-wide env var the plug reads, so these tests can't # safely run in parallel with anything else that might touch it. use AprsmeWeb.ConnCase, async: false alias AprsmeWeb.Plugs.HealthCheck setup do # Ensure ShutdownHandler isn't lingering in shutting_down: true from prior tests. if pid = Process.whereis(Aprsme.ShutdownHandler) do :sys.replace_state(pid, fn state -> %{state | shutting_down: false} end) end original = Application.get_env(:aprsme, :health_status, :healthy) Application.put_env(:aprsme, :health_status, :healthy) on_exit(fn -> Application.put_env(:aprsme, :health_status, original) end) :ok end describe "call/2 pass-through" do test "returns the conn unchanged for non-/health paths", %{conn: conn} do conn = %{conn | request_path: "/something-else"} result = HealthCheck.call(conn, []) assert result == conn refute result.halted end end describe "liveness probe" do test "returns 200 OK when healthy", %{conn: conn} do conn = %{conn | request_path: "/health"} result = HealthCheck.call(conn, probe_type: :liveness) assert result.status == 200 assert result.resp_body == "OK" assert result.halted end test "returns 200 even when readiness would fail (draining)", %{conn: conn} do original = Application.get_env(:aprsme, :health_status, :healthy) try do Application.put_env(:aprsme, :health_status, :draining) conn = %{conn | request_path: "/health"} result = HealthCheck.call(conn, probe_type: :liveness) assert result.status == 200 after Application.put_env(:aprsme, :health_status, original) end end end describe "readiness probe" do test "returns 200 OK under normal conditions", %{conn: conn} do conn = %{conn | request_path: "/health"} result = HealthCheck.call(conn, probe_type: :readiness) # Readiness also hits the DB and PubSub, which should work in test env. assert result.status in [200, 503] assert result.halted end test "returns 503 when health_status is :draining", %{conn: conn} do original = Application.get_env(:aprsme, :health_status, :healthy) try do Application.put_env(:aprsme, :health_status, :draining) conn = %{conn | request_path: "/health"} result = HealthCheck.call(conn, probe_type: :readiness) assert result.status == 503 assert result.resp_body =~ "draining" after Application.put_env(:aprsme, :health_status, original) end end test "defaults to readiness when no probe_type opt given", %{conn: conn} do conn = %{conn | request_path: "/health"} result = HealthCheck.call(conn, []) assert result.status in [200, 503] assert result.halted end end describe "init/1" do test "returns opts unchanged" do assert HealthCheck.init(probe_type: :liveness) == [probe_type: :liveness] assert HealthCheck.init([]) == [] end end describe "readiness probe when ShutdownHandler is marked shutting down" do test "returns 503 when ShutdownHandler reports shutting_down", %{conn: conn} do # Start a fake ShutdownHandler that always says "yes, shutting down". # Only start if not already running; if running, :sys.replace_state. original_shutdown_pid = Process.whereis(Aprsme.ShutdownHandler) # We can't easily replace the real ShutdownHandler without disrupting # the app. Instead, exercise the :draining short-circuit via health_status. original = Application.get_env(:aprsme, :health_status, :healthy) # credo:disable-for-next-line Jump.CredoChecks.ConditionalAssertion assert is_pid(original_shutdown_pid) or is_nil(original_shutdown_pid) # ShutdownHandler may or may not be running — legitimately conditional try do Application.put_env(:aprsme, :health_status, :draining) conn = %{conn | request_path: "/health"} result = HealthCheck.call(conn, probe_type: :readiness) assert result.status == 503 after Application.put_env(:aprsme, :health_status, original) end end end describe "liveness probe edge cases" do test "returns 200 when basic_health_checks is :ok", %{conn: conn} do conn = %{conn | request_path: "/health"} result = HealthCheck.call(conn, probe_type: :liveness) assert result.status == 200 assert result.resp_body =~ "OK" end end describe "call/2 halted conn short-circuit" do test "always halts the conn for /health paths", %{conn: conn} do conn = %{conn | request_path: "/health"} result = HealthCheck.call(conn, probe_type: :liveness) assert result.halted end end describe "shutting_down? helper paths" do test "returns 200 OK when no ShutdownHandler process is registered", %{conn: conn} do Application.put_env(:aprsme, :health_status, :healthy) original_pid = Process.whereis(Aprsme.ShutdownHandler) if original_pid do :erlang.unregister(Aprsme.ShutdownHandler) end try do conn = %{conn | request_path: "/health"} result = HealthCheck.call(conn, probe_type: :readiness) # ask_shutting_down?(nil) → false → readiness_status(_, false) → full_health_checks assert result.status == 200 after if original_pid && !Process.whereis(Aprsme.ShutdownHandler) do Process.register(original_pid, Aprsme.ShutdownHandler) end end end test "returns 200 OK even when registered ShutdownHandler-named pid is unresponsive", %{conn: conn} do Application.put_env(:aprsme, :health_status, :healthy) Application.put_env(:aprsme, :health_check_call_timeout_ms, 50) original_pid = Process.whereis(Aprsme.ShutdownHandler) # Register a dummy task pid under the name. GenServer.call to it will # time out (50ms); the catch clause in ask_if_alive returns false → readiness OK. if original_pid do :erlang.unregister(Aprsme.ShutdownHandler) end task = Task.async(fn -> Process.sleep(500) end) Process.register(task.pid, Aprsme.ShutdownHandler) try do conn = %{conn | request_path: "/health"} result = HealthCheck.call(conn, probe_type: :readiness) # The dummy pid is alive but won't respond to :shutting_down? → catch → false → OK assert result.status == 200 after Application.delete_env(:aprsme, :health_check_call_timeout_ms) if Process.whereis(Aprsme.ShutdownHandler) == task.pid do :erlang.unregister(Aprsme.ShutdownHandler) end Task.shutdown(task, :brutal_kill) if original_pid && !Process.whereis(Aprsme.ShutdownHandler) do Process.register(original_pid, Aprsme.ShutdownHandler) end end end end describe "liveness check failure path" do test "exercises basic_health_checks via Application.get_env in normal flow", %{conn: conn} do # The basic_health_checks function reads :aprsme :env. If that succeeds, # liveness returns :ok. We can't easily make Application.get_env crash, # but we can cover the success branch fully. conn = %{conn | request_path: "/health"} result = HealthCheck.call(conn, probe_type: :liveness) assert result.status == 200 assert result.resp_body == "OK" end end describe "ask_if_alive false branch" do test "ShutdownHandler-named registration pointing to a dead pid resolves to not-shutting-down", %{conn: conn} do Application.put_env(:aprsme, :health_status, :healthy) original_pid = Process.whereis(Aprsme.ShutdownHandler) if original_pid do :erlang.unregister(Aprsme.ShutdownHandler) end # Spawn a task that exits immediately, then register it. Process.alive? # on the exited pid returns false → ask_if_alive(false, _pid) → false. task = Task.async(fn -> :ok end) _ = Task.await(task) # Wait briefly to ensure pid is dead. Process.sleep(20) try do Process.register(task.pid, Aprsme.ShutdownHandler) rescue _ -> :ok end try do conn = %{conn | request_path: "/health"} result = HealthCheck.call(conn, probe_type: :readiness) # Readiness should pass because the dead pid → false → run full checks. assert result.status in [200, 503] after if Process.whereis(Aprsme.ShutdownHandler) == task.pid do try do :erlang.unregister(Aprsme.ShutdownHandler) rescue _ -> :ok end end if original_pid && !Process.whereis(Aprsme.ShutdownHandler) do Process.register(original_pid, Aprsme.ShutdownHandler) end end end end describe "readiness probe success path" do test "returns 200 with OK when all checks pass", %{conn: conn} do Application.put_env(:aprsme, :health_status, :healthy) conn = %{conn | request_path: "/health"} result = HealthCheck.call(conn, probe_type: :readiness) assert result.status == 200 assert result.resp_body == "OK" assert result.halted end test "returns 503 when ShutdownHandler reports shutting_down via the live process", %{conn: conn} do Application.put_env(:aprsme, :health_status, :healthy) pid = Process.whereis(Aprsme.ShutdownHandler) assert pid, "ShutdownHandler must be running to exercise the live shutting_down? path" :sys.replace_state(pid, fn state -> %{state | shutting_down: true} end) try do conn = %{conn | request_path: "/health"} result = HealthCheck.call(conn, probe_type: :readiness) assert result.status == 503 assert result.resp_body =~ "shutting down" after :sys.replace_state(pid, fn state -> %{state | shutting_down: false} end) end end end end