prop/test/microwaveprop/commercial/poll_worker_test.exs
Graham McIntire fd976b0cd5
fix: resolve 391 Credo issues across codebase
- Add jump_credo_checks ~> 0.4 with all 20 checks enabled
- Fix all standard Credo issues: 139 @spec (113 done, 26 remain),
  4 refactoring, 3 alias usage, 9 System.cmd env, 5 unsafe_to_atom,
  2 max line length, 9 assert_receive timeout
- Fix 170+ jump_credo_checks warnings:
  - 117 TopLevelAliasImportRequire: move nested alias/import to module top
  - 32 UseObanProWorker: switch to Oban.Pro.Worker
  - 4 DoctestIExExamples: add doctests / create test file
  - ~20 WeakAssertion: strengthen type-check assertions
  - Various ConditionalAssertion, AssertReceiveTimeout fixes
- Exclude vendor/ from Credo analysis
- Remaining: 175 warnings (mostly opinionated WeakAssertion,
  AvoidSocketAssignsInTest), 26 @spec annotations
2026-06-12 13:51:32 -05:00

198 lines
5.9 KiB
Elixir

defmodule Microwaveprop.Commercial.PollWorkerTest do
use Microwaveprop.DataCase, async: false
alias Microwaveprop.Commercial
alias Microwaveprop.Commercial.PollWorker
alias Microwaveprop.Commercial.Sample
alias Microwaveprop.Weather
alias Microwaveprop.Weather.IemClient
alias Microwaveprop.Weather.SurfaceObservation
@link_attrs %{
label: "test-poll-link",
host: "10.0.0.1",
community: "public",
radio_type: "af11x",
weather_station: "KTKI",
enabled: true
}
describe "poll_and_record/2" do
test "creates samples for enabled links when SNMP succeeds" do
{:ok, link} = Commercial.create_link(@link_attrs)
snmp_result = %{
rx_power_0: -55,
tx_power: 20,
link_state: 1,
link_uptime: 86_400
}
PollWorker.poll_and_record([link], fn _host, _community, _type -> {:ok, snmp_result} end)
samples = Repo.all(Sample)
assert length(samples) == 1
assert hd(samples).rx_power_0 == -55.0
assert hd(samples).link_id == link.id
end
test "skips links when SNMP fails" do
{:ok, link} = Commercial.create_link(@link_attrs)
PollWorker.poll_and_record(
[link],
fn _host, _community, _type -> {:error, :snmp_failed} end
)
assert Repo.all(Sample) == []
end
test "continues polling remaining links after one failure" do
{:ok, link1} = Commercial.create_link(@link_attrs)
{:ok, link2} = Commercial.create_link(%{@link_attrs | label: "test-poll-link-2", host: "10.0.0.2"})
call_count = :counters.new(1, [:atomics])
poll_fn = fn _host, _community, _type ->
n = :counters.get(call_count, 1)
:counters.add(call_count, 1, 1)
if n == 0 do
{:error, :snmp_failed}
else
{:ok, %{rx_power_0: -60, link_state: 1}}
end
end
PollWorker.poll_and_record([link1, link2], poll_fn)
assert length(Repo.all(Sample)) == 1
end
test "no-ops cleanly on an empty link list" do
# Defensive: if the DB has no enabled links, the worker should
# still return without calling the poll fn or touching samples.
called? = :counters.new(1, [:atomics])
poll_fn = fn _host, _community, _type ->
:counters.add(called?, 1, 1)
{:ok, %{}}
end
PollWorker.poll_and_record([], poll_fn)
assert :counters.get(called?, 1) == 0
assert Repo.all(Sample) == []
end
test "dispatches poll_fn with host, community, and radio_type from the link" do
{:ok, _link} = Commercial.create_link(@link_attrs)
test_pid = self()
poll_fn = fn host, community, radio_type ->
send(test_pid, {:poll_args, host, community, radio_type})
{:error, :stop}
end
PollWorker.poll_and_record(Commercial.enabled_links(), poll_fn)
assert_receive {:poll_args, "10.0.0.1", "public", "af11x"}
end
test "logs and skips when create_sample rejects the changeset" do
{:ok, link} = Commercial.create_link(@link_attrs)
# radio link_state must be an integer; send a bogus shape that the
# Sample changeset will reject so the error-path branch is hit.
PollWorker.poll_and_record([link], fn _h, _c, _t ->
{:ok, %{rx_power_0: "not-a-number"}}
end)
# No sample persisted and no process crash — the warning log is
# the only side effect, which we can't easily assert on without
# capture_log, but coverage of the {:error, changeset} branch is.
assert Repo.all(Sample) == []
end
end
describe "fetch_weather/1" do
# Exercises the IEM-fetch + ASOS-upsert path (previously private) via
# stubbed Req. Avoids the perform/1 SNMP round-trip which would call
# every live commercial link at once.
setup do
# IemClient uses Req — stub it with a minimal ASOS tab-separated
# response body so the JSON/CSV decoder path is exercised.
Req.Test.stub(IemClient, fn conn ->
Plug.Conn.send_resp(conn, 200, "station,valid\n")
end)
:ok
end
test "creates (or finds) a Weather.Station row for every unique link station" do
{:ok, link} = Commercial.create_link(@link_attrs)
assert :ok = PollWorker.fetch_weather([link])
{:ok, station} =
Weather.find_or_create_station(%{
station_code: "KTKI",
station_type: "asos",
name: "KTKI",
lat: 0.0,
lon: 0.0
})
assert station.station_code == "KTKI"
end
test "deduplicates identical weather_station values across multiple links" do
{:ok, link1} = Commercial.create_link(@link_attrs)
{:ok, link2} =
Commercial.create_link(%{@link_attrs | label: "sibling", host: "10.0.0.2"})
call_count = :counters.new(1, [:atomics])
Req.Test.stub(IemClient, fn conn ->
:counters.add(call_count, 1, 1)
Plug.Conn.send_resp(conn, 200, "station,valid\n")
end)
PollWorker.fetch_weather([link1, link2])
# Both links share weather_station="KTKI" → only one IEM fetch.
assert :counters.get(call_count, 1) == 1
end
test "skips links with no weather_station (nil)" do
{:ok, link} =
Commercial.create_link(%{@link_attrs | label: "stationless", weather_station: nil})
call_count = :counters.new(1, [:atomics])
Req.Test.stub(IemClient, fn conn ->
:counters.add(call_count, 1, 1)
Plug.Conn.send_resp(conn, 200, "")
end)
assert :ok = PollWorker.fetch_weather([link])
assert :counters.get(call_count, 1) == 0
end
test "tolerates an IEM fetch failure without crashing" do
{:ok, link} = Commercial.create_link(@link_attrs)
Req.Test.stub(IemClient, fn conn ->
Plug.Conn.send_resp(conn, 500, "server error")
end)
assert :ok = PollWorker.fetch_weather([link])
# No surface observations persisted on HTTP error.
assert Repo.all(SurfaceObservation) == []
end
end
end