204 lines
7.4 KiB
Elixir
204 lines
7.4 KiB
Elixir
defmodule Aprsme.SpatialPubSubTest do
|
|
use ExUnit.Case, async: false
|
|
|
|
alias Aprsme.SpatialPubSub
|
|
|
|
describe "viewport registration" do
|
|
test "date-line-crossing viewport registers without timeout" do
|
|
bounds = %{north: 10.0, south: -10.0, east: -170.0, west: 170.0}
|
|
client_id = "test_dateline_#{:rand.uniform(100_000)}"
|
|
|
|
# Before the fix, west=170 east=-170 generated ~341 grid cells.
|
|
# Now it correctly generates ~21 cells.
|
|
assert {:ok, _topic} = SpatialPubSub.register_viewport(client_id, bounds)
|
|
|
|
SpatialPubSub.unregister_client(client_id)
|
|
end
|
|
|
|
test "normal viewport registers successfully" do
|
|
bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
|
|
client_id = "test_normal_#{:rand.uniform(100_000)}"
|
|
|
|
assert {:ok, _topic} = SpatialPubSub.register_viewport(client_id, bounds)
|
|
|
|
SpatialPubSub.unregister_client(client_id)
|
|
end
|
|
|
|
test "update_viewport succeeds after registration" do
|
|
bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
|
|
new_bounds = %{north: 42.0, south: 41.0, east: -72.0, west: -73.0}
|
|
client_id = "test_update_#{:rand.uniform(100_000)}"
|
|
|
|
{:ok, _topic} = SpatialPubSub.register_viewport(client_id, bounds)
|
|
assert :ok = SpatialPubSub.update_viewport(client_id, new_bounds)
|
|
|
|
SpatialPubSub.unregister_client(client_id)
|
|
end
|
|
|
|
test "re-registering an existing client replaces the old viewport without inflating counts" do
|
|
client_id = "test_reregister_#{:rand.uniform(100_000)}"
|
|
bounds_1 = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
|
|
bounds_2 = %{north: 36.0, south: 35.0, east: -79.0, west: -80.0}
|
|
|
|
before_stats = SpatialPubSub.get_stats()
|
|
|
|
assert {:ok, _topic} = SpatialPubSub.register_viewport(client_id, bounds_1)
|
|
mid_stats = SpatialPubSub.get_stats()
|
|
assert mid_stats.clients_count == before_stats.clients_count + 1
|
|
|
|
assert {:ok, _topic} = SpatialPubSub.register_viewport(client_id, bounds_2)
|
|
after_stats = SpatialPubSub.get_stats()
|
|
|
|
assert after_stats.clients_count == before_stats.clients_count + 1
|
|
|
|
state = :sys.get_state(SpatialPubSub)
|
|
assert state.clients[client_id].bounds == bounds_2
|
|
|
|
SpatialPubSub.unregister_client(client_id)
|
|
end
|
|
end
|
|
|
|
describe "duplicate packet prevention" do
|
|
test "does not subscribe to postgres PubSub topic" do
|
|
# SpatialPubSub used to subscribe to "postgres:aprsme_packets" causing
|
|
# duplicate broadcasts (once from PacketConsumer direct cast, once from PubSub).
|
|
# Verify it no longer subscribes.
|
|
# The SpatialPubSub process should NOT be in the subscriber list for postgres topic
|
|
spatial_pid = Process.whereis(SpatialPubSub)
|
|
|
|
subscribed? =
|
|
Aprsme.PubSub
|
|
|> Registry.lookup("postgres:aprsme_packets")
|
|
|> Enum.any?(fn {pid, _} -> pid == spatial_pid end)
|
|
|
|
refute subscribed?, "SpatialPubSub should not subscribe to postgres:aprsme_packets"
|
|
end
|
|
end
|
|
|
|
describe "stats" do
|
|
test "get_stats returns expected keys" do
|
|
stats = SpatialPubSub.get_stats()
|
|
|
|
assert is_map(stats)
|
|
assert Map.has_key?(stats, :total_packets)
|
|
assert Map.has_key?(stats, :grid_cells)
|
|
end
|
|
|
|
test "grid cells increase after registering viewport" do
|
|
bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
|
|
client_id = "test_stats_#{:rand.uniform(100_000)}"
|
|
|
|
before_stats = SpatialPubSub.get_stats()
|
|
{:ok, _topic} = SpatialPubSub.register_viewport(client_id, bounds)
|
|
after_stats = SpatialPubSub.get_stats()
|
|
|
|
assert after_stats.grid_cells >= before_stats.grid_cells
|
|
|
|
SpatialPubSub.unregister_client(client_id)
|
|
end
|
|
end
|
|
|
|
describe "broadcast_packet" do
|
|
test "broadcasts to clients whose viewport contains the packet location" do
|
|
client_id = "test_bc_#{:rand.uniform(100_000)}"
|
|
bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
|
|
{:ok, topic} = SpatialPubSub.register_viewport(client_id, bounds)
|
|
|
|
Phoenix.PubSub.subscribe(Aprsme.PubSub, topic)
|
|
|
|
# Broadcast a packet inside the viewport.
|
|
assert :ok = SpatialPubSub.broadcast_packet(%{lat: 40.5, lon: -73.5, sender: "IN1"})
|
|
|
|
assert_receive {:spatial_packet, %{sender: "IN1"}}, 1_000
|
|
|
|
SpatialPubSub.unregister_client(client_id)
|
|
end
|
|
|
|
test "does not broadcast to clients outside the viewport" do
|
|
client_id = "test_outside_#{:rand.uniform(100_000)}"
|
|
bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
|
|
{:ok, topic} = SpatialPubSub.register_viewport(client_id, bounds)
|
|
|
|
Phoenix.PubSub.subscribe(Aprsme.PubSub, topic)
|
|
|
|
# Broadcast far outside bounds.
|
|
assert :ok = SpatialPubSub.broadcast_packet(%{lat: 0.0, lon: 0.0, sender: "OUT1"})
|
|
refute_receive {:spatial_packet, _}, 100
|
|
|
|
SpatialPubSub.unregister_client(client_id)
|
|
end
|
|
|
|
test "ignores packets with missing or invalid location" do
|
|
# Packet with nil coords should not crash.
|
|
assert :ok = SpatialPubSub.broadcast_packet(%{lat: nil, lon: nil})
|
|
assert :ok = SpatialPubSub.broadcast_packet(%{lat: "bad", lon: "bad"})
|
|
assert :ok = SpatialPubSub.broadcast_packet(%{})
|
|
end
|
|
|
|
test "binary-string coordinates in bounds are parsed" do
|
|
client_id = "test_strbounds_#{:rand.uniform(100_000)}"
|
|
bounds = %{north: "41.0", south: "40.0", east: "-73.0", west: "-74.0"}
|
|
assert {:ok, _topic} = SpatialPubSub.register_viewport(client_id, bounds)
|
|
SpatialPubSub.unregister_client(client_id)
|
|
end
|
|
end
|
|
|
|
describe "client lifecycle" do
|
|
test "unregister removes client from spatial index" do
|
|
client_id = "test_unreg_#{:rand.uniform(100_000)}"
|
|
bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
|
|
{:ok, _topic} = SpatialPubSub.register_viewport(client_id, bounds)
|
|
|
|
before_stats = SpatialPubSub.get_stats()
|
|
SpatialPubSub.unregister_client(client_id)
|
|
# Give cast time to process
|
|
_ = SpatialPubSub.get_stats()
|
|
after_stats = SpatialPubSub.get_stats()
|
|
|
|
assert after_stats.clients_count <= before_stats.clients_count
|
|
end
|
|
|
|
test "update_viewport returns error for unregistered client" do
|
|
bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
|
|
result = SpatialPubSub.update_viewport("never_registered_#{:rand.uniform(100_000)}", bounds)
|
|
assert result == {:error, :not_registered}
|
|
end
|
|
end
|
|
|
|
describe "when the server is not running" do
|
|
setup do
|
|
pid = Process.whereis(SpatialPubSub)
|
|
|
|
if pid do
|
|
GenServer.stop(pid)
|
|
end
|
|
|
|
on_exit(fn ->
|
|
if !Process.whereis(SpatialPubSub) do
|
|
start_supervised!({SpatialPubSub, []})
|
|
end
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
test "public APIs return safe defaults instead of crashing" do
|
|
bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
|
|
|
|
assert {:error, :not_running} = SpatialPubSub.register_viewport("missing", bounds)
|
|
assert {:error, :not_running} = SpatialPubSub.update_viewport("missing", bounds)
|
|
assert :ok = SpatialPubSub.unregister_client("missing")
|
|
assert :ok = SpatialPubSub.broadcast_packet(%{lat: 40.5, lon: -73.5})
|
|
assert :ok = SpatialPubSub.start_telemetry_reporting()
|
|
|
|
assert SpatialPubSub.get_stats() == %{
|
|
total_broadcasts: 0,
|
|
filtered_broadcasts: 0,
|
|
total_packets: 0,
|
|
clients_count: 0,
|
|
grid_cells: 0,
|
|
avg_clients_per_cell: 0.0
|
|
}
|
|
end
|
|
end
|
|
end
|