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:
parent
b148e37c8e
commit
cb2937f12c
3 changed files with 306 additions and 0 deletions
98
test/microwaveprop/geo_test.exs
Normal file
98
test/microwaveprop/geo_test.exs
Normal 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
|
||||
100
test/microwaveprop/qrz/record_test.exs
Normal file
100
test/microwaveprop/qrz/record_test.exs
Normal 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
|
||||
108
test/microwaveprop/radio/callsign_client_test.exs
Normal file
108
test/microwaveprop/radio/callsign_client_test.exs
Normal 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
|
||||
Loading…
Add table
Reference in a new issue