Beacons: - Scaffolded with phx.gen.live then reworked so reads are public and mutations go through a live_session gated by the new :require_admin on_mount hook in UserAuth - Beacon schema stores frequency (MHz), callsign, grid, lat/lon, power (W), and height above ground (m); grid auto-derives from lat/lon when left blank via Maidenhead.from_latlon - Adds Maidenhead.from_latlon/3 so we can compute grids locally instead of hitting an external API Users admin page: - /users and /users/:id/edit (admin-only) for listing, editing (callsign/name/email/is_admin), and deleting other users - Adds Accounts.list_users, admin_update_user, delete_user, and a dedicated admin_changeset on the User schema - Nav gains a "Users" link for admins and a "Beacons" link for everyone
135 lines
4.1 KiB
Elixir
135 lines
4.1 KiB
Elixir
defmodule Microwaveprop.Radio.MaidenheadTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Microwaveprop.Radio.Maidenhead
|
|
|
|
describe "valid?/1" do
|
|
test "accepts valid 4-char grid" do
|
|
assert Maidenhead.valid?("EM12")
|
|
end
|
|
|
|
test "accepts valid 6-char grid" do
|
|
assert Maidenhead.valid?("EM12ab")
|
|
end
|
|
|
|
test "is case-insensitive" do
|
|
assert Maidenhead.valid?("em12")
|
|
assert Maidenhead.valid?("EM12AB")
|
|
assert Maidenhead.valid?("em12ab")
|
|
end
|
|
|
|
test "rejects field chars outside A-R" do
|
|
refute Maidenhead.valid?("SZ12")
|
|
end
|
|
|
|
test "rejects subsquare chars outside a-x" do
|
|
refute Maidenhead.valid?("EM12yz")
|
|
end
|
|
|
|
test "rejects wrong length" do
|
|
refute Maidenhead.valid?("EM1")
|
|
refute Maidenhead.valid?("EM123")
|
|
refute Maidenhead.valid?("EM12abc")
|
|
end
|
|
|
|
test "rejects non-digit in square positions" do
|
|
refute Maidenhead.valid?("EMAB")
|
|
end
|
|
|
|
test "rejects empty string" do
|
|
refute Maidenhead.valid?("")
|
|
end
|
|
|
|
test "rejects nil" do
|
|
refute Maidenhead.valid?(nil)
|
|
end
|
|
end
|
|
|
|
describe "to_latlon/1" do
|
|
test "returns center of 4-char grid FN31" do
|
|
# FN31: F=5, N=13 -> lon = 5*20 - 180 + 3*2 + 1 = -73, lat = 13*10 - 90 + 1*1 + 0.5 = 41.5
|
|
assert {:ok, {lat, lon}} = Maidenhead.to_latlon("FN31")
|
|
assert_in_delta lat, 41.5, 0.01
|
|
assert_in_delta lon, -73.0, 0.01
|
|
end
|
|
|
|
test "returns center of 6-char grid FN31pr" do
|
|
# FN31pr: field F=5,N=13; square 3,1; subsquare p=15,r=17
|
|
# lon = 5*20 - 180 + 3*2 + 15*(5/60) + (5/120) = -100 + 6 + 1.25 + 0.0417 = -72.7083
|
|
# lat = 13*10 - 90 + 1*1 + 17*(2.5/60) + (2.5/120) = 40 + 1 + 0.7083 + 0.02083 = 41.7292
|
|
assert {:ok, {lat, lon}} = Maidenhead.to_latlon("FN31pr")
|
|
assert_in_delta lat, 41.7292, 0.01
|
|
assert_in_delta lon, -72.7083, 0.01
|
|
end
|
|
|
|
test "is case-insensitive" do
|
|
assert {:ok, {lat1, lon1}} = Maidenhead.to_latlon("FN31")
|
|
assert {:ok, {lat2, lon2}} = Maidenhead.to_latlon("fn31")
|
|
assert lat1 == lat2
|
|
assert lon1 == lon2
|
|
end
|
|
|
|
test "returns :error for invalid grid" do
|
|
assert :error = Maidenhead.to_latlon("ZZ99")
|
|
end
|
|
|
|
test "returns :error for nil" do
|
|
assert :error = Maidenhead.to_latlon(nil)
|
|
end
|
|
|
|
test "returns :error for non-binary" do
|
|
assert :error = Maidenhead.to_latlon(123)
|
|
end
|
|
|
|
test "handles 8-char extended grid EM12kp37" do
|
|
assert {:ok, {lat, lon}} = Maidenhead.to_latlon("EM12kp37")
|
|
assert is_float(lat)
|
|
assert is_float(lon)
|
|
# Should be within EM12kp subsquare
|
|
assert lat > 32.0 and lat < 33.0
|
|
assert lon > -98.0 and lon < -96.0
|
|
end
|
|
|
|
test "AA00 returns southwest corner center" do
|
|
# lon = 0*20 - 180 + 0*2 + 1 = -179, lat = 0*10 - 90 + 0*1 + 0.5 = -89.5
|
|
assert {:ok, {lat, lon}} = Maidenhead.to_latlon("AA00")
|
|
assert_in_delta lat, -89.5, 0.01
|
|
assert_in_delta lon, -179.0, 0.01
|
|
end
|
|
|
|
test "RR99 returns northeast corner center" do
|
|
# lon = 17*20 - 180 + 9*2 + 1 = 179, lat = 17*10 - 90 + 9*1 + 0.5 = 89.5
|
|
assert {:ok, {lat, lon}} = Maidenhead.to_latlon("RR99")
|
|
assert_in_delta lat, 89.5, 0.01
|
|
assert_in_delta lon, 179.0, 0.01
|
|
end
|
|
end
|
|
|
|
describe "from_latlon/3" do
|
|
test "round-trips 4-char grid FN31" do
|
|
{:ok, {lat, lon}} = Maidenhead.to_latlon("FN31")
|
|
assert Maidenhead.from_latlon(lat, lon, 4) == "FN31"
|
|
end
|
|
|
|
test "round-trips 6-char grid FN31pr" do
|
|
{:ok, {lat, lon}} = Maidenhead.to_latlon("FN31pr")
|
|
assert Maidenhead.from_latlon(lat, lon, 6) == "FN31pr"
|
|
end
|
|
|
|
test "defaults to precision 6" do
|
|
{:ok, {lat, lon}} = Maidenhead.to_latlon("EM12kp")
|
|
assert Maidenhead.from_latlon(lat, lon) == "EM12kp"
|
|
end
|
|
|
|
test "round-trips 8-char grid EM12kp37" do
|
|
{:ok, {lat, lon}} = Maidenhead.to_latlon("EM12kp37")
|
|
assert Maidenhead.from_latlon(lat, lon, 8) == "EM12kp37"
|
|
end
|
|
|
|
test "handles North Texas coordinates" do
|
|
# DFW area is approximately EM12/EM13
|
|
grid = Maidenhead.from_latlon(32.897, -97.038, 6)
|
|
assert String.starts_with?(grid, "EM12")
|
|
end
|
|
end
|
|
end
|