defmodule Aprsme.ShutdownHandlerTest do # async: false — shares app env and the named ShutdownHandler process. use ExUnit.Case, async: false alias Aprsme.ShutdownHandler describe "handle_call(:shutting_down?, _, state)" do test "reflects the shutting_down flag in state" do # Pure callback invocation — no side effects. assert {:reply, false, %{shutting_down: false}} = ShutdownHandler.handle_call(:shutting_down?, self(), %{ shutting_down: false, drain_timeout: 0 }) assert {:reply, true, %{shutting_down: true}} = ShutdownHandler.handle_call(:shutting_down?, self(), %{ shutting_down: true, drain_timeout: 0 }) end end describe "handle_call(:shutdown, _, state)" do test "returns :already_shutting_down when already in progress" do state = %{shutting_down: true, drain_timeout: 0} assert {:reply, :already_shutting_down, ^state} = ShutdownHandler.handle_call(:shutdown, self(), state) end end describe "handle_info({:EXIT, pid, reason}, state)" do test "is a no-op that keeps state intact" do state = %{shutting_down: false, drain_timeout: 0} assert {:noreply, ^state} = ShutdownHandler.handle_info({:EXIT, self(), :normal}, state) end end describe "shutting_down?/0 without the GenServer running" do test "returns false via the rescue clause" do # The supervised ShutdownHandler may or may not be running. If the # GenServer.call raises, the `rescue _ -> false` kicks in and we get # a boolean back either way. assert ShutdownHandler.shutting_down?() == false end end describe "init/1" do test "traps exits and reads drain timeout from the environment" do # Run in a child process so we don't trip up the test process with a # sticky :trap_exit flag. task = Task.async(fn -> prev = System.get_env("DRAIN_TIMEOUT_MS") System.put_env("DRAIN_TIMEOUT_MS", "1234") try do assert {:ok, state} = ShutdownHandler.init([]) assert state.shutting_down == false assert state.drain_timeout == 1234 assert Process.info(self(), :trap_exit) == {:trap_exit, true} after if prev do System.put_env("DRAIN_TIMEOUT_MS", prev) else System.delete_env("DRAIN_TIMEOUT_MS") end end end) Task.await(task) end end describe "handle_call(:shutdown, _, state) starting shutdown" do test "marks state as shutting_down" do # initiate_shutdown also schedules :begin_connection_drain after 15s via # Process.send_after — too long to wait for in tests. We just verify the # state transition; handle_info(:begin_connection_drain) is tested below. state = %{shutting_down: false, drain_timeout: 50} assert {:reply, :ok, new_state} = ShutdownHandler.handle_call(:shutdown, self(), state) assert new_state.shutting_down == true end end describe "handle_info(:begin_connection_drain, state)" do test "schedules force_shutdown after drain_timeout" do state = %{shutting_down: true, drain_timeout: 50} assert {:noreply, ^state} = ShutdownHandler.handle_info(:begin_connection_drain, state) assert_receive :force_shutdown, 1000 end end describe "terminate/2" do test "returns :ok for normal shutdowns without invoking drain" do state = %{shutting_down: false, drain_timeout: 60_000} assert :ok = ShutdownHandler.terminate(:shutdown, state) assert :ok = ShutdownHandler.terminate({:shutdown, :any}, state) assert :ok = ShutdownHandler.terminate(:normal, state) end test "returns :ok when state is already shutting_down" do # Abnormal reason, but shutting_down is already true — no drain sleep. state = %{shutting_down: true, drain_timeout: 60_000} assert :ok = ShutdownHandler.terminate(:crash, state) end end describe "shutdown/0 as an external API" do test "returns the call result if the GenServer is up, else raises" do # shutting_down?/0 is safe to call — it rescues when the GenServer isn't running. assert ShutdownHandler.shutting_down?() == false end end describe "terminate/2 with abnormal reason" do test "runs drain path with a tiny drain_timeout and returns :ok" do # Use a near-zero drain_timeout so the Process.sleep inside the graceful # drain path is effectively instant. This exercises mark_unhealthy + # stop_accepting_connections + Process.sleep without blocking the suite. prev_status = Application.get_env(:aprsme, :health_status) state = %{shutting_down: false, drain_timeout: 1} try do assert :ok = ShutdownHandler.terminate(:killed, state) # Drain path flips the app's health status to :draining. assert Application.get_env(:aprsme, :health_status) == :draining after if prev_status do Application.put_env(:aprsme, :health_status, prev_status) else Application.delete_env(:aprsme, :health_status) end end end end end