defmodule Microwaveprop.Pskr.ClientTest do use ExUnit.Case, async: false alias Microwaveprop.Pskr.Client setup do # Pre-register a placeholder under the global name so the Client # under test loses leader election and goes to :standby — this # avoids any outbound MQTT connection in the test environment. {:ok, placeholder} = Task.start_link(fn -> Process.sleep(:infinity) end) :global.unregister_name(Client) :yes = :global.register_name(Client, placeholder) on_exit(fn -> :global.unregister_name(Client) Process.exit(placeholder, :kill) end) %{placeholder: placeholder} end describe "start_link + init (standby branch)" do test "starts as :standby when the global name is already taken" do {:ok, pid} = GenServer.start_link(Client, name: __MODULE__.NotRegistered) state = :sys.get_state(pid) assert state.role == :standby assert state.socket == nil assert state.buffer == <<>> assert is_list(state.bands) assert is_binary(state.client_id) GenServer.stop(pid) end test "uses defaults for bands when none are passed" do {:ok, pid} = GenServer.start_link(Client, []) state = :sys.get_state(pid) assert "6m" in state.bands assert "2m" in state.bands assert "70cm" in state.bands GenServer.stop(pid) end test "honors custom bands list and aggregator name from opts" do {:ok, pid} = GenServer.start_link(Client, bands: ["2m"], aggregator: SomeOtherAggregator) state = :sys.get_state(pid) assert state.bands == ["2m"] assert state.aggregator == SomeOtherAggregator GenServer.stop(pid) end test "honors custom client_id" do {:ok, pid} = GenServer.start_link(Client, client_id: "test-client-42") state = :sys.get_state(pid) assert state.client_id == "test-client-42" GenServer.stop(pid) end end describe "tcp messages with mismatched socket" do test "tcp message for a stale socket is ignored", %{placeholder: _} do {:ok, pid} = GenServer.start_link(Client, []) # Send a TCP message with a fake socket that doesn't match state.socket. # state.socket is nil, but :tcp pattern matches when socket matches state.socket. # Since state.socket is nil, this falls through to the catch-all handle_info. send(pid, {:tcp, :stale_port, "data"}) Process.sleep(20) assert Process.alive?(pid) GenServer.stop(pid) end test "tcp_closed for a stale socket is ignored", %{placeholder: _} do {:ok, pid} = GenServer.start_link(Client, []) send(pid, {:tcp_closed, :stale_port}) Process.sleep(20) assert Process.alive?(pid) GenServer.stop(pid) end test "tcp_error for a stale socket is ignored", %{placeholder: _} do {:ok, pid} = GenServer.start_link(Client, []) send(pid, {:tcp_error, :stale_port, :reason}) Process.sleep(20) assert Process.alive?(pid) GenServer.stop(pid) end end describe ":reconnect message routing" do test "schedules a connect attempt for a standby (immediate :continue, :connect)" do {:ok, pid} = GenServer.start_link(Client, []) send(pid, :reconnect) Process.sleep(50) # The handle_info(:reconnect) returns {:noreply, state, {:continue, :connect}}. # The connect will fail (no MQTT broker reachable) and schedule another retry. # We just verify the process didn't crash on receipt. assert Process.alive?(pid) GenServer.stop(pid) end end describe "terminate/2" do test "is graceful when state has no socket" do {:ok, pid} = GenServer.start_link(Client, []) # Just normal stop — terminate(_, %{socket: nil_or_other}) GenServer.stop(pid) refute Process.alive?(pid) end end describe "standby handle_info" do test "nodeup re-runs election (stays standby while name is taken)" do {:ok, pid} = GenServer.start_link(Client, []) send(pid, {:nodeup, :test@nowhere}) Process.sleep(50) state = :sys.get_state(pid) assert state.role == :standby GenServer.stop(pid) end test "nodedown re-runs election (stays standby while name is taken)" do {:ok, pid} = GenServer.start_link(Client, []) send(pid, {:nodedown, :test@nowhere}) Process.sleep(50) state = :sys.get_state(pid) assert state.role == :standby GenServer.stop(pid) end test "unrelated info messages are ignored" do {:ok, pid} = GenServer.start_link(Client, []) send(pid, :unrelated_message) Process.sleep(20) assert Process.alive?(pid) GenServer.stop(pid) end test ":ping with no socket is a no-op" do {:ok, pid} = GenServer.start_link(Client, []) send(pid, :ping) Process.sleep(20) state = :sys.get_state(pid) assert state.socket == nil GenServer.stop(pid) end end end