defmodule Aprsme.Cluster.ConnectionManagerTest do use ExUnit.Case, async: false alias Aprsme.Cluster.ConnectionManager alias Aprsme.Cluster.LeaderElection setup do # Stop any running ConnectionManager process if Process.whereis(ConnectionManager) do GenServer.stop(ConnectionManager) end # Stop any running LeaderElection process if Process.whereis(LeaderElection) do GenServer.stop(LeaderElection) end # Ensure DynamicSupervisor is started case Process.whereis(Aprsme.DynamicSupervisor) do nil -> {:ok, _} = DynamicSupervisor.start_link(strategy: :one_for_one, name: Aprsme.DynamicSupervisor) pid when is_pid(pid) -> :ok end on_exit(fn -> # Try to stop processes but don't crash if they're already dead try do if Process.whereis(ConnectionManager) do GenServer.stop(ConnectionManager, :normal, 100) end catch :exit, _ -> :ok end try do if Process.whereis(LeaderElection) do GenServer.stop(LeaderElection, :normal, 100) end catch :exit, _ -> :ok end # Clean up any IsSupervisor processes if DynamicSupervisor exists if Process.whereis(Aprsme.DynamicSupervisor) do try do children = DynamicSupervisor.which_children(Aprsme.DynamicSupervisor) Enum.each(children, fn {_, pid, _, _} -> if is_pid(pid) do DynamicSupervisor.terminate_child(Aprsme.DynamicSupervisor, pid) end end) catch :exit, _ -> :ok end end end) :ok end describe "start_link/1" do test "starts the GenServer process" do assert {:ok, pid} = ConnectionManager.start_link([]) assert Process.alive?(pid) assert Process.whereis(ConnectionManager) == pid end test "registers with the module name" do {:ok, _pid} = ConnectionManager.start_link([]) assert Process.whereis(ConnectionManager) end end describe "initialization" do setup do # Start a minimal LeaderElection to prevent crashes Application.put_env(:aprsme, :cluster_enabled, false) {:ok, _} = LeaderElection.start_link([]) Process.sleep(150) :ok end test "subscribes to leadership changes" do {:ok, _pid} = ConnectionManager.start_link([]) # Verify subscription by publishing a test message Phoenix.PubSub.broadcast( Aprsme.PubSub, "cluster:leadership", {:leadership_change, :test_node, false} ) # Give time for message processing Process.sleep(50) # Process should still be alive after receiving message assert Process.whereis(ConnectionManager) end test "schedules initial state check" do {:ok, pid} = ConnectionManager.start_link([]) # With delay set to 0 in test config, the check fires immediately Process.sleep(50) # Process should still be alive assert Process.alive?(pid) end end describe "leadership state handling" do setup do # Start LeaderElection in non-clustered mode so it becomes leader Application.put_env(:aprsme, :cluster_enabled, false) {:ok, _} = LeaderElection.start_link([]) # Wait for election (100ms timer + processing) Process.sleep(150) # Start ConnectionManager {:ok, pid} = ConnectionManager.start_link([]) {:ok, pid: pid} end test "handles initial state check when leader", %{pid: pid} do # Send initial state check send(pid, :check_initial_state) Process.sleep(50) # Should handle without crashing assert Process.alive?(pid) end test "handles initial state check when not leader", %{pid: pid} do # Send initial state check send(pid, :check_initial_state) Process.sleep(50) # Should handle without crashing assert Process.alive?(pid) end end describe "leadership change handling" do setup do # Ensure LeaderElection is started Application.put_env(:aprsme, :cluster_enabled, false) {:ok, _} = LeaderElection.start_link([]) Process.sleep(150) {:ok, pid} = ConnectionManager.start_link([]) # With delay=0, initial check fires immediately; just wait for processing Process.sleep(50) {:ok, pid: pid} end test "starts connection when becoming leader", %{pid: pid} do # Send leadership change - this node became leader send(pid, {:leadership_change, node(), true}) Process.sleep(50) # Should handle the message without crashing assert Process.alive?(pid) end test "stops connection when losing leadership", %{pid: pid} do # First become leader and start connection send(pid, {:leadership_change, node(), true}) Process.sleep(50) # Then lose leadership send(pid, {:leadership_change, node(), false}) Process.sleep(50) # Should handle the message without crashing assert Process.alive?(pid) end test "ignores leadership changes for other nodes", %{pid: pid} do other_node = :other@host # Send leadership change for another node send(pid, {:leadership_change, other_node, true}) Process.sleep(50) # Should not affect this node assert Process.alive?(pid) end test "does not start connection twice", %{pid: pid} do # Become leader twice send(pid, {:leadership_change, node(), true}) Process.sleep(50) send(pid, {:leadership_change, node(), true}) Process.sleep(50) # Should handle duplicate leadership without issues assert Process.alive?(pid) end end describe "APRS connection management" do setup do # Ensure LeaderElection is started Application.put_env(:aprsme, :cluster_enabled, false) {:ok, _} = LeaderElection.start_link([]) Process.sleep(150) {:ok, pid} = ConnectionManager.start_link([]) # With delay=0, initial check fires immediately; just wait for processing Process.sleep(50) {:ok, pid: pid} end test "handles IsSupervisor already started error", %{pid: pid} do # Try to become leader send(pid, {:leadership_change, node(), true}) Process.sleep(50) # Should handle gracefully even if IsSupervisor fails to start assert Process.alive?(pid) end test "handles IsSupervisor start failure", %{pid: pid} do # Define a failing IsSupervisor defmodule FailingIsSupervisor do @moduledoc false def start_link(_opts) do {:error, :intentional_failure} end end # Send leadership change send(pid, {:leadership_change, node(), true}) Process.sleep(50) # Should handle failure gracefully assert Process.alive?(pid) end end describe "connection lifecycle" do setup do # Ensure LeaderElection is started Application.put_env(:aprsme, :cluster_enabled, false) {:ok, _} = LeaderElection.start_link([]) Process.sleep(150) {:ok, pid} = ConnectionManager.start_link([]) {:ok, pid: pid} end test "full lifecycle - become leader, lose leadership", %{pid: pid} do # With delay=0, initial check fires immediately; just wait for processing Process.sleep(50) # Become leader send(pid, {:leadership_change, node(), true}) Process.sleep(50) # Verify connection started (check state indirectly) assert Process.alive?(pid) # Lose leadership send(pid, {:leadership_change, node(), false}) Process.sleep(50) # Verify still alive after stopping connection assert Process.alive?(pid) end end end