- Add 17 missing @spec annotations (layouts, error_json, error_html, skewt_svg) - Move 12+ nested alias/import/require to module top level - Add phx-change/id attributes to 11 raw HTML <form> tags - Remove 4 unused LiveView assigns (:bounds, :data_provider) - Add 3 missing doctest references (HrrrNativeClient, BulkFetch, Accounts) - Break 2 long lines (path_compute.ex:382) - Strengthen weak test assertions (is_binary→byte_size, is_list→!=[]) - Replace Module.concat with Module.safe_concat (2 occurrences) - Replace length/1 > 0 with list != [] (9 occurrences) - Remove no-op assert true, fix no-assertion tests Remaining: 24 socket.assigns introspection warnings (deliberate test pattern for observable behavior testing), 1 formatter-resistant long line, 3 app-code usage warnings.
135 lines
4.4 KiB
Elixir
135 lines
4.4 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} = start_supervised({Task, fn -> Process.sleep(:infinity) end})
|
|
:global.unregister_name(Client)
|
|
:yes = :global.register_name(Client, placeholder)
|
|
|
|
on_exit(fn ->
|
|
:global.unregister_name(Client)
|
|
end)
|
|
|
|
%{placeholder: placeholder}
|
|
end
|
|
|
|
describe "start_link + init (standby branch)" do
|
|
test "starts as :standby when the global name is already taken" do
|
|
pid = start_supervised!({Client, name: __MODULE__.NotRegistered})
|
|
state = :sys.get_state(pid)
|
|
|
|
assert state.role == :standby
|
|
assert state.socket == nil
|
|
assert state.buffer == <<>>
|
|
assert Enum.all?(state.bands, &is_binary/1)
|
|
assert byte_size(state.client_id) > 0
|
|
end
|
|
|
|
test "uses defaults for bands when none are passed" do
|
|
pid = start_supervised!({Client, []})
|
|
state = :sys.get_state(pid)
|
|
|
|
assert "6m" in state.bands
|
|
assert "2m" in state.bands
|
|
assert "70cm" in state.bands
|
|
end
|
|
|
|
test "honors custom bands list and aggregator name from opts" do
|
|
pid = start_supervised!({Client, bands: ["2m"], aggregator: SomeOtherAggregator})
|
|
state = :sys.get_state(pid)
|
|
|
|
assert state.bands == ["2m"]
|
|
assert state.aggregator == SomeOtherAggregator
|
|
end
|
|
|
|
test "honors custom client_id" do
|
|
pid = start_supervised!({Client, client_id: "test-client-42"})
|
|
state = :sys.get_state(pid)
|
|
assert state.client_id == "test-client-42"
|
|
end
|
|
end
|
|
|
|
describe "tcp messages with mismatched socket" do
|
|
test "tcp message for a stale socket is ignored", %{placeholder: _} do
|
|
pid = start_supervised!({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"})
|
|
assert Process.alive?(pid)
|
|
assert :sys.get_state(pid).socket == nil
|
|
end
|
|
|
|
test "tcp_closed for a stale socket is ignored", %{placeholder: _} do
|
|
pid = start_supervised!({Client, []})
|
|
send(pid, {:tcp_closed, :stale_port})
|
|
assert Process.alive?(pid)
|
|
assert :sys.get_state(pid).socket == nil
|
|
end
|
|
|
|
test "tcp_error for a stale socket is ignored", %{placeholder: _} do
|
|
pid = start_supervised!({Client, []})
|
|
send(pid, {:tcp_error, :stale_port, :reason})
|
|
assert Process.alive?(pid)
|
|
assert :sys.get_state(pid).socket == nil
|
|
end
|
|
end
|
|
|
|
describe ":reconnect message routing" do
|
|
test "schedules a connect attempt for a standby (immediate :continue, :connect)" do
|
|
pid = start_supervised!({Client, []})
|
|
send(pid, :reconnect)
|
|
assert Process.alive?(pid)
|
|
end
|
|
end
|
|
|
|
describe "terminate/2" do
|
|
test "is graceful when state has no socket" do
|
|
pid = start_supervised!({Client, []})
|
|
# Just normal stop — terminate(_, %{socket: nil_or_other})
|
|
ref = Process.monitor(pid)
|
|
GenServer.stop(pid)
|
|
assert_receive {:DOWN, ^ref, :process, ^pid, _reason}
|
|
end
|
|
end
|
|
|
|
describe "standby handle_info" do
|
|
test "nodeup re-runs election (stays standby while name is taken)" do
|
|
pid = start_supervised!({Client, []})
|
|
send(pid, {:nodeup, :test@nowhere})
|
|
_ = :sys.get_state(pid)
|
|
|
|
state = :sys.get_state(pid)
|
|
assert state.role == :standby
|
|
end
|
|
|
|
test "nodedown re-runs election (stays standby while name is taken)" do
|
|
pid = start_supervised!({Client, []})
|
|
send(pid, {:nodedown, :test@nowhere})
|
|
_ = :sys.get_state(pid)
|
|
|
|
state = :sys.get_state(pid)
|
|
assert state.role == :standby
|
|
end
|
|
|
|
test "unrelated info messages are ignored" do
|
|
pid = start_supervised!({Client, []})
|
|
send(pid, :unrelated_message)
|
|
assert Process.alive?(pid)
|
|
assert :sys.get_state(pid).socket == nil
|
|
end
|
|
|
|
test ":ping with no socket is a no-op" do
|
|
pid = start_supervised!({Client, []})
|
|
send(pid, :ping)
|
|
_ = :sys.get_state(pid)
|
|
state = :sys.get_state(pid)
|
|
assert state.socket == nil
|
|
end
|
|
end
|
|
end
|