From 6b624e136575909cb7634a018144dbd4d04ed7c2 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 9 Feb 2026 17:21:16 -0600 Subject: [PATCH] more tests --- config/test.exs | 3 + lib/aprsme/application.ex | 8 +++ lib/aprsme/cluster/connection_manager.ex | 11 +++- lib/aprsme/log_sanitizer.ex | 10 +++ lib/aprsme/packet_pipeline_setup.ex | 25 ++++++-- .../cluster/connection_manager_test.exs | 63 ++++++++----------- .../live/map_live/movement_test.exs | 2 +- test/support/mock_helpers.ex | 16 ----- 8 files changed, 77 insertions(+), 61 deletions(-) diff --git a/config/test.exs b/config/test.exs index 773ee04..310d104 100644 --- a/config/test.exs +++ b/config/test.exs @@ -43,6 +43,9 @@ config :aprsme, AprsmeWeb.Telemetry, enabled: false # Disable cleanup scheduler in test environment config :aprsme, :cleanup_scheduler, enabled: false +# Speed up ConnectionManager init in test environment +config :aprsme, :connection_manager_init_delay, 0 + # Disable initialize replay delay in test environment config :aprsme, :initialize_replay_delay, 0 diff --git a/lib/aprsme/application.ex b/lib/aprsme/application.ex index e7820e2..2ba9b14 100644 --- a/lib/aprsme/application.ex +++ b/lib/aprsme/application.ex @@ -53,6 +53,14 @@ defmodule Aprsme.Application do Aprsme.PacketPipelineSupervisor ] + # Skip packet pipeline in test to avoid Sandbox ownership errors + children = + if Application.get_env(:aprsme, :env) == :test do + List.delete(children, Aprsme.PacketPipelineSupervisor) + else + children + end + children = children ++ redis_children() # Add shutdown handlers at the end, after everything else is started diff --git a/lib/aprsme/cluster/connection_manager.ex b/lib/aprsme/cluster/connection_manager.ex index a3fcb20..ba1b568 100644 --- a/lib/aprsme/cluster/connection_manager.ex +++ b/lib/aprsme/cluster/connection_manager.ex @@ -20,14 +20,15 @@ defmodule Aprsme.Cluster.ConnectionManager do Phoenix.PubSub.subscribe(Aprsme.PubSub, "cluster:leadership") # Check initial leadership state - Process.send_after(self(), :check_initial_state, 1000) + delay = Application.get_env(:aprsme, :connection_manager_init_delay, 1000) + Process.send_after(self(), :check_initial_state, delay) {:ok, %{connection_started: false}} end @impl true def handle_info(:check_initial_state, state) do - if LeaderElection.leader?() do + if leader_check() do Logger.info("This node is the leader, starting APRS-IS connection") start_aprs_connection() {:noreply, %{state | connection_started: true}} @@ -55,6 +56,12 @@ defmodule Aprsme.Cluster.ConnectionManager do end end + defp leader_check do + LeaderElection.leader?() + catch + :exit, _ -> false + end + defp start_aprs_connection do # Start the APRS-IS connection supervisor if not already started case DynamicSupervisor.start_child( diff --git a/lib/aprsme/log_sanitizer.ex b/lib/aprsme/log_sanitizer.ex index 113b78a..e60e15b 100644 --- a/lib/aprsme/log_sanitizer.ex +++ b/lib/aprsme/log_sanitizer.ex @@ -65,6 +65,16 @@ defmodule Aprsme.LogSanitizer do @doc """ Sanitize any data structure recursively """ + def sanitize(%{__struct__: _} = data) when is_exception(data) do + sanitize_string(Exception.message(data)) + end + + def sanitize(%{__struct__: _} = data) do + data + |> Map.from_struct() + |> sanitize() + end + def sanitize(data) when is_map(data) do data |> sanitize_map() diff --git a/lib/aprsme/packet_pipeline_setup.ex b/lib/aprsme/packet_pipeline_setup.ex index 10d6d5c..fed379f 100644 --- a/lib/aprsme/packet_pipeline_setup.ex +++ b/lib/aprsme/packet_pipeline_setup.ex @@ -21,14 +21,27 @@ defmodule Aprsme.PacketPipelineSetup do config = Application.get_env(:aprsme, :packet_pipeline, []) max_demand = config[:max_demand] || 50 - case GenStage.sync_subscribe(Aprsme.PacketConsumer, to: Aprsme.PacketProducer, max_demand: max_demand) do - {:ok, _subscription} -> - require Logger + try do + case GenStage.sync_subscribe(Aprsme.PacketConsumer, + to: Aprsme.PacketProducer, + max_demand: max_demand + ) do + {:ok, _subscription} -> + require Logger - Logger.info("GenStage packet pipeline subscription established") - {:noreply, state} + Logger.info("GenStage packet pipeline subscription established") + {:noreply, state} - {:error, reason} -> + {:error, reason} -> + require Logger + + Logger.error("Failed to establish GenStage subscription: #{inspect(reason)}") + # Retry after a delay + Process.send_after(self(), :setup_subscription, 1000) + {:noreply, state} + end + catch + :exit, reason -> require Logger Logger.error("Failed to establish GenStage subscription: #{inspect(reason)}") diff --git a/test/aprsme/cluster/connection_manager_test.exs b/test/aprsme/cluster/connection_manager_test.exs index cf946d9..7f2406b 100644 --- a/test/aprsme/cluster/connection_manager_test.exs +++ b/test/aprsme/cluster/connection_manager_test.exs @@ -79,7 +79,7 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do # Start a minimal LeaderElection to prevent crashes Application.put_env(:aprsme, :cluster_enabled, false) {:ok, _} = LeaderElection.start_link([]) - Process.sleep(200) + Process.sleep(150) :ok end @@ -94,7 +94,7 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do ) # Give time for message processing - Process.sleep(100) + Process.sleep(50) # Process should still be alive after receiving message assert Process.whereis(ConnectionManager) @@ -103,8 +103,8 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do test "schedules initial state check" do {:ok, pid} = ConnectionManager.start_link([]) - # Should receive :check_initial_state message after init - Process.sleep(1100) + # With delay set to 0 in test config, the check fires immediately + Process.sleep(50) # Process should still be alive assert Process.alive?(pid) @@ -116,8 +116,8 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do # Start LeaderElection in non-clustered mode so it becomes leader Application.put_env(:aprsme, :cluster_enabled, false) {:ok, _} = LeaderElection.start_link([]) - # Wait for election - Process.sleep(200) + # Wait for election (100ms timer + processing) + Process.sleep(150) # Start ConnectionManager {:ok, pid} = ConnectionManager.start_link([]) @@ -127,20 +127,16 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do test "handles initial state check when leader", %{pid: pid} do # Send initial state check send(pid, :check_initial_state) - Process.sleep(100) + Process.sleep(50) # Should handle without crashing assert Process.alive?(pid) end test "handles initial state check when not leader", %{pid: pid} do - # ConnectionManager will check with LeaderElection - # In test environment, it will get that it's the leader - # This is OK - we're just testing that it handles the check without crashing - # Send initial state check send(pid, :check_initial_state) - Process.sleep(100) + Process.sleep(50) # Should handle without crashing assert Process.alive?(pid) @@ -152,18 +148,18 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do # Ensure LeaderElection is started Application.put_env(:aprsme, :cluster_enabled, false) {:ok, _} = LeaderElection.start_link([]) - Process.sleep(200) + Process.sleep(150) {:ok, pid} = ConnectionManager.start_link([]) - # Wait for initial check - Process.sleep(1100) + # With delay=0, initial check fires immediately; just wait for processing + Process.sleep(50) {:ok, pid: pid} end test "starts connection when becoming leader", %{pid: pid} do # Send leadership change - this node became leader send(pid, {:leadership_change, node(), true}) - Process.sleep(100) + Process.sleep(50) # Should handle the message without crashing assert Process.alive?(pid) @@ -172,11 +168,11 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do test "stops connection when losing leadership", %{pid: pid} do # First become leader and start connection send(pid, {:leadership_change, node(), true}) - Process.sleep(100) + Process.sleep(50) # Then lose leadership send(pid, {:leadership_change, node(), false}) - Process.sleep(100) + Process.sleep(50) # Should handle the message without crashing assert Process.alive?(pid) @@ -187,7 +183,7 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do # Send leadership change for another node send(pid, {:leadership_change, other_node, true}) - Process.sleep(100) + Process.sleep(50) # Should not affect this node assert Process.alive?(pid) @@ -196,10 +192,10 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do test "does not start connection twice", %{pid: pid} do # Become leader twice send(pid, {:leadership_change, node(), true}) - Process.sleep(100) + Process.sleep(50) send(pid, {:leadership_change, node(), true}) - Process.sleep(100) + Process.sleep(50) # Should handle duplicate leadership without issues assert Process.alive?(pid) @@ -211,20 +207,18 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do # Ensure LeaderElection is started Application.put_env(:aprsme, :cluster_enabled, false) {:ok, _} = LeaderElection.start_link([]) - Process.sleep(200) + Process.sleep(150) {:ok, pid} = ConnectionManager.start_link([]) - Process.sleep(1100) + # With delay=0, initial check fires immediately; just wait for processing + Process.sleep(50) {:ok, pid: pid} end test "handles IsSupervisor already started error", %{pid: pid} do - # The APRS-IS connection is disabled in test environment, - # but we can still test that the ConnectionManager handles errors gracefully - # Try to become leader send(pid, {:leadership_change, node(), true}) - Process.sleep(100) + Process.sleep(50) # Should handle gracefully even if IsSupervisor fails to start assert Process.alive?(pid) @@ -239,12 +233,9 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do end end - # Temporarily replace the module reference - # This test is more conceptual as we can't easily mock module references - # Send leadership change send(pid, {:leadership_change, node(), true}) - Process.sleep(100) + Process.sleep(50) # Should handle failure gracefully assert Process.alive?(pid) @@ -256,26 +247,26 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do # Ensure LeaderElection is started Application.put_env(:aprsme, :cluster_enabled, false) {:ok, _} = LeaderElection.start_link([]) - Process.sleep(200) + Process.sleep(150) {:ok, pid} = ConnectionManager.start_link([]) {:ok, pid: pid} end test "full lifecycle - become leader, lose leadership", %{pid: pid} do - # Wait for initial check - Process.sleep(1100) + # With delay=0, initial check fires immediately; just wait for processing + Process.sleep(50) # Become leader send(pid, {:leadership_change, node(), true}) - Process.sleep(100) + Process.sleep(50) # Verify connection started (check state indirectly) assert Process.alive?(pid) # Lose leadership send(pid, {:leadership_change, node(), false}) - Process.sleep(100) + Process.sleep(50) # Verify still alive after stopping connection assert Process.alive?(pid) diff --git a/test/aprsme_web/live/map_live/movement_test.exs b/test/aprsme_web/live/map_live/movement_test.exs index 348441c..afab18f 100644 --- a/test/aprsme_web/live/map_live/movement_test.exs +++ b/test/aprsme_web/live/map_live/movement_test.exs @@ -72,7 +72,7 @@ defmodule AprsmeWeb.MapLive.MovementTest do # The view should not push a new_packet event for GPS drift # Using a longer timeout to avoid flaky tests on slower systems - refute_push_event(view, "new_packet", %{id: "TEST-1"}, 2000) + refute_push_event(view, "new_packet", %{id: "TEST-1"}, 200) end test "updates marker for significant movement", %{conn: conn} do diff --git a/test/support/mock_helpers.ex b/test/support/mock_helpers.ex index 58b3363..219f178 100644 --- a/test/support/mock_helpers.ex +++ b/test/support/mock_helpers.ex @@ -5,18 +5,6 @@ defmodule Aprsme.MockHelpers do def stub_packets_mock do # Stub the packets module to prevent external calls - Mox.stub(Aprsme.PacketsMock, :get_packets_for_callsign, fn _callsign -> - {:ok, []} - end) - - Mox.stub(Aprsme.PacketsMock, :get_packets_for_callsign_with_limit, fn _callsign, _limit -> - {:ok, []} - end) - - Mox.stub(Aprsme.PacketsMock, :get_packets_for_callsign_with_date_range, fn _callsign, _start_date, _end_date -> - {:ok, []} - end) - Mox.stub(Aprsme.PacketsMock, :get_recent_packets, fn _opts -> [] end) @@ -25,8 +13,4 @@ defmodule Aprsme.MockHelpers do [] end) end - - def stub_badpackets_mock do - Mox.stub_with(BadPacketsMock, BadPacketsStub) - end end