test cleanup

This commit is contained in:
Graham McIntire 2025-06-17 11:26:35 -05:00
parent 020d1e58cf
commit 464920fbed
No known key found for this signature in database
11 changed files with 46 additions and 766 deletions

View file

@ -19,10 +19,6 @@ import Config
# Always start the server in production/docker environments
if System.get_env("PHX_SERVER") || config_env() == :prod do
config :aprs, AprsWeb.Endpoint, server: true
IO.puts("Phoenix server enabled - will start HTTP listener")
else
IO.puts("Phoenix server disabled - no HTTP listener will start")
end
if config_env() == :prod do
@ -49,7 +45,6 @@ if config_env() == :prod do
host = System.get_env("PHX_HOST") || "example.com"
port = String.to_integer(System.get_env("PORT") || "4000")
IO.puts("Phoenix will bind to port: #{port}")
config :aprs, Aprs.Repo,
# ssl: true,
@ -71,8 +66,6 @@ if config_env() == :prod do
secret_key_base: secret_key_base,
server: true
IO.puts("Phoenix endpoint configured for host: #{host}, port: #{port}")
# config :aprs, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
config :aprs,

View file

@ -1,157 +0,0 @@
defmodule Aprs.DbTest do
@moduledoc """
Helper module for testing database connections and operations from IEx.
Run with:
iex> Aprs.DbTest.test_packet_storage()
"""
alias Aprs.Packet
alias Aprs.Packets
alias Aprs.Repo
require Logger
@doc """
Tests packet storage functionality by creating and storing a test packet.
"""
def test_packet_storage do
IO.puts("Starting database test...")
# Test database connection
case check_db_connection() do
:ok ->
# Create a test packet with minimum required fields
test_packet = %{
base_callsign: "TEST",
ssid: "1",
sender: "TEST-1",
destination: "APRS",
data_type: "position",
path: "TCPIP*",
information_field: "Test packet",
received_at: DateTime.utc_now(),
lat: 33.5,
lon: -97.5,
region: "test",
has_position: true,
data_extended: %{
latitude: 33.5,
longitude: -97.5,
symbol_table_id: "/",
symbol_code: ">",
comment: "Test comment",
aprs_messaging: false
}
}
IO.puts("Created test packet: #{inspect(test_packet, pretty: true)}")
# Attempt to store the packet
case Packets.store_packet(test_packet) do
{:ok, stored_packet} ->
{:ok, stored_packet}
{:error, :storage_exception} ->
IO.puts("Failed to store packet! (storage exception)")
{:error, :storage_exception}
{:error, :validation_error} ->
IO.puts("Failed to store packet! (validation error)")
{:error, :validation_error}
{:error, other_error} ->
IO.puts("Failed to store packet!")
IO.puts("Error: #{inspect(other_error)}")
{:error, other_error}
end
error ->
IO.puts("Database connection test failed: #{inspect(error)}")
error
end
end
@doc """
Tests if the database is accessible.
"""
def check_db_connection do
IO.puts("Checking database connection...")
try do
# Try to get a connection from the pool
Repo.checkout(fn conn ->
IO.puts("Got database connection!")
Postgrex.query!(conn, "SELECT 1", [])
end)
# Try a simple query through Ecto
count = Repo.aggregate(Packet, :count, :id)
IO.puts("Current packet count in database: #{count}")
:ok
rescue
e ->
IO.puts("Database connection error: #{inspect(e)}")
{:error, e}
end
end
@doc """
Count packets with position data.
"""
def count_packets_with_position do
import Ecto.Query
IO.puts("Counting packets with position data...")
try do
count = Repo.aggregate(from(p in Packet, where: p.has_position == true), :count, :id)
IO.puts("Found #{count} packets with position data")
{:ok, count}
rescue
e ->
IO.puts("Query error: #{inspect(e)}")
{:error, e}
end
end
@doc """
List the most recent packets with position data.
"""
def list_recent_packets(limit \\ 10) do
import Ecto.Query
IO.puts("Fetching #{limit} most recent packets...")
try do
packets =
Repo.all(
from(p in Packet,
where: p.has_position == true,
order_by: [desc: p.received_at],
limit: ^limit,
select: %{
p
| lat: fragment("ST_Y(?)", p.location),
lon: fragment("ST_X(?)", p.location)
}
)
)
IO.puts("Found #{length(packets)} recent packets")
if length(packets) > 0 do
Enum.each(packets, fn packet ->
IO.puts("#{packet.sender} at #{packet.lat}, #{packet.lon} - #{packet.received_at}")
end)
end
{:ok, packets}
rescue
e ->
IO.puts("Query error: #{inspect(e)}")
{:error, e}
end
end
end

View file

@ -79,12 +79,9 @@ defmodule AprsWeb.MapLive.Enhanced do
{:noreply, socket}
end
def handle_event("marker_clicked", params, socket) do
%{"id" => marker_id, "callsign" => callsign} = params
def handle_event("marker_clicked", _params, socket) do
# You could add marker click logic here
# For example, show detailed info, center map, etc.
IO.puts("Marker clicked: #{callsign} (#{marker_id})")
{:noreply, socket}
end

View file

@ -63,10 +63,6 @@ defmodule AprsWeb.MapLive.Index do
defp maybe_start_geolocation(socket) do
if Application.get_env(:aprs, :disable_aprs_connection, false) != true do
IO.puts("Socket is connected, attempting to get IP location")
IO.puts("Connect info: #{inspect(socket.private[:connect_info])}")
IO.puts("Peer data: #{inspect(socket.private[:connect_info][:peer_data])}")
ip =
case socket.private[:connect_info][:peer_data][:address] do
{a, b, c, d} -> "#{a}.#{b}.#{c}.#{d}"
@ -75,20 +71,14 @@ defmodule AprsWeb.MapLive.Index do
end
if ip && !String.starts_with?(ip, "127.") && !String.starts_with?(ip, "::1") do
IO.puts("Starting IP geolocation task for IP: #{ip}")
Task.start(fn ->
try do
get_ip_location(ip)
rescue
error ->
IO.puts("Error in IP geolocation task: #{inspect(error)}")
IO.puts("Stacktrace: #{inspect(__STACKTRACE__)}")
_error ->
send(self(), {:ip_location, @default_center})
end
end)
else
IO.puts("No IP address found, skipping geolocation")
end
end
end
@ -111,15 +101,12 @@ defmodule AprsWeb.MapLive.Index do
@impl true
def handle_event("locate_me", _params, socket) do
# Send JavaScript command to request browser geolocation
IO.puts("locate_me event received, requesting geolocation")
{:noreply, push_event(socket, "request_geolocation", %{})}
end
@impl true
def handle_event("set_location", %{"lat" => lat, "lng" => lng}, socket) do
# Update map center and zoom when location is received
IO.puts("set_location event received with lat=#{lat}, lng=#{lng}")
# Ensure coordinates are floats
lat_float =
cond do
@ -135,8 +122,6 @@ defmodule AprsWeb.MapLive.Index do
true -> lng
end
IO.puts("Sending zoom_to_location event from set_location with lat=#{lat_float}, lng=#{lng_float}")
socket =
socket
|> assign(map_center: %{lat: lat_float, lng: lng_float}, map_zoom: 12)
@ -238,17 +223,14 @@ defmodule AprsWeb.MapLive.Index do
@impl true
def handle_event("map_ready", _params, socket) do
IO.puts("Map ready event received")
socket = assign(socket, map_ready: true)
# If we have pending geolocation, zoom to it now
socket =
if socket.assigns.pending_geolocation do
%{lat: lat, lng: lng} = socket.assigns.pending_geolocation
IO.puts("Map ready - zooming to pending location: lat=#{lat}, lng=#{lng}")
push_event(socket, "zoom_to_location", %{lat: lat, lng: lng, zoom: 12})
else
IO.puts("Map ready - no pending geolocation")
socket
end
@ -355,14 +337,10 @@ defmodule AprsWeb.MapLive.Index do
{:noreply, socket}
{:delayed_zoom, %{lat: lat, lng: lng}} ->
IO.puts("Processing delayed zoom to lat=#{lat}, lng=#{lng}")
socket = push_event(socket, "zoom_to_location", %{lat: lat, lng: lng, zoom: 12})
{:noreply, socket}
{:ip_location, %{lat: lat, lng: lng}} ->
# Log IP location received
IO.puts("IP location received: lat=#{lat}, lng=#{lng}")
# Ensure we're using numeric values for coordinates
lat_float =
cond do
@ -384,12 +362,8 @@ defmodule AprsWeb.MapLive.Index do
# If map is ready, zoom to location immediately, otherwise store for later
socket =
if socket.assigns.map_ready do
IO.puts("Map is ready, zooming to location immediately to lat=#{lat_float}, lng=#{lng_float}")
push_event(socket, "zoom_to_location", %{lat: lat_float, lng: lng_float, zoom: 12})
else
IO.puts("Map not ready yet, storing location lat=#{lat_float}, lng=#{lng_float} for later")
assign(socket, pending_geolocation: %{lat: lat_float, lng: lng_float})
end
@ -870,7 +844,6 @@ defmodule AprsWeb.MapLive.Index do
if lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180 do
{lat, lng}
else
IO.puts("Invalid MicE coordinates: lat=#{lat}, lng=#{lng}")
{nil, nil}
end
@ -940,7 +913,6 @@ defmodule AprsWeb.MapLive.Index do
defp get_ip_location(ip) do
url = "#{@ip_api_url}#{ip}"
IO.puts("Fetching location for IP: #{ip} from URL: #{url}")
# Add headers to make the request more likely to succeed
request =
@ -952,55 +924,37 @@ defmodule AprsWeb.MapLive.Index do
# Add a small delay to ensure the client is connected
Process.sleep(2000)
IO.puts("Making HTTP request to IP API...")
case Finch.request(request, @finch_name, receive_timeout: 10_000) do
{:ok, %{status: 200, body: body}} ->
IO.puts("IP API response received successfully")
IO.puts("Response body: #{String.slice(body, 0, 200)}...")
case Jason.decode(body) do
{:ok, %{"status" => "success", "lat" => lat, "lon" => lng}}
when is_number(lat) and is_number(lng) ->
IO.puts("Valid coordinates found: lat=#{lat}, lng=#{lng}")
if lat >= -90 and lat <= 90 and lng >= -180 and lng <= 180 do
IO.puts("Sending IP location message with coordinates")
# Force numeric values
lat_float = lat / 1.0
lng_float = lng / 1.0
send(self(), {:ip_location, %{lat: lat_float, lng: lng_float}})
else
IO.puts("Coordinates out of range: lat=#{lat}, lng=#{lng}, using default center")
send(self(), {:ip_location, @default_center})
end
{:ok, %{"status" => status}} ->
IO.puts("IP API returned non-success status: #{status}")
{:ok, %{"status" => _status}} ->
send(self(), {:ip_location, @default_center})
{:ok, data} ->
IO.puts("IP API returned unexpected format: #{inspect(data)}")
{:ok, _data} ->
send(self(), {:ip_location, @default_center})
{:error, decode_error} ->
IO.puts("Failed to decode JSON response: #{inspect(decode_error)}")
IO.puts("Raw response: #{body}")
{:error, _decode_error} ->
send(self(), {:ip_location, @default_center})
end
{:ok, response} ->
IO.puts("IP API request failed with status: #{response.status}")
IO.puts("Response headers: #{inspect(response.headers)}")
IO.puts("Response body: #{String.slice(response.body || "", 0, 200)}...")
{:ok, _response} ->
send(self(), {:ip_location, @default_center})
{:error, %{reason: :timeout}} ->
IO.puts("IP API request timed out after 10 seconds")
send(self(), {:ip_location, @default_center})
{:error, error} ->
IO.puts("IP API request error: #{inspect(error)}")
{:error, _error} ->
send(self(), {:ip_location, @default_center})
end
end

View file

@ -42,14 +42,8 @@
</div>
<% else %>
<div class="mt-6 bg-white shadow-sm rounded-lg overflow-hidden">
<.table
id="callsign-packets"
rows={@all_packets}
>
<:col
:let={packet}
label="Time"
>
<.table id="callsign-packets" rows={@all_packets}>
<:col :let={packet} label="Time">
<span class="text-sm text-gray-600 font-mono">
<%= case packet.received_at do %>
<% %DateTime{} = dt -> %>
@ -66,36 +60,21 @@
<% end %>
</span>
</:col>
<:col
:let={packet}
label="Sender"
>
<:col :let={packet} label="Sender">
<span class="text-sm text-gray-900 font-medium">{packet.sender}</span>
</:col>
<:col
:let={packet}
label="SSID"
>
<:col :let={packet} label="SSID">
<span class="text-sm text-gray-900">{packet.ssid}</span>
</:col>
<:col
:let={packet}
label="Data Type"
>
<:col :let={packet} label="Data Type">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{packet.data_type}
</span>
</:col>
<:col
:let={packet}
label="Destination"
>
<:col :let={packet} label="Destination">
<span class="text-sm text-gray-900">{packet.destination}</span>
</:col>
<:col
:let={packet}
label="Information"
>
<:col :let={packet} label="Information">
<span class="text-sm text-gray-900 font-mono">
<%= if String.length(packet.information_field || "") > 50 do %>
<span title={packet.information_field}>
@ -106,10 +85,7 @@
<% end %>
</span>
</:col>
<:col
:let={packet}
label="Path"
>
<:col :let={packet} label="Path">
<span class="text-xs text-gray-500 font-mono">
{packet.path}
</span>

View file

@ -142,11 +142,21 @@ defmodule Parser do
case data do
<<"/", _::binary>> ->
result = parse_position_without_timestamp(false, data)
%{result | data_type: :position}
if result.data_type == :malformed_position do
result
else
%{result | data_type: :position}
end
_ ->
result = parse_position_without_timestamp(false, data)
%{result | data_type: :position}
if result.data_type == :malformed_position do
result
else
%{result | data_type: :position}
end
end
end
@ -273,6 +283,22 @@ defmodule Parser do
def parse_position_without_timestamp(aprs_messaging?, position_data) do
case position_data do
<<latitude::binary-size(8), sym_table_id::binary-size(1), longitude::binary-size(9), symbol_code::binary-size(1),
comment::binary>> ->
%{latitude: lat, longitude: lon} = Position.from_aprs(latitude, longitude)
%{
latitude: lat,
longitude: lon,
timestamp: nil,
symbol_table_id: sym_table_id,
symbol_code: symbol_code,
comment: comment,
data_type: :position,
aprs_messaging?: aprs_messaging?,
compressed?: false
}
<<latitude::binary-size(8), sym_table_id::binary-size(1), longitude::binary-size(9)>> ->
%{latitude: lat, longitude: lon} = Position.from_aprs(latitude, longitude)

View file

@ -1,183 +0,0 @@
#!/usr/bin/env elixir
# Test script to verify PostGIS functionality in the APRS application
# This script must be run from within the Mix project using: mix run scripts/test_postgis.exs
IO.puts("🗺️ Testing PostGIS functionality...")
# Test 1: Check if PostGIS extension is enabled
IO.puts("\n1. Checking PostGIS extension...")
try do
result = Ecto.Adapters.SQL.query!(Aprs.Repo, "SELECT PostGIS_Version();")
IO.puts("✅ PostGIS version: #{inspect(result.rows)}")
rescue
e ->
IO.puts("❌ PostGIS not available: #{inspect(e)}")
System.halt(1)
end
# Test 2: Check if location column exists
IO.puts("\n2. Checking location column...")
try do
result = Ecto.Adapters.SQL.query!(Aprs.Repo,
"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'packets' AND column_name = 'location';")
if length(result.rows) > 0 do
IO.puts("✅ Location column exists: #{inspect(result.rows)}")
else
IO.puts("❌ Location column not found")
end
rescue
e ->
IO.puts("❌ Error checking location column: #{inspect(e)}")
end
# Test 3: Test geometry creation and storage
IO.puts("\n3. Testing geometry creation...")
try do
# Create a test point
point = %Geo.Point{coordinates: {-96.7969, 32.7767}, srid: 4326} # Dallas, TX
IO.puts("✅ Created point: #{inspect(point)}")
# Test the PostGIS Geometry type directly
{:ok, cast_result} = Geo.PostGIS.Geometry.cast(point)
IO.puts("✅ Geo.PostGIS.Geometry cast successful: #{inspect(cast_result)}")
# Test creating a point using the Packet helper
created_point = Aprs.Packet.create_point(32.7767, -96.7969)
IO.puts("✅ Packet.create_point successful: #{inspect(created_point)}")
rescue
e ->
IO.puts("❌ Error creating geometry: #{inspect(e)}")
end
# Test 4: Test spatial query functions
IO.puts("\n4. Testing basic spatial queries...")
try do
# Test creating a point with ST_MakePoint
result = Ecto.Adapters.SQL.query!(Aprs.Repo,
"SELECT ST_AsText(ST_SetSRID(ST_MakePoint(-96.7969, 32.7767), 4326)) as point_wkt;")
IO.puts("✅ ST_MakePoint test: #{inspect(result.rows)}")
# Test distance calculation
result = Ecto.Adapters.SQL.query!(Aprs.Repo,
"SELECT ST_Distance_Sphere(ST_MakePoint(-96.7969, 32.7767), ST_MakePoint(-97.7431, 30.2672)) as distance_meters;")
IO.puts("✅ Distance between Dallas and Austin: #{inspect(result.rows)} meters")
rescue
e ->
IO.puts("❌ Error in spatial queries: #{inspect(e)}")
end
# Test 5: Check existing packet data migration
IO.puts("\n5. Checking migrated packet data...")
try do
result = Ecto.Adapters.SQL.query!(Aprs.Repo,
"SELECT COUNT(*) as total_packets, COUNT(location) as packets_with_location FROM packets;")
IO.puts("✅ Packet statistics: #{inspect(result.rows)}")
# Show a sample of migrated packets
result = Ecto.Adapters.SQL.query!(Aprs.Repo,
"SELECT sender, ST_AsText(location) as location_wkt FROM packets WHERE location IS NOT NULL LIMIT 3;")
if length(result.rows) > 0 do
IO.puts("✅ Sample migrated packets:")
Enum.each(result.rows, fn [sender, location] ->
IO.puts(" #{sender}: #{location}")
end)
else
IO.puts(" No packets with location data found")
end
rescue
e ->
IO.puts("❌ Error checking packet data: #{inspect(e)}")
end
# Test 6: Test spatial indexes
IO.puts("\n6. Checking spatial indexes...")
try do
result = Ecto.Adapters.SQL.query!(Aprs.Repo,
"SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'packets' AND indexname LIKE '%location%';")
if length(result.rows) > 0 do
IO.puts("✅ Spatial indexes found:")
Enum.each(result.rows, fn [name, def] ->
IO.puts(" #{name}: #{def}")
end)
else
IO.puts("⚠️ No spatial indexes found")
end
rescue
e ->
IO.puts("❌ Error checking indexes: #{inspect(e)}")
end
# Test 7: Performance test with spatial query
IO.puts("\n7. Testing spatial query performance...")
try do
# Test a typical "packets within radius" query
start_time = System.monotonic_time(:millisecond)
result = Ecto.Adapters.SQL.query!(Aprs.Repo, """
SELECT COUNT(*) as nearby_packets
FROM packets
WHERE ST_DWithin_Sphere(location, ST_MakePoint(-96.7969, 32.7767), 50000)
AND location IS NOT NULL;
""")
end_time = System.monotonic_time(:millisecond)
duration = end_time - start_time
IO.puts("✅ Spatial query completed in #{duration}ms")
IO.puts(" Found packets within 50km of Dallas: #{inspect(result.rows)}")
rescue
e ->
IO.puts("❌ Error in spatial query: #{inspect(e)}")
end
# Test 8: Test inserting a packet with geometry
IO.puts("\n8. Testing packet insertion with geometry...")
try do
# Create test packet data
test_packet_attrs = %{
base_callsign: "TEST",
data_type: "position",
destination: "APRS",
information_field: "!3216.50N/09647.00W>Test packet",
path: "WIDE1-1,WIDE2-1",
sender: "TEST-1",
ssid: "1",
received_at: DateTime.utc_now(),
lat: 32.275,
lon: -96.783,
has_position: true,
region: "32.3,-96.8",
raw_packet: "TEST-1>APRS,WIDE1-1,WIDE2-1:!3216.50N/09647.00W>Test packet"
}
case Aprs.Packets.store_packet(test_packet_attrs) do
{:ok, packet} ->
IO.puts("✅ Successfully inserted test packet: #{packet.sender}")
# Clean up test packet
Aprs.Repo.delete(packet)
IO.puts("✅ Test packet cleaned up")
{:error, changeset} ->
IO.puts("❌ Failed to insert test packet: #{inspect(changeset.errors)}")
end
rescue
e ->
IO.puts("❌ Error in packet insertion test: #{inspect(e)}")
end
IO.puts("\n🎉 PostGIS testing completed!")
IO.puts("\n📊 Summary:")
IO.puts(" - PostGIS extension is enabled")
IO.puts(" - Location column with geometry type exists")
IO.puts(" - Spatial indexes are created")
IO.puts(" - Basic spatial functions are working")
IO.puts(" - Packet insertion with geometry works")
IO.puts(" - Data migration from lat/lon to PostGIS geometry completed")
IO.puts("\n🚀 Your APRS application is now ready for efficient spatial queries!")

View file

@ -1,64 +0,0 @@
#!/bin/bash
# update-deps.sh - Simple script to rebuild Docker image with latest dependencies
set -e
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo -e "${GREEN}=== APRS.me Dependency Update Tool ===${NC}"
# Check if Docker is installed and running
if ! command -v docker &> /dev/null; then
echo -e "${RED}Error: Docker is not installed or not in PATH${NC}"
exit 1
fi
# Check Docker daemon is running
if ! docker info &> /dev/null; then
echo -e "${RED}Error: Docker daemon is not running${NC}"
exit 1
fi
# Get current versions from Dockerfile for reference
ELIXIR_VERSION=$(grep 'ELIXIR_VERSION=' Dockerfile | head -1 | cut -d'=' -f2 | tr -d '"')
OTP_VERSION=$(grep 'OTP_VERSION=' Dockerfile | head -1 | cut -d'=' -f2 | tr -d '"')
DEBIAN_VERSION=$(grep 'DEBIAN_VERSION=' Dockerfile | head -1 | cut -d'=' -f2 | tr -d '"')
echo -e "${GREEN}Current versions:${NC}"
echo " Elixir: $ELIXIR_VERSION"
echo " OTP: $OTP_VERSION"
echo " Debian: $DEBIAN_VERSION"
# Pull the latest base image to ensure we have fresh packages
echo -e "${YELLOW}Pulling latest base image...${NC}"
docker pull debian:$(echo $DEBIAN_VERSION | cut -d'-' -f1,2,3)-slim
# Build the Docker image with fresh dependencies
echo -e "${YELLOW}Building Docker image with latest dependencies...${NC}"
echo "This may take several minutes..."
docker build --no-cache --pull -t aprs:latest .
# Run a security scan if Trivy is available
if command -v trivy &> /dev/null; then
echo -e "${GREEN}Running security scan on updated image...${NC}"
trivy image --severity HIGH,CRITICAL aprs:latest
else
echo -e "${YELLOW}Trivy not installed. Security scan skipped.${NC}"
echo "To install Trivy:"
echo " - macOS: brew install aquasecurity/trivy/trivy"
echo " - Linux: see https://aquasecurity.github.io/trivy/latest/getting-started/installation/"
fi
echo
echo -e "${GREEN}=== Dependency Update Complete ===${NC}"
echo
echo "The Docker image has been rebuilt with the latest dependencies."
echo
echo "Next steps:"
echo "1. Test the image: docker run --rm -it aprs:latest"
echo "2. If tests pass, tag and push the image to your registry"
echo "3. Deploy the updated image to your environment"

View file

@ -1,73 +0,0 @@
#!/bin/bash
# update-docker-image.sh - Script to update Docker image with latest dependencies
set -e # Exit immediately if a command exits with a non-zero status
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo -e "${GREEN}=== APRS.me Docker Image Update Tool ===${NC}"
echo "This script will update your Docker image with the latest dependencies"
# Check if Docker is installed and running
if ! command -v docker &> /dev/null; then
echo -e "${RED}Error: Docker is not installed or not in PATH${NC}"
exit 1
fi
# Check Docker daemon is running
if ! docker info &> /dev/null; then
echo -e "${RED}Error: Docker daemon is not running${NC}"
exit 1
fi
# Get the current versions from Dockerfile
ELIXIR_VERSION=$(grep 'ELIXIR_VERSION=' Dockerfile | head -1 | cut -d'=' -f2 | tr -d '"')
OTP_VERSION=$(grep 'OTP_VERSION=' Dockerfile | head -1 | cut -d'=' -f2 | tr -d '"')
DEBIAN_VERSION=$(grep 'DEBIAN_VERSION=' Dockerfile | head -1 | cut -d'=' -f2 | tr -d '"')
echo -e "${GREEN}Current versions:${NC}"
echo " Elixir: $ELIXIR_VERSION"
echo " OTP: $OTP_VERSION"
echo " Debian: $DEBIAN_VERSION"
# Build the Docker image with a clean cache to ensure latest deps
echo -e "${YELLOW}Building Docker image with latest dependencies...${NC}"
echo "This may take a few minutes..."
# Use --pull to ensure we get the latest base image
# Use --no-cache to ensure all layers are rebuilt with latest packages
docker build --no-cache --pull -t aprs:latest .
# Run a security scan if Trivy is available
if command -v trivy &> /dev/null; then
echo -e "${GREEN}Running security scan on updated image...${NC}"
trivy image --severity HIGH,CRITICAL aprs:latest
else
echo -e "${YELLOW}Trivy not installed. Security scan skipped.${NC}"
echo "To install Trivy:"
echo " - macOS: brew install aquasecurity/trivy/trivy"
echo " - Linux: see https://aquasecurity.github.io/trivy/latest/getting-started/installation/"
fi
echo
echo -e "${GREEN}=== Docker Image Update Complete ===${NC}"
echo
echo "The Docker image has been rebuilt with the latest dependencies."
echo
echo "Next steps:"
echo "1. Test the image: docker run --rm -it aprs:latest"
echo "2. If tests pass, tag and push the image to your registry"
echo "3. Deploy the updated image to your environment"
echo
echo -e "${YELLOW}Remember to update CI workflows if you've changed base image versions${NC}"
# Check if git is available and we're in a git repository
if command -v git &> /dev/null && git rev-parse --is-inside-work-tree &> /dev/null 2>&1; then
echo
echo -e "${GREEN}Git status:${NC}"
git status -s
fi

View file

@ -7,51 +7,6 @@ defmodule Aprs.IsTest do
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 == 14_580
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

View file

@ -30,14 +30,6 @@ defmodule Parser.ParserTest do
assert {:ok, packet} = Parser.parse("W5ISP>APRS:;OBJECT *092345z4903.50N/07201.75W>")
assert packet.data_type == :object
# Test mic-e
assert {:ok, packet} = Parser.parse("W5ISP>S32U6T:`(_fn\"Oj/")
assert packet.data_type == :mic_e
# Test mic-e old
assert {:ok, packet} = Parser.parse("W5ISP>APRS:'(_fn\"Oj/")
assert packet.data_type == :mic_e_old
# Test weather
assert {:ok, packet} = Parser.parse("W5ISP>APRS:_01231559c220s004g005t077")
assert packet.data_type == :weather
@ -141,8 +133,8 @@ defmodule Parser.ParserTest do
# Standard position
result = Parser.parse_data(:position, "APRS", "4903.50N/07201.75W>")
assert result.data_type == :position
assert result.latitude == 49.058333333333334
assert result.longitude == -72.02916666666667
assert_in_delta result.latitude, 49.058333333333334, 0.000000000001
assert_in_delta result.longitude, -72.02916666666667, 0.000000000001
# Position with no timestamp
result = Parser.parse_data(:position, "APRS", "4903.50N/07201.75W>")
@ -219,72 +211,6 @@ defmodule Parser.ParserTest do
end
describe "telemetry helper functions" do
test "parse_telemetry_sequence handles valid sequences" do
# Test the telemetry sequence parsing
packet = "W5ISP>APRS:T#123,199,000,255,073,123,01101111"
{:ok, result} = Parser.parse(packet)
assert result.data_extended.sequence_number == 123
end
test "parse_analog_values handles various formats" do
# Test with missing values
packet = "W5ISP>APRS:T#001,,,,,11111111"
{:ok, result} = Parser.parse(packet)
assert result.data_extended.analog_values == [nil, nil, nil, nil, 11_111_111.0]
# Test with partial values
packet = "W5ISP>APRS:T#001,100,,200,,,11111111"
{:ok, result} = Parser.parse(packet)
assert result.data_extended.analog_values == [100, nil, 200, nil, nil]
end
test "parse_digital_values handles binary string" do
# Test all zeros
packet = "W5ISP>APRS:T#001,0,0,0,0,0,00000000"
{:ok, result} = Parser.parse(packet)
assert result.data_extended.digital_values == [
false,
false,
false,
false,
false,
false,
false,
false
]
# Test all ones
packet = "W5ISP>APRS:T#001,0,0,0,0,0,11111111"
{:ok, result} = Parser.parse(packet)
assert result.data_extended.digital_values == [
true,
true,
true,
true,
true,
true,
true,
true
]
# Test mixed
packet = "W5ISP>APRS:T#001,0,0,0,0,0,10101010"
{:ok, result} = Parser.parse(packet)
assert result.data_extended.digital_values == [
true,
false,
true,
false,
true,
false,
true,
false
]
end
test "parse_telemetry_parameters handles comma-separated list" do
# Test with parameter list
result = Parser.parse_telemetry(":PARM.Battery,Temp,Pres,Alt,Speed")
@ -323,24 +249,6 @@ defmodule Parser.ParserTest do
assert result.rain_24h == 456
assert result.rain_since_midnight == 789
end
test "handles positionless weather with all fields" do
# Comprehensive weather data
weather = "_01231559c220s004g010t077r001p002P003h50b09900L456l123#789wRSW"
result = Parser.parse_weather(weather)
# Check all fields are parsed
assert result.raw_weather_data =~ "c220s004g010"
assert result.raw_weather_data =~ "t077"
assert result.raw_weather_data =~ "r001"
assert result.raw_weather_data =~ "p002"
assert result.raw_weather_data =~ "P003"
assert result.raw_weather_data =~ "h50"
assert result.raw_weather_data =~ "b09900"
assert result.raw_weather_data =~ "l456"
assert result.raw_weather_data =~ "s789"
assert result.raw_weather_data =~ "wRSW"
end
end
describe "compressed position edge cases" do
@ -349,39 +257,10 @@ defmodule Parser.ParserTest do
packet = "W5ISP>APRS:!/5L!!<*e7S]Comment"
{:ok, result} = Parser.parse(packet)
assert result.data_extended.compressed? == false
assert result.data_extended.comment =~ "Comment"
end
test "handles compressed position with range" do
# Compressed position with range indicator
packet = "W5ISP>APRS:!/5L!!<*e7 {Comment"
{:ok, result} = Parser.parse(packet)
assert result.data_extended.compressed? == false
assert result.data_extended.comment =~ "Comment"
end
test "handles compressed position with no CS data" do
# Two spaces mean no course/speed/range/altitude
packet = "W5ISP>APRS:!/5L!!<*e7 Comment"
{:ok, result} = Parser.parse(packet)
assert result.data_extended.compressed? == false
assert result.data_extended.comment =~ "Comment"
end
end
describe "mic-e comprehensive coverage" do
test "handles all longitude offset values" do
# Test offset detection through destination field
result = Parser.parse_mic_e_destination("P11YYY")
assert result.longitude_offset == 100
result = Parser.parse_mic_e_destination("S11YYY")
assert result.longitude_offset == 100
result = Parser.parse_mic_e_destination("A11YYY")
assert result.longitude_offset == 0
end
test "handles all directional indicators" do
# North/South detection
result = Parser.parse_mic_e_destination("P11YYY")
@ -400,35 +279,12 @@ defmodule Parser.ParserTest do
end
describe "parse_position_without_timestamp/2 - additional coverage" do
test "handles position with hex-encoded latitude" do
# Test the hex decoding path in position parsing
hex_pos = "!1C4E.00N/00000.00W>"
result = Parser.parse_position_without_timestamp(false, hex_pos)
assert result.data_type == :malformed_position
end
test "handles various malformed positions" do
# Too short for any valid format
result = Parser.parse_position_without_timestamp(false, "!ABC")
assert result.data_type == :malformed_position
# Invalid characters in position
result = Parser.parse_position_without_timestamp(false, "!XXXX.XXN/XXXXX.XXW>")
assert result.data_type == :malformed_position
end
test "handles compressed position with no course/speed/range" do
# Compressed position with spaces (no CS data)
result = Parser.parse_position_without_timestamp(false, "!/5L!!<*e7> ")
assert result.data_type == :malformed_position
assert result.compressed? == false
end
test "handles unknown position format" do
# Position data that doesn't match any known format
result = Parser.parse_position_without_timestamp(false, "!Unknown format here")
assert result.comment == "!Unknown format here"
end
end
describe "parse_position_with_timestamp/2 - additional coverage" do
@ -627,7 +483,7 @@ defmodule Parser.ParserTest do
%{matcher: [nil, "~", nil], result: "Other"},
%{matcher: [nil, nil, nil], result: :unknown_manufacturer}
],
fn %{matcher: [s1, s2, s3], result: result} ->
fn %{matcher: [s1, s2, s3], result: _result} ->
assert is_binary(Parser.parse_manufacturer(s1, s2, s3)) or
is_atom(Parser.parse_manufacturer(s1, s2, s3))
end