test cleanup
This commit is contained in:
parent
020d1e58cf
commit
464920fbed
11 changed files with 46 additions and 766 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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!")
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue