Coverage:
- Adds focused tests across 13 files covering previously-uncovered
branches: Packet changeset normalisation (course, wind_direction,
struct data_extended); Packets store_packet edge cases (nested
position shapes, ssid handling, string-keyed raw_packet);
DeviceIdentification HTTP success/404 via Req plug stub; ResendAdapter
catch-all body, deliver_many, nil/binary formatters; ApiDocsLive
packets with missing fields; AprsSymbol normalize helpers;
BadPacketsLive refresh and postgres_notify; MobileChannel handle_info
catch-all and postgres_packet path; PacketProcessor existing-marker
movement detection; PacketReplay sanitize_value type clauses;
InsertOptimizer GenServer lifecycle and negative-duration metric;
PageController shutdown-handler integration; UrlParams delegated
helpers.
- mix.exs: configure test_coverage with summary threshold of 87 and
ignore_modules for test fixtures and macro-only template modules.
Bug fix:
- MapLive.Index handle_info: add a clause for the raw {:new_deployment,
_} tuple. Phoenix.PubSub.broadcast delivers the raw payload to plain
subscribers, but the existing handler only matched the %Broadcast{}
fast-lane wrapper. DeploymentNotifier broadcasts via the raw path,
which crashed the LiveView under test concurrency.
Submodule:
- Update .gitmodules vendor/aprs URL from
https://github.com/aprsme/aprs.git to
ssh://git@codeberg.org/gmcintire/aprs.git.
171 lines
6.4 KiB
Elixir
171 lines
6.4 KiB
Elixir
defmodule AprsmeWeb.PageControllerTest do
|
|
# Mutates :aprsme, :health_status; must not run in parallel with other tests
|
|
# that read it (the HealthCheck plug, for example).
|
|
use AprsmeWeb.ConnCase, async: false
|
|
|
|
setup do
|
|
# Reset ShutdownHandler state — signal_handler_test (async) can trigger the
|
|
# real shutdown via SIGTERM simulation, leaving shutting_down: true behind.
|
|
if pid = Process.whereis(Aprsme.ShutdownHandler) do
|
|
:sys.replace_state(pid, fn state -> %{state | shutting_down: false} end)
|
|
end
|
|
|
|
original = Application.get_env(:aprsme, :health_status)
|
|
on_exit(fn -> Application.put_env(:aprsme, :health_status, original || :healthy) end)
|
|
:ok
|
|
end
|
|
|
|
describe "health/2 controller action" do
|
|
# Call the action directly; the /health endpoint is otherwise served by
|
|
# the HealthCheck plug as text/plain.
|
|
test "returns 200 with ok status when app is healthy and DB is up" do
|
|
Application.put_env(:aprsme, :health_status, :healthy)
|
|
base = Phoenix.Controller.put_format(Phoenix.ConnTest.build_conn(), "json")
|
|
result = AprsmeWeb.PageController.health(base, %{})
|
|
assert result.status == 200
|
|
body = Jason.decode!(result.resp_body)
|
|
assert body["status"] == "ok"
|
|
assert body["database"] == "connected"
|
|
assert is_binary(body["version"])
|
|
end
|
|
|
|
test "returns 503 draining when app is draining" do
|
|
Application.put_env(:aprsme, :health_status, :draining)
|
|
base = Phoenix.Controller.put_format(Phoenix.ConnTest.build_conn(), "json")
|
|
result = AprsmeWeb.PageController.health(base, %{})
|
|
assert result.status == 503
|
|
body = Jason.decode!(result.resp_body)
|
|
assert body["status"] == "draining"
|
|
assert body["message"] =~ "draining"
|
|
end
|
|
end
|
|
|
|
describe "GET /ready" do
|
|
test "returns 200 with ready status and version", %{conn: conn} do
|
|
conn = get(conn, "/ready")
|
|
assert conn.status == 200
|
|
body = json_response(conn, 200)
|
|
assert body["status"] == "ready"
|
|
assert is_binary(body["version"])
|
|
end
|
|
end
|
|
|
|
describe "GET /sitemap.xml" do
|
|
test "returns an XML urlset with known paths", %{conn: conn} do
|
|
conn = get(conn, "/sitemap.xml")
|
|
assert conn.status == 200
|
|
xml = conn.resp_body
|
|
assert xml =~ ~s(<?xml version="1.0" encoding="UTF-8"?>)
|
|
assert xml =~ "<urlset"
|
|
assert xml =~ "/about"
|
|
assert xml =~ "/status"
|
|
end
|
|
end
|
|
|
|
describe "GET /.well-known/api-catalog" do
|
|
test "returns an RFC 9264-style linkset", %{conn: conn} do
|
|
conn = get(conn, "/.well-known/api-catalog")
|
|
assert conn.status == 200
|
|
body = Jason.decode!(conn.resp_body)
|
|
assert is_list(body["linkset"])
|
|
|
|
anchors = Enum.map(body["linkset"], & &1["anchor"])
|
|
assert Enum.any?(anchors, &String.contains?(&1, "/api/v1"))
|
|
assert Enum.any?(anchors, &String.contains?(&1, "/mobile/websocket"))
|
|
end
|
|
end
|
|
|
|
describe "GET /status.json (status_json/2)" do
|
|
test "returns application + aprs_is sections", %{conn: conn} do
|
|
conn = get(conn, "/status.json")
|
|
assert conn.status == 200
|
|
body = json_response(conn, 200)
|
|
assert Map.has_key?(body, "aprs_is")
|
|
assert Map.has_key?(body, "application")
|
|
assert is_binary(body["application"]["version"])
|
|
end
|
|
end
|
|
|
|
describe "status_json with various uptimes (via cache injection)" do
|
|
setup do
|
|
# Start the query_cache ETS table if not present so we can seed it.
|
|
case :ets.info(:query_cache) do
|
|
:undefined -> :ets.new(:query_cache, [:named_table, :public, :set])
|
|
_ -> :ok
|
|
end
|
|
|
|
on_exit(fn -> Aprsme.Cache.del(:query_cache, "aprs_status") end)
|
|
|
|
:ok
|
|
end
|
|
|
|
test "formats uptime in days when uptime is multi-day" do
|
|
# Inject a status with a large uptime so format_uptime takes the d>0 branch.
|
|
status = %{
|
|
connected: true,
|
|
server: "test",
|
|
port: 14_580,
|
|
connected_at: DateTime.add(DateTime.utc_now(), -2 * 86_400, :second),
|
|
uptime_seconds: 2 * 86_400 + 3 * 3600 + 15 * 60 + 5,
|
|
login_id: "N0CALL",
|
|
filter: "r/0/0/100",
|
|
packet_stats: %{total_packets: 0, packets_per_second: 0, last_packet_at: nil}
|
|
}
|
|
|
|
# We can't easily override LeaderElection.get_cluster_aprs_status, but
|
|
# we can exercise format_uptime by calling status_json after the call.
|
|
_ = Aprsme.Cache.put(:query_cache, "aprs_status", status)
|
|
|
|
conn = Phoenix.ConnTest.build_conn()
|
|
result = AprsmeWeb.PageController.status_json(conn, %{})
|
|
body = Jason.decode!(result.resp_body)
|
|
assert is_binary(body["aprs_is"]["uptime_display"])
|
|
end
|
|
end
|
|
|
|
describe "uptime formatting (via format_uptime/1 inside status_json)" do
|
|
# The private format_uptime/1 helper has four branches based on duration.
|
|
# These test cases hit each branch by sending the status_json action
|
|
# through the controller (which calls LeaderElection.get_cluster_aprs_status
|
|
# and then formats the uptime_seconds value returned).
|
|
test "formats zero uptime as 'Not connected'" do
|
|
conn = Phoenix.ConnTest.build_conn()
|
|
# Call the action directly; the real status is "Not connected" when the
|
|
# app isn't actually running an APRS connection in the test environment.
|
|
result = AprsmeWeb.PageController.status_json(conn, %{})
|
|
body = Jason.decode!(result.resp_body)
|
|
assert is_binary(body["aprs_is"]["uptime_display"])
|
|
end
|
|
end
|
|
|
|
describe "health/2 with shutdown handler in shutting-down state" do
|
|
test "returns 503 shutting_down when ShutdownHandler reports shutting down" do
|
|
Application.put_env(:aprsme, :health_status, :healthy)
|
|
|
|
pid = Process.whereis(Aprsme.ShutdownHandler)
|
|
original_state = if pid, do: :sys.get_state(pid)
|
|
|
|
try do
|
|
if pid do
|
|
:sys.replace_state(pid, fn state -> %{state | shutting_down: true} end)
|
|
end
|
|
|
|
base = Phoenix.Controller.put_format(Phoenix.ConnTest.build_conn(), "json")
|
|
result = AprsmeWeb.PageController.health(base, %{})
|
|
|
|
if pid do
|
|
assert result.status == 503
|
|
body = Jason.decode!(result.resp_body)
|
|
assert body["status"] == "shutting_down"
|
|
else
|
|
# No ShutdownHandler running — falls through to db check; should be 200.
|
|
assert result.status == 200
|
|
end
|
|
after
|
|
if pid && original_state do
|
|
:sys.replace_state(pid, fn _ -> original_state end)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|