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