dont connect to is server in test
This commit is contained in:
parent
3aad7db955
commit
6e8f1e434f
8 changed files with 816 additions and 52 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
53
test/aprs/connection_prevention_test.exs
Normal file
53
test/aprs/connection_prevention_test.exs
Normal file
|
|
@ -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
|
||||
148
test/aprs/is_test.exs
Normal file
148
test/aprs/is_test.exs
Normal file
|
|
@ -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
|
||||
203
test/aprs_web/integration/aprs_status_test.exs
Normal file
203
test/aprs_web/integration/aprs_status_test.exs
Normal file
|
|
@ -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
|
||||
158
test/support/README.md
Normal file
158
test/support/README.md
Normal file
|
|
@ -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
|
||||
169
test/support/aprs_is_mock.ex
Normal file
169
test/support/aprs_is_mock.ex
Normal file
|
|
@ -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
|
||||
|
|
@ -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__)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue