prop/test/microwaveprop/beacons_test.exs
Graham McIntire a5b3f1f3da Beacon detail map with range estimate and on_the_air flag
- Add on_the_air boolean to beacons (default true); surfaced as a
  checkbox on the form, a badge on the index, and in the detail list.
- Render a Leaflet map on the beacon show page with a marker at the
  beacon's lat/lon tooltipped with callsign + frequency. Marker is
  green when on air, gray when off.
- Compute and draw a reception-range estimate as concentric signal-
  strength rings. New Microwaveprop.Beacons.RangeEstimate solves a
  link budget (FSPL + O2/H2O absorption from BandConfig) at five RX
  thresholds (-100 to -145 dBm), then scales by 0.5 + score/100 from
  the latest Propagation.point_detail at the beacon's grid square, so
  current HRRR conditions shift the rings in or out.
- Re-enable the hourly PropagationGridWorker cron and freshness
  monitor in dev.exs so dev actually has HRRR-backed scores to feed
  the new estimator.
2026-04-08 13:29:58 -05:00

157 lines
4.7 KiB
Elixir

defmodule Microwaveprop.BeaconsTest do
use Microwaveprop.DataCase
import Microwaveprop.AccountsFixtures
import Microwaveprop.BeaconsFixtures
alias Microwaveprop.Beacons
alias Microwaveprop.Beacons.Beacon
@invalid_attrs %{
frequency_mhz: nil,
callsign: nil,
grid: nil,
lat: nil,
lon: nil,
power_mw: nil,
height_ft: nil
}
describe "list_beacons/0" do
test "returns all beacons regardless of creator" do
u1 = user_fixture()
u2 = user_fixture()
b1 = beacon_fixture(u1)
b2 = beacon_fixture(u2, callsign: "W5TX")
assert [^b1, ^b2] = Enum.sort_by(Beacons.list_beacons(), & &1.callsign)
end
end
describe "get_beacon!/1" do
test "returns the beacon with given id" do
user = user_fixture()
beacon = beacon_fixture(user)
assert Beacons.get_beacon!(beacon.id).id == beacon.id
end
end
describe "create_beacon/2" do
test "with valid data creates a beacon and records the creator" do
user = user_fixture()
attrs = valid_beacon_attrs()
assert {:ok, %Beacon{} = beacon} = Beacons.create_beacon(user, attrs)
assert beacon.frequency_mhz == attrs.frequency_mhz
assert beacon.callsign == "W5HN"
assert beacon.lat == attrs.lat
assert beacon.lon == attrs.lon
assert beacon.power_mw == attrs.power_mw
assert beacon.height_ft == attrs.height_ft
assert beacon.user_id == user.id
end
test "derives grid from lat/lon when grid is omitted" do
user = user_fixture()
# DFW is EM12
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(lat: 32.897, lon: -97.038))
assert String.starts_with?(beacon.grid, "EM12")
end
test "derives lat/lon from grid when lat/lon are omitted" do
user = user_fixture()
attrs =
valid_beacon_attrs(grid: "EM12kp")
|> Map.drop([:lat, :lon])
assert {:ok, beacon} = Beacons.create_beacon(user, attrs)
assert beacon.grid == "EM12kp"
assert_in_delta beacon.lat, 32.6458, 0.01
assert_in_delta beacon.lon, -97.125, 0.01
end
test "keeps an explicitly provided valid grid" do
user = user_fixture()
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(grid: "EM13"))
assert beacon.grid == "EM13"
end
test "rejects an invalid grid" do
user = user_fixture()
assert {:error, changeset} =
Beacons.create_beacon(user, valid_beacon_attrs(grid: "ZZ99"))
assert "is not a valid Maidenhead grid" in errors_on(changeset).grid
end
test "defaults on_the_air to true" do
user = user_fixture()
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs())
assert beacon.on_the_air == true
end
test "accepts on_the_air: false" do
user = user_fixture()
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(on_the_air: false))
assert beacon.on_the_air == false
end
test "upcases the callsign" do
user = user_fixture()
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(callsign: "w5hn"))
assert beacon.callsign == "W5HN"
end
test "rejects non-positive frequency" do
user = user_fixture()
assert {:error, changeset} =
Beacons.create_beacon(user, valid_beacon_attrs(frequency_mhz: 0))
assert "must be greater than 0" in errors_on(changeset).frequency_mhz
end
test "with invalid data returns error changeset" do
user = user_fixture()
assert {:error, %Ecto.Changeset{}} = Beacons.create_beacon(user, @invalid_attrs)
end
end
describe "update_beacon/2" do
test "with valid data updates the beacon" do
user = user_fixture()
beacon = beacon_fixture(user)
update_attrs = %{frequency_mhz: 24_192.1, power_mw: 5.0}
assert {:ok, %Beacon{} = beacon} = Beacons.update_beacon(beacon, update_attrs)
assert beacon.frequency_mhz == 24_192.1
assert beacon.power_mw == 5.0
end
test "with invalid data returns error changeset" do
user = user_fixture()
beacon = beacon_fixture(user)
assert {:error, %Ecto.Changeset{}} = Beacons.update_beacon(beacon, @invalid_attrs)
assert Beacons.get_beacon!(beacon.id).callsign == beacon.callsign
end
end
describe "delete_beacon/1" do
test "deletes the beacon" do
user = user_fixture()
beacon = beacon_fixture(user)
assert {:ok, %Beacon{}} = Beacons.delete_beacon(beacon)
assert_raise Ecto.NoResultsError, fn -> Beacons.get_beacon!(beacon.id) end
end
end
describe "change_beacon/1" do
test "returns a beacon changeset" do
user = user_fixture()
beacon = beacon_fixture(user)
assert %Ecto.Changeset{} = Beacons.change_beacon(beacon)
end
end
end