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