Add convenience methods to SanitizingFunction

Add a `sanitizeValue()` factory method and `if...` methods that can
be used to quickly build a `SanitizingFunction`.

Closes gh-39243
This commit is contained in:
Phillip Webb
2025-02-11 18:43:38 -08:00
parent 60adee359a
commit 3eee1f1ad0
5 changed files with 764 additions and 1 deletions

View File

@@ -16,6 +16,8 @@
package org.springframework.boot.actuate.endpoint;
import java.util.Locale;
import org.springframework.core.env.PropertySource;
/**
@@ -36,6 +38,8 @@ public final class SanitizableData {
private final String key;
private String lowerCaseKey;
private final Object value;
/**
@@ -67,6 +71,20 @@ public final class SanitizableData {
return this.key;
}
/**
* Return the key as a lowercase value.
* @return the key as a lowercase value
* @since 3.5.0
*/
public String getLowerCaseKey() {
String result = this.lowerCaseKey;
if (result == null && this.key != null) {
result = this.key.toLowerCase(Locale.getDefault());
this.lowerCaseKey = result;
}
return result;
}
/**
* Return the value of the data.
* @return the data value

View File

@@ -72,7 +72,7 @@ public class Sanitizer {
return SanitizableData.SANITIZED_VALUE;
}
for (SanitizingFunction sanitizingFunction : this.sanitizingFunctions) {
data = sanitizingFunction.apply(data);
data = sanitizingFunction.applyUnlessFiltered(data);
Object sanitizedValue = data.getValue();
if (!value.equals(sanitizedValue)) {
return sanitizedValue;

View File

@@ -16,12 +16,27 @@
package org.springframework.boot.actuate.endpoint;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import org.springframework.util.Assert;
/**
* Function that takes a {@link SanitizableData} and applies sanitization to the value, if
* necessary. Can be used by a {@link Sanitizer} to determine the sanitized value.
* <p>
* This interface also provides convenience methods that can help build a
* {@link SanitizingFunction} instances, for example to return from a {@code @Bean}
* method. See {@link #sanitizeValue()} for an example.
*
* @author Madhura Bhave
* @author Phillip Webb
* @since 2.6.0
* @see Sanitizer
*/
@FunctionalInterface
public interface SanitizingFunction {
@@ -33,4 +48,387 @@ public interface SanitizingFunction {
*/
SanitizableData apply(SanitizableData data);
/**
* Return an optional filter that determines if the sanitizing function applies.
* @return a predicate used to filter functions or {@code null} if no filter is
* declared
* @since 3.5.0
* @see #applyUnlessFiltered(SanitizableData)
*/
default Predicate<SanitizableData> filter() {
return null;
}
/**
* Apply the sanitizing function as long as the filter passes or there is no filter.
* @param data the data to sanitize
* @return the sanitized data or the original instance is no sanitization is applied
* @since 3.5.0
*/
default SanitizableData applyUnlessFiltered(SanitizableData data) {
return (filter() == null || filter().test(data)) ? apply(data) : data;
}
/**
* Return a new function with a filter that <em>also</em> applies if the data is
* likely to contain a sensitive value. This method can help construct a useful
* sanitizing function, but may not catch all sensitive data so care should be taken
* to test the results for your specific environment.
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifLikelySenstive() {
return ifLikelyCredential().ifLikelyUri().ifLikelySenstiveEnvironmentVariable().ifVcapServices();
}
/**
* Return a new function with a filter that <em>also</em> applies if the data is
* likely to contain a credential. This method can help construct a useful sanitizing
* function, but may not catch all sensitive data so care should be taken to test the
* results for your specific environment.
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifLikelyCredential() {
return ifKeyEndsWith("password", "secret", "key", "token").ifKeyContains("credentials");
}
/**
* Return a new function with a filter that <em>also</em> applies if the data is
* likely to contain a URI. This method can help construct a useful sanitizing
* function, but may not catch all sensitive data so care should be taken to test the
* results for your specific environment.
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifLikelyUri() {
return ifKeyEndsWith("uri", "uris", "url", "urls", "address", "addresses");
}
/**
* Return a new function with a filter that <em>also</em> applies if the data is
* likely to sensitive environment variable value. This method can help construct a
* useful sanitizing function, but may not catch all sensitive data so care should be
* taken to test the results for your specific environment.
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifLikelySenstiveEnvironmentVariable() {
return ifKeyMatches("sun.java.command", "^spring[._]application[._]json$");
}
/**
* Return a new function with a filter that <em>also</em> applies if the data is for
* VCAP services.
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifVcapServices() {
return ifKeyEquals("vcap_services").ifKeyMatches("^vcap\\.services.*$");
}
/**
* Return a new function with a filter that <em>also</em> applies if the data key is
* equal to any of the given values (ignoring case).
* @param values the case insensitive values that the key can equal
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifKeyEquals(String... values) {
Assert.notNull(values, "'values' must not be null");
return ifKeyMatchesIgnoringCase(String::equals, values);
}
/**
* Return a new function with a filter that <em>also</em> applies if the data key ends
* with any of the given values (ignoring case).
* @param suffixes the case insensitive suffixes that they key can end with
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifKeyEndsWith(String... suffixes) {
Assert.notNull(suffixes, "'suffixes' must not be null");
return ifKeyMatchesIgnoringCase(String::endsWith, suffixes);
}
/**
* Return a new function with a filter that <em>also</em> applies if the data key
* contains any of the given values (ignoring case).
* @param values the case insensitive values that the key can contain
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifKeyContains(String... values) {
Assert.notNull(values, "'values' must not be null");
return ifKeyMatchesIgnoringCase(String::contains, values);
}
/**
* Return a new function with a filter that <em>also</em> applies if the data key and
* any of the values match the given predicate. The predicate is only called with
* lower case values.
* @param predicate the predicate used to check the key against a value. The key is
* the first argument and the value is the second. Both are converted to lower case
* @param values the case insensitive values that the key can match
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifKeyMatchesIgnoringCase(BiPredicate<String, String> predicate, String... values) {
Assert.notNull(predicate, "'predicate' must not be null");
Assert.notNull(values, "'values' must not be null");
return ifMatches(Arrays.stream(values).map((value) -> onKeyIgnoringCase(predicate, value)).toList());
}
/**
* Return a new function with a filter that <em>also</em> applies if the data key
* matches any of the given regex patterns (ignoring case).
* @param regexes the case insensitive regexes that the key can match
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifKeyMatches(String... regexes) {
Assert.notNull(regexes, "'regexes' must not be null");
return ifKeyMatches(Arrays.stream(regexes).map(this::caseInsensitivePattern).toArray(Pattern[]::new));
}
/**
* Return a new function with a filter that <em>also</em> applies if the data key
* matches any of the given patterns.
* @param patterns the patterns that the key can match
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifKeyMatches(Pattern... patterns) {
Assert.notNull(patterns, "'patterns' must not be null");
return ifKeyMatches(Arrays.stream(patterns).map(Pattern::asMatchPredicate).toList());
}
/**
* Return a new function with a filter that <em>also</em> applies if the data key
* matches any of the given predicates.
* @param predicates the predicates that the key can match
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifKeyMatches(List<Predicate<String>> predicates) {
Assert.notNull(predicates, "'predicates' must not be null");
return ifMatches(predicates.stream().map(this::onKey).toList());
}
/**
* Return a new function with a filter that <em>also</em> applies if the data key
* matches any of the given predicate.
* @param predicate the predicate that the key can match
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifKeyMatches(Predicate<String> predicate) {
Assert.notNull(predicate, "'predicate' must not be null");
return ifMatches(onKey(predicate));
}
/**
* Return a new function with a filter that <em>also</em> applies if the data string
* value matches any of the given regex patterns (ignoring case).
* @param regexes the case insensitive regexes that the values string can match
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifValueStringMatches(String... regexes) {
Assert.notNull(regexes, "'regexes' must not be null");
return ifValueStringMatches(Arrays.stream(regexes).map(this::caseInsensitivePattern).toArray(Pattern[]::new));
}
/**
* Return a new function with a filter that <em>also</em> applies if the data string
* value matches any of the given patterns.
* @param patterns the patterns that the value string can match
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifValueStringMatches(Pattern... patterns) {
Assert.notNull(patterns, "'patterns' must not be null");
return ifValueStringMatches(Arrays.stream(patterns).map(Pattern::asMatchPredicate).toList());
}
/**
* Return a new function with a filter that <em>also</em> applies if the data string
* value matches any of the given predicates.
* @param predicates the predicates that the value string can match
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifValueStringMatches(List<Predicate<String>> predicates) {
Assert.notNull(predicates, "'predicates' must not be null");
return ifMatches(predicates.stream().map(this::onValueString).toList());
}
/**
* Return a new function with a filter that <em>also</em> applies if the data value
* matches any of the given predicates.
* @param predicates the predicates that the value can match
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifValueMatches(List<Predicate<Object>> predicates) {
Assert.notNull(predicates, "'predicates' must not be null");
return ifMatches(predicates.stream().map(this::onValue).toList());
}
/**
* Return a new function with a filter that <em>also</em> applies if the data string
* value matches the given predicate.
* @param predicate the predicate that the value string can match
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifValueStringMatches(Predicate<String> predicate) {
Assert.notNull(predicate, "'predicate' must not be null");
return ifMatches(onValueString(predicate));
}
/**
* Return a new function with a filter that <em>also</em> applies if the data value
* matches the given predicate.
* @param predicate the predicate that the value can match
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifValueMatches(Predicate<Object> predicate) {
Assert.notNull(predicate, "'predicate' must not be null");
return ifMatches((data) -> predicate.test(data.getValue()));
}
/**
* Return a new function with a filter that <em>also</em> applies if the data matches
* any of the given predicates.
* @param predicates the predicates that the data can match
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifMatches(List<Predicate<SanitizableData>> predicates) {
Assert.notNull(predicates, "'predicates' must not be null");
Predicate<SanitizableData> combined = null;
for (Predicate<SanitizableData> predicate : predicates) {
combined = (combined != null) ? combined.or(predicate) : predicate;
}
return ifMatches(combined);
}
/**
* Return a new function with a filter that <em>also</em> applies if the data matches
* the given predicate.
* @param predicate the predicate that the data can match
* @return a new sanitizing function with an updated {@link #filter()}
* @since 3.5.0
* @see #filter()
* @see #sanitizeValue()
*/
default SanitizingFunction ifMatches(Predicate<SanitizableData> predicate) {
Assert.notNull(predicate, "'predicate' must not be null");
Predicate<SanitizableData> filter = (filter() != null) ? filter().or(predicate) : predicate;
return new SanitizingFunction() {
@Override
public Predicate<SanitizableData> filter() {
return filter;
}
@Override
public SanitizableData apply(SanitizableData data) {
return SanitizingFunction.this.apply(data);
}
};
}
private Pattern caseInsensitivePattern(String regex) {
Assert.notNull(regex, "'regex' must not be null");
return Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
}
private Predicate<SanitizableData> onKeyIgnoringCase(BiPredicate<String, String> predicate, String value) {
Assert.notNull(predicate, "'predicate' must not be null");
Assert.notNull(value, "'value' must not be null");
String lowerCaseValue = value.toLowerCase(Locale.getDefault());
return (data) -> nullSafeTest(data.getLowerCaseKey(),
(lowerCaseKey) -> predicate.test(lowerCaseKey, lowerCaseValue));
}
private Predicate<SanitizableData> onKey(Predicate<String> predicate) {
Assert.notNull(predicate, "'predicate' must not be null");
return (data) -> nullSafeTest(data.getKey(), predicate);
}
private Predicate<SanitizableData> onValue(Predicate<Object> predicate) {
Assert.notNull(predicate, "'predicate' must not be null");
return (data) -> nullSafeTest(data.getValue(), predicate);
}
private Predicate<SanitizableData> onValueString(Predicate<String> predicate) {
Assert.notNull(predicate, "'predicate' must not be null");
return (data) -> nullSafeTest((data.getValue() != null) ? data.getValue().toString() : null, predicate);
}
private <T> boolean nullSafeTest(T value, Predicate<T> predicate) {
return value != null && predicate.test(value);
}
/**
* Factory method to return a {@link SanitizingFunction} that sanitizes the value.
* This method is often chained with one or more {@code if...} methods. For example:
* <pre class="code">
* return SanitizingFunction.sanitizeValue()
* .ifKeyContains("password", "secret")
* .ifValueStringMatches("^gh._[a-zA-Z0-9]{36}$");
* </pre>
* @return a {@link SanitizingFunction} that sanitizes values.
*/
static SanitizingFunction sanitizeValue() {
return SanitizableData::withSanitizedValue;
}
}

View File

@@ -93,6 +93,15 @@ class SanitizerTests {
assertThat(sanitizer.sanitize(password, true)).isEqualTo("------");
}
@Test
void overridingDefaultSanitizingFunctionWithFiltered() {
Sanitizer sanitizer = new Sanitizer(List.of(SanitizingFunction.sanitizeValue().ifLikelySenstive()));
SanitizableData other = new SanitizableData(null, "other", "123456");
SanitizableData password = new SanitizableData(null, "password", "123456");
assertThat(sanitizer.sanitize(other, true)).isEqualTo("123456");
assertThat(sanitizer.sanitize(password, true)).isEqualTo(SanitizableData.SANITIZED_VALUE);
}
@Test
void whenValueSanitizedLaterSanitizingFunctionsShouldBeSkipped() {
final String sameKey = "custom";

View File

@@ -0,0 +1,338 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import org.assertj.core.api.Condition;
import org.assertj.core.api.ObjectAssert;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SanitizingFunction}.
*
* @author Phillip Webb
*/
class SanitizingFunctionTests {
private static final SanitizableData data = data("key");
@Test
void applyUnlessFilteredWhenHasNoFilterReturnsFiltered() {
SanitizingFunction function = SanitizingFunction.sanitizeValue();
assertThat(function.apply(data)).has(sanitizedValue());
assertThat(function.applyUnlessFiltered(data)).has(sanitizedValue());
}
@Test
void applyUnlessFilteredWhenHasFilterTestingTrueReturnsFiltered() {
SanitizingFunction function = SanitizingFunction.sanitizeValue().ifMatches((data) -> true);
assertThat(function.apply(data)).has(sanitizedValue());
assertThat(function.applyUnlessFiltered(data)).has(sanitizedValue());
}
@Test
void applyUnlessFilteredWhenHasFilterTestingFalseReturnsUnfiltered() {
SanitizingFunction function = SanitizingFunction.sanitizeValue().ifMatches((data) -> false);
assertThat(function.apply(data)).has(sanitizedValue());
assertThat(function.applyUnlessFiltered(data)).has(unsanitizedValue());
}
@Test
void ifLikelySenstiveFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue().ifLikelySenstive();
assertThat(function).satisfies(this::likelyCredentialChecks, this::likelyUriChecks,
this::likelySenstiveEnvironmentVariableChecks, this::vcapServicesChecks);
}
@Test
void ifLikelyCredentialFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue().ifLikelyCredential();
assertThat(function).satisfies(this::likelyCredentialChecks);
}
private void likelyCredentialChecks(SanitizingFunction function) {
assertThatApplyingToKey(function, "password").has(sanitizedValue());
assertThatApplyingToKey(function, "database.password").has(sanitizedValue());
assertThatApplyingToKey(function, "PASSWORD").has(sanitizedValue());
assertThatApplyingToKey(function, "secret").has(sanitizedValue());
assertThatApplyingToKey(function, "key").has(sanitizedValue());
assertThatApplyingToKey(function, "token").has(sanitizedValue());
assertThatApplyingToKey(function, "credentials").has(sanitizedValue());
assertThatApplyingToKey(function, "thecredentialssecret").has(sanitizedValue());
assertThatApplyingToKey(function, "some.credentials.here").has(sanitizedValue());
assertThatApplyingToKey(function, "test").has(unsanitizedValue());
}
@Test
void ifLikelyUriFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue().ifLikelyUri();
assertThat(function).satisfies(this::likelyUriChecks);
}
private void likelyUriChecks(SanitizingFunction function) {
assertThatApplyingToKey(function, "uri").has(sanitizedValue());
assertThatApplyingToKey(function, "URI").has(sanitizedValue());
assertThatApplyingToKey(function, "database.uri").has(sanitizedValue());
assertThatApplyingToKey(function, "uris").has(sanitizedValue());
assertThatApplyingToKey(function, "url").has(sanitizedValue());
assertThatApplyingToKey(function, "urls").has(sanitizedValue());
assertThatApplyingToKey(function, "address").has(sanitizedValue());
assertThatApplyingToKey(function, "addresses").has(sanitizedValue());
assertThatApplyingToKey(function, "test").has(unsanitizedValue());
}
@Test
void ifLikelySenstiveEnvironmentVariableFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue().ifLikelySenstiveEnvironmentVariable();
assertThat(function).satisfies(this::likelySenstiveEnvironmentVariableChecks);
}
private void likelySenstiveEnvironmentVariableChecks(SanitizingFunction function) {
assertThatApplyingToKey(function, "sun.java.command").has(sanitizedValue());
assertThatApplyingToKey(function, "spring.application.json").has(sanitizedValue());
assertThatApplyingToKey(function, "SPRING_APPLICATION_JSON").has(sanitizedValue());
assertThatApplyingToKey(function, "some.other.json").has(unsanitizedValue());
}
@Test
void ifVcapServicesFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue().ifVcapServices();
assertThat(function).satisfies(this::vcapServicesChecks);
}
private void vcapServicesChecks(SanitizingFunction function) {
assertThatApplyingToKey(function, "vcap_services").has(sanitizedValue());
assertThatApplyingToKey(function, "vcap.services").has(sanitizedValue());
assertThatApplyingToKey(function, "vcap.services.whatever").has(sanitizedValue());
assertThatApplyingToKey(function, "notvcap.services").has(unsanitizedValue());
}
@Test
void ifKeyEqualsFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue().ifKeyEquals("spring", "test");
assertThatApplyingToKey(function, "spring").has(sanitizedValue());
assertThatApplyingToKey(function, "SPRING").has(sanitizedValue());
assertThatApplyingToKey(function, "SpRiNg").has(sanitizedValue());
assertThatApplyingToKey(function, "test").has(sanitizedValue());
assertThatApplyingToKey(function, "boot").has(unsanitizedValue());
assertThatApplyingToKey(function, "xspring").has(unsanitizedValue());
assertThatApplyingToKey(function, "springx").has(unsanitizedValue());
assertThatApplyingToKey(function, null).has(unsanitizedValue());
}
@Test
void ifKeyEndsWithFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue().ifKeyEndsWith("boot", "test");
assertThatApplyingToKey(function, "springboot").has(sanitizedValue());
assertThatApplyingToKey(function, "SPRINGboot").has(sanitizedValue());
assertThatApplyingToKey(function, "springBOOT").has(sanitizedValue());
assertThatApplyingToKey(function, "boot").has(sanitizedValue());
assertThatApplyingToKey(function, "atest").has(sanitizedValue());
assertThatApplyingToKey(function, "bootx").has(unsanitizedValue());
assertThatApplyingToKey(function, null).has(unsanitizedValue());
}
@Test
void ifKeyContainsFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue().ifKeyContains("oo", "ee");
assertThatApplyingToKey(function, "oo").has(sanitizedValue());
assertThatApplyingToKey(function, "OO").has(sanitizedValue());
assertThatApplyingToKey(function, "bOOt").has(sanitizedValue());
assertThatApplyingToKey(function, "boot").has(sanitizedValue());
assertThatApplyingToKey(function, "beet").has(sanitizedValue());
assertThatApplyingToKey(function, "spring").has(unsanitizedValue());
assertThatApplyingToKey(function, null).has(unsanitizedValue());
}
@Test
void ifKeyMatchesIgnoringCaseFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue()
.ifKeyMatchesIgnoringCase((key, value) -> key.startsWith(value) && key.endsWith(value), "x", "y");
assertThatApplyingToKey(function, "xtestx").has(sanitizedValue());
assertThatApplyingToKey(function, "XtestX").has(sanitizedValue());
assertThatApplyingToKey(function, "YY").has(sanitizedValue());
assertThatApplyingToKey(function, "xy").has(unsanitizedValue());
assertThatApplyingToKey(function, null).has(unsanitizedValue());
}
@Test
void ifKeyMatchesWithRegexFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue().ifKeyMatches("^sp.*$", "^bo.*$");
assertThatApplyingToKey(function, "spring").has(sanitizedValue());
assertThatApplyingToKey(function, "spin").has(sanitizedValue());
assertThatApplyingToKey(function, "SPRING").has(sanitizedValue());
assertThatApplyingToKey(function, "BOOT").has(sanitizedValue());
assertThatApplyingToKey(function, "xspring").has(unsanitizedValue());
assertThatApplyingToKey(function, null).has(unsanitizedValue());
}
@Test
void ifKeyMatchesWithPatternFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue().ifKeyMatches(Pattern.compile("^sp.*$"));
assertThatApplyingToKey(function, "spring").has(sanitizedValue());
assertThatApplyingToKey(function, "spin").has(sanitizedValue());
assertThatApplyingToKey(function, "SPRING").has(unsanitizedValue());
assertThatApplyingToKey(function, "xspring").has(unsanitizedValue());
assertThatApplyingToKey(function, null).has(unsanitizedValue());
}
@Test
void ifKeyMatchesWithPredicatesFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue()
.ifKeyMatches(List.of((key) -> key.startsWith("sp"), (key) -> key.startsWith("BO")));
assertThatApplyingToKey(function, "spring").has(sanitizedValue());
assertThatApplyingToKey(function, "spin").has(sanitizedValue());
assertThatApplyingToKey(function, "BO").has(sanitizedValue());
assertThatApplyingToKey(function, "SPRING").has(unsanitizedValue());
assertThatApplyingToKey(function, "boot").has(unsanitizedValue());
assertThatApplyingToKey(function, null).has(unsanitizedValue());
}
@Test
void ifKeyMatchesWithPredicateFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue().ifKeyMatches((key) -> key.startsWith("sp"));
assertThatApplyingToKey(function, "spring").has(sanitizedValue());
assertThatApplyingToKey(function, "spin").has(sanitizedValue());
assertThatApplyingToKey(function, "boot").has(unsanitizedValue());
assertThatApplyingToKey(function, null).has(unsanitizedValue());
}
@Test
void ifValueStringMatchesWithRegexesFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue().ifValueStringMatches("^sp.*$", "^bo.*$");
assertThatApplyingToValue(function, "spring").has(sanitizedValue());
assertThatApplyingToValue(function, "SPRING").has(sanitizedValue());
assertThatApplyingToValue(function, "boot").has(sanitizedValue());
assertThatApplyingToValue(function, "other").has(unsanitizedValue());
assertThatApplyingToKey(function, null).has(unsanitizedValue());
}
@Test
void ifValueStringMatchesWithPatternsFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue()
.ifValueStringMatches(Pattern.compile("^sp.*$"));
assertThatApplyingToValue(function, "spring").has(sanitizedValue());
assertThatApplyingToValue(function, "spin").has(sanitizedValue());
assertThatApplyingToValue(function, "SPRING").has(unsanitizedValue());
assertThatApplyingToValue(function, "xspring").has(unsanitizedValue());
assertThatApplyingToValue(function, null).has(unsanitizedValue());
}
@Test
void ifValueStringStringMatchesWithPredicatesFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue()
.ifValueStringMatches(List.of((value) -> value.startsWith("sp"), (value) -> value.startsWith("BO")));
assertThatApplyingToValue(function, "spring").has(sanitizedValue());
assertThatApplyingToValue(function, "spin").has(sanitizedValue());
assertThatApplyingToValue(function, "BO").has(sanitizedValue());
assertThatApplyingToValue(function, "SPRING").has(unsanitizedValue());
assertThatApplyingToValue(function, "boot").has(unsanitizedValue());
assertThatApplyingToValue(function, null).has(unsanitizedValue());
}
@Test
void ifValueStringMatchesWithPredicateFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue()
.ifValueStringMatches((value) -> value.startsWith("sp"));
assertThatApplyingToValue(function, "spring").has(sanitizedValue());
assertThatApplyingToValue(function, "spin").has(sanitizedValue());
assertThatApplyingToValue(function, "boot").has(unsanitizedValue());
assertThatApplyingToValue(function, null).has(unsanitizedValue());
}
@Test
void ifValueMatchesWithPredicatesFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue()
.ifValueMatches(List.of((value) -> value instanceof String string && string.startsWith("sp"),
(value) -> value instanceof String string && string.startsWith("BO")));
assertThatApplyingToValue(function, "spring").has(sanitizedValue());
assertThatApplyingToValue(function, "spin").has(sanitizedValue());
assertThatApplyingToValue(function, "BO").has(sanitizedValue());
assertThatApplyingToValue(function, "SPRING").has(unsanitizedValue());
assertThatApplyingToValue(function, "boot").has(unsanitizedValue());
assertThatApplyingToValue(function, 123).has(unsanitizedValue());
assertThatApplyingToValue(function, null).has(unsanitizedValue());
}
@Test
void ifValueMatchesWithPredicateFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue()
.ifValueMatches((value) -> value instanceof String string && string.startsWith("sp"));
assertThatApplyingToValue(function, "spring").has(sanitizedValue());
assertThatApplyingToValue(function, "spin").has(sanitizedValue());
assertThatApplyingToValue(function, "boot").has(unsanitizedValue());
assertThatApplyingToValue(function, 123).has(unsanitizedValue());
assertThatApplyingToKey(function, null).has(unsanitizedValue());
}
@Test
void ifMatchesPredicatesFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue()
.ifMatches(List.of((data) -> data.getKey().startsWith("sp") && "boot".equals(data.getValue()),
(data) -> data.getKey().startsWith("sp") && "framework".equals(data.getValue())));
assertThatApplying(function, data("spring", "boot")).is(sanitizedValue());
assertThatApplying(function, data("spring", "framework")).is(sanitizedValue());
assertThatApplying(function, data("spring", "data")).is(unsanitizedValue());
assertThatApplying(function, data("spring", null)).is(unsanitizedValue());
}
@Test
void ifMatchesPredicateFiltersExpected() {
SanitizingFunction function = SanitizingFunction.sanitizeValue()
.ifMatches((data) -> data.getKey().startsWith("sp") && "boot".equals(data.getValue()));
assertThatApplying(function, data("spring", "boot")).is(sanitizedValue());
assertThatApplying(function, data("spring", "framework")).is(unsanitizedValue());
assertThatApplying(function, data("spring", "data")).is(unsanitizedValue());
assertThatApplying(function, data("spring", null)).is(unsanitizedValue());
}
private ObjectAssert<SanitizableData> assertThatApplyingToKey(SanitizingFunction function, String key) {
return assertThatApplying(function, data(key));
}
private ObjectAssert<SanitizableData> assertThatApplyingToValue(SanitizingFunction function, Object value) {
return assertThatApplying(function, data("key", value));
}
private ObjectAssert<SanitizableData> assertThatApplying(SanitizingFunction function, SanitizableData data) {
return assertThat(function.applyUnlessFiltered(data)).as("%s:%s", data.getKey(), data.getValue());
}
private Condition<SanitizableData> sanitizedValue() {
return new Condition<>((data) -> Objects.equals(data.getValue(), SanitizableData.SANITIZED_VALUE),
"sanitized value");
}
private Condition<SanitizableData> unsanitizedValue() {
return new Condition<>((data) -> !Objects.equals(data.getValue(), SanitizableData.SANITIZED_VALUE),
"unsanitized value");
}
private static SanitizableData data(String key) {
return data(key, "value");
}
private static SanitizableData data(String key, Object value) {
return new SanitizableData(null, key, value);
}
}