diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/util/SecretMaskerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/util/SecretMaskerTest.java new file mode 100644 index 000000000..b720cd043 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/util/SecretMaskerTest.java @@ -0,0 +1,178 @@ +package stirling.software.proprietary.util; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link SecretMasker}. + * + *

Assumptions: + * - Key matching is case-insensitive via the pattern in SENSITIVE. + * - If the key matches a sensitive pattern, the value is replaced with "***REDACTED***". + * - Nested maps and lists are searched recursively. + * - Null maps and null values are ignored or returned as null. + * - Non-sensitive keys/values remain unchanged. + */ +class SecretMaskerTest { + + @Nested + @DisplayName("mask(Map) method") + class MaskMethod { + + @Test + @DisplayName("should return null when input map is null") + void shouldReturnNullWhenInputIsNull() { + assertNull(SecretMasker.mask(null)); + } + + @Test + @DisplayName("should mask simple sensitive keys at root level") + void shouldMaskSimpleSensitiveKeys() { + Map input = + Map.of( + "password", "mySecret", + "username", "john"); + + Map result = SecretMasker.mask(input); + + assertEquals("***REDACTED***", result.get("password")); + assertEquals("john", result.get("username")); + } + + @Test + @DisplayName("should mask keys case-insensitively and with special characters") + void shouldMaskKeysCaseInsensitive() { + Map input = + Map.of( + "Api-Key", "12345", + "TOKEN", "abcde", + "normal", "keepme"); + + Map result = SecretMasker.mask(input); + + assertEquals("***REDACTED***", result.get("Api-Key")); + assertEquals("***REDACTED***", result.get("TOKEN")); + assertEquals("keepme", result.get("normal")); + } + + @Test + @DisplayName("should mask nested map sensitive keys") + void shouldMaskNestedMapSensitiveKeys() { + Map input = + Map.of( + "outer", + Map.of( + "jwt", + "tokenValue", + "inner", + Map.of( + "secret", "deepValue", + "other", "ok"))); + + Map result = SecretMasker.mask(input); + + Map outer = (Map) result.get("outer"); + assertEquals("***REDACTED***", outer.get("jwt")); + Map inner = (Map) outer.get("inner"); + assertEquals("***REDACTED***", inner.get("secret")); + assertEquals("ok", inner.get("other")); + } + + @Test + @DisplayName("should mask sensitive keys inside lists") + void shouldMaskSensitiveKeysInsideLists() { + Map input = + Map.of( + "list", + List.of( + Map.of("token", "abc123"), + Map.of("username", "john"), + "stringValue")); + + Map result = SecretMasker.mask(input); + + List list = (List) result.get("list"); + Map first = (Map) list.get(0); + assertEquals("***REDACTED***", first.get("token")); + Map second = (Map) list.get(1); + assertEquals("john", second.get("username")); + assertEquals("stringValue", list.get(2)); + } + + @Test + @DisplayName("should ignore null values") + void shouldIgnoreNullValues() { + // IMPORTANT: Map.of(...) does not allow nulls -> use a mutable Map instead + Map input = new HashMap<>(); + input.put("password", null); + input.put("normal", null); + + Map result = SecretMasker.mask(input); + + // Null values are completely filtered out + assertFalse(result.containsKey("password")); + assertFalse(result.containsKey("normal")); + assertTrue(result.isEmpty(), "Result map should be empty if all entries were null"); + } + + @Test + @DisplayName("should not mask when key does not match pattern") + void shouldNotMaskWhenKeyNotSensitive() { + Map input = Map.of("email", "test@example.com"); + + Map result = SecretMasker.mask(input); + assertEquals("test@example.com", result.get("email")); + } + } + + @Nested + @DisplayName("Deep masking edge branches") + class DeepMaskBranches { + + @Test + @DisplayName("should filter out null values inside nested map") + void shouldFilterOutNullValuesInsideNestedMap() { + // outer -> { inner -> { "token": null, "username": "john" } } + Map inner = new HashMap<>(); + inner.put("token", null); // <- should be filtered out in the result (branch false) + inner.put("username", "john"); // <- should remain + + Map input = Map.of("outer", Map.of("inner", inner)); + + Map result = SecretMasker.mask(input); + + Map outer = (Map) result.get("outer"); + Map maskedInner = (Map) outer.get("inner"); + + // "token" was null -> should be completely absent (filter branch in deepMask(Map)) + assertFalse(maskedInner.containsKey("token")); + // "username" remains unchanged + assertEquals("john", maskedInner.get("username")); + } + + @Test + @DisplayName("should not mask when key is null (falls back to deepMask(value))") + void shouldNotMaskWhenKeyIsNull() { + // Map with null key: { null: "plainText", "password": "toHide" } + Map sensitive = new HashMap<>(); + sensitive.put(null, "plainText"); // <- key == null -> no masking, value stays + sensitive.put("password", "toHide"); // <- sensitive key -> will be masked + + Map input = Map.of("outer", sensitive); + + Map result = SecretMasker.mask(input); + + Map outer = (Map) result.get("outer"); + assertTrue(outer.containsKey(null), "Null key should be preserved"); + assertEquals("plainText", outer.get(null), "Value for null key must not be masked"); + assertEquals("***REDACTED***", outer.get("password"), "Sensitive keys must be masked"); + } + } +}