From a7071ecef18d407c79ea72addcb7a1af0286e5d6 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Thu, 23 Apr 2026 18:13:01 -0500 Subject: [PATCH] test: AprsIsMock, DeviceIdentification, PacketCleanupWorker coverage --- test/aprs_is_mock_test.exs | 93 +++++++++++++++++++ test/aprsme/device_identification_test.exs | 79 ++++++++++++++++ .../workers/packet_cleanup_worker_test.exs | 25 +++++ 3 files changed, 197 insertions(+) create mode 100644 test/aprs_is_mock_test.exs diff --git a/test/aprs_is_mock_test.exs b/test/aprs_is_mock_test.exs new file mode 100644 index 0000000..acaae38 --- /dev/null +++ b/test/aprs_is_mock_test.exs @@ -0,0 +1,93 @@ +defmodule AprsIsMockTest do + use ExUnit.Case, async: false + + setup do + if Process.whereis(AprsIsMock) do + GenServer.stop(AprsIsMock, :normal) + # Give OTP a moment to unregister the name + Process.sleep(10) + end + + on_exit(fn -> + if Process.whereis(AprsIsMock) do + try do + GenServer.stop(AprsIsMock, :normal, 100) + catch + :exit, _ -> :ok + end + end + end) + + :ok + end + + describe "get_status/0" do + test "returns a default disconnected status when not running" do + status = AprsIsMock.get_status() + + assert status.connected == false + assert status.server == "mock.aprs.test" + assert status.port == 14_580 + assert is_nil(status.connected_at) + assert is_map(status.packet_stats) + assert status.stored_packet_count == 0 + end + + test "computes uptime when the mock is connected" do + {:ok, _} = AprsIsMock.start_link([]) + :ok = AprsIsMock.simulate_connection_state(true) + + # Small sleep so uptime has a non-zero value. + Process.sleep(10) + status = AprsIsMock.get_status() + + assert status.connected == true + assert status.uptime_seconds >= 0 + end + + test "returns uptime 0 when no connected_at is set" do + {:ok, _} = AprsIsMock.start_link([]) + status = AprsIsMock.get_status() + assert status.uptime_seconds == 0 + end + end + + describe "send_message and filter helpers" do + test "filter helpers are :ok no-ops" do + assert AprsIsMock.set_filter("r/0/0/1") == :ok + assert AprsIsMock.list_active_filters() == :ok + end + + test "three-arg send_message is a :ok no-op without running server" do + assert AprsIsMock.send_message("TEST", "DEST", "hi") == :ok + end + + test "single-arg send_message dispatches to server when running" do + {:ok, _} = AprsIsMock.start_link([]) + assert AprsIsMock.send_message("hi") == :ok + end + end + + describe "simulate_packet/1" do + test "broadcasts to the endpoint without crashing" do + # Endpoint is started at boot in test env, so this just needs to not raise. + assert AprsIsMock.simulate_packet(%{sender: "TEST"}) == :ok + end + end + + describe "terminate/2" do + test "returns :ok on termination" do + {:ok, _} = AprsIsMock.start_link([]) + assert :ok = AprsIsMock.stop() + end + end + + describe "handle_info/2" do + test "ignores arbitrary messages" do + {:ok, pid} = AprsIsMock.start_link([]) + send(pid, :arbitrary_message) + Process.sleep(10) + assert Process.alive?(pid) + end + end +end diff --git a/test/aprsme/device_identification_test.exs b/test/aprsme/device_identification_test.exs index 96ab201..6b77206 100644 --- a/test/aprsme/device_identification_test.exs +++ b/test/aprsme/device_identification_test.exs @@ -203,6 +203,85 @@ defmodule Aprsme.DeviceIdentificationTest do end end + describe "upsert_devices/1 with all three groups" do + test "processes devices from mice and micelegacy groups" do + Repo.delete_all(Devices) + + json = %{ + "tocalls" => %{}, + "mice" => %{ + "]=" => %{"vendor" => "Mice", "model" => "MA"} + }, + "micelegacy" => %{ + "`_#" => %{"vendor" => "Legacy", "model" => "MB"} + } + } + + assert :ok = DeviceIdentification.upsert_devices(json) + assert %Devices{vendor: "Mice"} = Repo.get_by(Devices, identifier: "]=") + assert %Devices{vendor: "Legacy"} = Repo.get_by(Devices, identifier: "`_#") + end + + test "filters out non-whitelisted JSON keys" do + Repo.delete_all(Devices) + + # 'garbage' and 'unused_field' must be stripped by process_device_attrs. + json = %{ + "tocalls" => %{ + "APRS" => %{ + "vendor" => "V", + "model" => "M", + "garbage" => "should-be-dropped", + "unused_field" => 42 + } + } + } + + assert :ok = DeviceIdentification.upsert_devices(json) + assert %Devices{vendor: "V", model: "M"} = Repo.get_by(Devices, identifier: "APRS") + end + + test "handles nil features value" do + Repo.delete_all(Devices) + + json = %{ + "tocalls" => %{ + "APNIL" => %{"vendor" => "V", "model" => "M", "features" => nil} + } + } + + assert :ok = DeviceIdentification.upsert_devices(json) + dev = Repo.get_by(Devices, identifier: "APNIL") + assert is_nil(dev.features) + end + end + + describe "maybe_refresh_devices/0 with stale device" do + test "triggers refresh when latest device is older than one week" do + Repo.delete_all(Devices) + # 10 days ago. + old_time = + DateTime.utc_now() + |> DateTime.add(-10 * 24 * 3600, :second) + |> DateTime.to_naive() + |> NaiveDateTime.truncate(:second) + + Repo.insert!(%Devices{ + identifier: "STALEROW", + vendor: "v", + model: "m", + inserted_at: old_time, + updated_at: old_time + }) + + # Will attempt a refresh via the CircuitBreaker. Without a real HTTP + # stub, it may return an error tuple — either way the stale-branch was + # exercised. + result = DeviceIdentification.maybe_refresh_devices() + assert result == :ok or match?({:error, _}, result) + end + end + describe "lookup_device_by_identifier/1" do test "matches APSK21 to APS??? pattern" do # Seed the devices table from the JSON diff --git a/test/aprsme/workers/packet_cleanup_worker_test.exs b/test/aprsme/workers/packet_cleanup_worker_test.exs index 7a86e76..54b04de 100644 --- a/test/aprsme/workers/packet_cleanup_worker_test.exs +++ b/test/aprsme/workers/packet_cleanup_worker_test.exs @@ -89,4 +89,29 @@ defmodule Aprsme.Workers.PacketCleanupWorkerTest do assert :ok = PacketCleanupWorker.perform(%{"cleanup_days" => -1}) end end + + describe "perform/1 with extra fields that the guard ignores" do + test "accepts a map with cleanup_days plus unrelated keys" do + {:ok, _} = PartitionManager.ensure_partitions_exist() + + assert :ok = + PacketCleanupWorker.perform(%{ + "cleanup_days" => 30, + "random_field" => "ignored" + }) + end + + test "handles zero cleanup_days by falling through to default" do + {:ok, _} = PartitionManager.ensure_partitions_exist() + + # 0 doesn't match the `days > 0` guard. + assert :ok = PacketCleanupWorker.perform(%{"cleanup_days" => 0}) + end + + test "handles non-integer cleanup_days by falling through to default" do + {:ok, _} = PartitionManager.ensure_partitions_exist() + + assert :ok = PacketCleanupWorker.perform(%{"cleanup_days" => "string"}) + end + end end