From 6e8f1e434f0369dfaa77c5dde917368cc9304fb3 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 16 Jun 2025 12:45:35 -0500 Subject: [PATCH] dont connect to is server in test --- config/test.exs | 9 + lib/aprs/is/is.ex | 117 +++++----- test/aprs/connection_prevention_test.exs | 53 +++++ test/aprs/is_test.exs | 148 +++++++++++++ .../aprs_web/integration/aprs_status_test.exs | 203 ++++++++++++++++++ test/support/README.md | 158 ++++++++++++++ test/support/aprs_is_mock.ex | 169 +++++++++++++++ test/test_helper.exs | 11 + 8 files changed, 816 insertions(+), 52 deletions(-) create mode 100644 test/aprs/connection_prevention_test.exs create mode 100644 test/aprs/is_test.exs create mode 100644 test/aprs_web/integration/aprs_status_test.exs create mode 100644 test/support/README.md create mode 100644 test/support/aprs_is_mock.ex diff --git a/config/test.exs b/config/test.exs index 8f153b3..fd36825 100644 --- a/config/test.exs +++ b/config/test.exs @@ -24,6 +24,15 @@ config :aprs, AprsWeb.Endpoint, secret_key_base: "IV9+ENaw9i8xjReRk4sULRvRgsmFVTGQwQGGrf4G+Q/SFMeHBCNWRlPXQ2YvT36R", server: false +# Disable APRS-IS external connections in test environment +config :aprs, + aprs_is_server: nil, + aprs_is_port: nil, + aprs_is_default_filter: nil, + aprs_is_login_id: nil, + aprs_is_password: nil, + disable_aprs_connection: true + # Disable automatic migrations during tests config :aprs, auto_migrate: false diff --git a/lib/aprs/is/is.ex b/lib/aprs/is/is.ex index 976752c..3d7d346 100644 --- a/lib/aprs/is/is.ex +++ b/lib/aprs/is/is.ex @@ -13,55 +13,62 @@ defmodule Aprs.Is do @impl true def init(_opts) do - # Trap exits so we can gracefully shut down - Process.flag(:trap_exit, true) - - # Add a small delay to prevent rapid reconnection attempts - Process.sleep(2000) - - # Get startup parameters - server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net") - port = Application.get_env(:aprs, :aprs_is_port, 14_580) - default_filter = Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100") - aprs_user_id = Application.get_env(:aprs, :aprs_is_login_id, "W5ISP") - aprs_passcode = Application.get_env(:aprs, :aprs_is_password, "-1") - - # Record connection start time - connected_at = DateTime.utc_now() - - # Initialize packet statistics - packet_stats = %{ - total_packets: 0, - last_packet_at: nil, - packets_per_second: 0, - last_second_count: 0, - last_second_timestamp: System.system_time(:second) - } - - with {:ok, socket} <- connect_to_aprs_is(server, port), - :ok <- send_login_string(socket, aprs_user_id, aprs_passcode, default_filter) do - timer = create_timer(@aprs_timeout) - keepalive_timer = create_keepalive_timer(@keepalive_interval) - - {:ok, - %{ - server: server, - port: port, - socket: socket, - timer: timer, - keepalive_timer: keepalive_timer, - connected_at: connected_at, - packet_stats: packet_stats, - login_params: %{ - user_id: aprs_user_id, - passcode: aprs_passcode, - filter: default_filter - } - }} + # Prevent APRS-IS connections in test environment + if Application.get_env(:aprs, :env) == :test or + Application.get_env(:aprs, :disable_aprs_connection, false) do + Logger.warning("APRS-IS connection disabled in test environment") + {:stop, :test_environment_disabled} else - _ -> - Logger.error("Unable to establish connection or log in to APRS-IS") - {:stop, :aprs_connection_failed} + # Trap exits so we can gracefully shut down + Process.flag(:trap_exit, true) + + # Add a small delay to prevent rapid reconnection attempts + Process.sleep(2000) + + # Get startup parameters + server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net") + port = Application.get_env(:aprs, :aprs_is_port, 14_580) + default_filter = Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100") + aprs_user_id = Application.get_env(:aprs, :aprs_is_login_id, "W5ISP") + aprs_passcode = Application.get_env(:aprs, :aprs_is_password, "-1") + + # Record connection start time + connected_at = DateTime.utc_now() + + # Initialize packet statistics + packet_stats = %{ + total_packets: 0, + last_packet_at: nil, + packets_per_second: 0, + last_second_count: 0, + last_second_timestamp: System.system_time(:second) + } + + with {:ok, socket} <- connect_to_aprs_is(server, port), + :ok <- send_login_string(socket, aprs_user_id, aprs_passcode, default_filter) do + timer = create_timer(@aprs_timeout) + keepalive_timer = create_keepalive_timer(@keepalive_interval) + + {:ok, + %{ + server: server, + port: port, + socket: socket, + timer: timer, + keepalive_timer: keepalive_timer, + connected_at: connected_at, + packet_stats: packet_stats, + login_params: %{ + user_id: aprs_user_id, + passcode: aprs_passcode, + filter: default_filter + } + }} + else + _ -> + Logger.error("Unable to establish connection or log in to APRS-IS") + {:stop, :aprs_connection_failed} + end end end @@ -130,9 +137,16 @@ defmodule Aprs.Is do # Server methods defp connect_to_aprs_is(server, port) do - Logger.debug("Connecting to: #{server}:#{port}") - opts = [:binary, active: true] - :gen_tcp.connect(String.to_charlist(server), port, opts) + # Additional safeguard: prevent connections in test environment + if Application.get_env(:aprs, :env) == :test or + Application.get_env(:aprs, :disable_aprs_connection, false) do + Logger.warning("Attempted APRS-IS connection blocked in test environment") + {:error, :test_environment_blocked} + else + Logger.debug("Connecting to: #{server}:#{port}") + opts = [:binary, active: true] + :gen_tcp.connect(String.to_charlist(server), port, opts) + end end defp send_login_string(socket, aprs_user_id, aprs_passcode, filter) do @@ -271,7 +285,6 @@ defmodule Aprs.Is do try do # Store the packet if it has position data if has_position_data?(parsed_message) do - Logger.info("Storing packet with position data: #{inspect(parsed_message.sender)}") # Always set received_at timestamp to ensure consistency current_time = DateTime.truncate(DateTime.utc_now(), :microsecond) packet_data = Map.put(parsed_message, :received_at, current_time) diff --git a/test/aprs/connection_prevention_test.exs b/test/aprs/connection_prevention_test.exs new file mode 100644 index 0000000..739fc7f --- /dev/null +++ b/test/aprs/connection_prevention_test.exs @@ -0,0 +1,53 @@ +defmodule Aprs.ConnectionPreventionTest do + @moduledoc """ + Simple test to verify APRS connections are prevented in test environment. + """ + use ExUnit.Case, async: true + + test "APRS connection is disabled in test environment" do + # Verify we're in test environment + assert Mix.env() == :test + assert Application.get_env(:aprs, :env) == :test + + # Verify APRS connection is explicitly disabled + assert Application.get_env(:aprs, :disable_aprs_connection) == true + + # Verify APRS.Is process is not running + assert Process.whereis(Aprs.Is) == nil + end + + test "APRS configuration is safe for testing" do + # Verify server configuration is neutralized + server = Application.get_env(:aprs, :aprs_is_server) + assert server == nil or server == "mock.aprs.test" + + # Verify test credentials are used + login_id = Application.get_env(:aprs, :aprs_is_login_id) + assert login_id == "TEST" + + # Verify no real APRS servers in config + forbidden_servers = [ + "rotate.aprs2.net", + "dallas.aprs2.net", + "seattle.aprs2.net" + ] + + if server do + server_str = to_string(server) + + refute Enum.any?(forbidden_servers, fn forbidden -> + String.contains?(server_str, forbidden) + end) + end + end + + test "get_status works without external connection" do + # This should not attempt external connections + status = Aprs.Is.get_status() + + # Should return disconnected state + assert status.connected == false + assert status.uptime_seconds == 0 + assert status.connected_at == nil + end +end diff --git a/test/aprs/is_test.exs b/test/aprs/is_test.exs new file mode 100644 index 0000000..b0adfea --- /dev/null +++ b/test/aprs/is_test.exs @@ -0,0 +1,148 @@ +defmodule Aprs.IsTest do + @moduledoc """ + Tests to ensure APRS-IS external connections are properly blocked in test environment. + """ + use ExUnit.Case, async: true + + require Logger + + describe "APRS-IS connection prevention in test environment" do + test "APRS.Is module should not start in test environment" do + # Verify that the APRS.Is GenServer is not running + assert Process.whereis(Aprs.Is) == nil + end + + test "attempting to start APRS.Is should fail with test environment disabled" do + # Try to start the APRS.Is GenServer manually + result = Aprs.Is.start_link([]) + + # Should fail because we're in test environment + assert {:error, reason} = result + assert reason in [:test_environment_disabled, :normal] + end + + test "get_status should return disconnected state without external connection" do + status = Aprs.Is.get_status() + + # Should return disconnected status without attempting external connection + assert status.connected == false + assert status.server != nil + assert status.port != nil + assert status.connected_at == nil + assert status.uptime_seconds == 0 + end + + test "configuration should disable APRS connections in test" do + # Verify test configuration properly disables connections + assert Application.get_env(:aprs, :disable_aprs_connection) == true + assert Application.get_env(:aprs, :env) == :test + end + + test "APRS server configuration should be nil or test values in test environment" do + # Verify APRS server config is neutralized for tests + server = Application.get_env(:aprs, :aprs_is_server) + login_id = Application.get_env(:aprs, :aprs_is_login_id) + + # Server should be nil (disabled) or a test value + assert server == nil or server == "mock.aprs.test" + + # Login should be test value + assert login_id == "TEST" + end + end + + describe "APRS-IS mock functionality" do + setup do + # Start the mock if not already running + case GenServer.start_link(AprsIsMock, [], name: AprsIsMock) do + {:ok, pid} -> + on_exit(fn -> GenServer.stop(pid, :normal) end) + {:ok, mock_pid: pid} + + {:error, {:already_started, pid}} -> + {:ok, mock_pid: pid} + end + end + + test "mock should provide status without external connections", %{mock_pid: _pid} do + status = AprsIsMock.get_status() + + assert status.connected == false + assert status.server == "mock.aprs.test" + assert status.port == 14_580 + assert status.login_id == "TEST" + assert is_map(status.packet_stats) + end + + test "mock should handle message sending safely", %{mock_pid: _pid} do + # These should not attempt external connections + assert AprsIsMock.send_message("test message") == :ok + assert AprsIsMock.send_message("TEST", "DEST", "hello") == :ok + assert AprsIsMock.set_filter("r/0/0/1") == :ok + assert AprsIsMock.list_active_filters() == :ok + end + + test "mock can simulate packet reception for testing", %{mock_pid: _pid} do + # This allows tests to simulate receiving packets without external connections + test_packet = %{ + sender: "TEST-1", + destination: "APRS", + path: ["WIDE1-1", "WIDE2-1"], + data_type: :position, + latitude: 33.0, + longitude: -96.0 + } + + assert AprsIsMock.simulate_packet(test_packet) == :ok + end + + test "mock can simulate connection state changes", %{mock_pid: _pid} do + # Simulate connected state + assert AprsIsMock.simulate_connection_state(true) == :ok + status = AprsIsMock.get_status() + assert status.connected == true + assert status.connected_at != nil + + # Simulate disconnected state + assert AprsIsMock.simulate_connection_state(false) == :ok + status = AprsIsMock.get_status() + assert status.connected == false + assert status.connected_at == nil + end + end + + describe "network isolation verification" do + test "no external network calls should be made during test runs" do + # This test verifies that no actual TCP connections are attempted + # We can do this by checking that :gen_tcp.connect is not called + # with real APRS server addresses + + # Common APRS-IS servers that should never be contacted in tests + forbidden_servers = [ + "rotate.aprs2.net", + "dallas.aprs2.net", + "seattle.aprs2.net", + "chicago.aprs2.net", + "atlanta.aprs2.net" + ] + + # Verify these are not in the current configuration + current_server = Application.get_env(:aprs, :aprs_is_server) + + if current_server do + server_str = to_string(current_server) + + refute Enum.any?(forbidden_servers, fn forbidden -> + String.contains?(server_str, forbidden) + end), + "Test environment should not be configured with real APRS servers" + end + end + + test "test environment marker is properly set" do + # Verify we're definitely in test environment + assert Mix.env() == :test + assert Application.get_env(:aprs, :env) == :test + end + end +end diff --git a/test/aprs_web/integration/aprs_status_test.exs b/test/aprs_web/integration/aprs_status_test.exs new file mode 100644 index 0000000..3377098 --- /dev/null +++ b/test/aprs_web/integration/aprs_status_test.exs @@ -0,0 +1,203 @@ +defmodule AprsWeb.Integration.AprsStatusTest do + @moduledoc """ + Integration tests to verify that web interfaces work properly + without external APRS connections in the test environment. + """ + use AprsWeb.ConnCase + + import Phoenix.LiveViewTest + + describe "APRS status endpoints without external connections" do + test "status JSON endpoint returns proper response without APRS connection", %{conn: conn} do + conn = get(conn, "/api/status") + + assert response = json_response(conn, 200) + + # Should have basic structure even without APRS connection + assert Map.has_key?(response, "aprs_status") + assert Map.has_key?(response, "version") + assert Map.has_key?(response, "uptime") + + # APRS status should indicate disconnected state + aprs_status = response["aprs_status"] + assert aprs_status["connected"] == false + assert is_binary(aprs_status["server"]) + assert is_integer(aprs_status["port"]) + assert aprs_status["uptime_seconds"] == 0 + + # Should not contain real APRS server addresses + forbidden_servers = [ + "rotate.aprs2.net", + "dallas.aprs2.net", + "seattle.aprs2.net" + ] + + refute Enum.any?(forbidden_servers, fn server -> + String.contains?(aprs_status["server"], server) + end) + end + + test "main map page loads without APRS connection", %{conn: conn} do + {:ok, view, html} = live(conn, "/") + + # Page should load successfully + assert html =~ "APRS Map" + assert has_element?(view, "#aprs-map") + + # Should not show connected status + refute html =~ "Connected to APRS" + + # Should handle disconnected state gracefully + # Basic content should be present + assert html =~ "APRS" + end + + test "status live view works without APRS connection", %{conn: conn} do + # Try to access status page if it exists + case live(conn, "/status") do + {:ok, view, html} -> + # If status page exists, verify it handles disconnected state + assert html =~ "Status" + + # Should show disconnected state information + assert has_element?(view, "[data-testid='connection-status']") || + html =~ "disconnected" || + html =~ "not connected" + + {:error, {:live_redirect, %{to: "/"}}} -> + # If redirected to home, that's acceptable + :ok + + {:error, {:redirect, %{to: "/"}}} -> + # If redirected to home, that's acceptable + :ok + end + end + + test "packet data endpoints handle no external connection gracefully", %{conn: conn} do + # Test any packet-related endpoints + case get(conn, "/api/packets") do + %{status: 200} = conn -> + response = json_response(conn, 200) + # Should return empty or default data structure + assert is_list(response) or is_map(response) + + %{status: 404} -> + # Endpoint might not exist, which is fine + :ok + + %{status: status} when status in [500, 503] -> + # Should not crash with server errors + flunk("Packet endpoint crashed without APRS connection") + end + end + end + + describe "LiveView event handling without APRS" do + test "map events work without APRS connection", %{conn: conn} do + {:ok, view, _html} = live(conn, "/") + + # Test map bounds change event + bounds_params = %{ + "bounds" => %{ + "north" => "33.1", + "south" => "32.9", + "east" => "-95.9", + "west" => "-96.1" + } + } + + # Should not crash when handling events without APRS connection + assert render_hook(view, "bounds_changed", bounds_params) + + # Test map ready event + assert render_hook(view, "map_ready", %{}) + + # Test replay speed adjustment if available + speed_params = %{"speed" => "1.0"} + render_hook(view, "adjust_replay_speed", speed_params) + end + + test "real-time updates are disabled without APRS connection", %{conn: conn} do + {:ok, view, _html} = live(conn, "/") + + # Verify that the view is not subscribed to real-time APRS updates + # since there's no APRS connection + + # Send a test broadcast that would normally come from APRS.Is + test_packet = %{ + sender: "TEST-1", + latitude: 33.0, + longitude: -96.0, + timestamp: DateTime.utc_now() + } + + # This should not cause any updates since APRS.Is is not running + AprsWeb.Endpoint.broadcast("aprs_messages", "packet", test_packet) + + # Give a moment for any potential updates + Process.sleep(100) + + # View should remain stable (no crashes from missing APRS connection) + assert render(view) =~ "APRS Map" + end + end + + describe "error handling without external dependencies" do + test "application handles missing APRS connection gracefully", %{conn: conn} do + # Verify various endpoints don't crash when APRS.Is is not available + + # Home page + assert {:ok, _view, html} = live(conn, "/") + assert html =~ "APRS" + + # API status + conn = get(conn, "/api/status") + assert json_response(conn, 200) + + # Any authentication pages should still work + conn = build_conn() + conn = get(conn, "/users/register") + # Redirect or not found is acceptable + assert html_response(conn, 200) =~ "Register" or + conn.status in [302, 404] + end + + test "database operations work independently of APRS connection", %{conn: conn} do + # Verify that core application functionality works without APRS + + # Database should be accessible + assert Ecto.Adapters.SQL.query!(Aprs.Repo, "SELECT 1", []) + + # Web interface should load + {:ok, _view, html} = live(conn, "/") + assert html =~ "APRS" + + # This confirms the app can function without external APRS data + end + end + + describe "test environment verification" do + test "confirms we're in test environment" do + # Double-check we're actually in test mode + assert Mix.env() == :test + assert Application.get_env(:aprs, :env) == :test + assert Application.get_env(:aprs, :disable_aprs_connection) == true + end + + test "APRS.Is process is not running" do + # Verify the real APRS.Is GenServer is not started + assert Process.whereis(Aprs.Is) == nil + end + + test "no external network configuration in test" do + # Verify test config doesn't point to real servers + server = Application.get_env(:aprs, :aprs_is_server) + + # Should be nil or a test value + assert server == nil or + (is_binary(server) and String.contains?(server, "test")) or + (is_binary(server) and String.contains?(server, "mock")) + end + end +end diff --git a/test/support/README.md b/test/support/README.md new file mode 100644 index 0000000..14eeefa --- /dev/null +++ b/test/support/README.md @@ -0,0 +1,158 @@ +# Test Environment APRS Isolation + +This directory contains support files to ensure that the APRS.me application does not make external network connections during testing. + +## Overview + +The APRS.me application normally connects to external APRS-IS (Automatic Packet Reporting System - Internet Service) servers to receive real-time amateur radio packet data. During testing, we need to prevent these external connections to ensure: + +1. **Test Isolation**: Tests run independently without external dependencies +2. **Network Security**: No unintended external connections during CI/CD +3. **Performance**: Tests run faster without network delays +4. **Reliability**: Tests don't fail due to external service availability + +## Implementation + +### Configuration-Based Prevention + +The primary mechanism is environment-based configuration in `config/test.exs`: + +```elixir +# Disable APRS-IS external connections in test environment +config :aprs, + aprs_is_server: nil, + aprs_is_port: nil, + aprs_is_default_filter: nil, + aprs_is_login_id: nil, + aprs_is_password: nil, + disable_aprs_connection: true +``` + +### Application-Level Guards + +The `Aprs.Application` module only starts the APRS-IS supervisor in `:prod` and `:dev` environments: + +```elixir +children = + if Application.get_env(:aprs, :env) in [:prod, :dev] do + children ++ [Aprs.Is.IsSupervisor] + else + children + end +``` + +### Module-Level Safeguards + +The `Aprs.Is` module includes additional safeguards to prevent accidental connections: + +- Early termination if started in test environment +- Connection blocking in the `connect_to_aprs_is/2` function +- Safe fallback responses for status queries + +### Mock Implementation + +The `AprsIsMock` module (`aprs_is_mock.ex`) provides a test-safe implementation that: + +- Mimics the interface of the real `Aprs.Is` module +- Returns realistic status information without external connections +- Allows simulation of packet reception for testing +- Provides connection state simulation for comprehensive testing + +## Usage in Tests + +### Basic Setup + +The mock is automatically configured in `test_helper.exs`: + +```elixir +# Ensure no external APRS connections during tests +Application.put_env(:aprs, :disable_aprs_connection, true) +Code.require_file("support/aprs_is_mock.ex", __DIR__) +``` + +### Using the Mock + +```elixir +# Start the mock in your test setup +{:ok, _pid} = AprsIsMock.start_link() + +# Get status (no external connection) +status = AprsIsMock.get_status() + +# Simulate packet reception +test_packet = %{sender: "TEST-1", latitude: 33.0, longitude: -96.0} +AprsIsMock.simulate_packet(test_packet) + +# Simulate connection state changes +AprsIsMock.simulate_connection_state(true) # connected +AprsIsMock.simulate_connection_state(false) # disconnected +``` + +### Testing Network Isolation + +The `Aprs.IsTest` module includes comprehensive tests to verify: + +- APRS.Is module doesn't start in test environment +- Configuration properly disables connections +- No real APRS servers are contacted +- Mock provides expected functionality + +## Verification + +To verify that external connections are properly blocked: + +1. **Run the test suite**: `mix test test/aprs/is_test.exs` +2. **Check configuration**: Ensure test config sets `disable_aprs_connection: true` +3. **Monitor network**: During test runs, no connections should be made to APRS-IS servers +4. **Review logs**: Test environment should log connection prevention messages + +## Security Notes + +- Never commit real APRS credentials to test configurations +- Use placeholder values like "TEST" for callsigns in tests +- Ensure production credentials are only available in production environment +- Regularly audit test configurations to prevent credential leakage + +## Common APRS-IS Servers (Blocked in Tests) + +The following servers should never be contacted during testing: + +- `rotate.aprs2.net` (Primary rotation server) +- `dallas.aprs2.net` (Dallas server) +- `seattle.aprs2.net` (Seattle server) +- `chicago.aprs2.net` (Chicago server) +- `atlanta.aprs2.net` (Atlanta server) + +Any test configuration pointing to these servers indicates a misconfiguration. + +## Troubleshooting + +### Tests Hanging or Timing Out + +If tests hang, check: +- APRS.Is process isn't starting (`Process.whereis(Aprs.Is)` should return `nil`) +- Test configuration has `disable_aprs_connection: true` +- No real server addresses in test config + +### External Connection Errors in Tests + +If you see TCP connection errors during tests: +- Verify the application supervision tree excludes APRS-IS in test +- Check that `Mix.env()` returns `:test` +- Ensure test_helper.exs properly configures the environment + +### Mock Not Working + +If the mock doesn't provide expected responses: +- Verify `AprsIsMock` is properly loaded in test_helper.exs +- Check that the mock is started in test setup +- Ensure mock methods match the real module's interface + +## Contributing + +When adding new APRS-related functionality: + +1. **Update the mock** to include new methods +2. **Add test coverage** for network isolation +3. **Verify configuration** prevents external connections +4. **Document any new** test isolation requirements \ No newline at end of file diff --git a/test/support/aprs_is_mock.ex b/test/support/aprs_is_mock.ex new file mode 100644 index 0000000..cb82be1 --- /dev/null +++ b/test/support/aprs_is_mock.ex @@ -0,0 +1,169 @@ +defmodule AprsIsMock do + @moduledoc """ + Mock implementation of Aprs.Is for testing purposes. + This ensures no external APRS connections are made during tests. + """ + + use GenServer + + require Logger + + def start_link(opts \\ []) do + GenServer.start_link(__MODULE__, opts, name: __MODULE__) + end + + @impl true + def init(_opts) do + Logger.info("Starting APRS.Is mock for testing") + + # Mock connection state + initial_state = %{ + connected: false, + server: "mock.aprs.test", + port: 14_580, + connected_at: nil, + login_id: "TEST", + filter: "r/33/-96/100", + packet_stats: %{ + total_packets: 0, + last_packet_at: nil, + packets_per_second: 0, + last_second_count: 0, + last_second_timestamp: System.system_time(:second) + }, + stored_packet_count: 0 + } + + {:ok, initial_state} + end + + # Client API - Mock implementations + + def stop do + Logger.info("Stopping APRS.Is mock") + GenServer.stop(__MODULE__, :normal) + end + + def get_status do + case Process.whereis(__MODULE__) do + nil -> + # Mock disconnected state + %{ + connected: false, + server: "mock.aprs.test", + port: 14_580, + connected_at: nil, + uptime_seconds: 0, + login_id: "TEST", + filter: "r/33/-96/100", + packet_stats: %{ + total_packets: 0, + last_packet_at: nil, + packets_per_second: 0, + last_second_count: 0, + last_second_timestamp: System.system_time(:second) + }, + stored_packet_count: 0 + } + + _pid -> + try do + GenServer.call(__MODULE__, :get_status, 5000) + catch + :exit, _ -> + # Fallback mock state + %{ + connected: false, + server: "mock.aprs.test", + port: 14_580, + connected_at: nil, + uptime_seconds: 0, + login_id: "TEST", + filter: "r/33/-96/100", + packet_stats: %{ + total_packets: 0, + last_packet_at: nil, + packets_per_second: 0, + last_second_count: 0, + last_second_timestamp: System.system_time(:second) + }, + stored_packet_count: 0 + } + end + end + end + + def set_filter(filter_string) do + Logger.debug("Mock: Setting filter to #{filter_string}") + :ok + end + + def list_active_filters do + Logger.debug("Mock: Listing active filters") + :ok + end + + def send_message(from, to, message) do + Logger.debug("Mock: Sending message from #{from} to #{to}: #{message}") + :ok + end + + def send_message(message) do + Logger.debug("Mock: Sending message: #{message}") + GenServer.call(__MODULE__, {:send_message, message}) + end + + # Server callbacks + + @impl true + def handle_call({:send_message, message}, _from, state) do + Logger.debug("Mock: Handling send message: #{message}") + {:reply, :ok, state} + end + + def handle_call(:get_status, _from, state) do + mock_status = %{ + state + | connected: false, + uptime_seconds: 0 + } + + {:reply, mock_status, state} + end + + def handle_call({:set_connection_state, connected}, _from, state) do + new_state = %{state | connected: connected, connected_at: if(connected, do: DateTime.utc_now())} + {:reply, :ok, new_state} + end + + @impl true + def handle_info(msg, state) do + Logger.debug("Mock: Received unexpected message: #{inspect(msg)}") + {:noreply, state} + end + + @impl true + def terminate(reason, _state) do + Logger.info("Mock APRS.Is terminating: #{inspect(reason)}") + :ok + end + + # Helper functions for testing + + def simulate_packet(packet_data) do + # Simulate receiving an APRS packet for testing purposes. + # This can be used in tests to trigger packet processing without + # connecting to external servers. + Logger.debug("Mock: Simulating packet: #{inspect(packet_data)}") + + # Broadcast to live clients like the real implementation would + AprsWeb.Endpoint.broadcast("aprs_messages", "packet", packet_data) + + :ok + end + + def simulate_connection_state(connected \\ true) do + # Simulate connection state changes for testing. + GenServer.call(__MODULE__, {:set_connection_state, connected}) + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 66f6ef7..3282f7a 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,2 +1,13 @@ ExUnit.start() Ecto.Adapters.SQL.Sandbox.mode(Aprs.Repo, :manual) + +# Ensure no external APRS connections during tests +Application.put_env(:aprs, :disable_aprs_connection, true) +Application.put_env(:aprs, :aprs_is_server, nil) +Application.put_env(:aprs, :aprs_is_port, nil) +Application.put_env(:aprs, :aprs_is_login_id, "TEST") +Application.put_env(:aprs, :aprs_is_password, "-1") +Application.put_env(:aprs, :aprs_is_default_filter, "r/0/0/1") + +# Load the APRS mock for testing +Code.require_file("support/aprs_is_mock.ex", __DIR__)