prop/test/microwaveprop/beacons/range_estimate_test.exs
Graham McIntire cb8445f329
fix: resolve 158/183 credo --strict warnings
- Add 17 missing @spec annotations (layouts, error_json, error_html, skewt_svg)
- Move 12+ nested alias/import/require to module top level
- Add phx-change/id attributes to 11 raw HTML <form> tags
- Remove 4 unused LiveView assigns (:bounds, :data_provider)
- Add 3 missing doctest references (HrrrNativeClient, BulkFetch, Accounts)
- Break 2 long lines (path_compute.ex:382)
- Strengthen weak test assertions (is_binary→byte_size, is_list→!=[])
- Replace Module.concat with Module.safe_concat (2 occurrences)
- Replace length/1 > 0 with list != [] (9 occurrences)
- Remove no-op assert true, fix no-assertion tests

Remaining: 24 socket.assigns introspection warnings (deliberate test
pattern for observable behavior testing), 1 formatter-resistant long
line, 3 app-code usage warnings.
2026-06-12 15:47:15 -05:00

133 lines
4.3 KiB
Elixir

defmodule Microwaveprop.Beacons.RangeEstimateTest do
use Microwaveprop.DataCase
alias Microwaveprop.Beacons.Beacon
alias Microwaveprop.Beacons.RangeEstimate
defp beacon(overrides \\ %{}) do
base = %Beacon{
id: "00000000-0000-0000-0000-000000000000",
frequency_mhz: 10_368.1,
callsign: "W5HN",
grid: "EM12",
lat: 32.897,
lon: -97.038,
power_mw: 10_000.0,
height_ft: 100,
on_the_air: true
}
struct!(base, overrides)
end
describe "mw_to_dbm/1" do
test "1 mW is 0 dBm" do
assert_in_delta RangeEstimate.mw_to_dbm(1.0), 0.0, 0.001
end
test "1 W (1000 mW) is 30 dBm" do
assert_in_delta RangeEstimate.mw_to_dbm(1000.0), 30.0, 0.001
end
test "10 W (10_000 mW) is 40 dBm" do
assert_in_delta RangeEstimate.mw_to_dbm(10_000.0), 40.0, 0.001
end
end
describe "nearest_band_mhz/1" do
test "10368.1 MHz maps to 10000 (10 GHz band)" do
assert RangeEstimate.nearest_band_mhz(10_368.1) == 10_000
end
test "24192.1 MHz maps to 24000" do
assert RangeEstimate.nearest_band_mhz(24_192.1) == 24_000
end
test "47088 MHz maps to 47000" do
assert RangeEstimate.nearest_band_mhz(47_088.0) == 47_000
end
end
describe "estimate/1" do
test "returns a map with band info, center score, and per-grid cells" do
result = RangeEstimate.estimate(beacon())
assert result.band_mhz == 10_000
assert result.center_score == 50
assert is_float(result.eirp_dbm)
assert result.cells != []
assert result.grid_step == 0.125
assert result.tiers != []
end
test "each cell carries lat/lon, distance, rx_dbm, tier label/color" do
result = RangeEstimate.estimate(beacon())
for cell <- result.cells do
assert is_float(cell.lat)
assert is_float(cell.lon)
assert is_float(cell.distance_km)
assert is_integer(cell.rx_dbm)
assert byte_size(cell.label) > 0
assert byte_size(cell.color) > 0
# Detection floor enforced
assert cell.rx_dbm >= -145
end
end
test "cells closer to the beacon have stronger rx_dbm" do
result = RangeEstimate.estimate(beacon())
[closest | _] = Enum.sort_by(result.cells, & &1.distance_km)
[farthest | _] = Enum.sort_by(result.cells, &(-&1.distance_km))
assert closest.rx_dbm > farthest.rx_dbm
end
test "a more powerful beacon produces more cells" do
weak = RangeEstimate.estimate(beacon(power_mw: 10.0))
strong = RangeEstimate.estimate(beacon(power_mw: 10_000.0))
assert length(strong.cells) > length(weak.cells)
end
test "uses a default score of 50 when no propagation data exists" do
result = RangeEstimate.estimate(beacon())
# No scores in test DB -> center defaults, cells all use 50.
assert result.center_score == 50
assert result.valid_time == nil
# All cells should have score 50 when there's no HRRR data.
assert Enum.all?(result.cells, fn c -> c.score == 50 end)
end
test "cells align with the 0.125 deg HRRR grid" do
result = RangeEstimate.estimate(beacon())
for cell <- result.cells do
# lat and lon should both be multiples of 0.125
lat_mod = abs(cell.lat / 0.125 - round(cell.lat / 0.125))
lon_mod = abs(cell.lon / 0.125 - round(cell.lon / 0.125))
assert lat_mod < 0.01
assert lon_mod < 0.01
end
end
test "short-circuits with empty cells for beacons below 5.76 GHz" do
# 432 MHz with 1 W at unrestricted range would generate >100k cells
# because atmospheric loss is negligible -- encoding that payload
# into the map's data attribute was locking up browsers in prod.
result = RangeEstimate.estimate(beacon(frequency_mhz: 432.38, power_mw: 1000.0))
assert result.cells == []
assert result.max_range_km == 0.0
# Still reports the nearest configured band for display purposes.
assert is_integer(result.band_mhz)
end
test "short-circuits at exactly 5760 MHz threshold" do
above = RangeEstimate.estimate(beacon(frequency_mhz: 5760.0, power_mw: 1000.0))
below = RangeEstimate.estimate(beacon(frequency_mhz: 5759.9, power_mw: 1000.0))
assert above.cells != []
assert below.cells == []
end
end
end