test: cover geo, qrz record, callsign client

34 characterization tests for pure/wrapper modules. Documents existing
behavior — no production code changes.
This commit is contained in:
Graham McIntire 2026-04-16 13:54:53 -05:00
parent b148e37c8e
commit cb2937f12c
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
3 changed files with 306 additions and 0 deletions

View file

@ -0,0 +1,98 @@
defmodule Microwaveprop.GeoTest do
use ExUnit.Case, async: true
alias Microwaveprop.Geo
alias Microwaveprop.Radio.Maidenhead
describe "haversine_km/4" do
test "returns 0.0 for identical points" do
assert Geo.haversine_km(32.9, -96.8, 32.9, -96.8) == 0.0
end
test "matches a known long-distance pair (DFW to Newington, CT)" do
assert_in_delta Geo.haversine_km(32.9, -96.8, 41.714, -72.727), 2333.3, 1.0
end
test "computes a half-circumference for antipodal equator points" do
# Half the Earth's circumference at R = 6371 km is ~20015 km.
assert_in_delta Geo.haversine_km(0.0, 0.0, 0.0, 180.0), 20_015.0, 1.0
end
test "is symmetric in its arguments" do
forward = Geo.haversine_km(32.9, -96.8, 41.714, -72.727)
reverse = Geo.haversine_km(41.714, -72.727, 32.9, -96.8)
assert_in_delta forward, reverse, 0.0001
end
test "one degree of latitude is roughly 111 km" do
assert_in_delta Geo.haversine_km(32.0, -96.0, 33.0, -96.0), 111.195, 0.1
end
end
describe "bearing_deg/4" do
test "returns 0.0 for due north" do
assert Geo.bearing_deg(32.0, -96.0, 33.0, -96.0) == 0.0
end
test "returns ~90 for due east at the equator" do
assert_in_delta Geo.bearing_deg(0.0, 0.0, 0.0, 1.0), 90.0, 0.1
end
test "returns 180.0 for due south" do
assert Geo.bearing_deg(32.0, -96.0, 31.0, -96.0) == 180.0
end
test "returns a bearing in the range [0, 360)" do
bearing = Geo.bearing_deg(32.0, -96.0, 32.0, -97.0)
assert bearing >= 0.0 and bearing < 360.0
assert_in_delta bearing, 270.0, 1.0
end
test "rounds to one decimal place" do
# DFW to Newington - pick any pair, the result must have at most 1 decimal.
bearing = Geo.bearing_deg(32.9, -96.8, 41.714, -72.727)
assert Float.round(bearing, 1) == bearing
end
end
describe "maidenhead_center/1" do
test "returns the center of a 4-character grid" do
# EM12 is lat 32-33, lon -98 to -96, center (32.5, -97.0).
assert Geo.maidenhead_center("EM12") == {32.5, -97.0}
end
test "returns the center of a 6-character grid at higher precision" do
{lat, lon} = Geo.maidenhead_center("EM12kp")
assert_in_delta lat, 32.6458, 0.001
assert_in_delta lon, -97.125, 0.001
end
test "accepts lowercase grids" do
assert Geo.maidenhead_center("em12") == {32.5, -97.0}
end
test "returns nil for invalid grid characters" do
assert Geo.maidenhead_center("ZZZZ") == nil
end
end
describe "latlon_to_grid4/2" do
test "encodes the DFW area as EM12" do
assert Geo.latlon_to_grid4(32.9, -96.8) == "EM12"
end
test "encodes Newington, CT as FN31" do
assert Geo.latlon_to_grid4(41.714, -72.727) == "FN31"
end
test "encodes the prime-meridian/equator origin as JJ00" do
assert Geo.latlon_to_grid4(0.0, 0.0) == "JJ00"
end
test "agrees with Maidenhead.from_latlon/3 at precision 4 for typical US coords" do
for {lat, lon} <- [{32.9, -96.8}, {41.714, -72.727}, {40.0, -105.0}, {47.6, -122.3}] do
assert Geo.latlon_to_grid4(lat, lon) == Maidenhead.from_latlon(lat, lon, 4)
end
end
end
end

View file

@ -0,0 +1,100 @@
defmodule Microwaveprop.Qrz.RecordTest do
use ExUnit.Case, async: true
alias Microwaveprop.Qrz.Record
describe "from_map/1" do
test "returns a struct with all fields nil for an empty map" do
assert %Record{
call: nil,
fname: nil,
name: nil,
grid: nil,
addr1: nil,
addr2: nil,
state: nil,
zip: nil,
country: nil,
lat: nil,
lon: nil
} = Record.from_map(%{})
end
test "parses every known string field" do
data = %{
"call" => "W1AW",
"fname" => "Hiram Percy",
"name" => "Maxim",
"grid" => "FN31pr",
"addr1" => "225 Main St",
"addr2" => "Newington",
"state" => "CT",
"zip" => "06111",
"country" => "United States"
}
record = Record.from_map(data)
assert record.call == "W1AW"
assert record.fname == "Hiram Percy"
assert record.name == "Maxim"
assert record.grid == "FN31pr"
assert record.addr1 == "225 Main St"
assert record.addr2 == "Newington"
assert record.state == "CT"
assert record.zip == "06111"
assert record.country == "United States"
end
test "parses lat and lon as floats from numeric strings" do
record = Record.from_map(%{"lat" => "41.714775", "lon" => "-72.727260"})
assert record.lat == 41.714775
assert record.lon == -72.727260
end
test "parses partial float prefixes via Float.parse/1" do
# Float.parse/1 returns {41.714, "abc"} — the parser keeps the float.
assert %Record{lat: 41.714} = Record.from_map(%{"lat" => "41.714abc"})
end
test "returns nil for unparseable float strings" do
assert %Record{lat: nil} = Record.from_map(%{"lat" => "not-a-number"})
end
test "treats non-binary string values as nil" do
assert %Record{call: nil} = Record.from_map(%{"call" => 123})
end
test "treats non-binary lat/lon values as nil" do
# The parser only accepts binaries — a raw float is rejected.
assert %Record{lat: nil, lon: nil} = Record.from_map(%{"lat" => 41.714, "lon" => -72.727})
end
test "leaves a field as nil when the key is absent" do
assert %Record{call: "W1AW", fname: nil} = Record.from_map(%{"call" => "W1AW"})
end
test "treats an explicit nil value the same as a missing key" do
assert %Record{call: nil} = Record.from_map(%{"call" => nil})
end
test "ignores unknown XML keys" do
record = Record.from_map(%{"call" => "W1AW", "eqsl" => "1", "dxcc" => "291"})
assert record.call == "W1AW"
# No crash, no struct pollution.
assert record |> Map.from_struct() |> Map.keys() |> Enum.sort() ==
[:addr1, :addr2, :call, :country, :fname, :grid, :lat, :lon, :name, :state, :zip]
end
test "is JSON-encodable via Jason" do
record = Record.from_map(%{"call" => "W1AW", "lat" => "41.714"})
json = Jason.encode!(record)
decoded = Jason.decode!(json)
assert decoded["call"] == "W1AW"
assert decoded["lat"] == 41.714
end
end
end

View file

@ -0,0 +1,108 @@
defmodule Microwaveprop.Radio.CallsignClientTest do
use Microwaveprop.DataCase, async: false
alias Microwaveprop.CallsignLocation.Location
alias Microwaveprop.Qrz.Client
alias Microwaveprop.Radio.CallsignClient
setup do
Client.reset_session()
:ok
end
describe "locate/1" do
test "reshapes a cached CallsignLocation into the public shape" do
{:ok, _} =
%Location{}
|> Location.changeset(%{
callsign: "W1AW",
latitude: 41.714,
longitude: -72.727,
gridsquare: "FN31pr58"
})
|> Repo.insert()
assert {:ok, result} = CallsignClient.locate("W1AW")
assert result == %{
callsign: "W1AW",
gridsquare: "FN31pr58",
lat: 41.714,
lon: -72.727
}
end
test "returns only callsign, gridsquare, lat, lon (drops name and address)" do
{:ok, _} =
%Location{}
|> Location.changeset(%{
callsign: "W1AW",
latitude: 41.714,
longitude: -72.727,
gridsquare: "FN31pr58"
})
|> Repo.insert()
assert {:ok, result} = CallsignClient.locate("W1AW")
assert result |> Map.keys() |> Enum.sort() == [:callsign, :gridsquare, :lat, :lon]
end
test "renames latitude/longitude to lat/lon" do
{:ok, _} =
%Location{}
|> Location.changeset(%{
callsign: "K5QE",
latitude: 30.5,
longitude: -94.1,
gridsquare: "EM30wn"
})
|> Repo.insert()
assert {:ok, %{lat: 30.5, lon: -94.1}} = CallsignClient.locate("K5QE")
end
test "propagates {:error, reason} tuples from the underlying lookup" do
Req.Test.expect(Client, 2, fn conn ->
params = Plug.Conn.fetch_query_params(conn).query_params
if Map.has_key?(params, "username") do
Plug.Conn.send_resp(conn, 200, """
<?xml version="1.0" encoding="utf-8"?>
<QRZDatabase version="1.34">
<Session><Key>session123</Key></Session>
</QRZDatabase>
""")
else
Plug.Conn.send_resp(conn, 200, """
<?xml version="1.0" encoding="utf-8"?>
<QRZDatabase version="1.34">
<Session>
<Error>Not found: INVALID</Error>
<Key>session123</Key>
</Session>
</QRZDatabase>
""")
end
end)
assert {:error, _reason} = CallsignClient.locate("INVALID")
end
test "upcases lookups, matching a cached row inserted in different casing" do
# The underlying CallsignLocation.lookup/1 upcases the callsign before
# the DB read, so a mixed-case request finds an uppercase cache hit.
{:ok, _} =
%Location{}
|> Location.changeset(%{
callsign: "W1AW",
latitude: 41.714,
longitude: -72.727,
gridsquare: "FN31pr58"
})
|> Repo.insert()
assert {:ok, %{callsign: "W1AW"}} = CallsignClient.locate("w1aw")
end
end
end