aprs.me/test/aprsme/packet_sanitizer_test.exs
Graham McIntire 403300f696
test: broaden unit & property-test coverage (61.8% → 64.7%)
Adds ~800 new tests (incl. 27 StreamData properties) across 22 modules
that previously had little or no coverage. Highlights:

- Pure utility modules (LogSanitizer, PacketFieldWhitelist,
  PacketSanitizer, DeviceParser, CoordinateUtils, BoundsUtils,
  ParamUtils, Convert, WeatherUnits) get thorough unit + property
  coverage including UTF-8 truncation boundaries, antimeridian
  longitude wrap, Haversine symmetry, and unit-conversion inverses.
- JSON view modules (CallsignJSON, ErrorJSON, ChangesetJSON) verified
  for envelope shape and error templating.
- Plugs (HealthCheck, RateLimiter, ApiCSRF) exercised for happy-path
  and halt paths.
- GenServer modules (RegexCache, CleanupScheduler, DeploymentNotifier)
  tested without touching the supervised singleton where possible.
- Drops the orphaned map_live/map_helpers_test.exs whose module name
  collided with the new live/shared/coordinate_utils_test.exs.
2026-04-23 13:32:09 -05:00

156 lines
5.4 KiB
Elixir
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

defmodule Aprsme.PacketSanitizerTest do
use ExUnit.Case, async: true
use ExUnitProperties
alias Aprsme.PacketSanitizer
describe "sanitize_packet/1" do
test "leaves short strings alone" do
packet = %{sender: "K5ABC", base_callsign: "K5ABC"}
assert PacketSanitizer.sanitize_packet(packet) == packet
end
test "truncates oversized sender to 20 bytes" do
long = String.duplicate("A", 50)
result = PacketSanitizer.sanitize_packet(%{sender: long})
assert byte_size(result.sender) == 20
end
test "truncates oversized base_callsign to 20 bytes" do
long = String.duplicate("B", 100)
result = PacketSanitizer.sanitize_packet(%{base_callsign: long})
assert byte_size(result.base_callsign) == 20
end
test "truncates raw_packet to 5000 bytes" do
long = String.duplicate("x", 6000)
result = PacketSanitizer.sanitize_packet(%{raw_packet: long})
assert byte_size(result.raw_packet) == 5000
end
test "leaves non-binary values unchanged" do
packet = %{lat: 30.5, lon: -95.0, has_position: true, altitude: 100}
assert PacketSanitizer.sanitize_packet(packet) == packet
end
test "leaves keys without length limits alone" do
long = String.duplicate("y", 10_000)
result = PacketSanitizer.sanitize_packet(%{some_field_nobody_limits: long})
assert result.some_field_nobody_limits == long
end
test "strips null bytes from values inside the data map" do
packet = %{data: %{"information_field" => "helloworld"}}
result = PacketSanitizer.sanitize_packet(packet)
assert result.data["information_field"] == "helloworld"
end
test "truncates and strips null bytes in the data map" do
long = "a" <> <<0>> <> String.duplicate("b", 6000)
result = PacketSanitizer.sanitize_packet(%{data: %{"information_field" => long}})
sanitized = result.data["information_field"]
refute String.contains?(sanitized, <<0>>)
assert byte_size(sanitized) <= 5000
end
test "leaves non-binary data values alone" do
packet = %{data: %{"count" => 7, "enabled" => true}}
assert PacketSanitizer.sanitize_packet(packet) == packet
end
test "leaves :data key alone when it's not a map" do
packet = %{data: "not a map"}
assert PacketSanitizer.sanitize_packet(packet) == packet
end
end
describe "UTF-8 boundary safety" do
test "truncation never produces invalid UTF-8" do
# Use a 3-byte character repeated so any naive byte-slice hits mid-char.
s = String.duplicate("", 50)
result = PacketSanitizer.sanitize_packet(%{sender: s})
assert String.valid?(result.sender)
assert byte_size(result.sender) <= 20
end
test "strings exactly at the limit are kept intact" do
exact = String.duplicate("a", 20)
result = PacketSanitizer.sanitize_packet(%{sender: exact})
assert result.sender == exact
end
test "emoji mid-truncation produces valid UTF-8" do
# Emoji are 4 bytes in UTF-8; 20 byte limit will cut mid-character.
s = String.duplicate("😀", 10)
result = PacketSanitizer.sanitize_packet(%{sender: s})
assert String.valid?(result.sender)
assert byte_size(result.sender) <= 20
end
end
describe "property: sanitization respects declared limits" do
# Mirrors the @max_lengths module attribute so changes there get caught.
@limited_fields %{
sender: 20,
base_callsign: 20,
destination: 20,
ssid: 10,
symbol_code: 5,
symbol_table_id: 5,
path: 500,
raw_packet: 5000,
comment: 2000,
message_text: 2000
}
property "sanitized values never exceed their declared byte limit" do
check all(
{field, limit} <- member_of(Map.to_list(@limited_fields)),
value <- string(:printable)
) do
result = PacketSanitizer.sanitize_packet(%{field => value})
assert byte_size(result[field]) <= limit
end
end
property "sanitized values always remain valid UTF-8" do
check all(
{field, _limit} <- member_of(Map.to_list(@limited_fields)),
value <- string(:utf8)
) do
result = PacketSanitizer.sanitize_packet(%{field => value})
assert String.valid?(result[field])
end
end
property "sanitize_packet is idempotent" do
check all(
{field, _limit} <- member_of(Map.to_list(@limited_fields)),
value <- string(:printable)
) do
once = PacketSanitizer.sanitize_packet(%{field => value})
twice = PacketSanitizer.sanitize_packet(once)
assert once == twice
end
end
property "values under the limit are passed through unchanged" do
check all(
{field, limit} <- member_of(Map.to_list(@limited_fields)),
value <- string(:alphanumeric, max_length: 5),
byte_size(value) <= limit
) do
result = PacketSanitizer.sanitize_packet(%{field => value})
assert result[field] == value
end
end
property "null bytes are always stripped from data map string values" do
check all(parts <- list_of(string(:printable), max_length: 10)) do
value = Enum.join(parts, <<0>>)
result = PacketSanitizer.sanitize_packet(%{data: %{"random_key" => value}})
refute String.contains?(result.data["random_key"], <<0>>)
end
end
end
end