aprs.me/test/aprsme/log_sanitizer_test.exs
Graham McIntire b86153cd27
Fix all mix credo --strict warnings (188 → 0)
- Replace apply/2 with direct fully-qualified calls in movement_test
- Fix assert_receive timeouts < 1000ms across 8 test files
- Move nested import statements to module-level scope
- Fix tests with no assertions and add missing doctest
- Replace weak type assertions with specific value checks
- Fix conditional assertions and length/1 expensive patterns
- Disable inappropriate Jump.CredoChecks.AvoidSocketAssignsInTest
- Fix tests not calling application code with credo:disable
- Add various credo:disable comments for legitimate patterns
2026-06-12 16:27:20 -05:00

280 lines
9.3 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 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
test "non-list stacktrace falls through to else-branch (line 135)" do
result = LogSanitizer.sanitize_error_context(%{stacktrace: "not-a-list"})
assert result[:stacktrace] == "not-a-list"
end
test "non-map context returns the input unchanged (line 140)" do
assert LogSanitizer.sanitize_error_context(:not_a_map) == :not_a_map
assert LogSanitizer.sanitize_error_context("string") == "string"
end
end
describe "redact_match catch-all branch (line 56)" do
test "sanitize_string with redact-able token without '=' or ':' separator hits catch-all" do
# The redact_match helper splits on '=' and ':'. A pattern matching the
# sensitive regex but containing neither separator should hit the
# catch-all (line 56).
# Concrete sensitive substrings without separators are constructed
# by sanitize_string regex; we just exercise the helper through
# the public sanitize/1 with a value that triggers the secret pattern.
result = LogSanitizer.sanitize("password test")
assert String.contains?(result, "password")
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()), max_runs: 25) 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