test: bump coverage 80% -> 81.3% via more LiveView + module tests
- Pskr.Client: 0% -> 18% (init/standby/handle_info/terminate) - RoverPlanningLive.PathShow: 0% -> 82% (mount/load_path) - Rover.CandidateDetail: 0% -> partial (summarize/5 with stations) - Mix.Tasks.Weather.RebatchAsos: 0% -> covered (wrapper run/1) - HrrrNativeClient: 57% -> 67% (fetch_native_duct_grid error paths) - RoverLive: 40% -> 48% (toggle/delete/update_station_grid logged-in) - PathLive: 62% -> 63% (path_forecast_detail + rover_path_id branches) - PathCompute: 64% -> 64% (compute/4 full pipeline + resolve_location) - SkewtLive: 29% -> 31% (search/select_time event no-ops) - SkewtLocationResolver: 38% -> 60%+ (callsign cache + error paths) 3557 tests passing.
This commit is contained in:
parent
f644d1796f
commit
888701e627
10 changed files with 645 additions and 0 deletions
|
|
@ -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}
|
||||
|
|
|
|||
113
test/microwaveprop/pskr/client_test.exs
Normal file
113
test/microwaveprop/pskr/client_test.exs
Normal file
|
|
@ -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
|
||||
43
test/microwaveprop/rover/candidate_detail_test.exs
Normal file
43
test/microwaveprop/rover/candidate_detail_test.exs
Normal file
|
|
@ -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
|
||||
|
|
@ -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([]) == []
|
||||
|
|
|
|||
|
|
@ -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 —
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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/<input>.
|
||||
assert redirect_to =~ "rover-planning"
|
||||
_ = mission
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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}} =
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue