prop/test/microwaveprop/pskr/client_test.exs
Graham McIntire 363646f34f
test: enable :slow tests by default + add coverage tests across modules
Removed the `exclude: [:slow]` from test_helper.exs — the slow-tagged
wgrib2 fixture tests pass with the binary on PATH and pull
Microwaveprop.Weather.Grib2.Wgrib2 from 23% to 90%+ in coverage runs.

Additional tests:
- Pskr.Client: tcp/tcp_closed/tcp_error stale-socket fall-throughs
- Weather context: weather_point_detail/3, latest_grid_valid_time/0,
  available_weather_valid_times/0, available_hrdps_valid_times/0
- PathCompute.compute/4: with HRRR profile near the path
- RoverPathProfileWorker: real Path row + skip-when-complete
- RoverLive: URL stations encoding, add_station error paths
- PathLive: rover_path_id branches (404, malformed UUID)

3599 tests, 0 failures.
2026-05-08 09:28:51 -05:00

155 lines
4.9 KiB
Elixir

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