aprs.me/test/aprsme_web/live/shared/bounds_utils_test.exs
Graham McIntire 403300f696
test: broaden unit & property-test coverage (61.8% → 64.7%)
Adds ~800 new tests (incl. 27 StreamData properties) across 22 modules
that previously had little or no coverage. Highlights:

- Pure utility modules (LogSanitizer, PacketFieldWhitelist,
  PacketSanitizer, DeviceParser, CoordinateUtils, BoundsUtils,
  ParamUtils, Convert, WeatherUnits) get thorough unit + property
  coverage including UTF-8 truncation boundaries, antimeridian
  longitude wrap, Haversine symmetry, and unit-conversion inverses.
- JSON view modules (CallsignJSON, ErrorJSON, ChangesetJSON) verified
  for envelope shape and error templating.
- Plugs (HealthCheck, RateLimiter, ApiCSRF) exercised for happy-path
  and halt paths.
- GenServer modules (RegexCache, CleanupScheduler, DeploymentNotifier)
  tested without touching the supervised singleton where possible.
- Drops the orphaned map_live/map_helpers_test.exs whose module name
  collided with the new live/shared/coordinate_utils_test.exs.
2026-04-23 13:32:09 -05:00

237 lines
8 KiB
Elixir

defmodule AprsmeWeb.Live.Shared.BoundsUtilsTest do
use ExUnit.Case, async: true
use ExUnitProperties
alias AprsmeWeb.Live.Shared.BoundsUtils
defp world_bounds, do: %{north: 85.0, south: -85.0, east: 180.0, west: -180.0}
defp usa_bounds, do: %{north: 49.0, south: 25.0, east: -67.0, west: -125.0}
# crosses antimeridian
defp pacific_bounds, do: %{north: 60.0, south: -60.0, east: -170.0, west: 170.0}
describe "valid_bounds?/1" do
test "accepts valid bounds" do
assert BoundsUtils.valid_bounds?(world_bounds())
assert BoundsUtils.valid_bounds?(usa_bounds())
end
test "rejects bounds with north <= south" do
refute BoundsUtils.valid_bounds?(%{north: 10.0, south: 20.0, east: 0.0, west: -10.0})
refute BoundsUtils.valid_bounds?(%{north: 10.0, south: 10.0, east: 0.0, west: -10.0})
end
test "rejects bounds with out-of-range latitude" do
refute BoundsUtils.valid_bounds?(%{north: 91.0, south: 0.0, east: 10.0, west: -10.0})
refute BoundsUtils.valid_bounds?(%{north: 10.0, south: -91.0, east: 10.0, west: -10.0})
end
test "rejects non-map input" do
refute BoundsUtils.valid_bounds?(nil)
refute BoundsUtils.valid_bounds?(%{})
refute BoundsUtils.valid_bounds?("bounds")
end
test "rejects bounds with non-numeric values" do
refute BoundsUtils.valid_bounds?(%{north: "10", south: 0.0, east: 0.0, west: 0.0})
end
end
describe "compare_bounds/2" do
test "two nils are equal" do
assert BoundsUtils.compare_bounds(nil, nil)
end
test "nil vs map is not equal" do
refute BoundsUtils.compare_bounds(nil, usa_bounds())
refute BoundsUtils.compare_bounds(usa_bounds(), nil)
end
test "identical bounds compare equal" do
assert BoundsUtils.compare_bounds(usa_bounds(), usa_bounds())
end
test "bounds equal within rounding to 4 places" do
a = %{north: 10.00001, south: 0.0, east: 0.0, west: -10.0}
b = %{north: 10.00002, south: 0.0, east: 0.0, west: -10.0}
assert BoundsUtils.compare_bounds(a, b)
end
test "bounds different beyond 4 places compare unequal" do
a = %{north: 10.0001, south: 0.0, east: 0.0, west: -10.0}
b = %{north: 10.0003, south: 0.0, east: 0.0, west: -10.0}
refute BoundsUtils.compare_bounds(a, b)
end
test "integer vs equivalent float compares equal" do
a = %{north: 10, south: 0, east: 5, west: -5}
b = %{north: 10.0, south: 0.0, east: 5.0, west: -5.0}
assert BoundsUtils.compare_bounds(a, b)
end
end
describe "within_bounds?/2" do
test "coordinates inside USA bounds match" do
assert BoundsUtils.within_bounds?({40.0, -100.0}, usa_bounds())
assert BoundsUtils.within_bounds?(%{lat: 40.0, lon: -100.0}, usa_bounds())
end
test "coordinates outside USA bounds don't match" do
refute BoundsUtils.within_bounds?({0.0, 0.0}, usa_bounds())
refute BoundsUtils.within_bounds?(%{lat: 60.0, lon: -100.0}, usa_bounds())
end
test "coordinates on antimeridian-crossing bounds" do
# Pacific bounds go from west=170 to east=-170, wrapping across 180.
assert BoundsUtils.within_bounds?({0.0, 175.0}, pacific_bounds())
assert BoundsUtils.within_bounds?({0.0, -175.0}, pacific_bounds())
refute BoundsUtils.within_bounds?({0.0, 0.0}, pacific_bounds())
end
test "nil coordinates are never within bounds" do
refute BoundsUtils.within_bounds?(%{lat: nil, lon: nil}, usa_bounds())
refute BoundsUtils.within_bounds?(%{}, usa_bounds())
end
end
describe "filter_packets_by_bounds/2" do
test "filters a list of packets" do
packets = [
%{lat: 40.0, lon: -100.0, id: "in"},
%{lat: 0.0, lon: 0.0, id: "out"}
]
result = BoundsUtils.filter_packets_by_bounds(packets, usa_bounds())
assert length(result) == 1
assert hd(result).id == "in"
end
test "filters a map of packets" do
packets = %{
"a" => %{lat: 40.0, lon: -100.0},
"b" => %{lat: 0.0, lon: 0.0}
}
result = BoundsUtils.filter_packets_by_bounds(packets, usa_bounds())
assert Map.has_key?(result, "a")
refute Map.has_key?(result, "b")
end
test "returns empty on empty input" do
assert BoundsUtils.filter_packets_by_bounds([], usa_bounds()) == []
assert BoundsUtils.filter_packets_by_bounds(%{}, usa_bounds()) == %{}
end
end
describe "reject_packets_by_bounds/2" do
test "returns keys for packets outside bounds" do
packets = %{
"in" => %{lat: 40.0, lon: -100.0},
"out" => %{lat: 0.0, lon: 0.0}
}
assert BoundsUtils.reject_packets_by_bounds(packets, usa_bounds()) == ["out"]
end
test "returns empty list when everything is in bounds" do
packets = %{"a" => %{lat: 40.0, lon: -100.0}}
assert BoundsUtils.reject_packets_by_bounds(packets, usa_bounds()) == []
end
end
describe "normalize_bounds/1" do
test "converts string keys to atoms" do
result =
BoundsUtils.normalize_bounds(%{
"north" => 49.0,
"south" => 25.0,
"east" => -67.0,
"west" => -125.0
})
assert result == usa_bounds()
end
test "converts string values to floats" do
result =
BoundsUtils.normalize_bounds(%{
"north" => "49.0",
"south" => "25.0",
"east" => "-67.0",
"west" => "-125.0"
})
assert result.north == 49.0
assert result.south == 25.0
end
test "passes through already-atom bounds" do
assert BoundsUtils.normalize_bounds(usa_bounds()) == usa_bounds()
end
test "returns nil for unsupported input" do
assert BoundsUtils.normalize_bounds(nil) == nil
assert BoundsUtils.normalize_bounds(%{foo: "bar"}) == nil
assert BoundsUtils.normalize_bounds("bounds") == nil
end
end
describe "calculate_bounds_from_center_and_zoom/2" do
test "centered bounds contain the center" do
center = %{lat: 40.0, lng: -100.0}
bounds = BoundsUtils.calculate_bounds_from_center_and_zoom(center, 5)
assert bounds.south <= center.lat and bounds.north >= center.lat
assert bounds.west <= center.lng and bounds.east >= center.lng
end
test "higher zoom produces tighter bounds" do
center = %{lat: 40.0, lng: -100.0}
b_low = BoundsUtils.calculate_bounds_from_center_and_zoom(center, 3)
b_high = BoundsUtils.calculate_bounds_from_center_and_zoom(center, 15)
low_width = b_low.east - b_low.west
high_width = b_high.east - b_high.west
assert high_width < low_width
end
test "center-symmetric about the origin produces symmetric bounds" do
bounds = BoundsUtils.calculate_bounds_from_center_and_zoom(%{lat: 0.0, lng: 0.0}, 5)
assert_in_delta bounds.north, -bounds.south, 0.0001
assert_in_delta bounds.east, -bounds.west, 0.0001
end
end
describe "property: filter + reject partition the input" do
property "every packet key ends up in exactly one of filter/reject" do
packet_gen =
fixed_map(%{
lat: float(min: -89.0, max: 89.0),
lon: float(min: -179.0, max: 179.0)
})
check all(packets <- map_of(string(:alphanumeric, min_length: 1, max_length: 5), packet_gen)) do
bounds = usa_bounds()
kept = packets |> BoundsUtils.filter_packets_by_bounds(bounds) |> Map.keys()
dropped = BoundsUtils.reject_packets_by_bounds(packets, bounds)
assert Enum.sort(kept ++ dropped) == packets |> Map.keys() |> Enum.sort()
assert kept -- dropped == kept
end
end
end
describe "property: within_bounds? matches manual range check (non-wrapping bounds)" do
property "matches manual check for any coordinate against USA bounds" do
usa = usa_bounds()
check all(
lat <- float(min: -90.0, max: 90.0),
lon <- float(min: -180.0, max: 180.0)
) do
expected =
lat >= usa.south and lat <= usa.north and
lon >= usa.west and lon <= usa.east
assert BoundsUtils.within_bounds?({lat, lon}, usa) == expected
end
end
end
end