diff --git a/test/aprsme/is/is_supervisor_test.exs b/test/aprsme/is/is_supervisor_test.exs new file mode 100644 index 0000000..5b50444 --- /dev/null +++ b/test/aprsme/is/is_supervisor_test.exs @@ -0,0 +1,27 @@ +defmodule Aprsme.Is.IsSupervisorTest do + use ExUnit.Case, async: false + + alias Aprsme.Is.IsSupervisor + + describe "init/1" do + test "returns a supervisor spec with Aprsme.Is as a child" do + # init/1 returns the {:ok, sup_flags, child_specs} tuple wrapped via + # Supervisor.init/2 — the safest assertion is that the return type is + # a valid supervisor init result. + assert {:ok, {_sup_flags, child_specs}} = IsSupervisor.init(:ok) + assert is_list(child_specs) + assert Enum.any?(child_specs, fn spec -> spec.id == Aprsme.Is end) + end + end + + describe "start_link/1" do + test "the supervisor module exists and is callable" do + # We can't actually start the supervisor because Aprsme.Is.init/1 stops + # itself in the test environment via :test_environment_disabled. + # Verify the function exists and accepts the expected opts. + Code.ensure_loaded(IsSupervisor) + assert function_exported?(IsSupervisor, :start_link, 1) + assert function_exported?(IsSupervisor, :init, 1) + end + end +end diff --git a/test/aprsme/is_test.exs b/test/aprsme/is_test.exs index a1d53ba..49ef36e 100644 --- a/test/aprsme/is_test.exs +++ b/test/aprsme/is_test.exs @@ -498,6 +498,146 @@ defmodule Aprsme.IsTest do end end + # Local loopback TCP socket that exercises the active-socket branches. + # The server side just accepts and discards; the client's send/setopts calls + # still go through the real kernel. Returns {listen_sock, client_sock}. + defp loopback_socket do + {:ok, listen} = :gen_tcp.listen(0, [:binary, active: false, reuseaddr: true]) + {:ok, port} = :inet.port(listen) + + # Accept on a spawned process so connect doesn't block. + test_pid = self() + + spawn_link(fn -> + case :gen_tcp.accept(listen, 5_000) do + {:ok, server_sock} -> send(test_pid, {:server_sock, server_sock}) + _ -> :ok + end + end) + + {:ok, client} = :gen_tcp.connect(~c"127.0.0.1", port, [:binary, active: false]) + + receive do + {:server_sock, _s} -> :ok + after + 1_000 -> :ok + end + + on_exit = fn -> + _ = :gen_tcp.close(client) + _ = :gen_tcp.close(listen) + end + + {client, listen, on_exit} + end + + describe "handle_call({:send_message, _}, _, state) with active socket" do + setup do + ensure_ets_table() + {client, _listen, cleanup} = loopback_socket() + on_exit(cleanup) + {:ok, socket: client} + end + + test "sends a message and returns :ok", %{socket: socket} do + state = build_state(%{socket: socket}) + + capture_log(fn -> + assert {:reply, :ok, ^state} = + Aprsme.Is.handle_call({:send_message, "hello"}, self(), state) + end) + end + + test "handles send error by closing socket and scheduling reconnect", %{socket: socket} do + # Close the socket before send; the send call will return an error. + :gen_tcp.close(socket) + state = build_state(%{socket: socket}) + + capture_log(fn -> + assert {:reply, {:error, _reason}, new_state} = + Aprsme.Is.handle_call({:send_message, "fail"}, self(), state) + + assert is_nil(new_state.socket) + assert is_nil(new_state.timer) + assert is_nil(new_state.keepalive_timer) + end) + end + end + + describe "handle_info(:send_keepalive) with active socket" do + setup do + {client, _listen, cleanup} = loopback_socket() + on_exit(cleanup) + {:ok, socket: client} + end + + test "sends keepalive and schedules next", %{socket: socket} do + state = build_state(%{socket: socket, keepalive_timer: nil}) + + capture_log(fn -> + assert {:noreply, new_state} = Aprsme.Is.handle_info(:send_keepalive, state) + assert is_reference(new_state.keepalive_timer) + Process.cancel_timer(new_state.keepalive_timer) + end) + end + + test "stops on send error", %{socket: socket} do + :gen_tcp.close(socket) + state = build_state(%{socket: socket}) + + capture_log(fn -> + assert {:stop, :normal, _state} = Aprsme.Is.handle_info(:send_keepalive, state) + end) + end + end + + describe "handle_info({:backpressure, _}) with active socket" do + setup do + {client, _listen, cleanup} = loopback_socket() + on_exit(cleanup) + {:ok, socket: client} + end + + test "activate sets backpressure and installs safety valve", %{socket: socket} do + state = build_state(%{socket: socket, backpressure_active: false}) + + capture_log(fn -> + assert {:noreply, new_state} = + Aprsme.Is.handle_info({:backpressure, :activate}, state) + + assert new_state.backpressure_active + assert is_reference(new_state.safety_valve_timer) + Process.cancel_timer(new_state.safety_valve_timer) + end) + end + + test "deactivate clears backpressure and restores timer", %{socket: socket} do + state = build_state(%{socket: socket, backpressure_active: true, safety_valve_timer: nil}) + + capture_log(fn -> + assert {:noreply, new_state} = + Aprsme.Is.handle_info({:backpressure, :deactivate}, state) + + refute new_state.backpressure_active + assert is_reference(new_state.timer) + Process.cancel_timer(new_state.timer) + end) + end + + test "safety_valve forces resume when socket is active", %{socket: socket} do + state = build_state(%{socket: socket, backpressure_active: true, safety_valve_timer: nil}) + + capture_log(fn -> + assert {:noreply, new_state} = + Aprsme.Is.handle_info(:backpressure_safety_valve, state) + + refute new_state.backpressure_active + assert is_reference(new_state.timer) + Process.cancel_timer(new_state.timer) + end) + end + end + describe "send_message/3 and /1 helpers" do test "three-arg send_message delegates to send_message/1 without server running" do # GenServer not running → send_message/1 returns {:error, :not_connected}.