diff --git a/test/microwaveprop/propagation/path_compute_test.exs b/test/microwaveprop/propagation/path_compute_test.exs index 0d61ce3b..283041f4 100644 --- a/test/microwaveprop/propagation/path_compute_test.exs +++ b/test/microwaveprop/propagation/path_compute_test.exs @@ -132,6 +132,80 @@ defmodule Microwaveprop.Propagation.PathComputeTest do end end + describe "compute/4 — full pipeline" do + setup do + Req.Test.stub(Microwaveprop.Terrain.ElevationClient, fn conn -> + params = Plug.Conn.fetch_query_params(conn).query_params + lat_count = params["latitude"] |> String.split(",") |> length() + Req.Test.json(conn, %{"elevation" => List.duplicate(200.0, lat_count)}) + end) + + :ok + end + + @station_params %{ + src_height_m: 10.0, + dst_height_m: 10.0, + tx_power_dbm: 30.0, + src_gain_dbi: 20.0, + dst_gain_dbi: 20.0 + } + + test "computes a result for a coordinate-pair input" do + assert {:ok, result} = + PathCompute.compute("32.9, -97.0", "33.5, -96.5", 10_000, @station_params) + + assert result.band_mhz == 10_000 + assert result.dist_km > 0 + assert is_float(result.bearing) + assert is_map(result.loss_budget) + assert is_map(result.power_budget) + # No HRRR profiles in the test DB → empty hrrr_points. + assert is_list(result.hrrr_points) + # Microwave band → no ionosphere readout. + assert result.ionosphere == nil + # No sounding rows in the test DB. + assert result.sounding == nil + end + + test "returns {:error, _} when source resolution fails" do + assert {:error, _msg} = + PathCompute.compute("", "32.9, -97.0", 10_000, @station_params) + end + + test "returns {:error, _} when destination resolution fails" do + assert {:error, _msg} = + PathCompute.compute("32.9, -97.0", "", 10_000, @station_params) + end + + test "falls back to a default band config for unknown band_mhz" do + # Unknown band falls back to BandConfig.get(10_000). + assert {:ok, result} = + PathCompute.compute("32.9, -97.0", "33.5, -96.5", 9_999, @station_params) + + assert result.band_mhz == 9_999 + # Result band_config should be the 10G default. + assert result.band_config + end + + test "60 GHz band exercises the high-h2o-absorption path" do + assert {:ok, result} = + PathCompute.compute("32.9, -97.0", "33.5, -96.5", 47_000, @station_params) + + # 47 GHz has substantially more h2o absorption than 10 GHz. + assert result.loss_budget.h2o > 0 + end + + test "Maidenhead grid input round-trips through resolve" do + assert {:ok, result} = + PathCompute.compute("EM12", "EM13", 10_000, @station_params) + + assert result.dist_km > 0 + assert result.source.kind == :grid + assert result.destination.kind == :grid + end + end + describe "compute_power_budget/2" do test "computes rx power and margins" do loss = %{total: 140.0, fspl: 130.0, o2: 3.0, h2o: 5.0, rain: 0.0, diffraction: 2.0} diff --git a/test/microwaveprop/pskr/client_test.exs b/test/microwaveprop/pskr/client_test.exs new file mode 100644 index 00000000..78ad686a --- /dev/null +++ b/test/microwaveprop/pskr/client_test.exs @@ -0,0 +1,113 @@ +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 "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 diff --git a/test/microwaveprop/rover/candidate_detail_test.exs b/test/microwaveprop/rover/candidate_detail_test.exs new file mode 100644 index 00000000..4b995822 --- /dev/null +++ b/test/microwaveprop/rover/candidate_detail_test.exs @@ -0,0 +1,43 @@ +defmodule Microwaveprop.Rover.CandidateDetailTest do + use ExUnit.Case, async: true + + alias Microwaveprop.Rover.CandidateDetail + + describe "summarize/5" do + test "returns the candidate's grid label and an empty links list when no stations are passed" do + candidate = %{lat: 32.9, lon: -97.0} + result = CandidateDetail.summarize(candidate, [], 10_000, ~U[2026-04-29 12:00:00Z], :ssb) + + assert is_binary(result.grid) + # 10-character grid for that point. + assert String.length(result.grid) == 10 + assert result.links == [] + end + + test "computes per-link summary fields when stations are supplied" do + # SRTM tiles are not present in test, so elev profile returns []. + # `summarize/5` handles that gracefully and still returns a link. + candidate = %{lat: 32.9, lon: -97.0} + + stations = [ + %{callsign: "W5LUA", lat: 33.05, lon: -96.6}, + %{callsign: "W5HN", lat: 32.7, lon: -97.4} + ] + + result = + CandidateDetail.summarize(candidate, stations, 10_000, ~U[2026-04-29 12:00:00Z], :ssb) + + assert length(result.links) == 2 + + Enum.each(result.links, fn link -> + assert is_binary(link.callsign) + assert is_float(link.distance_km) + assert is_binary(link.bearing) + assert is_float(link.bearing_deg) + # Profile is a list (may be empty if no SRTM tiles in test, or + # populated if a tiles_dir env var is set). + assert is_list(link.profile) + end) + end + end +end diff --git a/test/microwaveprop/weather/hrrr_native_client_test.exs b/test/microwaveprop/weather/hrrr_native_client_test.exs index 3ed105f7..1f5031a1 100644 --- a/test/microwaveprop/weather/hrrr_native_client_test.exs +++ b/test/microwaveprop/weather/hrrr_native_client_test.exs @@ -553,6 +553,47 @@ defmodule Microwaveprop.Weather.HrrrNativeClientTest do end end + describe "fetch_native_duct_grid/4 with injected http_get" do + setup do + prev = Application.get_env(:microwaveprop, :hrrr_native_http_get) + + on_exit(fn -> + if prev, + do: Application.put_env(:microwaveprop, :hrrr_native_http_get, prev), + else: Application.delete_env(:microwaveprop, :hrrr_native_http_get) + end) + + :ok + end + + @grid_spec %{ + lon_start: -97.0, + lon_count: 2, + lon_step: 0.1, + lat_start: 32.7, + lat_count: 2, + lat_step: 0.1 + } + + test "propagates idx HTTP non-200 status as an error tuple" do + Application.put_env(:microwaveprop, :hrrr_native_http_get, fn _url, _opts -> + {:ok, %{status: 404, body: ""}} + end) + + assert {:error, "HRRR native idx HTTP 404"} = + HrrrNativeClient.fetch_native_duct_grid(~D[2026-04-29], 12, @grid_spec) + end + + test "propagates idx transport error" do + Application.put_env(:microwaveprop, :hrrr_native_http_get, fn _url, _opts -> + {:error, :timeout} + end) + + assert {:error, :timeout} = + HrrrNativeClient.fetch_native_duct_grid(~D[2026-04-29], 12, @grid_spec) + end + end + describe "duct_byte_ranges/1 boundary inputs" do test "empty idx entries yields an empty range list" do assert HrrrNativeClient.duct_byte_ranges([]) == [] diff --git a/test/microwaveprop_web/live/path_live_test.exs b/test/microwaveprop_web/live/path_live_test.exs index 6d438fcc..d415f23b 100644 --- a/test/microwaveprop_web/live/path_live_test.exs +++ b/test/microwaveprop_web/live/path_live_test.exs @@ -417,6 +417,66 @@ defmodule MicrowavepropWeb.PathLiveTest do end end + describe "rover_path_id handle_params branch" do + test "non-existent UUID flashes error and pushes to /path", %{conn: conn} do + assert {:error, {:live_redirect, %{to: "/path"}}} = + live(conn, ~p"/path?rover_path_id=#{Ecto.UUID.generate()}") + end + + test "non-UUID string falls through cleanly", %{conn: conn} do + # A non-UUID id can't be Ecto.UUID.cast'd → load_cached_path returns + # {:error, :not_found} → flash + push_navigate to /path. + assert {:error, {:live_redirect, %{to: "/path"}}} = + live(conn, ~p"/path?rover_path_id=not-a-uuid") + end + end + + describe "path_forecast_detail event" do + test "no-op when no result has been computed yet", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/path") + + # No result in assigns → fall through `else: _ -> {:noreply, socket}`. + assert render_hook(view, "path_forecast_detail", %{"iso" => "2026-04-29T12:00:00Z"}) =~ + "Path Calculator" + end + + test "no-op for unparseable iso even after a result is in place", %{conn: conn} do + params = %{ + "source" => "32.9, -97.0", + "destination" => "33.5, -96.5", + "band" => "10000" + } + + {:ok, view, _html} = live(conn, ~p"/path?#{params}") + # Let async compute_path finish. + _ = render(view) + + # Garbage iso → falls into the `else` arm of the with-statement. + assert render_hook(view, "path_forecast_detail", %{"iso" => "not-iso"}) =~ + "Path Calculator" + end + + test "pushes a payload event when a result is in place and iso is valid", %{conn: conn} do + params = %{ + "source" => "32.9, -97.0", + "destination" => "33.5, -96.5", + "band" => "10000" + } + + {:ok, view, _html} = live(conn, ~p"/path?#{params}") + _ = render(view) + + iso = + DateTime.utc_now() + |> DateTime.truncate(:second) + |> DateTime.to_iso8601() + + # No score in DB for this point/time → returns the `unavailable: true` + # payload, but still exercises the success branch of the handler. + assert render_hook(view, "path_forecast_detail", %{"iso" => iso}) =~ "Path Calculator" + end + end + describe "property: band round-trip in form" do property "for every configured band, submitting the form re-renders with that band selected" do # Re-stub the elevation client for each property iteration — diff --git a/test/microwaveprop_web/live/rover_live_test.exs b/test/microwaveprop_web/live/rover_live_test.exs index b22d0698..81b1d4a0 100644 --- a/test/microwaveprop_web/live/rover_live_test.exs +++ b/test/microwaveprop_web/live/rover_live_test.exs @@ -105,6 +105,47 @@ defmodule MicrowavepropWeb.RoverLiveTest do end end + describe "additional event handlers (anonymous)" do + test "toggle_only_good flips the filter assign without recomputing", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/rover") + render_hook(view, "toggle_only_good", %{}) + # Page still renders. + assert render(view) =~ "Rover planning" + end + + test "toggle_station flips a station's selected flag", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/rover") + + # Anonymous mount uses "call:CALLSIGN" ids for default stations. + render_hook(view, "toggle_station", %{"id" => "call:W5LUA"}) + assert render(view) =~ "Rover planning" + end + + test "delete_station removes a station from anonymous list", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/rover") + + render_hook(view, "delete_station", %{"id" => "call:W5LUA"}) + + html = render(view) + assert html =~ "W5HN" + end + + test "close_candidate_detail clears the detail panel", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/rover") + + render_hook(view, "close_candidate_detail", %{}) + assert render(view) =~ "Rover planning" + end + + test "update_station_grid edits a station's grid in the anonymous list", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/rover") + + render_hook(view, "update_station_grid", %{"station_id" => "call:W5LUA", "grid" => "EM13xx"}) + # No crash; page still renders. + assert render(view) =~ "Rover planning" + end + end + describe "logged-in mount" do setup :register_and_log_in_user @@ -139,6 +180,58 @@ defmodule MicrowavepropWeb.RoverLiveTest do assert Enum.any?(Rover.list_stations(user), &(&1.callsign == "K5TX")) end + test "delete_station removes a persisted station for the logged-in user", %{conn: conn, user: user} do + {:ok, view, _html} = live(conn, ~p"/rover") + + [first | _] = Rover.list_stations(user) + render_hook(view, "delete_station", %{"id" => to_string(first.id)}) + + assert length(Rover.list_stations(user)) == 2 + end + + test "toggle_station flips the persisted selected flag", %{conn: conn, user: user} do + {:ok, view, _html} = live(conn, ~p"/rover") + + [first | _] = Rover.list_stations(user) + render_hook(view, "toggle_station", %{"id" => to_string(first.id)}) + + reloaded = Repo.reload(first) + assert reloaded.selected != first.selected + end + + test "update_station_grid updates the grid for a persisted station", %{conn: conn, user: user} do + {:ok, view, _html} = live(conn, ~p"/rover") + + [first | _] = Rover.list_stations(user) + render_hook(view, "update_station_grid", %{"station_id" => to_string(first.id), "grid" => "EM12kp"}) + + reloaded = Repo.reload(first) + assert String.upcase(reloaded.grid) == "EM12KP" + end + + test "update_station_grid surfaces error for invalid grid input", %{conn: conn, user: user} do + {:ok, view, _html} = live(conn, ~p"/rover") + + [first | _] = Rover.list_stations(user) + + html = + render_hook(view, "update_station_grid", %{"station_id" => to_string(first.id), "grid" => "ZZZ99"}) + + assert html =~ "invalid grid" + end + + test "update_station_grid is a no-op for blank input", %{conn: conn, user: user} do + {:ok, view, _html} = live(conn, ~p"/rover") + + [first | _] = Rover.list_stations(user) + original_grid = first.grid + + render_hook(view, "update_station_grid", %{"station_id" => to_string(first.id), "grid" => ""}) + + reloaded = Repo.reload(first) + assert reloaded.grid == original_grid + end + test "set_home_qth persists the user's home QTH", %{conn: conn, user: user} do {:ok, view, _html} = live(conn, ~p"/rover") diff --git a/test/microwaveprop_web/live/rover_planning_live/path_show_test.exs b/test/microwaveprop_web/live/rover_planning_live/path_show_test.exs new file mode 100644 index 00000000..e059cef3 --- /dev/null +++ b/test/microwaveprop_web/live/rover_planning_live/path_show_test.exs @@ -0,0 +1,95 @@ +defmodule MicrowavepropWeb.RoverPlanningLive.PathShowTest do + use MicrowavepropWeb.ConnCase, async: true + + import Ecto.Query + import Phoenix.LiveViewTest + + alias Microwaveprop.AccountsFixtures + alias Microwaveprop.Repo + alias Microwaveprop.Rover + alias Microwaveprop.RoverPlanning + alias Microwaveprop.RoverPlanning.Path, as: RoverPath + alias Microwaveprop.Terrain.ElevationClient + + setup do + Req.Test.stub(ElevationClient, fn conn -> + params = Plug.Conn.fetch_query_params(conn).query_params + lat_count = params["latitude"] |> String.split(",") |> length() + Req.Test.json(conn, %{"elevation" => List.duplicate(200.0, lat_count)}) + end) + + :ok + end + + defp create_mission(user) do + {:ok, _} = Rover.create_location(user, %{lat: 32.0, lon: -97.0, status: :good}) + + {:ok, mission} = + RoverPlanning.create_mission(user, %{ + "name" => "Test mission", + "band_mhz" => 10_000, + "only_known_good" => true, + "rover_height_ft" => 8.0, + "station_height_ft" => 30.0, + "stations" => %{"0" => %{"input" => "EM12kp", "position" => 0}} + }) + + mission + end + + describe "mount" do + test "redirects to mission show with flash when path is not found", %{conn: conn} do + user = AccountsFixtures.user_fixture() + conn = log_in_user(conn, user) + mission = create_mission(user) + + assert {:error, {:live_redirect, %{to: redirect_to, flash: flash}}} = + live(conn, ~p"/rover-planning/#{mission.id}/paths/#{Ecto.UUID.generate()}") + + assert redirect_to == "/rover-planning/#{mission.id}" + assert flash["error"] =~ "not found" + end + + test "renders the path detail page when given a valid path id", %{conn: conn} do + user = AccountsFixtures.user_fixture() + conn = log_in_user(conn, user) + mission = create_mission(user) + + [first_path | _] = + Repo.all(from(p in RoverPath, where: p.mission_id == ^mission.id)) + + # Populate result so the page renders the body. + result_payload = %{ + "verdict" => "CLEAR", + "distance_km" => 25.5, + "path_points" => [ + %{"dist_km" => 0.0, "elev" => 200, "beam" => 200, "r1" => 0}, + %{"dist_km" => 12.5, "elev" => 220, "beam" => 220, "r1" => 5}, + %{"dist_km" => 25.5, "elev" => 240, "beam" => 240, "r1" => 0} + ], + "verdict_label" => "CLEAR" + } + + {:ok, path} = + first_path + |> RoverPath.changeset(%{result: result_payload, status: :complete}) + |> Repo.update() + + {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}/paths/#{path.id}") + assert html =~ "Test mission" + end + + test "redirects when mission_id is not a UUID", %{conn: conn} do + user = AccountsFixtures.user_fixture() + conn = log_in_user(conn, user) + mission = create_mission(user) + + assert {:error, {:live_redirect, %{to: redirect_to}}} = + live(conn, ~p"/rover-planning/not-uuid/paths/#{Ecto.UUID.generate()}") + + # When mission_id can't be cast, push_navigate goes to /rover-planning/. + assert redirect_to =~ "rover-planning" + _ = mission + end + end +end diff --git a/test/microwaveprop_web/live/skewt_live_test.exs b/test/microwaveprop_web/live/skewt_live_test.exs index 01a84b80..efafbd30 100644 --- a/test/microwaveprop_web/live/skewt_live_test.exs +++ b/test/microwaveprop_web/live/skewt_live_test.exs @@ -3,6 +3,81 @@ defmodule MicrowavepropWeb.SkewtLiveTest do import Phoenix.LiveViewTest + alias Microwaveprop.Weather.HrrrProfile + + describe "with HRRR profile data near the queried location" do + setup do + profile_data = [ + %{"pres" => 1000.0, "hght" => 110, "tmpc" => 25.0, "dwpc" => 18.0}, + %{"pres" => 925.0, "hght" => 770, "tmpc" => 18.0, "dwpc" => 12.0}, + %{"pres" => 850.0, "hght" => 1500, "tmpc" => 14.0, "dwpc" => 8.0}, + %{"pres" => 700.0, "hght" => 3000, "tmpc" => 0.0, "dwpc" => -10.0}, + %{"pres" => 500.0, "hght" => 5500, "tmpc" => -20.0, "dwpc" => -30.0} + ] + + # Insert near EM12kp (32.646, -97.125) + now = DateTime.truncate(DateTime.utc_now(), :second) + + %HrrrProfile{} + |> HrrrProfile.changeset(%{ + valid_time: now, + run_time: now, + lat: 32.65, + lon: -97.12, + profile: profile_data, + hpbl_m: 1500.0, + pwat_mm: 25.0, + surface_temp_c: 25.0, + surface_dewpoint_c: 18.0, + surface_pressure_mb: 1013.0, + surface_refractivity: 320.5, + min_refractivity_gradient: -45.0, + ducting_detected: false + }) + |> Microwaveprop.Repo.insert!() + + :ok + end + + test "renders the time selector when valid_times list is non-empty", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/skewt?q=EM12kp") + _ = render_async(view) + # We can't reliably assert HRRR data renders since fetch_rich_cell + # tries an HTTP call that fails — but the page should not crash. + assert render(view) =~ "Skew-T" + end + end + + describe "select_time event" do + test "ignored when no location is set yet", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/skewt") + + # No location resolved → select_time falls through to the no-op. + result = render_hook(view, "select_time", %{"valid_time" => "2026-04-29T12:00:00Z"}) + + # Render still works. + assert result =~ "Skew-T" + end + + test "ignored when valid_time is unparseable", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/skewt?q=EM12kp") + render_async(view) + + result = render_hook(view, "select_time", %{"valid_time" => "not-a-timestamp"}) + + assert result =~ "Skew-T" + end + end + + describe "search event" do + test "patches URL when user submits the form", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/skewt") + + render_hook(view, "search", %{"q" => "EM13"}) + assert render_async(view) =~ "EM13" + end + end + describe "GET /skewt" do test "renders search bar with no results when no query is supplied", %{conn: conn} do {:ok, _view, html} = live(conn, ~p"/skewt") diff --git a/test/microwaveprop_web/live/skewt_location_resolver_test.exs b/test/microwaveprop_web/live/skewt_location_resolver_test.exs index 401a287d..4498cfe4 100644 --- a/test/microwaveprop_web/live/skewt_location_resolver_test.exs +++ b/test/microwaveprop_web/live/skewt_location_resolver_test.exs @@ -1,6 +1,7 @@ defmodule MicrowavepropWeb.SkewtLocationResolverTest do use Microwaveprop.DataCase, async: false + alias Microwaveprop.CallsignLocation.Location alias MicrowavepropWeb.SkewtLocationResolver describe "classify/1" do @@ -29,6 +30,36 @@ defmodule MicrowavepropWeb.SkewtLocationResolverTest do end end + describe "resolve/1 — callsign branch" do + test "resolves a cached callsign via CallsignLocation lookup" do + # Seed the cache so the resolver doesn't hit QRZ. + %Location{} + |> Location.changeset(%{ + callsign: "W5ISP", + latitude: 33.0, + longitude: -97.0, + gridsquare: "EM13QC" + }) + |> Microwaveprop.Repo.insert!() + + assert {:ok, resolved} = SkewtLocationResolver.resolve("W5ISP") + assert resolved.source == :callsign + assert resolved.lat == 33.0 + assert resolved.lon == -97.0 + assert resolved.label =~ "W5ISP" + end + + test "surfaces the lookup error message for an unknown callsign" do + # No cached row + a QRZ stub that 404s. + Req.Test.stub(Microwaveprop.Qrz.Client, fn conn -> + Plug.Conn.send_resp(conn, 404, "Not Found") + end) + + assert {:error, msg} = SkewtLocationResolver.resolve("XX9XYZ") + assert msg =~ "callsign lookup failed" + end + end + describe "resolve/1 — grid square branch" do test "returns lat/lon for a valid grid square" do assert {:ok, %{lat: lat, lon: lon, label: "EM12KP", source: :grid}} = diff --git a/test/mix/tasks/weather_rebatch_asos_test.exs b/test/mix/tasks/weather_rebatch_asos_test.exs index 0c4625de..53c8f3fa 100644 --- a/test/mix/tasks/weather_rebatch_asos_test.exs +++ b/test/mix/tasks/weather_rebatch_asos_test.exs @@ -86,4 +86,24 @@ defmodule Mix.Tasks.Weather.RebatchAsosTest do output = ExUnit.CaptureIO.capture_io(fn -> RebatchAsos.run([]) end) assert output =~ "Nothing to do" end + + describe "Mix task wrapper" do + test "run/1 with no args is a no-op" do + output = + ExUnit.CaptureIO.capture_io(fn -> + assert :ok = Mix.Tasks.Weather.RebatchAsos.run([]) + end) + + assert output =~ "Nothing to do" + end + + test "run/1 with --dry-run flag" do + output = + ExUnit.CaptureIO.capture_io(fn -> + assert :ok = Mix.Tasks.Weather.RebatchAsos.run(["--dry-run"]) + end) + + assert output =~ "Nothing to do" + end + end end