Adds tests covering: - Aprsme.Packet edge cases (already committed) - Encoding valid_codepoint? branches via slow grapheme path and encoding_info shapes - ErrorHandler database/network error categorization (CastError, ConstraintError, QueryError, Postgrex.Error, RuntimeError circuit_open/timeout, custom map :reason) - HealthCheck plug ask_shutting_down? nil and unresponsive-pid paths - SpatialPubSub ensure_float branches, dateline broadcast wrap-around, monitored client DOWN handler, no-coordinate broadcast skip - DataBuilder weather popup, weather-only fallback, tuple value conversion, string weather convert_unit branches - DeviceIdentification fetch_devices_from_url default-arg head - PageController format_uptime branches via fake Aprsme.Is GenServer - WeatherLive.CallsignView compare_timestamps branches via :weather_packet messages - PacketConsumer field-conversion private helpers via crafted handle_events events
258 lines
8.7 KiB
Elixir
258 lines
8.7 KiB
Elixir
defmodule Aprsme.EncodingTest do
|
|
use ExUnit.Case
|
|
|
|
alias Aprsme.Encoding
|
|
|
|
describe "sanitize_string/1 fast-path for clean strings" do
|
|
test "returns clean ASCII string unchanged" do
|
|
input = "VALID1>APRS,WIDE1-1:Test packet"
|
|
assert Encoding.sanitize_string(input) == input
|
|
end
|
|
|
|
test "returns clean ASCII string with allowed whitespace unchanged" do
|
|
input = "Hello\tWorld\nNew line\r\nCRLF"
|
|
result = Encoding.sanitize_string(input)
|
|
assert result == input
|
|
end
|
|
|
|
test "still cleans strings with control characters" do
|
|
# String with null byte (control character that should be removed)
|
|
input = "Hello\x00World"
|
|
result = Encoding.sanitize_string(input)
|
|
assert result == "HelloWorld"
|
|
end
|
|
|
|
test "still cleans strings with DEL character" do
|
|
input = "Hello\x7FWorld"
|
|
result = Encoding.sanitize_string(input)
|
|
assert result == "HelloWorld"
|
|
end
|
|
|
|
test "handles empty string" do
|
|
assert Encoding.sanitize_string("") == ""
|
|
end
|
|
|
|
test "handles non-binary input" do
|
|
assert Encoding.sanitize_string(nil) == ""
|
|
assert Encoding.sanitize_string(123) == ""
|
|
end
|
|
|
|
test "handles valid UTF-8 multi-byte characters" do
|
|
input = "Café résumé"
|
|
result = Encoding.sanitize_string(input)
|
|
assert String.valid?(result)
|
|
assert String.contains?(result, "Caf")
|
|
end
|
|
|
|
test "handles latin1 encoded binary" do
|
|
# Latin1 bytes for accented characters (>= 160, valid Latin1 printable)
|
|
invalid_binary = <<85, 78, 73, 211, 78, 32, 80, 65, 78, 65, 77, 69, 209, 65>>
|
|
result = Encoding.sanitize_string(invalid_binary)
|
|
assert String.valid?(result)
|
|
# 211 = Ó, 209 = Ñ — both above 159, so they convert to real characters
|
|
assert result == "UNIÓN PANAMEÑA"
|
|
end
|
|
end
|
|
|
|
describe "sanitize_string/1 C1 control range (128-159)" do
|
|
test "latin1 bytes 128-159 become replacement characters instead of being silently deleted" do
|
|
input = <<72, 101, 108, 108, 111, 130>>
|
|
result = Encoding.sanitize_string(input)
|
|
assert String.valid?(result)
|
|
assert String.contains?(result, "\uFFFD")
|
|
end
|
|
|
|
test "latin1 byte 128 becomes replacement character" do
|
|
input = <<65, 128, 66>>
|
|
result = Encoding.sanitize_string(input)
|
|
assert String.valid?(result)
|
|
assert result == "A\uFFFDB"
|
|
end
|
|
|
|
test "latin1 byte 159 becomes replacement character" do
|
|
input = <<65, 159, 66>>
|
|
result = Encoding.sanitize_string(input)
|
|
assert String.valid?(result)
|
|
assert result == "A\uFFFDB"
|
|
end
|
|
|
|
test "latin1 bytes above 159 still convert to proper characters" do
|
|
input = <<67, 97, 102, 233>>
|
|
result = Encoding.sanitize_string(input)
|
|
assert String.valid?(result)
|
|
assert result == "Café"
|
|
end
|
|
end
|
|
|
|
describe "encoding_info/1" do
|
|
test "returns the byte_count and char_count for valid UTF-8" do
|
|
assert %{valid_utf8: true, byte_count: 5, char_count: 5} = Encoding.encoding_info("Hello")
|
|
end
|
|
|
|
test "returns the invalid_at offset for invalid UTF-8" do
|
|
info = Encoding.encoding_info(<<65, 0xFF, 66>>)
|
|
assert info.valid_utf8 == false
|
|
assert info.byte_count == 3
|
|
assert info.char_count == nil
|
|
assert is_integer(info.invalid_at)
|
|
end
|
|
|
|
test "returns the non-binary fallback shape for non-binary input" do
|
|
info = Encoding.encoding_info(:not_a_binary)
|
|
assert info == %{valid_utf8: false, byte_count: 0, char_count: nil, invalid_at: nil}
|
|
|
|
info2 = Encoding.encoding_info(123)
|
|
assert info2 == %{valid_utf8: false, byte_count: 0, char_count: nil, invalid_at: nil}
|
|
end
|
|
end
|
|
|
|
describe "valid_codepoint? branches via the slow grapheme path" do
|
|
test "tab byte (9) survives sanitization in mixed-content strings" do
|
|
# 9 (tab) + 130 (C1 control) forces the slow path; tab survives.
|
|
input = <<72, 9, 130, 73>>
|
|
result = Encoding.sanitize_string(input)
|
|
assert String.valid?(result)
|
|
assert String.contains?(result, "\t")
|
|
end
|
|
|
|
test "newline byte (10) survives sanitization in mixed-content strings" do
|
|
input = <<72, 10, 130, 73>>
|
|
result = Encoding.sanitize_string(input)
|
|
assert String.valid?(result)
|
|
assert String.contains?(result, "\n")
|
|
end
|
|
|
|
test "carriage return byte (13) survives sanitization in mixed-content strings" do
|
|
input = <<72, 13, 130, 73>>
|
|
result = Encoding.sanitize_string(input)
|
|
assert String.valid?(result)
|
|
assert String.contains?(result, "\r")
|
|
end
|
|
|
|
test "other C0 control bytes are stripped in slow path" do
|
|
# Byte 1 is a C0 control + byte 130 to trigger slow path.
|
|
input = <<72, 1, 130, 73>>
|
|
result = Encoding.sanitize_string(input)
|
|
assert String.valid?(result)
|
|
refute String.contains?(result, <<1>>)
|
|
end
|
|
end
|
|
|
|
describe "to_float_safe/1" do
|
|
test "parses valid numeric string" do
|
|
assert Encoding.to_float_safe("3.14") == {:ok, 3.14}
|
|
assert Encoding.to_float_safe("42") == {:ok, 42.0}
|
|
end
|
|
|
|
test "trims whitespace" do
|
|
assert Encoding.to_float_safe(" 3.14 ") == {:ok, 3.14}
|
|
end
|
|
|
|
test "truncates strings longer than 30 characters" do
|
|
# "1.0" padded with trailing zeros — valid after truncation
|
|
long = "1." <> String.duplicate("0", 40)
|
|
assert Encoding.to_float_safe(long) == {:ok, 1.0}
|
|
end
|
|
|
|
test "returns nil for out-of-range float" do
|
|
assert is_nil(Encoding.to_float_safe("9.1e15"))
|
|
assert is_nil(Encoding.to_float_safe("-9.1e15"))
|
|
end
|
|
|
|
test "returns value for in-range float" do
|
|
assert {:ok, f} = Encoding.to_float_safe("1000000.0")
|
|
assert f == 1_000_000.0
|
|
end
|
|
|
|
test "returns nil for invalid string" do
|
|
assert is_nil(Encoding.to_float_safe("abc"))
|
|
assert is_nil(Encoding.to_float_safe(""))
|
|
end
|
|
|
|
test "returns nil for non-binary input" do
|
|
assert is_nil(Encoding.to_float_safe(nil))
|
|
assert is_nil(Encoding.to_float_safe(123))
|
|
assert is_nil(Encoding.to_float_safe(:atom))
|
|
end
|
|
|
|
test "handles scientific notation" do
|
|
assert {:ok, f} = Encoding.to_float_safe("1.5e3")
|
|
assert f == 1500.0
|
|
end
|
|
|
|
test "handles zero" do
|
|
assert Encoding.to_float_safe("0") == {:ok, 0.0}
|
|
assert Encoding.to_float_safe("0.0") == {:ok, 0.0}
|
|
end
|
|
end
|
|
|
|
describe "has_weather_data/4" do
|
|
test "returns true when single field present" do
|
|
assert Encoding.has_weather_data(72.0, nil, nil, nil)
|
|
assert Encoding.has_weather_data(nil, 50, nil, nil)
|
|
assert Encoding.has_weather_data(nil, nil, 5.0, nil)
|
|
assert Encoding.has_weather_data(nil, nil, nil, 1013.25)
|
|
end
|
|
|
|
test "returns true when multiple fields present" do
|
|
assert Encoding.has_weather_data(72.0, 50, 5.0, 1013.25)
|
|
end
|
|
|
|
test "returns false when all nil" do
|
|
refute Encoding.has_weather_data(nil, nil, nil, nil)
|
|
end
|
|
|
|
test "treats zero as valid weather data" do
|
|
assert Encoding.has_weather_data(0, nil, nil, nil)
|
|
assert Encoding.has_weather_data(nil, 0, nil, nil)
|
|
end
|
|
|
|
test "treats negative values as valid weather data" do
|
|
assert Encoding.has_weather_data(-10, nil, nil, nil)
|
|
end
|
|
end
|
|
|
|
describe "to_hex/1" do
|
|
test "returns empty string for non-binary" do
|
|
assert Encoding.to_hex(nil) == ""
|
|
assert Encoding.to_hex(123) == ""
|
|
end
|
|
|
|
test "handles boundary byte values" do
|
|
assert Encoding.to_hex(<<0>>) == "00"
|
|
assert Encoding.to_hex(<<255>>) == "FF"
|
|
end
|
|
end
|
|
|
|
describe "strip_mice_telemetry/1" do
|
|
test "removes Mic-E telemetry prefix from comment" do
|
|
assert Encoding.strip_mice_telemetry("!w>`!Clb=6.4m/s t=-66.7C") == "Clb=6.4m/s t=-66.7C"
|
|
assert Encoding.strip_mice_telemetry("!w_'!Clb=4.6m/s t=-3.8C") == "Clb=4.6m/s t=-3.8C"
|
|
assert Encoding.strip_mice_telemetry("!w_P!Clb=4.8m/s t=-65.7C") == "Clb=4.8m/s t=-65.7C"
|
|
assert Encoding.strip_mice_telemetry("!w;i!Clb=2.5m/s t=-66.4C") == "Clb=2.5m/s t=-66.4C"
|
|
assert Encoding.strip_mice_telemetry("!w;D!Clb=2.8m/s t=-65.3C") == "Clb=2.8m/s t=-65.3C"
|
|
end
|
|
|
|
test "removes multiple consecutive Mic-E telemetry sequences" do
|
|
assert Encoding.strip_mice_telemetry("!w>`!!abc!Normal comment") == "Normal comment"
|
|
end
|
|
|
|
test "does not remove exclamation marks from middle of comment" do
|
|
assert Encoding.strip_mice_telemetry("Normal! comment!") == "Normal! comment!"
|
|
end
|
|
|
|
test "leaves normal comments unchanged" do
|
|
assert Encoding.strip_mice_telemetry("Normal comment") == "Normal comment"
|
|
assert Encoding.strip_mice_telemetry("Hello World") == "Hello World"
|
|
assert Encoding.strip_mice_telemetry("") == ""
|
|
end
|
|
|
|
test "handles nil input" do
|
|
assert Encoding.strip_mice_telemetry(nil) == nil
|
|
end
|
|
|
|
test "handles non-binary input" do
|
|
assert Encoding.strip_mice_telemetry(123) == 123
|
|
end
|
|
end
|
|
end
|