Fix LiveView crashes and stabilize test suite
Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 4m31s

This commit is contained in:
Graham McIntire 2026-08-01 09:26:47 -05:00
parent db57b3a1c7
commit 959f3a3f69
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
24 changed files with 54 additions and 41 deletions

View file

@ -25,9 +25,6 @@ deps-audit:
cargo-audit: cargo-audit:
cargo audit --file rust/prop_grid_rs/Cargo.lock cargo audit --file rust/prop_grid_rs/Cargo.lock
sobelow:
MIX_ENV=test mix sobelow --config --quiet
format: format:
MIX_ENV=test mix format --check-formatted MIX_ENV=test mix format --check-formatted

View file

@ -118,6 +118,12 @@ defmodule MicrowavepropWeb.Admin.ContactEditLive do
end end
end end
# Swoosh's test adapter delivers the email back to the calling process.
# Approval/rejection notifications are sent from this LiveView process,
# so consume the adapter message instead of crashing on an unknown info.
@impl true
def handle_info({:email, %Swoosh.Email{}}, socket), do: {:noreply, socket}
defp normalize_note(""), do: nil defp normalize_note(""), do: nil
defp normalize_note(note), do: String.trim(note) defp normalize_note(note), do: String.trim(note)

View file

@ -109,7 +109,7 @@ defmodule MicrowavepropWeb.Admin.MonitorLive.Index do
</tr> </tr>
</thead> </thead>
<tbody id="monitors" phx-update="stream"> <tbody id="monitors" phx-update="stream">
<tr :for={{id, monitor} <- @streams.monitors}> <tr :for={{id, monitor} <- @streams.monitors} id={id}>
<td class="font-semibold">{monitor.name}</td> <td class="font-semibold">{monitor.name}</td>
<td class="text-sm"> <td class="text-sm">
<%= if monitor.hardware_type do %> <%= if monitor.hardware_type do %>

View file

@ -433,7 +433,11 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
</.form> </.form>
</div> </div>
<form phx-change="filter_status" class="mb-4 flex items-center gap-2"> <form
id="rover-status-filter-form"
phx-change="filter_status"
class="mb-4 flex items-center gap-2"
>
<label for="status-filter" class="text-sm font-medium">Show:</label> <label for="status-filter" class="text-sm font-medium">Show:</label>
<select <select
id="status-filter" id="status-filter"

View file

@ -55,9 +55,9 @@ defmodule Microwaveprop.Beacons.RangeEstimateTest do
assert result.band_mhz == 10_000 assert result.band_mhz == 10_000
assert result.center_score == 50 assert result.center_score == 50
assert is_float(result.eirp_dbm) assert is_float(result.eirp_dbm)
assert result.cells != [] assert Enum.any?(result.cells)
assert result.grid_step == 0.125 assert result.grid_step == 0.125
assert result.tiers != [] assert Enum.any?(result.tiers)
end end
test "each cell carries lat/lon, distance, rx_dbm, tier label/color" do test "each cell carries lat/lon, distance, rx_dbm, tier label/color" do

View file

@ -10,8 +10,6 @@ defmodule Microwaveprop.LoggerFormatTest do
alias LoggerJSON.Formatters.Basic alias LoggerJSON.Formatters.Basic
require Logger
@moduletag capture_log: false @moduletag capture_log: false
describe "structured JSON formatter" do describe "structured JSON formatter" do

View file

@ -4,7 +4,7 @@ defmodule Microwaveprop.PromExTest do
describe "dashboards/0" do describe "dashboards/0" do
test "returns the configured dashboard list" do test "returns the configured dashboard list" do
dashboards = Microwaveprop.PromEx.dashboards() dashboards = Microwaveprop.PromEx.dashboards()
assert dashboards != [] assert Enum.any?(dashboards)
assert {:prom_ex, "application.json"} in dashboards assert {:prom_ex, "application.json"} in dashboards
end end
end end
@ -12,7 +12,7 @@ defmodule Microwaveprop.PromExTest do
describe "plugins/0" do describe "plugins/0" do
test "includes the custom InstrumentPlugin alongside the PromEx defaults" do test "includes the custom InstrumentPlugin alongside the PromEx defaults" do
plugins = Microwaveprop.PromEx.plugins() plugins = Microwaveprop.PromEx.plugins()
assert plugins != [] assert Enum.any?(plugins)
assert Microwaveprop.PromEx.InstrumentPlugin in plugins assert Microwaveprop.PromEx.InstrumentPlugin in plugins
assert PromEx.Plugins.Beam in plugins assert PromEx.Plugins.Beam in plugins
end end

View file

@ -97,7 +97,7 @@ defmodule Microwaveprop.Propagation.EmeTest do
for {_label, mhz_str} <- BandConfig.band_options() do for {_label, mhz_str} <- BandConfig.band_options() do
{mhz, _} = Integer.parse(mhz_str) {mhz, _} = Integer.parse(mhz_str)
rho = Eme.moon_albedo(mhz) rho = Eme.moon_albedo(mhz)
assert is_number(rho) and rho > 0.0 and rho < 0.2 assert rho > 0.0 and rho < 0.2
end end
end end
end end

View file

@ -1,7 +1,6 @@
defmodule Microwaveprop.Propagation.RecalibratorTest do defmodule Microwaveprop.Propagation.RecalibratorTest do
use Microwaveprop.DataCase, async: true use Microwaveprop.DataCase, async: true
alias Microwaveprop.Propagation.BandWeights
alias Microwaveprop.Propagation.Recalibrator alias Microwaveprop.Propagation.Recalibrator
alias Microwaveprop.Radio.Contact alias Microwaveprop.Radio.Contact
alias Microwaveprop.Weather.HrrrProfile alias Microwaveprop.Weather.HrrrProfile

View file

@ -6,6 +6,7 @@ defmodule Microwaveprop.Radio.ContactEditTest do
alias Microwaveprop.Radio alias Microwaveprop.Radio
alias Microwaveprop.Radio.Contact alias Microwaveprop.Radio.Contact
alias Microwaveprop.Radio.ContactEdit alias Microwaveprop.Radio.ContactEdit
alias Microwaveprop.Radio.Contacts
alias Microwaveprop.Terrain.ElevationClient alias Microwaveprop.Terrain.ElevationClient
alias Microwaveprop.Weather.HrrrClient alias Microwaveprop.Weather.HrrrClient
alias Microwaveprop.Weather.IemClient alias Microwaveprop.Weather.IemClient
@ -538,14 +539,15 @@ defmodule Microwaveprop.Radio.ContactEditTest do
sentinel = :stale_payload sentinel = :stale_payload
ten_min = 10 * 60 * 1_000 ten_min = 10 * 60 * 1_000
Cache.put({Radio, :total_contact_count}, sentinel, ten_min) Cache.put({Contacts, :total_contact_count}, sentinel, ten_min)
Cache.put({Radio, :contact_map_payload}, sentinel, ten_min) Cache.put({Contacts, :contact_map_payload}, sentinel, ten_min)
Cache.put({MicrowavepropWeb.ContactMapController, :gzipped_payload}, sentinel, ten_min) Cache.put({MicrowavepropWeb.ContactMapController, :gzipped_payload}, sentinel, ten_min)
{:ok, _} = Radio.apply_owner_edit(contact, user, %{"private" => true}) {:ok, _} = Radio.apply_owner_edit(contact, user, %{"private" => true})
assert :ets.lookup(:microwaveprop_cache, {Radio, :total_contact_count}) == [] assert :ets.lookup(:microwaveprop_cache, {Contacts, :total_contact_count}) == []
assert :ets.lookup(:microwaveprop_cache, {Radio, :contact_map_payload}) == []
assert :ets.lookup(:microwaveprop_cache, {Contacts, :contact_map_payload}) == []
assert :ets.lookup(:microwaveprop_cache, {MicrowavepropWeb.ContactMapController, :gzipped_payload}) == assert :ets.lookup(:microwaveprop_cache, {MicrowavepropWeb.ContactMapController, :gzipped_payload}) ==
[] []

View file

@ -36,11 +36,12 @@ defmodule Microwaveprop.Weather.GridCacheValkey.EmptyCacheTest do
GridCache.clear() GridCache.clear()
on_exit(fn -> on_exit(fn ->
if pid && Process.alive?(pid) do if Process.whereis(Conn) == pid do
Process.unregister(Conn) Process.unregister(Conn)
Process.exit(pid, :kill)
end end
Process.exit(pid, :kill)
if prev_url, if prev_url,
do: Application.put_env(:microwaveprop, :valkey_url, prev_url), do: Application.put_env(:microwaveprop, :valkey_url, prev_url),
else: Application.delete_env(:microwaveprop, :valkey_url) else: Application.delete_env(:microwaveprop, :valkey_url)
@ -153,11 +154,12 @@ defmodule Microwaveprop.Weather.GridCacheValkey.DataCacheTest do
GridCache.clear() GridCache.clear()
on_exit(fn -> on_exit(fn ->
if pid && Process.alive?(pid) do if Process.whereis(Conn) == pid do
Process.unregister(Conn) Process.unregister(Conn)
Process.exit(pid, :kill)
end end
Process.exit(pid, :kill)
if prev_url, if prev_url,
do: Application.put_env(:microwaveprop, :valkey_url, prev_url), do: Application.put_env(:microwaveprop, :valkey_url, prev_url),
else: Application.delete_env(:microwaveprop, :valkey_url) else: Application.delete_env(:microwaveprop, :valkey_url)

View file

@ -112,7 +112,7 @@ defmodule Microwaveprop.Weather.NexradClientTest do
Enum.reduce(48..52, pixels, fn y, acc -> Enum.reduce(48..52, pixels, fn y, acc ->
Enum.reduce(48..52, acc, fn x, inner_acc -> Enum.reduce(48..52, acc, fn x, inner_acc ->
offset = y * width + x offset = y * width + x
<<pre::binary-size(offset), _::8, post::binary>> = inner_acc <<pre::binary-size(^offset), _::8, post::binary>> = inner_acc
<<pre::binary, 120::8, post::binary>> <<pre::binary, 120::8, post::binary>>
end) end)
end) end)
@ -149,7 +149,7 @@ defmodule Microwaveprop.Weather.NexradClientTest do
Enum.reduce(48..52, acc, fn x, inner_acc -> Enum.reduce(48..52, acc, fn x, inner_acc ->
offset = y * width + x offset = y * width + x
value = if rem(x + y, 2) == 0, do: 100, else: 200 value = if rem(x + y, 2) == 0, do: 100, else: 200
<<pre::binary-size(offset), _::8, post::binary>> = inner_acc <<pre::binary-size(^offset), _::8, post::binary>> = inner_acc
<<pre::binary, value::8, post::binary>> <<pre::binary, value::8, post::binary>>
end) end)
end) end)

View file

@ -60,7 +60,7 @@ defmodule Microwaveprop.Workers.IonosphereFetchWorkerTest do
test "exposes a non-empty list of CONUS ionosonde stations" do test "exposes a non-empty list of CONUS ionosonde stations" do
stations = IonosphereFetchWorker.stations() stations = IonosphereFetchWorker.stations()
assert stations != [] assert Enum.any?(stations)
# Every station entry has the shape the fetcher expects, with a # Every station entry has the shape the fetcher expects, with a
# plausible CONUS lat/lon envelope. # plausible CONUS lat/lon envelope.

View file

@ -45,7 +45,7 @@ defmodule MicrowavepropWeb.UserSettingsControllerTest do
assert get_session(new_password_conn, :user_token) != get_session(conn, :user_token) assert get_session(new_password_conn, :user_token) != get_session(conn, :user_token)
assert get_flash(new_password_conn, :info) =~ assert Phoenix.Flash.get(new_password_conn.assigns.flash, :info) =~
"Password updated successfully" "Password updated successfully"
assert Accounts.get_user_by_email_and_password(user.email, "new valid password") assert Accounts.get_user_by_email_and_password(user.email, "new valid password")

View file

@ -7,10 +7,13 @@ defmodule MicrowavepropWeb.WeatherTileControllerTest do
setup do setup do
GridCache.clear() GridCache.clear()
ProfilesFile.prune_older_than(~U[2099-12-31 23:59:59Z])
ScalarFile.prune_older_than(~U[2099-12-31 23:59:59Z])
on_exit(fn -> on_exit(fn ->
GridCache.clear() GridCache.clear()
ProfilesFile.prune_older_than(~U[2099-12-31 23:59:59Z]) ProfilesFile.prune_older_than(~U[2099-12-31 23:59:59Z])
ScalarFile.prune_older_than(~U[2099-12-31 23:59:59Z])
end) end)
:ok :ok

View file

@ -1,8 +1,6 @@
defmodule MicrowavepropWeb.AboutLiveTest do defmodule MicrowavepropWeb.AboutLiveTest do
use MicrowavepropWeb.ConnCase, async: true use MicrowavepropWeb.ConnCase, async: true
import Phoenix.LiveViewTest
alias Microwaveprop.Repo alias Microwaveprop.Repo
describe "page content" do describe "page content" do

View file

@ -7,8 +7,6 @@ defmodule MicrowavepropWeb.ContactLive.IndexTest do
alias Microwaveprop.Radio.Contact alias Microwaveprop.Radio.Contact
alias Microwaveprop.Repo alias Microwaveprop.Repo
alias Microwaveprop.Terrain.ElevationClient alias Microwaveprop.Terrain.ElevationClient
alias Microwaveprop.Weather.HrrrNativeProfile
alias Microwaveprop.Weather.NarrProfile
setup do setup do
Req.Test.stub(ElevationClient, fn conn -> Req.Test.stub(ElevationClient, fn conn ->

View file

@ -15,7 +15,6 @@ defmodule MicrowavepropWeb.ContactLive.ShowCoverageTest do
alias Microwaveprop.Terrain.ElevationClient alias Microwaveprop.Terrain.ElevationClient
alias Microwaveprop.Terrain.TerrainProfile alias Microwaveprop.Terrain.TerrainProfile
alias Microwaveprop.Weather alias Microwaveprop.Weather
alias Microwaveprop.Weather.HrrrNativeProfile
alias Microwaveprop.Weather.HrrrProfile alias Microwaveprop.Weather.HrrrProfile
alias Microwaveprop.Weather.NarrProfile alias Microwaveprop.Weather.NarrProfile
alias Microwaveprop.Weather.SolarIndex alias Microwaveprop.Weather.SolarIndex

View file

@ -12,10 +12,8 @@ defmodule MicrowavepropWeb.ContactLive.ShowHydrationTest do
alias Microwaveprop.Terrain.ElevationClient alias Microwaveprop.Terrain.ElevationClient
alias Microwaveprop.Terrain.TerrainProfile alias Microwaveprop.Terrain.TerrainProfile
alias Microwaveprop.Weather alias Microwaveprop.Weather
alias Microwaveprop.Weather.HrrrNativeProfile
alias Microwaveprop.Weather.HrrrProfile alias Microwaveprop.Weather.HrrrProfile
alias Microwaveprop.Weather.IemreObservation alias Microwaveprop.Weather.IemreObservation
alias Microwaveprop.Weather.NarrProfile
alias Microwaveprop.Weather.Sounding alias Microwaveprop.Weather.Sounding
alias Microwaveprop.Weather.SurfaceObservation alias Microwaveprop.Weather.SurfaceObservation

View file

@ -6,6 +6,11 @@ defmodule MicrowavepropWeb.ImportLiveTest do
alias Microwaveprop.Radio.ImportRun alias Microwaveprop.Radio.ImportRun
alias Microwaveprop.Repo alias Microwaveprop.Repo
defp log_in_submitter(conn, email) do
user = Microwaveprop.AccountsFixtures.user_fixture(%{email: email})
log_in_user(conn, user)
end
defp insert_run(attrs) do defp insert_run(attrs) do
defaults = %{ defaults = %{
submitter_email: "importer@example.com", submitter_email: "importer@example.com",
@ -30,6 +35,7 @@ defmodule MicrowavepropWeb.ImportLiveTest do
describe "mount" do describe "mount" do
test "renders the initial state of a pending import", %{conn: conn} do test "renders the initial state of a pending import", %{conn: conn} do
run = insert_run(%{total_rows: 25, submitter_email: "alice@example.com"}) run = insert_run(%{total_rows: 25, submitter_email: "alice@example.com"})
conn = log_in_submitter(conn, run.submitter_email)
{:ok, _lv, html} = live(conn, ~p"/imports/#{run.id}") {:ok, _lv, html} = live(conn, ~p"/imports/#{run.id}")
@ -43,17 +49,17 @@ defmodule MicrowavepropWeb.ImportLiveTest do
assert html =~ ~s(id="import-error-count") assert html =~ ~s(id="import-error-count")
end end
test "redirects to /submit with a flash when the import is not found", %{conn: conn} do test "redirects to / with a flash when the import is not found", %{conn: conn} do
missing_id = Ecto.UUID.generate() missing_id = Ecto.UUID.generate()
assert {:error, {:live_redirect, %{to: "/submit", flash: flash}}} = assert {:error, {:live_redirect, %{to: "/", flash: flash}}} =
live(conn, ~p"/imports/#{missing_id}") live(conn, ~p"/imports/#{missing_id}")
assert flash["error"] =~ "Import not found" assert flash["error"] =~ "Import not found"
end end
test "redirects to /submit for an obviously bad id", %{conn: conn} do test "redirects to / for an obviously bad id", %{conn: conn} do
assert {:error, {:live_redirect, %{to: "/submit"}}} = assert {:error, {:live_redirect, %{to: "/"}}} =
live(conn, ~p"/imports/not-a-uuid") live(conn, ~p"/imports/not-a-uuid")
end end
end end
@ -61,6 +67,7 @@ defmodule MicrowavepropWeb.ImportLiveTest do
describe "live updates" do describe "live updates" do
test "updates counters when an import_progress message is broadcast", %{conn: conn} do test "updates counters when an import_progress message is broadcast", %{conn: conn} do
run = insert_run(%{total_rows: 100}) run = insert_run(%{total_rows: 100})
conn = log_in_submitter(conn, run.submitter_email)
{:ok, lv, _html} = live(conn, ~p"/imports/#{run.id}") {:ok, lv, _html} = live(conn, ~p"/imports/#{run.id}")
@ -91,6 +98,7 @@ defmodule MicrowavepropWeb.ImportLiveTest do
test "shows the complete UI once status flips to complete", %{conn: conn} do test "shows the complete UI once status flips to complete", %{conn: conn} do
run = insert_run(%{total_rows: 3}) run = insert_run(%{total_rows: 3})
conn = log_in_submitter(conn, run.submitter_email)
{:ok, lv, _html} = live(conn, ~p"/imports/#{run.id}") {:ok, lv, _html} = live(conn, ~p"/imports/#{run.id}")
@ -127,6 +135,8 @@ defmodule MicrowavepropWeb.ImportLiveTest do
errors: %{"2" => ["band is invalid"]} errors: %{"2" => ["band is invalid"]}
}) })
conn = log_in_submitter(conn, run.submitter_email)
{:ok, _lv, html} = live(conn, ~p"/imports/#{run.id}") {:ok, _lv, html} = live(conn, ~p"/imports/#{run.id}")
assert html =~ ~s(id="import-errors-section") assert html =~ ~s(id="import-errors-section")

View file

@ -90,7 +90,7 @@ defmodule MicrowavepropWeb.StatusLiveTest do
end end
end end
defp create_narr_candidate(attrs \\ []) do defp create_narr_candidate(attrs) do
lat = Keyword.get(attrs, :lat, 33.0) lat = Keyword.get(attrs, :lat, 33.0)
lon = Keyword.get(attrs, :lon, -97.0) lon = Keyword.get(attrs, :lon, -97.0)
ts = Keyword.get(attrs, :qso_timestamp, ~U[2010-06-15 18:00:00Z]) ts = Keyword.get(attrs, :qso_timestamp, ~U[2010-06-15 18:00:00Z])

View file

@ -252,7 +252,7 @@ defmodule MicrowavepropWeb.UserProfileLiveTest do
test "owners see their assigned beacon monitors", %{conn: conn} do test "owners see their assigned beacon monitors", %{conn: conn} do
user = user_fixture(%{callsign: "W5ISP"}) user = user_fixture(%{callsign: "W5ISP"})
{:ok, monitor} = {:ok, _monitor} =
BeaconMonitors.create_hardware(user, %{ BeaconMonitors.create_hardware(user, %{
"name" => "Shack Pi", "name" => "Shack Pi",
"hardware_type" => "RTL-SDR", "hardware_type" => "RTL-SDR",

View file

@ -59,8 +59,8 @@ defmodule MicrowavepropWeb.Plugs.RemoteIpPropertyTest do
defp in_cidr?({a, b, c, d}, {{na, nb, nc, nd}, prefix}) do defp in_cidr?({a, b, c, d}, {{na, nb, nc, nd}, prefix}) do
host = <<a, b, c, d>> host = <<a, b, c, d>>
network = <<na, nb, nc, nd>> network = <<na, nb, nc, nd>>
<<host_prefix::bitstring-size(prefix), _::bitstring>> = host <<host_prefix::bitstring-size(^prefix), _::bitstring>> = host
<<net_prefix::bitstring-size(prefix), _::bitstring>> = network <<net_prefix::bitstring-size(^prefix), _::bitstring>> = network
host_prefix == net_prefix host_prefix == net_prefix
end end

View file

@ -8,7 +8,6 @@ defmodule MicrowavepropWeb.TelemetryTest do
test "returns a flat list of Telemetry.Metrics structs" do test "returns a flat list of Telemetry.Metrics structs" do
metrics = Telemetry.metrics() metrics = Telemetry.metrics()
assert metrics != []
assert Enum.any?(metrics) assert Enum.any?(metrics)
for m <- metrics do for m <- metrics do