prop/test/microwaveprop_web/live/contact_live/show_test.exs
Graham McIntire cc3dc41a6b
Fix all medium findings and split contact_live_test.exs
- Fix N+1 queries in radio.ex (preload :user)
- Add encryption_salt to session options
- Consolidate token deletion to single query
- Wrap gunzip in try/rescue for corrupt files
- Add Cache.match_delete/1 to encapsulate ETS access
- Precompute sec_i_factor_ref as module attribute
- Guard EXLA.Backend config to dev/test only
- Split 3505-line contact_live_test.exs into 4 files
- Replace Process.sleep with :sys.get_state synchronization
- Replace Process.alive? with DOM output assertions
- Clarify router.ex comment for non-live_session routes
- Update findings.md to reflect fixes
2026-05-29 15:53:04 -05:00

568 lines
18 KiB
Elixir

defmodule MicrowavepropWeb.ContactLive.ShowTest do
use MicrowavepropWeb.ConnCase, async: true
import Phoenix.LiveViewTest
alias Microwaveprop.Radio.Contact
alias Microwaveprop.Repo
alias Microwaveprop.Terrain.ElevationClient
alias Microwaveprop.Weather.HrrrNativeProfile
alias Microwaveprop.Weather.NarrProfile
setup do
Req.Test.stub(ElevationClient, fn conn ->
Req.Test.json(conn, [])
end)
Req.Test.stub(Microwaveprop.Weather.HrrrClient, fn conn ->
Plug.Conn.send_resp(conn, 404, "not found")
end)
Req.Test.stub(Microwaveprop.Weather.SolarClient, fn conn ->
Req.Test.text(conn, "")
end)
:ok
end
defp create_contact(attrs \\ %{}) do
default = %{
station1: "W5XD",
station2: "K5TR",
qso_timestamp: ~U[2026-03-28 18:00:00Z],
mode: "CW",
band: Decimal.new("1296"),
grid1: "EM12",
grid2: "EM00",
pos1: %{"lat" => 32.9, "lon" => -97.0},
pos2: %{"lat" => 30.3, "lon" => -97.7},
distance_km: Decimal.new("295")
}
merged = Map.merge(default, attrs)
{user_id, changeset_attrs} = Map.pop(merged, :user_id)
{:ok, contact} =
%Contact{user_id: user_id}
|> Contact.changeset(changeset_attrs)
|> Repo.insert()
contact
end
describe "Show private visibility" do
import Microwaveprop.AccountsFixtures
test "anonymous viewer 404s on a private contact", %{conn: conn} do
contact = create_contact(%{private: true})
assert_raise Ecto.NoResultsError, fn ->
live(conn, ~p"/contacts/#{contact.id}")
end
end
test "non-owning user 404s on a private contact", %{conn: conn} do
owner = user_fixture()
other = user_fixture()
contact = create_contact(%{private: true, user_id: owner.id})
conn = log_in_user(conn, other)
assert_raise Ecto.NoResultsError, fn ->
live(conn, ~p"/contacts/#{contact.id}")
end
end
test "owner can view their own private contact", %{conn: conn} do
owner = user_fixture()
contact = create_contact(%{private: true, user_id: owner.id})
conn = log_in_user(conn, owner)
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "W5XD"
end
test "admin can view anyone's private contact", %{conn: conn} do
owner = user_fixture()
admin = user_fixture() |> Ecto.Changeset.change(is_admin: true) |> Repo.update!()
contact = create_contact(%{private: true, user_id: owner.id})
conn = log_in_user(conn, admin)
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "W5XD"
end
end
describe "Show" do
test "renders contact detail fields", %{conn: conn} do
contact = create_contact()
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "W5XD"
assert html =~ "K5TR"
assert html =~ "2026-03-28"
end
test "shows surface observations section", %{conn: conn} do
contact = create_contact()
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "Surface Observations"
end
test "shows sounding section", %{conn: conn} do
contact = create_contact()
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "Soundings"
end
test "shows solar index section", %{conn: conn} do
contact = create_contact()
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "Solar Conditions"
end
test "renders with sort assigns", %{conn: conn} do
contact = create_contact()
{:ok, lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "Surface Observations"
assert html =~ "Soundings"
assert render(lv) =~ contact.station1
end
test "renders sub-10-GHz bands in MHz", %{conn: conn} do
contact = create_contact(%{band: Decimal.new("432")})
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "432 MHz"
end
test "renders 50 MHz in MHz", %{conn: conn} do
contact = create_contact(%{band: Decimal.new("50")})
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "50 MHz"
end
test "renders 1296 MHz in MHz", %{conn: conn} do
contact = create_contact(%{band: Decimal.new("1296")})
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "1296 MHz"
refute html =~ "1.3 GHz"
end
test "renders 2304 MHz in MHz", %{conn: conn} do
contact = create_contact(%{band: Decimal.new("2304")})
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "2304 MHz"
refute html =~ "2.3 GHz"
end
test "renders 10 GHz in GHz (threshold)", %{conn: conn} do
contact = create_contact(%{band: Decimal.new("10000")})
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "10 GHz"
end
test "renders 24 GHz in GHz", %{conn: conn} do
contact = create_contact(%{band: Decimal.new("24000")})
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "24 GHz"
end
test "raises for bad UUID", %{conn: conn} do
assert_raise Ecto.NoResultsError, fn ->
live(conn, ~p"/contacts/#{Ecto.UUID.generate()}")
end
end
test "shows atmospheric profile section with NARR source for pre-HRRR contact", %{conn: conn} do
contact =
create_contact(%{
qso_timestamp: ~U[2010-06-15 18:00:00Z],
pos1: %{"lat" => 32.9, "lon" => -97.0},
pos2: %{"lat" => 30.3, "lon" => -97.7}
})
{:ok, _} =
Repo.insert(%NarrProfile{
valid_time: ~U[2010-06-15 18:00:00Z],
lat: 33.0,
lon: -97.0,
profile: [
%{"pres" => 850.0, "tmpc" => 15.0, "dwpc" => 10.0, "hght" => 1500.0}
],
surface_temp_c: 25.0,
surface_dewpoint_c: 18.0,
surface_pressure_mb: 1012.0,
hpbl_m: 1200.0,
pwat_mm: 30.0,
surface_refractivity: 320.0,
min_refractivity_gradient: -50.0,
ducting_detected: false
})
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
html = render_async(lv, 2_000)
assert html =~ "Atmospheric Profile"
assert html =~ "NARR"
assert html =~ "1200"
assert html =~ "33.0"
end
test "skew-T uses native HRRR profile when backfilled", %{conn: conn} do
contact =
create_contact(%{
qso_timestamp: ~U[2024-06-15 18:00:00Z],
pos1: %{"lat" => 32.9, "lon" => -97.0},
pos2: %{"lat" => 30.3, "lon" => -97.7}
})
{:ok, _} =
Repo.insert(%NarrProfile{
valid_time: ~U[2024-06-15 18:00:00Z],
lat: 32.9,
lon: -97.0,
profile: [%{"pres" => 850.0, "tmpc" => 15.0, "dwpc" => 10.0, "hght" => 1500.0}],
surface_temp_c: 25.0,
surface_dewpoint_c: 18.0,
surface_pressure_mb: 1012.0,
hpbl_m: 1200.0,
pwat_mm: 30.0,
surface_refractivity: 320.0,
min_refractivity_gradient: -50.0,
ducting_detected: false
})
{:ok, _} =
%HrrrNativeProfile{}
|> HrrrNativeProfile.changeset(%{
valid_time: ~U[2024-06-15 18:00:00Z],
run_time: ~U[2024-06-15 18:00:00Z],
lat: 32.9,
lon: -97.0,
level_count: 3,
heights_m: [10.0, 1500.0, 9500.0],
temp_k: [298.0, 288.0, 228.0],
spfh: [0.012, 0.006, 0.0001],
pressure_pa: [101_000.0, 85_000.0, 30_000.0],
u_wind_ms: [2.0, 5.0, 20.0],
v_wind_ms: [1.0, 2.0, 5.0],
tke_m2s2: [0.5, 0.2, 0.05]
})
|> Repo.insert()
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
html = render_async(lv, 2_000)
assert html =~ "HRRR native"
assert html =~ "300.0"
end
end
describe "Show interactive events" do
import Microwaveprop.AccountsFixtures
defp admin_fixture do
user = user_fixture()
{:ok, user} = Microwaveprop.Accounts.admin_update_user(user, %{is_admin: true})
user
end
test "toggle_hrrr_profile flips the atmospheric-profile collapse state", %{conn: conn} do
contact = create_contact()
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
before_html = render(lv)
toggled = render_click(lv, "toggle_hrrr_profile", %{})
back = render_click(lv, "toggle_hrrr_profile", %{})
assert is_binary(toggled)
assert is_binary(back)
assert back =~ contact.station1 and before_html =~ contact.station1
end
test "toggle_terrain flips the terrain-section collapse state", %{conn: conn} do
contact = create_contact()
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
html = render_click(lv, "toggle_terrain", %{})
assert is_binary(html)
end
test "toggle_profile (sounding expansion) tracks an id in the MapSet", %{conn: conn} do
contact = create_contact()
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
html = render_click(lv, "toggle_profile", %{"id" => "11111111-1111-1111-1111-111111111111"})
assert is_binary(html)
html2 = render_click(lv, "toggle_profile", %{"id" => "11111111-1111-1111-1111-111111111111"})
assert is_binary(html2)
end
test "sort obs table by date toggles sort direction", %{conn: conn} do
contact = create_contact()
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
render_click(lv, "sort", %{"field" => "observed_at", "table" => "obs"})
html = render_click(lv, "sort", %{"field" => "observed_at", "table" => "obs"})
assert html =~ "Surface Observations"
end
test "sort obs table exercises every known field sort key", %{conn: conn} do
contact = create_contact()
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
for field <- ~w(station_name observed_at temp_f dewpoint_f relative_humidity sea_level_pressure_mb unknown_field) do
html = render_click(lv, "sort", %{"field" => field, "table" => "obs"})
assert html =~ "Surface Observations", "sort by #{field} lost the obs table"
end
end
test "sort soundings table exercises every known field sort key", %{conn: conn} do
contact = create_contact()
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
for field <- ~w(station_name observed_at unknown_field) do
html = render_click(lv, "sort", %{"field" => field, "table" => "soundings"})
assert html =~ "Soundings", "sort by #{field} lost the soundings table"
end
end
test "sort soundings table by date keeps the view rendering", %{conn: conn} do
contact = create_contact()
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
html = render_click(lv, "sort", %{"field" => "observed_at", "table" => "soundings"})
assert html =~ "Soundings"
end
test "toggle_flag is denied to anonymous viewers (flash + no change)", %{conn: conn} do
contact = create_contact()
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
html = render_click(lv, "toggle_flag", %{})
assert html =~ "Admins only"
refute Repo.get!(Contact, contact.id).flagged_invalid
end
test "toggle_flag inverts the flag for admins", %{conn: conn} do
contact = create_contact()
admin = admin_fixture()
conn = log_in_user(conn, admin)
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
render_click(lv, "toggle_flag", %{})
refreshed = Repo.get!(Contact, contact.id)
assert refreshed.flagged_invalid == true
render_click(lv, "toggle_flag", %{})
assert Repo.get!(Contact, contact.id).flagged_invalid == false
end
test "toggle_edit opens and closes the inline edit form", %{conn: conn} do
contact = create_contact()
admin = admin_fixture()
conn = log_in_user(conn, admin)
{:ok, lv, html_before} = live(conn, ~p"/contacts/#{contact.id}")
refute html_before =~ ~s(phx-submit="submit_edit")
html_open = render_click(lv, "toggle_edit", %{})
assert html_open =~ ~s(phx-submit="submit_edit")
html_closed = render_click(lv, "toggle_edit", %{})
refute html_closed =~ ~s(phx-submit="submit_edit")
end
test "submit_edit by a non-logged-in user flashes a login-required error", %{conn: conn} do
contact = create_contact()
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
html =
render_submit(lv, "submit_edit", %{
"edit" => %{"station1" => "N5XX"}
})
assert html =~ "logged in to edit"
end
test "submit_edit as admin applies a simple callsign change", %{conn: conn} do
contact = create_contact(%{station1: "N5OLD"})
admin = admin_fixture()
conn = log_in_user(conn, admin)
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
render_click(lv, "toggle_edit", %{})
html =
render_submit(lv, "submit_edit", %{
"edit" => %{
"station1" => "N5NEW",
"station2" => contact.station2,
"grid1" => contact.grid1,
"grid2" => contact.grid2,
"mode" => contact.mode
}
})
assert html =~ "Contact updated"
assert Repo.get!(Contact, contact.id).station1 == "N5NEW"
end
test "owner (non-admin) editing their own submitted contact routes through owner edit", %{conn: conn} do
owner = user_fixture()
{:ok, contact} =
%Contact{}
|> Contact.changeset(%{
station1: owner.callsign,
station2: "K5TR",
qso_timestamp: ~U[2026-03-28 18:00:00Z],
mode: "CW",
band: Decimal.new("1296"),
grid1: "EM12",
grid2: "EM00",
pos1: %{"lat" => 32.9, "lon" => -97.0},
pos2: %{"lat" => 30.3, "lon" => -97.7},
distance_km: Decimal.new("295"),
user_submitted: true,
submitter_email: owner.email
})
|> Ecto.Changeset.change(user_id: owner.id)
|> Repo.insert()
conn = log_in_user(conn, owner)
{:ok, lv, _} = live(conn, ~p"/contacts/#{contact.id}")
render_click(lv, "toggle_edit", %{})
html =
render_submit(lv, "submit_edit", %{
"edit" => %{
"station1" => owner.callsign,
"station2" => "K9NEW",
"grid1" => contact.grid1,
"grid2" => contact.grid2,
"mode" => contact.mode
}
})
assert html =~ "Contact updated"
assert Repo.get!(Contact, contact.id).station2 == "K9NEW"
end
test "non-owner non-admin submitting an edit creates a pending ContactEdit", %{conn: conn} do
contact = create_contact()
stranger = user_fixture()
conn = log_in_user(conn, stranger)
{:ok, lv, _} = live(conn, ~p"/contacts/#{contact.id}")
render_click(lv, "toggle_edit", %{})
html =
render_submit(lv, "submit_edit", %{
"edit" => %{
"station1" => contact.station1,
"station2" => "K9SUGGEST",
"grid1" => contact.grid1,
"grid2" => contact.grid2,
"mode" => contact.mode
}
})
assert Repo.get!(Contact, contact.id).station2 == contact.station2
assert Repo.aggregate(Microwaveprop.Radio.ContactEdit, :count, :id) == 1
assert html =~ "submitted" or html =~ "suggest" or html =~ "pending"
end
test "renders the contact detail with a real surface observation row", %{conn: conn} do
alias Microwaveprop.Weather
alias Microwaveprop.Weather.SurfaceObservation
contact = create_contact()
{:ok, station} =
Weather.find_or_create_station(%{
station_code: "KDFW",
station_type: "asos",
name: "DFW Airport",
lat: 32.90,
lon: -97.02
})
{:ok, _} =
%SurfaceObservation{}
|> SurfaceObservation.changeset(%{
station_id: station.id,
observed_at: ~U[2026-03-28 18:00:00Z],
temp_f: 72.0,
dewpoint_f: 55.0,
relative_humidity: 55.0,
sea_level_pressure_mb: 1015.0
})
|> Repo.insert()
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
html = render_async(lv, 2_000)
assert html =~ "Surface Observations"
end
test "renders the contact detail with a real sounding row", %{conn: conn} do
alias Microwaveprop.Weather
alias Microwaveprop.Weather.Sounding
contact = create_contact()
{:ok, station} =
Weather.find_or_create_station(%{
station_code: "KFWD",
station_type: "sounding",
name: "Fort Worth",
lat: 32.83,
lon: -97.30
})
{:ok, _} =
%Sounding{}
|> Sounding.changeset(%{
station_id: station.id,
observed_at: ~U[2026-03-28 12:00:00Z],
profile: [
%{"pres" => 1000.0, "tmpc" => 20.0, "dwpc" => 15.0, "hght" => 100.0},
%{"pres" => 850.0, "tmpc" => 12.0, "dwpc" => 8.0, "hght" => 1500.0}
],
level_count: 2,
surface_temp_c: 20.0,
surface_dewpoint_c: 15.0
})
|> Repo.insert()
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
html = render_async(lv, 2_000)
assert html =~ "KFWD"
end
test "validate_edit always returns :noreply without touching the DB", %{conn: conn} do
contact = create_contact()
admin = admin_fixture()
conn = log_in_user(conn, admin)
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
render_click(lv, "toggle_edit", %{})
html =
render_change(lv, "validate_edit", %{
"edit" => %{"station1" => "N5TYPED"}
})
assert html =~ ~s(phx-submit="submit_edit")
assert Repo.get!(Contact, contact.id).station1 == "W5XD"
end
end
end