test: AprsIsMock, DeviceIdentification, PacketCleanupWorker coverage

This commit is contained in:
Graham McIntire 2026-04-23 18:13:01 -05:00
parent 816806e6e2
commit a7071ecef1
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
3 changed files with 197 additions and 0 deletions

View file

@ -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

View file

@ -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

View file

@ -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