- Delete dead config keys and test file (typo'd aprsme_is_*, vacuous disable test) - Remove duplicate function clauses and identical function pairs - Rename misleading one_hour_ago variable to one_day_ago - Strip stale Oban comment - Remove TestHelpers time function duplicates - Inline Aprsme.Schema module (only 1 caller) - Deduplicate prod esbuild config - Thin SymbolRenderer delegations, inline TimeUtils wrappers - Remove redundant DeploymentNotifier GenServer polling - Replace random fallback in get_callsign_key with sentinel
332 lines
11 KiB
Elixir
332 lines
11 KiB
Elixir
defmodule AprsmeWeb.Live.Shared.PacketUtilsTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias AprsmeWeb.Live.Shared.PacketUtils
|
|
|
|
describe "get_callsign_key/1" do
|
|
test "returns sender callsign when no object/item name" do
|
|
assert PacketUtils.get_callsign_key(%{sender: "K5ABC"}) == "K5ABC"
|
|
end
|
|
|
|
test "prefers object_name when present" do
|
|
packet = %{sender: "K5ABC", object_name: "REPEATER1", data_type: "object"}
|
|
assert PacketUtils.get_callsign_key(packet) == "REPEATER1"
|
|
end
|
|
|
|
test "prefers item_name when present" do
|
|
packet = %{sender: "K5ABC", item_name: "EVENT", data_type: "item"}
|
|
assert PacketUtils.get_callsign_key(packet) == "EVENT"
|
|
end
|
|
|
|
test "object_name wins over item_name" do
|
|
packet = %{object_name: "OBJ", item_name: "ITEM", sender: "K5ABC"}
|
|
assert PacketUtils.get_callsign_key(packet) == "OBJ"
|
|
end
|
|
|
|
test "returns __unknown__ for non-maps" do
|
|
assert PacketUtils.get_callsign_key(nil) == "__unknown__"
|
|
end
|
|
|
|
test "returns __unknown__ when every field is empty" do
|
|
packet = %{sender: "", object_name: "", item_name: ""}
|
|
assert PacketUtils.get_callsign_key(packet) == "__unknown__"
|
|
end
|
|
|
|
test "strips whitespace from object/item names" do
|
|
assert PacketUtils.get_callsign_key(%{object_name: " REPEATER "}) == "REPEATER"
|
|
end
|
|
end
|
|
|
|
describe "prune_oldest_packets/2" do
|
|
test "returns the same map when size is within limit" do
|
|
packets = %{"a" => %{}, "b" => %{}}
|
|
assert PacketUtils.prune_oldest_packets(packets, 10) == packets
|
|
end
|
|
|
|
test "keeps the newest packets by received_at DateTime" do
|
|
now = DateTime.utc_now()
|
|
older = DateTime.add(now, -3600, :second)
|
|
oldest = DateTime.add(now, -7200, :second)
|
|
|
|
packets = %{
|
|
"a" => %{received_at: now},
|
|
"b" => %{received_at: older},
|
|
"c" => %{received_at: oldest}
|
|
}
|
|
|
|
result = PacketUtils.prune_oldest_packets(packets, 2)
|
|
assert map_size(result) == 2
|
|
assert Map.has_key?(result, "a")
|
|
assert Map.has_key?(result, "b")
|
|
refute Map.has_key?(result, "c")
|
|
end
|
|
|
|
test "supports integer timestamps via string-keyed received_at" do
|
|
packets = %{
|
|
"a" => %{"received_at" => 1000},
|
|
"b" => %{"received_at" => 500}
|
|
}
|
|
|
|
result = PacketUtils.prune_oldest_packets(packets, 1)
|
|
assert Map.keys(result) == ["a"]
|
|
end
|
|
|
|
test "treats packets without received_at as oldest (timestamp 0)" do
|
|
now = DateTime.utc_now()
|
|
packets = %{"a" => %{received_at: now}, "b" => %{no_date: true}}
|
|
result = PacketUtils.prune_oldest_packets(packets, 1)
|
|
assert Map.has_key?(result, "a")
|
|
end
|
|
|
|
test "prunes to zero when max_count is 0 and size exceeds it" do
|
|
packets = %{"a" => %{received_at: DateTime.utc_now()}}
|
|
assert PacketUtils.prune_oldest_packets(packets, 0) == %{}
|
|
end
|
|
end
|
|
|
|
describe "parse_trail_duration/1" do
|
|
test "accepts known values" do
|
|
for hours <- [1, 6, 12, 24, 48, 168] do
|
|
assert PacketUtils.parse_trail_duration(to_string(hours)) == hours
|
|
end
|
|
end
|
|
|
|
test "rejects arbitrary numbers not in allowlist" do
|
|
assert PacketUtils.parse_trail_duration("3") == 1
|
|
assert PacketUtils.parse_trail_duration("100") == 1
|
|
assert PacketUtils.parse_trail_duration("-1") == 1
|
|
end
|
|
|
|
test "returns default for garbage and non-string input" do
|
|
assert PacketUtils.parse_trail_duration("abc") == 1
|
|
assert PacketUtils.parse_trail_duration("24abc") == 1
|
|
assert PacketUtils.parse_trail_duration(nil) == 1
|
|
assert PacketUtils.parse_trail_duration(24) == 1
|
|
end
|
|
end
|
|
|
|
describe "parse_historical_hours/1" do
|
|
test "accepts known values" do
|
|
for hours <- [1, 3, 6, 12, 24] do
|
|
assert PacketUtils.parse_historical_hours(to_string(hours)) == hours
|
|
end
|
|
end
|
|
|
|
test "rejects values not in allowlist" do
|
|
assert PacketUtils.parse_historical_hours("48") == 1
|
|
assert PacketUtils.parse_historical_hours("0") == 1
|
|
end
|
|
|
|
test "returns default for garbage" do
|
|
assert PacketUtils.parse_historical_hours(nil) == 1
|
|
assert PacketUtils.parse_historical_hours("abc") == 1
|
|
end
|
|
end
|
|
|
|
describe "get_packet_field/3" do
|
|
test "retrieves atom key" do
|
|
assert PacketUtils.get_packet_field(%{sender: "K5ABC"}, :sender) == "K5ABC"
|
|
end
|
|
|
|
test "falls back to string key" do
|
|
assert PacketUtils.get_packet_field(%{"sender" => "K5ABC"}, :sender) == "K5ABC"
|
|
end
|
|
|
|
test "falls back to data_extended atom key" do
|
|
assert PacketUtils.get_packet_field(%{data_extended: %{temperature: 72}}, :temperature) == 72
|
|
end
|
|
|
|
test "falls back to data_extended string key" do
|
|
assert PacketUtils.get_packet_field(%{"data_extended" => %{"humidity" => 50}}, :humidity) == 50
|
|
end
|
|
|
|
test "returns default when no match" do
|
|
assert PacketUtils.get_packet_field(%{}, :whatever, :fallback) == :fallback
|
|
end
|
|
|
|
test "defaults to nil when no default given" do
|
|
assert PacketUtils.get_packet_field(%{}, :whatever) == nil
|
|
end
|
|
|
|
test "handles nil data_extended gracefully" do
|
|
assert PacketUtils.get_packet_field(%{data_extended: nil}, :temperature, 0) == 0
|
|
end
|
|
end
|
|
|
|
describe "get_weather_field/2" do
|
|
test "reads from top-level atom key" do
|
|
assert PacketUtils.get_weather_field(%{temperature: 72}, :temperature) == 72
|
|
end
|
|
|
|
test "reads from data_extended" do
|
|
packet = %{data_extended: %{pressure: 1013}}
|
|
assert PacketUtils.get_weather_field(packet, :pressure) == 1013
|
|
end
|
|
|
|
test "returns nil when absent" do
|
|
assert PacketUtils.get_weather_field(%{}, :temperature) == nil
|
|
end
|
|
end
|
|
|
|
describe "has_weather_data?/1" do
|
|
test "detects weather data in top-level fields" do
|
|
assert PacketUtils.has_weather_data?(%{temperature: 72})
|
|
assert PacketUtils.has_weather_data?(%{humidity: 50})
|
|
end
|
|
|
|
test "detects weather data in data_extended" do
|
|
assert PacketUtils.has_weather_data?(%{data_extended: %{temperature: 72}})
|
|
end
|
|
|
|
test "returns false when no weather fields are present" do
|
|
refute PacketUtils.has_weather_data?(%{sender: "K5ABC"})
|
|
refute PacketUtils.has_weather_data?(%{})
|
|
end
|
|
end
|
|
|
|
describe "packet_within_time_threshold?/2" do
|
|
test "returns true when packet is after threshold" do
|
|
now = DateTime.utc_now()
|
|
older = DateTime.add(now, -3600, :second)
|
|
assert PacketUtils.packet_within_time_threshold?(%{received_at: now}, older)
|
|
end
|
|
|
|
test "returns true when packet equals threshold" do
|
|
now = DateTime.utc_now()
|
|
assert PacketUtils.packet_within_time_threshold?(%{received_at: now}, now)
|
|
end
|
|
|
|
test "returns false when packet is older than threshold" do
|
|
now = DateTime.utc_now()
|
|
older = DateTime.add(now, -3600, :second)
|
|
refute PacketUtils.packet_within_time_threshold?(%{received_at: older}, now)
|
|
end
|
|
|
|
test "treats packet with no received_at as within threshold" do
|
|
assert PacketUtils.packet_within_time_threshold?(%{}, DateTime.utc_now())
|
|
end
|
|
|
|
test "accepts integer epoch threshold" do
|
|
now = DateTime.utc_now()
|
|
older_epoch = DateTime.to_unix(now) - 3600
|
|
assert PacketUtils.packet_within_time_threshold?(%{received_at: now}, older_epoch)
|
|
end
|
|
|
|
test "accepts ISO8601 string threshold" do
|
|
now = DateTime.utc_now()
|
|
older = now |> DateTime.add(-3600, :second) |> DateTime.to_iso8601()
|
|
assert PacketUtils.packet_within_time_threshold?(%{received_at: now}, older)
|
|
end
|
|
end
|
|
|
|
describe "get_timestamp/1" do
|
|
test "returns ISO8601 for DateTime" do
|
|
dt = ~U[2024-01-15 12:00:00Z]
|
|
assert PacketUtils.get_timestamp(%{received_at: dt}) == "2024-01-15T12:00:00Z"
|
|
end
|
|
|
|
test "returns ISO8601 for NaiveDateTime" do
|
|
ndt = ~N[2024-01-15 12:00:00]
|
|
result = PacketUtils.get_timestamp(%{received_at: ndt})
|
|
assert result =~ "2024-01-15T12:00:00"
|
|
end
|
|
|
|
test "reformats ISO8601 strings" do
|
|
# Any valid ISO8601 string parses and re-emits.
|
|
assert PacketUtils.get_timestamp(%{received_at: "2024-01-15T12:00:00Z"}) ==
|
|
"2024-01-15T12:00:00Z"
|
|
end
|
|
|
|
test "passes through unparseable strings" do
|
|
assert PacketUtils.get_timestamp(%{received_at: "whenever"}) == "whenever"
|
|
end
|
|
|
|
test "returns empty string for nil or missing" do
|
|
assert PacketUtils.get_timestamp(%{received_at: nil}) == ""
|
|
assert PacketUtils.get_timestamp(%{}) == ""
|
|
end
|
|
end
|
|
|
|
describe "generate_callsign/1" do
|
|
test "joins base_callsign and ssid with a dash" do
|
|
assert PacketUtils.generate_callsign(%{base_callsign: "K5ABC", ssid: "9"}) == "K5ABC-9"
|
|
end
|
|
|
|
test "returns base_callsign only when ssid is nil" do
|
|
assert PacketUtils.generate_callsign(%{base_callsign: "K5ABC", ssid: nil}) == "K5ABC"
|
|
end
|
|
|
|
test "returns base_callsign only when ssid is empty" do
|
|
assert PacketUtils.generate_callsign(%{base_callsign: "K5ABC", ssid: ""}) == "K5ABC"
|
|
end
|
|
|
|
test "returns empty when base_callsign is missing" do
|
|
assert PacketUtils.generate_callsign(%{}) == ""
|
|
end
|
|
end
|
|
|
|
describe "display_name/1" do
|
|
test "prefers object_name over everything" do
|
|
packet = %{sender: "K5ABC", object_name: "REP", item_name: "ITEM"}
|
|
assert PacketUtils.display_name(packet) == "REP"
|
|
end
|
|
|
|
test "falls back to item_name when no object_name" do
|
|
packet = %{sender: "K5ABC", item_name: "ITEM"}
|
|
assert PacketUtils.display_name(packet) == "ITEM"
|
|
end
|
|
|
|
test "falls back to sender when neither is present" do
|
|
assert PacketUtils.display_name(%{sender: "K5ABC"}) == "K5ABC"
|
|
end
|
|
|
|
test "strips whitespace from names" do
|
|
assert PacketUtils.display_name(%{object_name: " REP "}) == "REP"
|
|
end
|
|
|
|
test "ignores blank object_name/item_name (treated as absent)" do
|
|
packet = %{sender: "K5ABC", object_name: " ", item_name: ""}
|
|
assert PacketUtils.display_name(packet) == "K5ABC"
|
|
end
|
|
end
|
|
|
|
describe "map_label/1" do
|
|
test "returns object_name for data_type=object" do
|
|
packet = %{data_type: "object", object_name: "REP", sender: "K5ABC"}
|
|
assert PacketUtils.map_label(packet) == "REP"
|
|
end
|
|
|
|
test "returns item_name for data_type=item" do
|
|
packet = %{data_type: "item", item_name: "EVENT", sender: "K5ABC"}
|
|
assert PacketUtils.map_label(packet) == "EVENT"
|
|
end
|
|
|
|
test "falls back to sender when object_name empty on object type" do
|
|
packet = %{data_type: "object", object_name: "", sender: "K5ABC"}
|
|
assert PacketUtils.map_label(packet) == "K5ABC"
|
|
end
|
|
|
|
test "returns sender for position packets" do
|
|
packet = %{data_type: "position", sender: "K5ABC"}
|
|
assert PacketUtils.map_label(packet) == "K5ABC"
|
|
end
|
|
end
|
|
|
|
describe "filter_packets_by_time_and_bounds/3" do
|
|
test "filters by both predicates" do
|
|
now = DateTime.utc_now()
|
|
older = DateTime.add(now, -3600, :second)
|
|
threshold = DateTime.add(now, -1800, :second)
|
|
bounds = %{north: 50.0, south: 30.0, east: -90.0, west: -110.0}
|
|
|
|
packets = %{
|
|
"in_and_recent" => %{lat: 40.0, lon: -100.0, received_at: now},
|
|
"in_but_old" => %{lat: 40.0, lon: -100.0, received_at: older},
|
|
"out_and_recent" => %{lat: 0.0, lon: 0.0, received_at: now}
|
|
}
|
|
|
|
result = PacketUtils.filter_packets_by_time_and_bounds(packets, bounds, threshold)
|
|
assert Map.keys(result) == ["in_and_recent"]
|
|
end
|
|
end
|
|
end
|