77 unit tests + 23 property tests across four parallel agents. - ContactLive.Show (72.01% → ~75%): every factor-note branch (humidity/time/td/refractivity/pressure/season/pwat), all propagation_mechanism classifications, apply_admin_edit/owner_edit/ submit_user_edit error paths, internal_network? IP shapes, expanded sounding skew-T, composite-score [0,100] property. - PathLive + BeaconLive.Show + Weather: coordinate-pair resolver, invalid grid, empty-DB edge cases for iemre_for_*, narr_for_*, nearest_native_duct_*, find_nearest_rtma, recent_surface_obs; properties for round_to_hrrr_grid/round_to_iemre_grid/band round-trip/beacon-id URL round-trip. - Viewshed + Duct + SoundingParams: find_reach_km edge cases, destination_point, effective_reach_km fractions, detect_ducts trailing-duct + guard branches; 13 properties including flat- terrain-fully-visible, thicker-duct-lower-freq, M-profile monotonicity, feature-vector deterministic length. - Workers + Mix tasks: GefsFetchWorker 500/429/403 + malformed ISO8601, HrrrNativeGridWorker snap-to-3-decimals + empty DB, IonosphereFetchWorker 404 + non-tabular body, RadarBackfill --dry-run + --year + --limit, NexradBackfill --limit 0, Backtest --feature + --all; year-filter property covering 2015..2026. 170 → 205 properties, 2666 → 2743 tests, 0 failures.
346 lines
12 KiB
Elixir
346 lines
12 KiB
Elixir
defmodule MicrowavepropWeb.BeaconLiveTest do
|
|
use MicrowavepropWeb.ConnCase
|
|
use ExUnitProperties
|
|
|
|
import Microwaveprop.AccountsFixtures
|
|
import Microwaveprop.BeaconsFixtures
|
|
import Phoenix.LiveViewTest
|
|
|
|
alias Microwaveprop.Accounts
|
|
alias Microwaveprop.Beacons
|
|
|
|
defp promote_to_admin(user) do
|
|
{:ok, user} = Accounts.admin_update_user(user, %{is_admin: true})
|
|
user
|
|
end
|
|
|
|
defp admin_user_fixture do
|
|
promote_to_admin(user_fixture())
|
|
end
|
|
|
|
defp approved_beacon_fixture(user, attrs \\ %{}) do
|
|
beacon = beacon_fixture(user, attrs)
|
|
{:ok, beacon} = Beacons.approve_beacon(beacon)
|
|
beacon
|
|
end
|
|
|
|
describe "Index (public)" do
|
|
test "lists approved beacons without logging in", %{conn: conn} do
|
|
beacon = approved_beacon_fixture(user_fixture())
|
|
{:ok, _view, html} = live(conn, ~p"/beacons")
|
|
assert html =~ "Beacons"
|
|
assert html =~ beacon.callsign
|
|
end
|
|
|
|
test "hides unapproved beacons from the public list", %{conn: conn} do
|
|
_pending = beacon_fixture(user_fixture(), callsign: "W5PEN")
|
|
{:ok, _view, html} = live(conn, ~p"/beacons")
|
|
refute html =~ "W5PEN"
|
|
end
|
|
|
|
test "anonymous sees the Submit Beacon button", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, ~p"/beacons")
|
|
assert html =~ "Submit Beacon"
|
|
end
|
|
|
|
test "authenticated user sees the Submit Beacon button", %{conn: conn} do
|
|
user = user_fixture()
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/beacons")
|
|
assert html =~ "Submit Beacon"
|
|
end
|
|
|
|
test "admin sees pending beacons section", %{conn: conn} do
|
|
pending = beacon_fixture(user_fixture(), callsign: "W5PEN")
|
|
conn = log_in_user(conn, admin_user_fixture())
|
|
{:ok, _view, html} = live(conn, ~p"/beacons")
|
|
assert html =~ "Pending approval"
|
|
assert html =~ pending.callsign
|
|
end
|
|
|
|
test "non-admin does not see pending beacons section", %{conn: conn} do
|
|
_pending = beacon_fixture(user_fixture(), callsign: "W5PEN")
|
|
conn = log_in_user(conn, user_fixture())
|
|
{:ok, _view, html} = live(conn, ~p"/beacons")
|
|
refute html =~ "Pending approval"
|
|
refute html =~ "W5PEN"
|
|
end
|
|
|
|
test "admin can approve a pending beacon", %{conn: conn} do
|
|
pending = beacon_fixture(user_fixture(), callsign: "W5PEN")
|
|
conn = log_in_user(conn, admin_user_fixture())
|
|
{:ok, view, _html} = live(conn, ~p"/beacons")
|
|
|
|
html =
|
|
view
|
|
|> element("a[phx-click][data-confirm='Approve this beacon?']")
|
|
|> render_click()
|
|
|
|
assert html =~ "Approved #{pending.callsign}"
|
|
assert Beacons.get_beacon!(pending.id).approved == true
|
|
end
|
|
end
|
|
|
|
describe "Show (public)" do
|
|
test "renders beacon details without logging in", %{conn: conn} do
|
|
beacon = approved_beacon_fixture(user_fixture())
|
|
{:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")
|
|
assert html =~ beacon.callsign
|
|
assert html =~ "10,368.1"
|
|
end
|
|
|
|
test "shows pending badge on unapproved beacons", %{conn: conn} do
|
|
beacon = beacon_fixture(user_fixture())
|
|
{:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")
|
|
assert html =~ "Pending approval"
|
|
end
|
|
|
|
test "plot-path-to-beacon link pre-populates tx_power + gain to match beacon EIRP",
|
|
%{conn: conn} do
|
|
# power_mw: 10_000 → EIRP 40 dBm → default-gain 30 dBi → tx 10 dBm
|
|
beacon = approved_beacon_fixture(user_fixture(), power_mw: 10_000.0)
|
|
{:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")
|
|
|
|
assert html =~ "tx_power_dbm=10.0"
|
|
assert html =~ "src_gain_dbi=30.0"
|
|
end
|
|
|
|
test "low-EIRP beacon clamps tx_power to 0 dBm floor and lowers gain", %{conn: conn} do
|
|
# power_mw: 100 → EIRP 20 dBm → tx 0 dBm (clamped), gain 20 dBi
|
|
beacon = approved_beacon_fixture(user_fixture(), power_mw: 100.0, callsign: "W5LOW")
|
|
{:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")
|
|
|
|
assert html =~ "tx_power_dbm=0.0"
|
|
assert html =~ "src_gain_dbi=20.0"
|
|
end
|
|
end
|
|
|
|
describe "Form" do
|
|
test "authenticated non-admin can access /beacons/new", %{conn: conn} do
|
|
user = user_fixture()
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/beacons/new")
|
|
assert html =~ "New beacon"
|
|
end
|
|
|
|
test "anonymous user can access /beacons/new", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, ~p"/beacons/new")
|
|
assert html =~ "New beacon"
|
|
end
|
|
|
|
test "anonymous submission creates an unapproved beacon with no user_id", %{conn: conn} do
|
|
{:ok, form_live, _html} = live(conn, ~p"/beacons/new")
|
|
|
|
attrs = valid_beacon_attrs(callsign: "W5ANON")
|
|
|
|
assert {:ok, _view, html} =
|
|
form_live
|
|
|> form("#beacon-form", beacon: attrs)
|
|
|> render_submit()
|
|
|> follow_redirect(conn, ~p"/beacons")
|
|
|
|
assert html =~ "awaiting admin approval"
|
|
|
|
beacon =
|
|
Enum.find(Beacons.list_pending_beacons(), fn b -> b.callsign == "W5ANON" end)
|
|
|
|
assert beacon
|
|
assert beacon.user_id == nil
|
|
assert beacon.approved == false
|
|
end
|
|
|
|
test "non-admin submission creates an unapproved beacon", %{conn: conn} do
|
|
user = user_fixture()
|
|
conn = log_in_user(conn, user)
|
|
{:ok, form_live, _html} = live(conn, ~p"/beacons/new")
|
|
|
|
attrs = valid_beacon_attrs()
|
|
|
|
assert {:ok, _view, _html} =
|
|
form_live
|
|
|> form("#beacon-form", beacon: attrs)
|
|
|> render_submit()
|
|
|> follow_redirect(conn, ~p"/beacons")
|
|
|
|
beacon = List.first(Beacons.list_pending_beacons())
|
|
assert beacon.callsign == attrs.callsign
|
|
assert beacon.approved == false
|
|
end
|
|
|
|
test "admin edits a beacon", %{conn: conn} do
|
|
admin = admin_user_fixture()
|
|
beacon = beacon_fixture(admin)
|
|
conn = log_in_user(conn, admin)
|
|
|
|
{:ok, form_live, _} = live(conn, ~p"/beacons/#{beacon}/edit")
|
|
|
|
assert {:ok, _view, html} =
|
|
form_live
|
|
|> form("#beacon-form", beacon: %{power_mw: 25_000.0})
|
|
|> render_submit()
|
|
|> follow_redirect(conn, ~p"/beacons")
|
|
|
|
assert html =~ "Beacon updated"
|
|
end
|
|
|
|
test "non-admin cannot access /beacons/:id/edit", %{conn: conn} do
|
|
beacon = beacon_fixture(user_fixture())
|
|
conn = log_in_user(conn, user_fixture())
|
|
assert {:error, {:redirect, %{to: "/"}}} = live(conn, ~p"/beacons/#{beacon}/edit")
|
|
end
|
|
end
|
|
|
|
describe "Show — lazy coverage" do
|
|
test "does not compute the estimate or embed cells on mount", %{conn: conn} do
|
|
beacon = approved_beacon_fixture(user_fixture(), frequency_mhz: 10_368.1)
|
|
{:ok, view, html} = live(conn, ~p"/beacons/#{beacon}")
|
|
|
|
# Map div exists but data-cells is absent or empty until the user
|
|
# flips the toggle — we don't want to pay the RangeEstimate cost
|
|
# on every page load.
|
|
refute html =~ "data-cells"
|
|
assert has_element?(view, "input[phx-click='toggle_coverage']")
|
|
end
|
|
|
|
test "flipping the toggle computes the estimate and pushes coverage data", %{conn: conn} do
|
|
beacon = approved_beacon_fixture(user_fixture(), frequency_mhz: 10_368.1)
|
|
{:ok, view, html} = live(conn, ~p"/beacons/#{beacon}")
|
|
|
|
# Before the click the coverage info bar is absent.
|
|
refute html =~ "cells rendered"
|
|
|
|
render_click(element(view, "input[phx-click='toggle_coverage']"))
|
|
html = render(view)
|
|
|
|
# After toggling on, the coverage info bar (which only renders
|
|
# when @estimate is populated and @show_coverage is true) shows.
|
|
assert html =~ "cells rendered"
|
|
assert html =~ "Atm loss"
|
|
end
|
|
|
|
test "toggle_coverage is a no-op for beacons below 5.76 GHz", %{conn: conn} do
|
|
beacon = approved_beacon_fixture(user_fixture(), frequency_mhz: 1296.0)
|
|
{:ok, view, html} = live(conn, ~p"/beacons/#{beacon}")
|
|
|
|
assert html =~ "5.76 GHz and up only"
|
|
refute html =~ "data-cells"
|
|
refute html =~ "cells rendered"
|
|
|
|
# Even if a client-side hack forces the click, the server refuses.
|
|
render_click(view, "toggle_coverage")
|
|
refute render(view) =~ "cells rendered"
|
|
end
|
|
end
|
|
|
|
describe "Show — extra rendering + event coverage" do
|
|
test "notes section renders when beacon has notes", %{conn: conn} do
|
|
beacon =
|
|
approved_beacon_fixture(user_fixture(), notes: "Co-located with WX radar, 12h/day.")
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")
|
|
|
|
assert html =~ "Notes"
|
|
assert html =~ "Co-located with WX radar, 12h/day."
|
|
end
|
|
|
|
test "on_the_air=false renders the 'No' ghost badge instead of success", %{conn: conn} do
|
|
beacon = approved_beacon_fixture(user_fixture(), on_the_air: false)
|
|
{:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")
|
|
|
|
assert html =~ "badge-ghost"
|
|
# HEEx interpolation may emit whitespace — assert on the
|
|
# class-to-text sequence rather than `>No<` tightly.
|
|
assert Regex.match?(~r/badge-ghost[^>]*>\s*No\s*</s, html)
|
|
end
|
|
|
|
test "bearing is rendered as 'Omni' when stored as omni", %{conn: conn} do
|
|
beacon = approved_beacon_fixture(user_fixture(), bearing: "omni")
|
|
{:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")
|
|
|
|
assert html =~ "Omni"
|
|
end
|
|
|
|
test "admin sees Approve button on unapproved beacon and the click approves it",
|
|
%{conn: conn} do
|
|
admin = admin_user_fixture()
|
|
pending = beacon_fixture(admin)
|
|
conn = log_in_user(conn, admin)
|
|
|
|
{:ok, view, html} = live(conn, ~p"/beacons/#{pending}")
|
|
assert html =~ "Approve"
|
|
assert html =~ "Pending approval"
|
|
|
|
render_click(element(view, "button", "Approve"))
|
|
|
|
refute render(view) =~ "Pending approval"
|
|
assert Beacons.get_beacon!(pending.id).approved == true
|
|
end
|
|
|
|
test "non-admin does not see the Approve button even on unapproved beacons",
|
|
%{conn: conn} do
|
|
user = user_fixture()
|
|
pending = beacon_fixture(user)
|
|
conn = log_in_user(conn, user)
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/beacons/#{pending}")
|
|
assert html =~ "Pending approval"
|
|
refute html =~ ">Approve<"
|
|
end
|
|
|
|
test "handle_info({:updated, beacon}) refreshes the displayed callsign", %{conn: conn} do
|
|
beacon = approved_beacon_fixture(user_fixture(), callsign: "W5OLD")
|
|
{:ok, view, html} = live(conn, ~p"/beacons/#{beacon}")
|
|
assert html =~ "W5OLD"
|
|
|
|
# Preload :user onto the updated struct — the production broadcast
|
|
# path emits NotLoaded user and the template derefs beacon.user.callsign.
|
|
updated =
|
|
beacon
|
|
|> Microwaveprop.Repo.preload(:user)
|
|
|> Map.put(:callsign, "W5NEW")
|
|
|
|
send(view.pid, {:updated, updated})
|
|
|
|
assert render(view) =~ "W5NEW"
|
|
end
|
|
|
|
test "handle_info({:deleted, beacon}) redirects back to the index with a flash", %{conn: conn} do
|
|
beacon = approved_beacon_fixture(user_fixture())
|
|
{:ok, view, _html} = live(conn, ~p"/beacons/#{beacon}")
|
|
|
|
send(view.pid, {:deleted, beacon})
|
|
|
|
assert_redirect(view, ~p"/beacons")
|
|
end
|
|
|
|
test "handle_info for a different beacon id is a no-op", %{conn: conn} do
|
|
beacon = approved_beacon_fixture(user_fixture(), callsign: "W5STAY")
|
|
other = approved_beacon_fixture(user_fixture(), callsign: "W5OTHER")
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/beacons/#{beacon}")
|
|
|
|
# An unrelated beacon update should NOT change the visible callsign.
|
|
{:ok, other_updated} = Beacons.update_beacon(other, %{callsign: "W5DIFF"})
|
|
send(view.pid, {:updated, other_updated})
|
|
|
|
html = render(view)
|
|
assert html =~ "W5STAY"
|
|
refute html =~ "W5DIFF"
|
|
end
|
|
end
|
|
|
|
describe "Show — property: URL beacon id round-trip" do
|
|
property "mounting /beacons/:id for a created beacon always renders its callsign" do
|
|
# Wrap each run in a sandbox checkout so Repo inserts are isolated.
|
|
check all(suffix <- integer(1..1_000_000), max_runs: 5) do
|
|
callsign = "W5P#{Integer.to_string(suffix, 36)}" |> String.upcase() |> String.slice(0, 6)
|
|
beacon = approved_beacon_fixture(user_fixture(), callsign: callsign)
|
|
|
|
conn = Phoenix.ConnTest.build_conn()
|
|
{:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")
|
|
|
|
assert html =~ callsign
|
|
end
|
|
end
|
|
end
|
|
end
|