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.
258 lines
8.2 KiB
Elixir
258 lines
8.2 KiB
Elixir
defmodule Aprsme.LogSanitizerTest do
|
|
use ExUnit.Case, async: true
|
|
use ExUnitProperties
|
|
|
|
alias Aprsme.LogSanitizer
|
|
alias Aprsme.Support.FakeSensitiveStruct
|
|
|
|
describe "sanitize_map/1" do
|
|
test "redacts known sensitive atom keys" do
|
|
result = LogSanitizer.sanitize_map(%{password: "hunter2", user: "alice"})
|
|
assert result[:password] == "[REDACTED]"
|
|
assert result[:user] == "alice"
|
|
end
|
|
|
|
test "redacts known sensitive string keys" do
|
|
result = LogSanitizer.sanitize_map(%{"secret" => "xyz", "other" => "keep"})
|
|
assert result["secret"] == "[REDACTED]"
|
|
assert result["other"] == "keep"
|
|
end
|
|
|
|
test "leaves maps without sensitive keys alone" do
|
|
map = %{foo: 1, bar: "baz"}
|
|
assert LogSanitizer.sanitize_map(map) == map
|
|
end
|
|
|
|
test "redacts every sensitive field when all present" do
|
|
fields = [
|
|
:password,
|
|
:passcode,
|
|
:auth_token,
|
|
:api_key,
|
|
:secret,
|
|
:token,
|
|
:authorization,
|
|
:csrf_token,
|
|
:session_token,
|
|
:private_key,
|
|
:database_url,
|
|
:secret_key_base,
|
|
:signing_salt
|
|
]
|
|
|
|
input = Map.new(fields, fn f -> {f, "value"} end)
|
|
result = LogSanitizer.sanitize_map(input)
|
|
|
|
for f <- fields do
|
|
assert result[f] == "[REDACTED]", "expected #{inspect(f)} to be redacted"
|
|
end
|
|
end
|
|
|
|
test "returns non-map input unchanged" do
|
|
assert LogSanitizer.sanitize_map("hello") == "hello"
|
|
assert LogSanitizer.sanitize_map(nil) == nil
|
|
assert LogSanitizer.sanitize_map(42) == 42
|
|
end
|
|
end
|
|
|
|
describe "sanitize_string/1" do
|
|
test "redacts patterns like password=secret" do
|
|
assert LogSanitizer.sanitize_string("password=hunter2") == "password=[REDACTED]"
|
|
end
|
|
|
|
test "redacts patterns like token: abc" do
|
|
# Pattern is "token[=:]\s*\S+" — `:` followed by non-whitespace.
|
|
result = LogSanitizer.sanitize_string("token:abc123")
|
|
assert result == "token=[REDACTED]"
|
|
end
|
|
|
|
test "redacts multiple occurrences in a single string" do
|
|
input = "password=foo secret=bar"
|
|
result = LogSanitizer.sanitize_string(input)
|
|
assert result =~ "password=[REDACTED]"
|
|
assert result =~ "secret=[REDACTED]"
|
|
end
|
|
|
|
test "is case-insensitive" do
|
|
assert LogSanitizer.sanitize_string("PASSWORD=X") =~ "[REDACTED]"
|
|
assert LogSanitizer.sanitize_string("Token=Y") =~ "[REDACTED]"
|
|
end
|
|
|
|
test "returns non-binary input unchanged" do
|
|
assert LogSanitizer.sanitize_string(nil) == nil
|
|
assert LogSanitizer.sanitize_string(42) == 42
|
|
assert LogSanitizer.sanitize_string(%{}) == %{}
|
|
end
|
|
|
|
test "leaves strings without sensitive patterns alone" do
|
|
assert LogSanitizer.sanitize_string("all clear here") == "all clear here"
|
|
end
|
|
end
|
|
|
|
describe "sanitize/1 (recursive)" do
|
|
test "walks nested maps" do
|
|
input = %{
|
|
outer: %{password: "x", inner: %{token: "y", keep: "ok"}}
|
|
}
|
|
|
|
result = LogSanitizer.sanitize(input)
|
|
assert result.outer.password == "[REDACTED]"
|
|
assert result.outer.inner.token == "[REDACTED]"
|
|
assert result.outer.inner.keep == "ok"
|
|
end
|
|
|
|
test "walks lists" do
|
|
input = [%{password: "x"}, %{keep: 1}, "password=y"]
|
|
[first, second, third] = LogSanitizer.sanitize(input)
|
|
assert first.password == "[REDACTED]"
|
|
assert second == %{keep: 1}
|
|
assert third == "password=[REDACTED]"
|
|
end
|
|
|
|
test "sanitizes exception structs via Exception.message/1" do
|
|
exception = %RuntimeError{message: "failed with password=abc"}
|
|
result = LogSanitizer.sanitize(exception)
|
|
assert is_binary(result)
|
|
assert result =~ "[REDACTED]"
|
|
refute result =~ "abc"
|
|
end
|
|
|
|
test "converts non-exception structs and sanitizes them" do
|
|
result = LogSanitizer.sanitize(%FakeSensitiveStruct{password: "x", name: "ok"})
|
|
assert result[:password] == "[REDACTED]"
|
|
assert result[:name] == "ok"
|
|
end
|
|
|
|
test "passes through scalar values" do
|
|
assert LogSanitizer.sanitize(42) == 42
|
|
assert LogSanitizer.sanitize(:atom) == :atom
|
|
assert LogSanitizer.sanitize(true) == true
|
|
assert LogSanitizer.sanitize(nil) == nil
|
|
end
|
|
end
|
|
|
|
describe "sanitize_packet_data/1" do
|
|
test "drops raw_packet, comment, status, and data_extended" do
|
|
input = %{
|
|
raw_packet: "should be gone",
|
|
comment: "also gone",
|
|
status: "gone",
|
|
data_extended: %{foo: :bar},
|
|
sender: "K5ABC"
|
|
}
|
|
|
|
result = LogSanitizer.sanitize_packet_data(input)
|
|
refute Map.has_key?(result, :raw_packet)
|
|
refute Map.has_key?(result, :comment)
|
|
refute Map.has_key?(result, :status)
|
|
refute Map.has_key?(result, :data_extended)
|
|
assert result[:sender] == "K5ABC"
|
|
end
|
|
|
|
test "also redacts sensitive keys that sneak through" do
|
|
result = LogSanitizer.sanitize_packet_data(%{password: "x", sender: "K5ABC"})
|
|
assert result[:password] == "[REDACTED]"
|
|
end
|
|
|
|
test "returns non-map input unchanged" do
|
|
assert LogSanitizer.sanitize_packet_data("raw string") == "raw string"
|
|
end
|
|
end
|
|
|
|
describe "sanitize_connection_info/1" do
|
|
test "always redacts password and passcode even if missing" do
|
|
result = LogSanitizer.sanitize_connection_info(%{host: "rotate.aprs.net"})
|
|
assert result[:password] == "[REDACTED]"
|
|
assert result[:passcode] == "[REDACTED]"
|
|
assert result[:host] == "rotate.aprs.net"
|
|
end
|
|
|
|
test "redacts pre-existing password values" do
|
|
result = LogSanitizer.sanitize_connection_info(%{password: "real", passcode: "12345"})
|
|
assert result[:password] == "[REDACTED]"
|
|
assert result[:passcode] == "[REDACTED]"
|
|
end
|
|
|
|
test "returns non-map input unchanged" do
|
|
assert LogSanitizer.sanitize_connection_info(nil) == nil
|
|
end
|
|
end
|
|
|
|
describe "sanitize_error_context/1" do
|
|
test "truncates stacktraces longer than 5 entries" do
|
|
stacktrace = Enum.map(1..10, fn i -> {:module, :fun, [i], []} end)
|
|
result = LogSanitizer.sanitize_error_context(%{stacktrace: stacktrace})
|
|
assert length(result[:stacktrace]) == 5
|
|
end
|
|
|
|
test "leaves stacktraces under 5 entries alone" do
|
|
stacktrace = [{:module, :fun, [], []}]
|
|
result = LogSanitizer.sanitize_error_context(%{stacktrace: stacktrace})
|
|
assert result[:stacktrace] == stacktrace
|
|
end
|
|
|
|
test "still sanitizes sensitive keys" do
|
|
result = LogSanitizer.sanitize_error_context(%{password: "x", stacktrace: []})
|
|
assert result[:password] == "[REDACTED]"
|
|
end
|
|
end
|
|
|
|
describe "log_data/1" do
|
|
test "sanitizes each value in a keyword list" do
|
|
result = LogSanitizer.log_data(user: "alice", config: %{secret: "x"})
|
|
assert result[:user] == "alice"
|
|
assert result[:config][:secret] == "[REDACTED]"
|
|
end
|
|
|
|
test "falls back to sanitize/1 for non-keyword input" do
|
|
assert LogSanitizer.log_data(%{password: "x"})[:password] == "[REDACTED]"
|
|
assert LogSanitizer.log_data("password=x") == "password=[REDACTED]"
|
|
end
|
|
end
|
|
|
|
describe "property: sanitize/1 is idempotent" do
|
|
property "running sanitize twice equals running it once on maps" do
|
|
check all(map <- map_of(atom(:alphanumeric), term())) do
|
|
once = LogSanitizer.sanitize(map)
|
|
twice = LogSanitizer.sanitize(once)
|
|
assert once == twice
|
|
end
|
|
end
|
|
|
|
property "running sanitize_string twice equals running it once" do
|
|
check all(string <- string(:printable)) do
|
|
once = LogSanitizer.sanitize_string(string)
|
|
twice = LogSanitizer.sanitize_string(once)
|
|
assert once == twice
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "property: sensitive values never leak through" do
|
|
property "any value stored under a sensitive atom key is replaced" do
|
|
sensitive_fields = [
|
|
:password,
|
|
:passcode,
|
|
:auth_token,
|
|
:api_key,
|
|
:secret,
|
|
:token,
|
|
:authorization,
|
|
:csrf_token,
|
|
:session_token,
|
|
:private_key,
|
|
:database_url,
|
|
:secret_key_base,
|
|
:signing_salt
|
|
]
|
|
|
|
check all(
|
|
field <- member_of(sensitive_fields),
|
|
value <- string(:printable, min_length: 1)
|
|
) do
|
|
result = LogSanitizer.sanitize_map(%{field => value})
|
|
assert result[field] == "[REDACTED]"
|
|
end
|
|
end
|
|
end
|
|
end
|