- 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
441 lines
12 KiB
Elixir
441 lines
12 KiB
Elixir
defmodule AprsmeWeb.MapLive.PacketUtilsTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias AprsmeWeb.Live.Shared.PacketUtils, as: SharedPacketUtils
|
|
alias AprsmeWeb.MapLive.PacketUtils
|
|
|
|
describe "weather popup generation" do
|
|
test "generates weather popup with actual weather data" do
|
|
packet = %{
|
|
id: 1,
|
|
base_callsign: "TEST",
|
|
sender: "TEST-1",
|
|
ssid: "1",
|
|
data_type: "position",
|
|
symbol_table_id: "/",
|
|
symbol_code: "_",
|
|
lat: 32.7767,
|
|
lon: -96.7970,
|
|
received_at: DateTime.utc_now(),
|
|
temperature: 72.0,
|
|
humidity: 45.0,
|
|
wind_direction: 180,
|
|
wind_speed: 10.0,
|
|
wind_gust: 15.0,
|
|
pressure: 1013.2,
|
|
rain_1h: 0.0,
|
|
rain_24h: 0.1,
|
|
rain_since_midnight: 0.2
|
|
}
|
|
|
|
result = PacketUtils.build_packet_data(packet, true)
|
|
|
|
# result verified by subsequent key access
|
|
assert result["popup"]
|
|
|
|
popup = result["popup"]
|
|
|
|
# Check that it's a weather popup
|
|
assert popup =~ "Weather Report"
|
|
assert popup =~ "data-timestamp="
|
|
|
|
# Check weather data is present
|
|
assert popup =~ "Temperature: 72.0°F"
|
|
assert popup =~ "Humidity: 45.0%"
|
|
assert popup =~ "Wind: 180° at 10.0 mph, gusts to 15.0 mph"
|
|
assert popup =~ "Pressure: 1013.2 hPa"
|
|
assert popup =~ "Rain (1h): 0.0 in"
|
|
assert popup =~ "Rain (24h): 0.1 in"
|
|
assert popup =~ "Rain (since midnight): 0.2 in"
|
|
|
|
# Check callsign and links
|
|
assert popup =~ "TEST-1"
|
|
assert popup =~ "/TEST-1"
|
|
assert popup =~ "/info/TEST-1"
|
|
assert popup =~ "/weather/TEST-1"
|
|
assert popup =~ "weather charts"
|
|
end
|
|
|
|
test "generates weather popup with N/A for missing weather data" do
|
|
packet = %{
|
|
id: 1,
|
|
base_callsign: "TEST",
|
|
sender: "TEST-1",
|
|
ssid: "1",
|
|
data_type: "position",
|
|
symbol_table_id: "/",
|
|
symbol_code: "_",
|
|
lat: 32.7767,
|
|
lon: -96.7970,
|
|
received_at: DateTime.utc_now(),
|
|
# Add at least one non-nil weather field to make it a weather packet
|
|
temperature: 75,
|
|
humidity: nil,
|
|
pressure: nil,
|
|
wind_speed: nil,
|
|
wind_direction: nil,
|
|
wind_gust: nil,
|
|
rain_1h: nil,
|
|
rain_24h: nil,
|
|
rain_since_midnight: nil
|
|
}
|
|
|
|
result = PacketUtils.build_packet_data(packet, true)
|
|
|
|
# result verified by subsequent key access
|
|
assert result["popup"]
|
|
|
|
popup = result["popup"]
|
|
|
|
# Check that it's a weather popup
|
|
assert popup =~ "Weather Report"
|
|
assert popup =~ "data-timestamp="
|
|
|
|
# Check temperature is shown, others show N/A
|
|
assert popup =~ "Temperature: 75°F"
|
|
assert popup =~ "Humidity: N/A%"
|
|
assert popup =~ "Wind: N/A° at N/A mph, gusts to N/A mph"
|
|
assert popup =~ "Pressure: N/A hPa"
|
|
assert popup =~ "Rain (1h): N/A in"
|
|
assert popup =~ "Rain (24h): N/A in"
|
|
assert popup =~ "Rain (since midnight): N/A in"
|
|
end
|
|
|
|
test "generates weather popup with partial weather data" do
|
|
packet = %{
|
|
id: 1,
|
|
base_callsign: "TEST",
|
|
sender: "TEST-1",
|
|
ssid: "1",
|
|
data_type: "position",
|
|
symbol_table_id: "/",
|
|
symbol_code: "_",
|
|
lat: 32.7767,
|
|
lon: -96.7970,
|
|
received_at: DateTime.utc_now(),
|
|
temperature: 85.0,
|
|
humidity: 60.0
|
|
# Missing wind, pressure, rain data
|
|
}
|
|
|
|
result = PacketUtils.build_packet_data(packet, true)
|
|
|
|
# result verified by subsequent key access
|
|
assert result["popup"]
|
|
|
|
popup = result["popup"]
|
|
|
|
# Check that it's a weather popup
|
|
assert popup =~ "Weather Report"
|
|
assert popup =~ "data-timestamp="
|
|
|
|
# Check available weather data
|
|
assert popup =~ "Temperature: 85.0°F"
|
|
assert popup =~ "Humidity: 60.0%"
|
|
|
|
# Check missing data shows N/A
|
|
assert popup =~ "Wind: N/A° at N/A mph, gusts to N/A mph"
|
|
assert popup =~ "Pressure: N/A hPa"
|
|
assert popup =~ "Rain (1h): N/A in"
|
|
assert popup =~ "Rain (24h): N/A in"
|
|
assert popup =~ "Rain (since midnight): N/A in"
|
|
end
|
|
|
|
test "generates standard popup for non-weather packets" do
|
|
packet = %{
|
|
id: 1,
|
|
base_callsign: "TEST",
|
|
sender: "TEST-1",
|
|
ssid: "1",
|
|
data_type: "position",
|
|
symbol_table_id: "/",
|
|
symbol_code: ">",
|
|
lat: 32.7767,
|
|
lon: -96.7970,
|
|
received_at: DateTime.utc_now(),
|
|
comment: "Test comment"
|
|
}
|
|
|
|
result = PacketUtils.build_packet_data(packet, true)
|
|
|
|
# result verified by subsequent key access
|
|
assert result["popup"]
|
|
|
|
popup = result["popup"]
|
|
|
|
# Check that it's NOT a weather popup
|
|
refute popup =~ "Weather Report"
|
|
# All popups now have data-timestamp for cache busting
|
|
assert popup =~ "data-timestamp="
|
|
|
|
# Check standard popup content
|
|
assert popup =~ "TEST-1"
|
|
assert popup =~ "/TEST-1"
|
|
assert popup =~ "/info/TEST-1"
|
|
assert popup =~ "Test comment"
|
|
end
|
|
|
|
test "generates weather popup with timestamp" do
|
|
received_at = DateTime.utc_now()
|
|
|
|
packet = %{
|
|
id: 1,
|
|
base_callsign: "TEST",
|
|
sender: "TEST-1",
|
|
ssid: "1",
|
|
data_type: "position",
|
|
symbol_table_id: "/",
|
|
symbol_code: "_",
|
|
lat: 32.7767,
|
|
lon: -96.7970,
|
|
received_at: received_at,
|
|
temperature: 72.0,
|
|
humidity: 45.0
|
|
}
|
|
|
|
result = PacketUtils.build_packet_data(packet, true)
|
|
|
|
# result verified by subsequent key access
|
|
assert result["popup"]
|
|
|
|
popup = result["popup"]
|
|
|
|
# Check timestamp is included
|
|
assert popup =~ "aprs-timestamp"
|
|
assert popup =~ Calendar.strftime(received_at, "%Y-%m-%d %H:%M:%S UTC")
|
|
end
|
|
|
|
test "handles weather data from data_extended field" do
|
|
packet = %{
|
|
id: 1,
|
|
base_callsign: "TEST",
|
|
sender: "TEST-1",
|
|
ssid: "1",
|
|
data_type: "position",
|
|
symbol_table_id: "/",
|
|
symbol_code: "_",
|
|
lat: 32.7767,
|
|
lon: -96.7970,
|
|
received_at: DateTime.utc_now(),
|
|
data_extended: %{
|
|
temperature: 75.0,
|
|
humidity: 50.0,
|
|
wind_direction: 270,
|
|
wind_speed: 12.0,
|
|
pressure: 1015.0
|
|
}
|
|
}
|
|
|
|
result = PacketUtils.build_packet_data(packet, true)
|
|
|
|
# result verified by subsequent key access
|
|
assert result["popup"]
|
|
|
|
popup = result["popup"]
|
|
|
|
# Check weather data from data_extended
|
|
assert popup =~ "Temperature: 75.0°F"
|
|
assert popup =~ "Humidity: 50.0%"
|
|
assert popup =~ "Wind: 270° at 12.0 mph"
|
|
assert popup =~ "Pressure: 1015.0 hPa"
|
|
end
|
|
|
|
test "handles weather data with string keys in data_extended" do
|
|
packet = %{
|
|
id: 1,
|
|
base_callsign: "TEST",
|
|
sender: "TEST-1",
|
|
ssid: "1",
|
|
data_type: "position",
|
|
symbol_table_id: "/",
|
|
symbol_code: "_",
|
|
lat: 32.7767,
|
|
lon: -96.7970,
|
|
received_at: DateTime.utc_now(),
|
|
data_extended: %{
|
|
"temperature" => 80.0,
|
|
"humidity" => 55.0,
|
|
"wind_direction" => 90,
|
|
"wind_speed" => 8.0
|
|
}
|
|
}
|
|
|
|
result = PacketUtils.build_packet_data(packet, true)
|
|
|
|
# result verified by subsequent key access
|
|
assert result["popup"]
|
|
|
|
popup = result["popup"]
|
|
|
|
# Check weather data from data_extended with string keys
|
|
assert popup =~ "Temperature: 80.0°F"
|
|
assert popup =~ "Humidity: 55.0%"
|
|
assert popup =~ "Wind: 90° at 8.0 mph"
|
|
end
|
|
end
|
|
|
|
describe "weather packet detection" do
|
|
test "identifies weather packets by weather data fields" do
|
|
weather_packet = %{
|
|
data_type: "position",
|
|
symbol_table_id: "/",
|
|
symbol_code: "_",
|
|
temperature: 75
|
|
}
|
|
|
|
assert SharedPacketUtils.has_weather_data?(weather_packet)
|
|
end
|
|
|
|
test "identifies weather packets with humidity" do
|
|
weather_packet = %{
|
|
data_type: "weather",
|
|
symbol_table_id: "/",
|
|
symbol_code: ">",
|
|
humidity: 80
|
|
}
|
|
|
|
assert SharedPacketUtils.has_weather_data?(weather_packet)
|
|
end
|
|
|
|
test "does not identify non-weather packets" do
|
|
regular_packet = %{
|
|
data_type: "position",
|
|
symbol_table_id: "/",
|
|
symbol_code: ">"
|
|
}
|
|
|
|
refute SharedPacketUtils.has_weather_data?(regular_packet)
|
|
end
|
|
|
|
test "has_weather_packets? returns false for non-existent callsign" do
|
|
# This should return false since the database call will fail in test context
|
|
refute PacketUtils.has_weather_packets?("NONEXISTENT")
|
|
end
|
|
|
|
test "has_weather_packets? handles invalid input gracefully" do
|
|
refute PacketUtils.has_weather_packets?(nil)
|
|
refute PacketUtils.has_weather_packets?("")
|
|
refute PacketUtils.has_weather_packets?(123)
|
|
end
|
|
end
|
|
|
|
describe "weather field extraction" do
|
|
test "extracts weather fields from packet" do
|
|
packet = %{
|
|
temperature: 72.0,
|
|
humidity: 45.0,
|
|
wind_direction: 180,
|
|
wind_speed: 10.0
|
|
}
|
|
|
|
assert PacketUtils.get_weather_field(packet, :temperature) == 72.0
|
|
assert PacketUtils.get_weather_field(packet, :humidity) == 45.0
|
|
assert PacketUtils.get_weather_field(packet, :wind_direction) == 180
|
|
assert PacketUtils.get_weather_field(packet, :wind_speed) == 10.0
|
|
end
|
|
|
|
test "extracts weather fields from data_extended" do
|
|
packet = %{
|
|
data_extended: %{
|
|
temperature: 75.0,
|
|
humidity: 50.0
|
|
}
|
|
}
|
|
|
|
assert PacketUtils.get_weather_field(packet, :temperature) == 75.0
|
|
assert PacketUtils.get_weather_field(packet, :humidity) == 50.0
|
|
end
|
|
|
|
test "returns N/A for missing weather fields" do
|
|
packet = %{}
|
|
|
|
assert PacketUtils.get_weather_field(packet, :temperature) == "N/A"
|
|
assert PacketUtils.get_weather_field(packet, :humidity) == "N/A"
|
|
assert PacketUtils.get_weather_field(packet, :wind_direction) == "N/A"
|
|
end
|
|
|
|
test "handles string keys in data_extended" do
|
|
packet = %{
|
|
data_extended: %{
|
|
"temperature" => 80.0,
|
|
"humidity" => 55.0
|
|
}
|
|
}
|
|
|
|
assert PacketUtils.get_weather_field(packet, :temperature) == 80.0
|
|
assert PacketUtils.get_weather_field(packet, :humidity) == 55.0
|
|
end
|
|
|
|
test "handles string keys in packet" do
|
|
packet = %{
|
|
"temperature" => 85.0,
|
|
"humidity" => 60.0
|
|
}
|
|
|
|
assert PacketUtils.get_weather_field(packet, :temperature) == 85.0
|
|
assert PacketUtils.get_weather_field(packet, :humidity) == 60.0
|
|
end
|
|
end
|
|
|
|
describe "convert_tuples_to_strings/1" do
|
|
test "converts a bare tuple to an inspect string" do
|
|
assert PacketUtils.convert_tuples_to_strings({1, 2}) == "{1, 2}"
|
|
end
|
|
|
|
test "converts tuple values inside maps" do
|
|
result = PacketUtils.convert_tuples_to_strings(%{a: {1, 2}, b: "keep"})
|
|
assert result.a == "{1, 2}"
|
|
assert result.b == "keep"
|
|
end
|
|
|
|
test "walks nested maps" do
|
|
result = PacketUtils.convert_tuples_to_strings(%{outer: %{inner: {:ok, "v"}}})
|
|
assert result.outer.inner =~ "{:ok,"
|
|
end
|
|
|
|
test "walks lists" do
|
|
assert PacketUtils.convert_tuples_to_strings([{1, 2}, {3, 4}]) == ["{1, 2}", "{3, 4}"]
|
|
end
|
|
|
|
test "leaves structs alone" do
|
|
dt = ~U[2024-01-15 12:00:00Z]
|
|
assert PacketUtils.convert_tuples_to_strings(dt) == dt
|
|
end
|
|
|
|
test "passes through scalars" do
|
|
assert PacketUtils.convert_tuples_to_strings(42) == 42
|
|
assert PacketUtils.convert_tuples_to_strings("hello") == "hello"
|
|
assert PacketUtils.convert_tuples_to_strings(nil) == nil
|
|
assert PacketUtils.convert_tuples_to_strings(:atom) == :atom
|
|
end
|
|
end
|
|
|
|
describe "callsign generation" do
|
|
test "generates callsign with SSID" do
|
|
packet = %{
|
|
base_callsign: "TEST",
|
|
ssid: "1"
|
|
}
|
|
|
|
assert SharedPacketUtils.generate_callsign(packet) == "TEST-1"
|
|
end
|
|
|
|
test "generates callsign without SSID" do
|
|
packet = %{
|
|
base_callsign: "TEST",
|
|
ssid: nil
|
|
}
|
|
|
|
assert SharedPacketUtils.generate_callsign(packet) == "TEST"
|
|
end
|
|
|
|
test "generates callsign with empty SSID" do
|
|
packet = %{
|
|
base_callsign: "TEST",
|
|
ssid: ""
|
|
}
|
|
|
|
assert SharedPacketUtils.generate_callsign(packet) == "TEST"
|
|
end
|
|
end
|
|
end
|