defmodule Microwaveprop.AprsTest do use ExUnit.Case, async: false alias Ecto.Adapters.SQL alias Ecto.Adapters.SQL.Sandbox alias Microwaveprop.Aprs alias Microwaveprop.AprsRepo setup do pid = Sandbox.start_owner!(AprsRepo, shared: true) on_exit(fn -> Sandbox.stop_owner(pid) end) :ok end defp insert_packet(opts) do fields = Keyword.merge( [ sender: "TEST-A", base_callsign: "TEST-A", lat: 33.0, lon: -97.0, path: "WIDE1*", received_at: NaiveDateTime.utc_now(), has_position: true, is_item: false, is_object: false ], opts ) SQL.query!( AprsRepo, """ INSERT INTO packets (id, sender, base_callsign, lat, lon, path, received_at, has_position, is_item, is_object, inserted_at, updated_at) VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, now(), now()) """, [ fields[:sender], fields[:base_callsign], fields[:lat], fields[:lon], fields[:path], fields[:received_at], fields[:has_position], fields[:is_item], fields[:is_object] ] ) :ok end describe "recent_packets_with_paths/1" do test "filters out is_item, is_object, has_position=false, empty path, and null lat/lon" do now = NaiveDateTime.utc_now() since = DateTime.add(DateTime.utc_now(), -3600, :second) # 1 valid row :ok = insert_packet(sender: "TEST-VALID", path: "K5GVL-10*,WIDE1-1", received_at: now) # 7 rejected rows :ok = insert_packet(sender: "TEST-ITEM", is_item: true, received_at: now) :ok = insert_packet(sender: "TEST-OBJECT", is_object: true, received_at: now) :ok = insert_packet(sender: "TEST-NOPOS", has_position: false, received_at: now) :ok = insert_packet(sender: "TEST-EMPTYPATH", path: "", received_at: now) :ok = insert_packet(sender: "TEST-NULLPATH", path: nil, received_at: now) :ok = insert_packet(sender: "TEST-NULLLAT", lat: nil, received_at: now) :ok = insert_packet(sender: "TEST-NULLLON", lon: nil, received_at: now) rows = Aprs.recent_packets_with_paths(since: since) assert length(rows) == 1 assert hd(rows).sender == "TEST-VALID" end test "honors :since" do now = DateTime.utc_now() recent = NaiveDateTime.add(NaiveDateTime.utc_now(), -1800, :second) old = NaiveDateTime.add(NaiveDateTime.utc_now(), -5400, :second) :ok = insert_packet(sender: "TEST-RECENT", received_at: recent) :ok = insert_packet(sender: "TEST-OLD", received_at: old) since = DateTime.add(now, -3600, :second) rows = Aprs.recent_packets_with_paths(since: since) senders = Enum.map(rows, & &1.sender) assert "TEST-RECENT" in senders refute "TEST-OLD" in senders end test "honors :limit" do since = DateTime.add(DateTime.utc_now(), -3600, :second) Enum.each(1..5, fn i -> # Stagger by seconds so ordering is deterministic. ts = NaiveDateTime.add(NaiveDateTime.utc_now(), -i, :second) :ok = insert_packet(sender: "TEST-L#{i}", received_at: ts) end) rows = Aprs.recent_packets_with_paths(since: since, limit: 2) assert length(rows) == 2 end test "returns rows in received_at ascending order" do since = DateTime.add(DateTime.utc_now(), -3600, :second) base = NaiveDateTime.utc_now() :ok = insert_packet(sender: "TEST-MID", received_at: NaiveDateTime.add(base, -120, :second)) :ok = insert_packet(sender: "TEST-OLDEST", received_at: NaiveDateTime.add(base, -300, :second)) :ok = insert_packet(sender: "TEST-NEWEST", received_at: NaiveDateTime.add(base, -10, :second)) rows = since |> then(&Aprs.recent_packets_with_paths(since: &1)) |> Enum.filter(&String.starts_with?(&1.sender, "TEST-")) senders = Enum.map(rows, & &1.sender) assert senders == ["TEST-OLDEST", "TEST-MID", "TEST-NEWEST"] end test "decodes Decimal lat/lon as floats" do since = DateTime.add(DateTime.utc_now(), -3600, :second) :ok = insert_packet(sender: "TEST-FLOAT", lat: 33.123, lon: -97.456) rows = Aprs.recent_packets_with_paths(since: since) row = Enum.find(rows, &(&1.sender == "TEST-FLOAT")) assert is_float(row.lat) assert is_float(row.lon) assert_in_delta row.lat, 33.123, 0.0001 assert_in_delta row.lon, -97.456, 0.0001 end test "decodes received_at as a UTC DateTime" do since = DateTime.add(DateTime.utc_now(), -3600, :second) :ok = insert_packet(sender: "TEST-TZ") rows = Aprs.recent_packets_with_paths(since: since) row = Enum.find(rows, &(&1.sender == "TEST-TZ")) assert %DateTime{} = row.received_at assert row.received_at.time_zone == "Etc/UTC" end end describe "station_positions/1" do test "returns the most recent fix per callsign and omits unknown callsigns" do base = NaiveDateTime.utc_now() :ok = insert_packet( sender: "TEST-A", base_callsign: "TEST-A", lat: 30.0, lon: -90.0, received_at: NaiveDateTime.add(base, -3600, :second) ) :ok = insert_packet( sender: "TEST-A", base_callsign: "TEST-A", lat: 31.0, lon: -91.0, received_at: NaiveDateTime.add(base, -1800, :second) ) :ok = insert_packet( sender: "TEST-A", base_callsign: "TEST-A", lat: 32.5, lon: -92.5, received_at: NaiveDateTime.add(base, -60, :second) ) :ok = insert_packet( sender: "TEST-B", base_callsign: "TEST-B", lat: 40.0, lon: -100.0, received_at: NaiveDateTime.add(base, -300, :second) ) result = Aprs.station_positions(["TEST-A", "TEST-B", "TEST-C"]) assert result |> Map.keys() |> Enum.sort() == ["TEST-A", "TEST-B"] {lat_a, lon_a, heard_a} = result["TEST-A"] assert_in_delta lat_a, 32.5, 0.0001 assert_in_delta lon_a, -92.5, 0.0001 assert %DateTime{} = heard_a assert heard_a.time_zone == "Etc/UTC" {lat_b, lon_b, _heard_b} = result["TEST-B"] assert_in_delta lat_b, 40.0, 0.0001 assert_in_delta lon_b, -100.0, 0.0001 refute Map.has_key?(result, "TEST-C") end test "empty list short-circuits to %{} without querying" do # Stop the sandbox owner so a real query would crash with NoConnectionError. Sandbox.checkin(AprsRepo) assert Aprs.station_positions([]) == %{} end end end