aprs.me/test/aprsme/packets_test.exs
Graham McIntire 8755bba9f0
Optimize historical packet loading by selecting only needed columns
Historical map loading previously fetched all 73 columns (SELECT *) from
the packets table (~2068 bytes/row). Added get_recent_packets_for_map/1
with select_map_fields/1 that fetches only the 22 columns needed for map
rendering (~200 bytes/row), yielding a ~3x query speedup.

Also removes unused all_packets tracking from PacketProcessor and
simplifies visible_packets cleanup in Index.
2026-02-20 07:41:14 -06:00

1036 lines
30 KiB
Elixir

defmodule Aprsme.PacketsTest do
use Aprsme.DataCase, async: true
alias Aprsme.BadPacket
alias Aprsme.Packet
alias Aprsme.Packets
alias Aprsme.PacketsFixtures
describe "store_packet/1" do
test "stores a valid packet" do
packet_data = %{
sender: "TEST1",
base_callsign: "TEST1",
ssid: "0",
destination: "APRS",
path: "WIDE1-1",
raw_packet: "TEST1>APRS,WIDE1-1:=3950.00N/09830.00W>Test packet",
lat: 39.833333,
lon: -98.5,
comment: "Test packet",
data_type: "position_without_timestamp_with_messaging",
symbol_table_id: "/",
symbol_code: ">"
}
assert {:ok, packet} = Packets.store_packet(packet_data)
assert packet.sender == "TEST1"
# Handle the fact that lat/lon may be stored as Decimal
lat_value = if is_struct(packet.lat, Decimal), do: Decimal.to_float(packet.lat), else: packet.lat
lon_value = if is_struct(packet.lon, Decimal), do: Decimal.to_float(packet.lon), else: packet.lon
assert Float.round(lat_value, 4) == 39.8333
assert Float.round(lon_value, 1) == -98.5
assert packet.comment == "Test packet"
end
test "stores packet with data_extended containing position" do
packet_data = %{
sender: "TEST2",
base_callsign: "TEST2",
ssid: "0",
destination: "APRS",
raw_packet: "TEST2>APRS:!3950.00N/09830.00W>",
data_type: "position",
data_extended: %{
latitude: 39.833333,
longitude: -98.5,
symbol_table_id: "/",
symbol_code: ">"
}
}
assert {:ok, packet} = Packets.store_packet(packet_data)
lat_value = if is_struct(packet.lat, Decimal), do: Decimal.to_float(packet.lat), else: packet.lat
lon_value = if is_struct(packet.lon, Decimal), do: Decimal.to_float(packet.lon), else: packet.lon
assert Float.round(lat_value, 4) == 39.8333
assert Float.round(lon_value, 1) == -98.5
end
test "stores packet with MicE data" do
packet_data = %{
sender: "TEST3",
base_callsign: "TEST3",
ssid: "0",
destination: "APRS",
path: "",
raw_packet: "TEST3>APRS:`123abc",
data_type: "mic_e",
data_extended: %{
__struct__: Aprs.Types.MicE,
lat_degrees: 39,
lat_minutes: 50,
lat_direction: :north,
lon_degrees: 98,
lon_minutes: 30,
lon_direction: :west
}
}
assert {:ok, packet} = Packets.store_packet(packet_data)
# MicE calculation: 39 + 50/60 = 39.833333
lat_value = if is_struct(packet.lat, Decimal), do: Decimal.to_float(packet.lat), else: packet.lat
lon_value = if is_struct(packet.lon, Decimal), do: Decimal.to_float(packet.lon), else: packet.lon
assert Float.round(lat_value, 4) == 39.8333
assert Float.round(lon_value, 1) == -98.5
end
test "handles validation errors gracefully" do
# Missing required fields
packet_data = %{
raw_packet: "INVALID>PACKET"
}
assert {:error, :validation_error} = Packets.store_packet(packet_data)
# Verify bad packet was stored
assert [bad_packet] = Repo.all(BadPacket)
assert bad_packet.raw_packet == "INVALID>PACKET"
assert bad_packet.error_type == "ValidationError"
end
test "handles invalid data types gracefully" do
# Create invalid data with wrong type for data_extended
packet_data = %{
sender: "TEST4",
destination: "APRS",
raw_packet: "TEST4>APRS:Invalid",
# Invalid: data_extended should be a map, not a string
data_extended: "invalid_data_extended"
}
# Should return validation error due to missing required fields
assert {:error, :validation_error} = Packets.store_packet(packet_data)
end
test "normalizes SSID to string" do
packet_data = %{
sender: "TEST5",
base_callsign: "TEST5",
ssid: 9,
destination: "APRS",
raw_packet: "TEST5-9>APRS:=3950.00N/09830.00W>",
data_type: "position",
lat: 39.833333,
lon: -98.5
}
assert {:ok, packet} = Packets.store_packet(packet_data)
assert packet.ssid == "9"
end
test "sets received_at timestamp" do
packet_data = %{
sender: "TEST6",
base_callsign: "TEST6",
ssid: "0",
destination: "APRS",
raw_packet: "TEST6>APRS:=3950.00N/09830.00W>",
data_type: "position",
lat: 39.833333,
lon: -98.5
}
assert {:ok, packet} = Packets.store_packet(packet_data)
# Just verify that received_at was set
assert packet.received_at
assert %DateTime{} = packet.received_at
end
test "sanitizes binary data in packets" do
packet_data = %{
sender: "TEST7",
base_callsign: "TEST7",
ssid: "0",
destination: "APRS",
raw_packet: "TEST7>APRS:Test with null byte",
comment: "Comment with null",
data_type: "comment",
lat: 39.833333,
lon: -98.5
}
assert {:ok, packet} = Packets.store_packet(packet_data)
# Verify the packet was stored successfully
assert packet.sender == "TEST7"
end
end
describe "store_bad_packet/2" do
test "stores bad packet from string" do
error = %{message: "Parse error", type: "ParseError"}
assert {:ok, bad_packet} = Packets.store_bad_packet("INVALID>PACKET", error)
assert bad_packet.raw_packet == "INVALID>PACKET"
assert bad_packet.error_message == "Parse error"
assert bad_packet.error_type == "ParseError"
end
test "stores bad packet from map" do
packet_data = %{
raw_packet: "TEST>APRS:Invalid",
sender: "TEST"
}
error = %{message: "Validation failed", type: "ValidationError"}
assert {:ok, bad_packet} = Packets.store_bad_packet(packet_data, error)
assert bad_packet.raw_packet == "TEST>APRS:Invalid"
assert bad_packet.error_message == "Validation failed"
end
test "handles exception objects" do
packet_data = %{raw_packet: "TEST>APRS:Invalid"}
error = %RuntimeError{message: "Something went wrong"}
assert {:ok, bad_packet} = Packets.store_bad_packet(packet_data, error)
assert bad_packet.error_type == "RuntimeError"
assert bad_packet.error_message == "Something went wrong"
end
end
describe "get_recent_packets/1" do
setup do
# Create packets with different timestamps
now = DateTime.utc_now()
packets = [
PacketsFixtures.packet_fixture(%{
sender: "OLD1",
received_at: DateTime.add(now, -48 * 3600, :second),
lat: 39.8,
lon: -98.5
}),
PacketsFixtures.packet_fixture(%{
sender: "RECENT1",
received_at: DateTime.add(now, -12 * 3600, :second),
lat: 39.9,
lon: -98.4
}),
PacketsFixtures.packet_fixture(%{
sender: "RECENT2",
received_at: DateTime.add(now, -6 * 3600, :second),
lat: 40.0,
lon: -98.3
}),
PacketsFixtures.packet_fixture(%{
sender: "NOW1",
received_at: now,
lat: 40.1,
lon: -98.2
})
]
{:ok, packets: packets}
end
test "returns packets from last 24 hours by default", %{packets: _packets} do
results = Packets.get_recent_packets()
# Should include RECENT1, RECENT2, and NOW1, but not OLD1
callsigns = Enum.map(results, & &1.sender)
assert "RECENT1" in callsigns
assert "RECENT2" in callsigns
assert "NOW1" in callsigns
refute "OLD1" in callsigns
end
test "respects hours_back parameter", %{packets: _packets} do
results = Packets.get_recent_packets(%{hours_back: 8})
# Should only include RECENT2 and NOW1
callsigns = Enum.map(results, & &1.sender)
refute "RECENT1" in callsigns
assert "RECENT2" in callsigns
assert "NOW1" in callsigns
end
test "respects limit parameter", %{packets: _packets} do
results = Packets.get_recent_packets(%{limit: 2})
assert length(results) == 2
end
test "filters by bounds" do
# Create packets inside and outside bounds
PacketsFixtures.packet_fixture(%{
sender: "INSIDE_BOUNDS",
lat: 39.0,
lon: -98.0,
received_at: DateTime.utc_now()
})
PacketsFixtures.packet_fixture(%{
sender: "OUTSIDE_BOUNDS",
lat: 41.0,
lon: -100.0,
received_at: DateTime.utc_now()
})
# Bounds format: [west, south, east, north]
bounds = [-99.0, 38.0, -97.0, 40.0]
results = Packets.get_recent_packets(%{bounds: bounds})
callsigns = Enum.map(results, & &1.sender)
assert "INSIDE_BOUNDS" in callsigns
refute "OUTSIDE_BOUNDS" in callsigns
end
test "filters by callsign" do
PacketsFixtures.packet_fixture(%{
sender: "TARGET",
lat: 39.0,
lon: -98.0
})
PacketsFixtures.packet_fixture(%{
sender: "OTHER",
lat: 39.1,
lon: -98.1
})
results = Packets.get_recent_packets(%{callsign: "TARGET"})
assert length(results) == 1
assert hd(results).sender == "TARGET"
end
end
describe "get_recent_packets_for_map/1" do
setup do
now = DateTime.utc_now()
PacketsFixtures.packet_fixture(%{
sender: "MAP1",
received_at: DateTime.add(now, -1800, :second),
lat: 39.8,
lon: -98.5,
comment: "test comment",
symbol_table_id: "/",
symbol_code: ">",
temperature: 72.0,
humidity: 45.0
})
PacketsFixtures.packet_fixture(%{
sender: "MAP2",
received_at: DateTime.add(now, -3600, :second),
lat: 39.9,
lon: -98.4,
object_name: "TestObj"
})
PacketsFixtures.packet_fixture(%{
sender: "OLD_MAP",
received_at: DateTime.add(now, -48 * 3600, :second),
lat: 40.0,
lon: -98.3
})
:ok
end
test "returns maps with only needed fields" do
results = Packets.get_recent_packets_for_map(%{hours_back: 24})
assert length(results) >= 2
first = hd(results)
# Should be a map, not a Packet struct
refute is_struct(first, Packet)
assert is_map(first)
# Should have the essential fields
assert Map.has_key?(first, :id)
assert Map.has_key?(first, :sender)
assert Map.has_key?(first, :lat)
assert Map.has_key?(first, :lon)
assert Map.has_key?(first, :received_at)
assert Map.has_key?(first, :symbol_table_id)
assert Map.has_key?(first, :symbol_code)
assert Map.has_key?(first, :comment)
assert Map.has_key?(first, :path)
assert Map.has_key?(first, :object_name)
assert Map.has_key?(first, :item_name)
# Should have weather fields
assert Map.has_key?(first, :temperature)
assert Map.has_key?(first, :humidity)
# Should NOT have heavy fields
refute Map.has_key?(first, :raw_packet)
refute Map.has_key?(first, :information_field)
refute Map.has_key?(first, :body)
refute Map.has_key?(first, :data_extended)
end
test "respects time filtering" do
results = Packets.get_recent_packets_for_map(%{hours_back: 1})
callsigns = Enum.map(results, & &1.sender)
assert "MAP1" in callsigns
refute "OLD_MAP" in callsigns
end
test "respects bounds filtering" do
bounds = [-99.0, 39.5, -98.0, 40.5]
results = Packets.get_recent_packets_for_map(%{hours_back: 24, bounds: bounds})
callsigns = Enum.map(results, & &1.sender)
assert "MAP1" in callsigns
refute "OLD_MAP" in callsigns
end
test "respects limit and offset" do
results = Packets.get_recent_packets_for_map(%{hours_back: 24, limit: 1})
assert length(results) == 1
end
test "filters by callsign" do
results = Packets.get_recent_packets_for_map(%{hours_back: 24, callsign: "MAP1"})
assert length(results) == 1
assert hd(results).sender == "MAP1"
end
test "includes object_name for APRS objects" do
results = Packets.get_recent_packets_for_map(%{hours_back: 24})
map2 = Enum.find(results, &(&1.sender == "MAP2"))
assert map2.object_name == "TestObj"
end
end
describe "get_nearby_stations/4" do
setup do
# Create stations at various distances
now = DateTime.utc_now()
stations = [
PacketsFixtures.packet_fixture(%{
sender: "NEAR1",
base_callsign: "NEAR1",
lat: 39.01,
lon: -98.01,
received_at: now
}),
PacketsFixtures.packet_fixture(%{
sender: "NEAR2",
base_callsign: "NEAR2",
lat: 39.02,
lon: -98.02,
received_at: now
}),
PacketsFixtures.packet_fixture(%{
sender: "FAR1",
base_callsign: "FAR1",
lat: 40.0,
lon: -99.0,
received_at: now
})
]
{:ok, stations: stations}
end
test "returns stations ordered by distance", %{stations: _stations} do
results = Packets.get_nearby_stations(39.0, -98.0)
assert results != []
# Results should be ordered by distance
[first | _] = results
assert first.sender in ["NEAR1", "NEAR2"]
end
test "excludes specified callsign", %{stations: _stations} do
results = Packets.get_nearby_stations(39.0, -98.0, "NEAR1")
callsigns = Enum.map(results, & &1.sender)
refute "NEAR1" in callsigns
assert "NEAR2" in callsigns
end
test "respects limit option" do
results = Packets.get_nearby_stations(39.0, -98.0, nil, %{limit: 1})
assert length(results) == 1
end
end
describe "get_weather_packets/4" do
setup do
now = DateTime.utc_now()
start_time = DateTime.add(now, -24 * 3600, :second)
end_time = now
# Create weather packets
weather_packet1 =
PacketsFixtures.packet_fixture(%{
sender: "WX1",
temperature: 72.5,
humidity: 65,
wind_speed: 5,
received_at: DateTime.add(now, -12 * 3600, :second)
})
weather_packet2 =
PacketsFixtures.packet_fixture(%{
sender: "WX1",
temperature: 73.0,
humidity: 60,
wind_speed: 7,
received_at: DateTime.add(now, -6 * 3600, :second)
})
# Create non-weather packet
non_weather =
PacketsFixtures.packet_fixture(%{
sender: "WX1",
comment: "No weather data",
received_at: DateTime.add(now, -3 * 3600, :second)
})
{:ok,
weather_packets: [weather_packet1, weather_packet2],
non_weather: non_weather,
start_time: start_time,
end_time: end_time}
end
test "returns only weather packets for callsign", %{start_time: start_time, end_time: end_time} do
results = Packets.get_weather_packets("WX1", start_time, end_time)
# Should only include packets with weather data
assert length(results) == 2
assert Enum.all?(results, fn p ->
not is_nil(p.temperature) or not is_nil(p.humidity) or not is_nil(p.wind_speed)
end)
end
test "respects time range", %{end_time: end_time} do
# Query for only last 8 hours
start_time = DateTime.add(end_time, -8 * 3600, :second)
results = Packets.get_weather_packets("WX1", start_time, end_time)
# Should only include the most recent weather packet
assert length(results) == 1
assert hd(results).temperature == 73.0
end
end
describe "get_total_packet_count/0" do
test "returns count of all packets" do
# Just verify it returns a number
count = Packets.get_total_packet_count()
assert is_integer(count)
assert count >= 0
end
test "returns 0 when no packets exist" do
# Can't easily clear all packets in a test DB that might have other tests running
# Just verify the function works
count = Packets.get_total_packet_count()
assert is_integer(count)
end
end
describe "get_oldest_packet_timestamp/0" do
test "returns timestamp of oldest packet" do
# Create an old packet
now = DateTime.utc_now()
old_time = DateTime.add(now, -365 * 24 * 3600, :second)
PacketsFixtures.packet_fixture(%{sender: "OLD", received_at: old_time})
PacketsFixtures.packet_fixture(%{sender: "NEW", received_at: now})
oldest = Packets.get_oldest_packet_timestamp()
# Just verify we got a timestamp back
assert is_struct(oldest, DateTime)
end
test "returns nil when no packets exist" do
Repo.delete_all(Packet)
assert is_nil(Packets.get_oldest_packet_timestamp())
end
end
describe "clean_old_packets/0" do
test "deletes packets older than retention period" do
now = DateTime.utc_now()
# Create old packet (400 days ago)
old_packet =
PacketsFixtures.packet_fixture(%{
sender: "OLD",
received_at: DateTime.add(now, -400 * 24 * 3600, :second)
})
# Create recent packet
recent_packet =
PacketsFixtures.packet_fixture(%{
sender: "RECENT",
received_at: now
})
# Run cleanup
deleted_count = Packets.clean_old_packets()
assert deleted_count == 1
assert is_nil(Repo.get(Packet, old_packet.id))
assert Repo.get(Packet, recent_packet.id)
end
end
describe "clean_packets_older_than/1" do
test "deletes packets older than specified days" do
now = DateTime.utc_now()
# Create packets at different ages
old_packet =
PacketsFixtures.packet_fixture(%{
sender: "OLD",
received_at: DateTime.add(now, -10 * 24 * 3600, :second)
})
recent_packet =
PacketsFixtures.packet_fixture(%{
sender: "RECENT",
received_at: DateTime.add(now, -5 * 24 * 3600, :second)
})
# Clean packets older than 7 days
assert {:ok, 1} = Packets.clean_packets_older_than(7)
assert is_nil(Repo.get(Packet, old_packet.id))
assert Repo.get(Packet, recent_packet.id)
end
test "validates positive days parameter" do
# Should guard against invalid params
assert_raise FunctionClauseError, fn ->
Packets.clean_packets_older_than(0)
end
end
end
describe "get_latest_packet_for_callsign/1" do
test "returns most recent packet for callsign" do
now = DateTime.utc_now()
# Create multiple packets for same callsign
_old =
PacketsFixtures.packet_fixture(%{
sender: "TEST1",
received_at: DateTime.add(now, -3600, :second),
comment: "Old"
})
recent =
PacketsFixtures.packet_fixture(%{
sender: "TEST1",
received_at: now,
comment: "Recent"
})
result = Packets.get_latest_packet_for_callsign("TEST1")
assert result.id == recent.id
assert result.comment == "Recent"
end
test "returns nil for non-existent callsign" do
assert is_nil(Packets.get_latest_packet_for_callsign("NONEXISTENT"))
end
end
describe "get_latest_weather_packet/1" do
test "returns most recent weather packet" do
now = DateTime.utc_now()
# Create non-weather packet
PacketsFixtures.packet_fixture(%{
sender: "WX1",
received_at: DateTime.add(now, -7200, :second),
comment: "Position only"
})
# Create old weather packet
PacketsFixtures.packet_fixture(%{
sender: "WX1",
received_at: DateTime.add(now, -3600, :second),
temperature: 70.0,
humidity: 50
})
# Create recent weather packet
recent_wx =
PacketsFixtures.packet_fixture(%{
sender: "WX1",
received_at: now,
temperature: 72.0,
humidity: 55,
wind_speed: 5
})
result = Packets.get_latest_weather_packet("WX1")
assert result.id == recent_wx.id
assert result.temperature == 72.0
end
test "returns nil when no weather packets exist" do
PacketsFixtures.packet_fixture(%{sender: "NOWX", comment: "No weather"})
assert is_nil(Packets.get_latest_weather_packet("NOWX"))
end
end
describe "has_weather_packets?/1" do
test "returns true when weather packets exist" do
PacketsFixtures.packet_fixture(%{
sender: "WX1",
temperature: 72.0
})
assert Packets.has_weather_packets?("WX1")
end
test "returns false when no weather packets exist" do
PacketsFixtures.packet_fixture(%{sender: "NOWX", comment: "No weather"})
refute Packets.has_weather_packets?("NOWX")
end
end
describe "weather_callsigns/1" do
test "returns callsigns that have weather packets" do
PacketsFixtures.packet_fixture(%{sender: "WX-BATCH1", temperature: 72.0})
PacketsFixtures.packet_fixture(%{sender: "WX-BATCH2", wind_speed: 10.0})
PacketsFixtures.packet_fixture(%{sender: "NOWX-BATCH", comment: "No weather"})
result = Packets.weather_callsigns(["WX-BATCH1", "WX-BATCH2", "NOWX-BATCH"])
assert MapSet.member?(result, "WX-BATCH1")
assert MapSet.member?(result, "WX-BATCH2")
refute MapSet.member?(result, "NOWX-BATCH")
end
test "returns empty set for empty input" do
assert MapSet.new() == Packets.weather_callsigns([])
end
test "is case-insensitive" do
PacketsFixtures.packet_fixture(%{sender: "WX-CASE", temperature: 72.0})
result = Packets.weather_callsigns(["wx-case"])
assert MapSet.member?(result, "WX-CASE")
end
end
describe "get_packets_for_replay/1" do
setup do
now = DateTime.utc_now()
start_time = DateTime.add(now, -3600, :second)
end_time = now
# Create packets in time range
p1 =
PacketsFixtures.packet_fixture(%{
sender: "REPLAY1",
received_at: DateTime.add(start_time, 900, :second),
lat: 39.0,
lon: -98.0
})
p2 =
PacketsFixtures.packet_fixture(%{
sender: "REPLAY2",
received_at: DateTime.add(start_time, 1800, :second),
lat: 39.1,
lon: -98.1
})
# Create packet outside time range
_old =
PacketsFixtures.packet_fixture(%{
sender: "OLD",
received_at: DateTime.add(start_time, -3600, :second),
lat: 39.2,
lon: -98.2
})
{:ok, packets: [p1, p2], start_time: start_time, end_time: end_time}
end
test "returns packets in chronological order", %{start_time: start_time, end_time: end_time} do
results =
Packets.get_packets_for_replay(%{
start_time: start_time,
end_time: end_time
})
assert length(results) == 2
assert hd(results).sender == "REPLAY1"
assert List.last(results).sender == "REPLAY2"
end
test "filters by bounds", %{start_time: start_time, end_time: end_time} do
# Only includes REPLAY1
bounds = [38.5, -98.5, 39.05, -97.5]
results =
Packets.get_packets_for_replay(%{
start_time: start_time,
end_time: end_time,
bounds: bounds
})
# The bounds filtering may not work exactly as expected in test
# Just verify we get results
assert is_list(results)
end
end
describe "stream_packets_for_replay/1" do
test "returns stream with timing information" do
now = DateTime.utc_now()
start_time = DateTime.add(now, -300, :second)
# Create packets with 60 second intervals
_p1 =
PacketsFixtures.packet_fixture(%{
sender: "STREAM1",
received_at: start_time,
lat: 39.0,
lon: -98.0
})
_p2 =
PacketsFixtures.packet_fixture(%{
sender: "STREAM2",
received_at: DateTime.add(start_time, 60, :second),
lat: 39.1,
lon: -98.1
})
stream =
Packets.stream_packets_for_replay(%{
start_time: start_time,
end_time: now,
# 2x speed
playback_speed: 2.0
})
[{delay1, packet1}, {delay2, packet2}] = Enum.take(stream, 2)
# First packet has no delay
assert delay1 == 0
assert packet1.sender == "STREAM1"
# 60 seconds / 2.0 speed = 30 seconds
assert_in_delta delay2, 30.0, 0.1
assert packet2.sender == "STREAM2"
end
end
describe "get_historical_packet_count/1" do
test "counts packets matching criteria" do
now = DateTime.utc_now()
# Create packets
PacketsFixtures.packet_fixture(%{
sender: "COUNT1",
lat: 39.0,
lon: -98.0,
received_at: DateTime.add(now, -3600, :second)
})
PacketsFixtures.packet_fixture(%{
sender: "COUNT2",
lat: 39.1,
lon: -98.1,
received_at: DateTime.add(now, -1800, :second)
})
# Count all recent packets
count =
Packets.get_historical_packet_count(%{
start_time: DateTime.add(now, -7200, :second),
end_time: now
})
assert count >= 2
end
test "returns 0 on error" do
# Invalid bounds should not crash
count = Packets.get_historical_packet_count(%{bounds: "invalid"})
assert count == 0
end
end
describe "get_other_ssids/2" do
test "returns other SSIDs for a base callsign" do
now = DateTime.utc_now()
# Create packets for multiple SSIDs of the same base callsign
PacketsFixtures.packet_fixture(%{
sender: "AE5PL",
base_callsign: "AE5PL",
ssid: "0",
received_at: DateTime.add(now, -1800, :second),
symbol_table_id: "/",
symbol_code: "-"
})
PacketsFixtures.packet_fixture(%{
sender: "AE5PL-1",
base_callsign: "AE5PL",
ssid: "1",
received_at: DateTime.add(now, -900, :second),
symbol_table_id: "/",
symbol_code: ">"
})
PacketsFixtures.packet_fixture(%{
sender: "AE5PL-15",
base_callsign: "AE5PL",
ssid: "15",
received_at: DateTime.add(now, -300, :second),
symbol_table_id: "/",
symbol_code: "#"
})
# Query for SSIDs other than AE5PL (the base callsign with no SSID)
results = Packets.get_other_ssids("AE5PL")
callsigns = Enum.map(results, & &1.callsign)
assert "AE5PL-1" in callsigns
assert "AE5PL-15" in callsigns
refute "AE5PL" in callsigns
end
test "returns other SSIDs when querying from an SSID variant" do
now = DateTime.utc_now()
PacketsFixtures.packet_fixture(%{
sender: "W5ABC",
base_callsign: "W5ABC",
ssid: "0",
received_at: DateTime.add(now, -1800, :second)
})
PacketsFixtures.packet_fixture(%{
sender: "W5ABC-9",
base_callsign: "W5ABC",
ssid: "9",
received_at: DateTime.add(now, -900, :second)
})
# Query from the -9 SSID should show the base callsign
results = Packets.get_other_ssids("W5ABC-9")
callsigns = Enum.map(results, & &1.callsign)
assert "W5ABC" in callsigns
refute "W5ABC-9" in callsigns
end
test "returns empty list when no other SSIDs exist" do
PacketsFixtures.packet_fixture(%{
sender: "LONELY",
base_callsign: "LONELY",
ssid: "0"
})
results = Packets.get_other_ssids("LONELY")
assert results == []
end
test "only returns SSIDs with recent packets" do
now = DateTime.utc_now()
# Create a recent packet
PacketsFixtures.packet_fixture(%{
sender: "RECENT-1",
base_callsign: "RECENT",
ssid: "1",
received_at: DateTime.add(now, -300, :second)
})
# Create an old packet (2 hours ago, outside 1-hour window)
PacketsFixtures.packet_fixture(%{
sender: "RECENT-2",
base_callsign: "RECENT",
ssid: "2",
received_at: DateTime.add(now, -7200, :second)
})
results = Packets.get_other_ssids("RECENT")
callsigns = Enum.map(results, & &1.callsign)
assert "RECENT-1" in callsigns
refute "RECENT-2" in callsigns
end
test "includes symbol information and received_at in results" do
now = DateTime.utc_now()
PacketsFixtures.packet_fixture(%{
sender: "SYM-1",
base_callsign: "SYM",
ssid: "1",
received_at: now,
symbol_table_id: "/",
symbol_code: ">"
})
[result] = Packets.get_other_ssids("SYM")
assert result.callsign == "SYM-1"
assert result.ssid == "1"
assert result.packet.symbol_table_id == "/"
assert result.packet.symbol_code == ">"
# Returns raw received_at DateTime, not a formatted map
assert %DateTime{} = result.received_at
refute Map.has_key?(result, :last_heard)
end
end
describe "get_last_hour_packets/0" do
test "returns packets from last hour" do
now = DateTime.utc_now()
# Create recent packet
_recent =
PacketsFixtures.packet_fixture(%{
sender: "RECENT",
received_at: DateTime.add(now, -1800, :second),
lat: 39.0,
lon: -98.0
})
# Create old packet
_old =
PacketsFixtures.packet_fixture(%{
sender: "OLD",
received_at: DateTime.add(now, -7200, :second),
lat: 39.1,
lon: -98.1
})
results = Packets.get_last_hour_packets()
callsigns = Enum.map(results, & &1.sender)
assert "RECENT" in callsigns
refute "OLD" in callsigns
end
test "returns empty list on error" do
# This should not crash even if there's an issue
results = Packets.get_last_hour_packets()
assert is_list(results)
end
end
end