Simplify and revise PlaceholderParserTests for consistency

This commit is contained in:
Sam Brannen 2025-05-10 14:16:49 +02:00
parent 5a2cbc1ab3
commit 90453643cc
1 changed files with 99 additions and 107 deletions

View File

@ -16,7 +16,7 @@
package org.springframework.util;
import java.util.Properties;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.Nested;
@ -36,13 +36,13 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link PlaceholderParser}.
*
* @author Stephane Nicoll
* @author Sam Brannen
*/
class PlaceholderParserTests {
@ -54,11 +54,11 @@ class PlaceholderParserTests {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("placeholders")
void placeholderIsReplaced(String text, String expected) {
Properties properties = new Properties();
properties.setProperty("firstName", "John");
properties.setProperty("nested0", "first");
properties.setProperty("nested1", "Name");
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
Map<String, String> properties = Map.of(
"firstName", "John",
"nested0", "first",
"nested1", "Name");
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream<Arguments> placeholders() {
@ -79,13 +79,13 @@ class PlaceholderParserTests {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("nestedPlaceholders")
void nestedPlaceholdersAreReplaced(String text, String expected) {
Properties properties = new Properties();
properties.setProperty("p1", "v1");
properties.setProperty("p2", "v2");
properties.setProperty("p3", "${p1}:${p2}"); // nested placeholders
properties.setProperty("p4", "${p3}"); // deeply nested placeholders
properties.setProperty("p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
Map<String, String> properties = Map.of(
"p1", "v1",
"p2", "v2",
"p3", "${p1}:${p2}", // nested placeholders
"p4", "${p3}", // deeply nested placeholders
"p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream<Arguments> nestedPlaceholders() {
@ -101,19 +101,15 @@ class PlaceholderParserTests {
@Test
void parseWithSinglePlaceholder() {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
assertThat(this.parser.replacePlaceholders("${firstName}", resolver))
.isEqualTo("John");
verify(resolver).resolvePlaceholder("firstName");
verifyNoMoreInteractions(resolver);
assertThat(this.parser.replacePlaceholders("${firstName}", resolver)).isEqualTo("John");
verifyPlaceholderResolutions(resolver, "firstName");
}
@Test
void parseWithPlaceholderAndPrefixText() {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
assertThat(this.parser.replacePlaceholders("This is ${firstName}", resolver))
.isEqualTo("This is John");
verify(resolver).resolvePlaceholder("firstName");
verifyNoMoreInteractions(resolver);
assertThat(this.parser.replacePlaceholders("This is ${firstName}", resolver)).isEqualTo("This is John");
verifyPlaceholderResolutions(resolver, "firstName");
}
@Test
@ -121,31 +117,25 @@ class PlaceholderParserTests {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John", "lastName", "Smith");
assertThat(this.parser.replacePlaceholders("User: ${firstName} - ${lastName}.", resolver))
.isEqualTo("User: John - Smith.");
verify(resolver).resolvePlaceholder("firstName");
verify(resolver).resolvePlaceholder("lastName");
verifyNoMoreInteractions(resolver);
verifyPlaceholderResolutions(resolver, "firstName", "lastName");
}
@Test
void parseWithNestedPlaceholderInKey() {
PlaceholderResolver resolver = mockPlaceholderResolver(
"nested", "Name", "firstName", "John");
assertThat(this.parser.replacePlaceholders("${first${nested}}", resolver))
.isEqualTo("John");
PlaceholderResolver resolver = mockPlaceholderResolver("nested", "Name", "firstName", "John");
assertThat(this.parser.replacePlaceholders("${first${nested}}", resolver)).isEqualTo("John");
verifyPlaceholderResolutions(resolver, "nested", "firstName");
}
@Test
void parseWithMultipleNestedPlaceholdersInKey() {
PlaceholderResolver resolver = mockPlaceholderResolver(
"nested0", "first", "nested1", "Name", "firstName", "John");
assertThat(this.parser.replacePlaceholders("${${nested0}${nested1}}", resolver))
.isEqualTo("John");
PlaceholderResolver resolver = mockPlaceholderResolver("nested0", "first", "nested1", "Name", "firstName", "John");
assertThat(this.parser.replacePlaceholders("${${nested0}${nested1}}", resolver)).isEqualTo("John");
verifyPlaceholderResolutions(resolver, "nested0", "nested1", "firstName");
}
@Test
void placeholdersWithSeparatorAreHandledAsIs() {
void placeholderValueContainingSeparatorIsHandledAsIs() {
PlaceholderResolver resolver = mockPlaceholderResolver("my:test", "value");
assertThat(this.parser.replacePlaceholders("${my:test}", resolver)).isEqualTo("value");
verifyPlaceholderResolutions(resolver, "my:test");
@ -153,17 +143,17 @@ class PlaceholderParserTests {
@Test
void placeholdersWithoutEscapeCharAreNotEscaped() {
PlaceholderResolver resolver = mockPlaceholderResolver("test", "value");
assertThat(this.parser.replacePlaceholders("\\${test}", resolver)).isEqualTo("\\value");
verifyPlaceholderResolutions(resolver, "test");
PlaceholderResolver resolver = mockPlaceholderResolver("p1", "v1");
assertThat(this.parser.replacePlaceholders("\\${p1}", resolver)).isEqualTo("\\v1");
verifyPlaceholderResolutions(resolver, "p1");
}
@Test
void textWithInvalidPlaceholderIsMerged() {
void textWithInvalidPlaceholderSyntaxIsMerged() {
String text = "test${of${with${and${";
ParsedValue parsedValue = this.parser.parse(text);
assertThat(parsedValue.parts()).singleElement().isInstanceOfSatisfying(
TextPart.class, textPart -> assertThat(textPart.text()).isEqualTo(text));
assertThat(parsedValue.parts()).singleElement().isInstanceOfSatisfying(TextPart.class,
textPart -> assertThat(textPart.text()).isEqualTo(text));
}
}
@ -176,11 +166,11 @@ class PlaceholderParserTests {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("placeholders")
void placeholderIsReplaced(String text, String expected) {
Properties properties = new Properties();
properties.setProperty("firstName", "John");
properties.setProperty("nested0", "first");
properties.setProperty("nested1", "Name");
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
Map<String, String> properties = Map.of(
"firstName", "John",
"nested0", "first",
"nested1", "Name");
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream<Arguments> placeholders() {
@ -199,14 +189,14 @@ class PlaceholderParserTests {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("nestedPlaceholders")
void nestedPlaceholdersAreReplaced(String text, String expected) {
Properties properties = new Properties();
properties.setProperty("p1", "v1");
properties.setProperty("p2", "v2");
properties.setProperty("p3", "${p1}:${p2}"); // nested placeholders
properties.setProperty("p4", "${p3}"); // deeply nested placeholders
properties.setProperty("p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder
properties.setProperty("p6", "${p1}:${p2}:${bogus:def}"); // unresolvable w/ default
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
Map<String, String> properties = Map.of(
"p1", "v1",
"p2", "v2",
"p3", "${p1}:${p2}", // nested placeholders
"p4", "${p3}", // deeply nested placeholders
"p5", "${p1}:${p2}:${bogus}", // unresolvable placeholder
"p6", "${p1}:${p2}:${bogus:def}"); // unresolvable w/ default
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream<Arguments> nestedPlaceholders() {
@ -225,11 +215,11 @@ class PlaceholderParserTests {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("exactMatchPlaceholders")
void placeholdersWithExactMatchAreConsidered(String text, String expected) {
Properties properties = new Properties();
properties.setProperty("prefix://my-service", "example-service");
properties.setProperty("px", "prefix");
properties.setProperty("p1", "${prefix://my-service}");
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
Map<String, String> properties = Map.of(
"prefix://my-service", "example-service",
"px", "prefix",
"p1", "${prefix://my-service}");
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream<Arguments> exactMatchPlaceholders() {
@ -242,74 +232,55 @@ class PlaceholderParserTests {
@Test
void parseWithKeyEqualsToText() {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "Steve");
assertThat(this.parser.replacePlaceholders("${firstName}", resolver))
.isEqualTo("Steve");
assertThat(this.parser.replacePlaceholders("${firstName}", resolver)).isEqualTo("Steve");
verifyPlaceholderResolutions(resolver, "firstName");
}
@Test
void parseWithHardcodedFallback() {
PlaceholderResolver resolver = mockPlaceholderResolver();
assertThat(this.parser.replacePlaceholders("${firstName:Steve}", resolver))
.isEqualTo("Steve");
assertThat(this.parser.replacePlaceholders("${firstName:Steve}", resolver)).isEqualTo("Steve");
verifyPlaceholderResolutions(resolver, "firstName:Steve", "firstName");
}
@Test
void parseWithNestedPlaceholderInKeyUsingFallback() {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
assertThat(this.parser.replacePlaceholders("${first${invalid:Name}}", resolver))
.isEqualTo("John");
assertThat(this.parser.replacePlaceholders("${first${invalid:Name}}", resolver)).isEqualTo("John");
verifyPlaceholderResolutions(resolver, "invalid:Name", "invalid", "firstName");
}
@Test
void parseWithFallbackUsingPlaceholder() {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
assertThat(this.parser.replacePlaceholders("${invalid:${firstName}}", resolver))
.isEqualTo("John");
assertThat(this.parser.replacePlaceholders("${invalid:${firstName}}", resolver)).isEqualTo("John");
verifyPlaceholderResolutions(resolver, "invalid", "firstName");
}
}
@Nested // Tests with the use of the escape character
/**
* Tests that use the escape character.
*/
@Nested
class EscapedTests {
private final PlaceholderParser parser = new PlaceholderParser("${", "}", ":", '\\', true);
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("escapedInNestedPlaceholders")
void escapedSeparatorInNestedPlaceholder(String text, String expected) {
Properties properties = new Properties();
properties.setProperty("app.environment", "qa");
properties.setProperty("app.service", "protocol");
properties.setProperty("protocol://host/qa/name", "protocol://example.com/qa/name");
properties.setProperty("service/host/qa/name", "https://example.com/qa/name");
properties.setProperty("service/host/qa/name:value", "https://example.com/qa/name-value");
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
}
static Stream<Arguments> escapedInNestedPlaceholders() {
return Stream.of(
Arguments.of("${protocol\\://host/${app.environment}/name}", "protocol://example.com/qa/name"),
Arguments.of("${${app.service}\\://host/${app.environment}/name}", "protocol://example.com/qa/name"),
Arguments.of("${service/host/${app.environment}/name:\\value}", "https://example.com/qa/name"),
Arguments.of("${service/host/${name\\:value}/}", "${service/host/${name:value}/}"));
}
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("escapedPlaceholders")
void escapedPlaceholderIsNotReplaced(String text, String expected) {
PlaceholderResolver resolver = mockPlaceholderResolver(
"firstName", "John", "nested0", "first", "nested1", "Name",
Map<String, String> properties = Map.of(
"firstName", "John",
"${test}", "John",
"p1", "v1", "p2", "\\${p1:default}", "p3", "${p2}",
"p1", "v1",
"p2", "\\${p1:default}",
"p3", "${p2}",
"p4", "adc${p0:\\${p1}}",
"p5", "adc${\\${p0}:${p1}}",
"p6", "adc${p0:def\\${p1}}",
"p7", "adc\\${");
assertThat(this.parser.replacePlaceholders(text, resolver)).isEqualTo(expected);
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream<Arguments> escapedPlaceholders() {
@ -324,18 +295,15 @@ class PlaceholderParserTests {
Arguments.of("${p4}", "adc${p1}"),
Arguments.of("${p5}", "adcv1"),
Arguments.of("${p6}", "adcdef${p1}"),
Arguments.of("${p7}", "adc\\${"));
Arguments.of("${p7}", "adc\\${")
);
}
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("escapedSeparators")
void escapedSeparatorIsNotReplaced(String text, String expected) {
Properties properties = new Properties();
properties.setProperty("first:Name", "John");
properties.setProperty("nested0", "first");
properties.setProperty("nested1", "Name");
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
Map<String, String> properties = Map.of("first:Name", "John");
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream<Arguments> escapedSeparators() {
@ -345,6 +313,26 @@ class PlaceholderParserTests {
);
}
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("escapedSeparatorsInNestedPlaceholders")
void escapedSeparatorInNestedPlaceholderIsNotReplaced(String text, String expected) {
Map<String, String> properties = Map.of(
"app.environment", "qa",
"app.service", "protocol",
"protocol://host/qa/name", "protocol://example.com/qa/name",
"service/host/qa/name", "https://example.com/qa/name",
"service/host/qa/name:value", "https://example.com/qa/name-value");
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream<Arguments> escapedSeparatorsInNestedPlaceholders() {
return Stream.of(
Arguments.of("${protocol\\://host/${app.environment}/name}", "protocol://example.com/qa/name"),
Arguments.of("${${app.service}\\://host/${app.environment}/name}", "protocol://example.com/qa/name"),
Arguments.of("${service/host/${app.environment}/name:\\value}", "https://example.com/qa/name"),
Arguments.of("${service/host/${name\\:value}/}", "${service/host/${name:value}/}"));
}
}
@Nested
@ -354,34 +342,38 @@ class PlaceholderParserTests {
@Test
void textWithCircularReference() {
PlaceholderResolver resolver = mockPlaceholderResolver("pL", "${pR}", "pR", "${pL}");
assertThatThrownBy(() -> this.parser.replacePlaceholders("${pL}", resolver))
Map<String, String> properties = Map.of(
"pL", "${pR}",
"pR", "${pL}");
assertThatThrownBy(() -> this.parser.replacePlaceholders("${pL}", properties::get))
.isInstanceOf(PlaceholderResolutionException.class)
.hasMessage("Circular placeholder reference 'pL' in value \"${pL}\" <-- \"${pR}\" <-- \"${pL}\"");
}
@Test
void unresolvablePlaceholderIsReported() {
PlaceholderResolver resolver = mockPlaceholderResolver();
assertThatExceptionOfType(PlaceholderResolutionException.class)
.isThrownBy(() -> this.parser.replacePlaceholders("${bogus}", resolver))
.withMessage("Could not resolve placeholder 'bogus' in value \"${bogus}\"")
.isThrownBy(() -> this.parser.replacePlaceholders("X${bogus}Z", placeholderName -> null))
.withMessage("Could not resolve placeholder 'bogus' in value \"X${bogus}Z\"")
.withNoCause();
}
@Test
void unresolvablePlaceholderInNestedPlaceholderIsReportedWithChain() {
PlaceholderResolver resolver = mockPlaceholderResolver("p1", "v1", "p2", "v2",
Map<String, String> properties = Map.of(
"p1", "v1",
"p2", "v2",
"p3", "${p1}:${p2}:${bogus}");
assertThatExceptionOfType(PlaceholderResolutionException.class)
.isThrownBy(() -> this.parser.replacePlaceholders("${p3}", resolver))
.isThrownBy(() -> this.parser.replacePlaceholders("${p3}", properties::get))
.withMessage("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\" <-- \"${p3}\"")
.withNoCause();
}
}
PlaceholderResolver mockPlaceholderResolver(String... pairs) {
private static PlaceholderResolver mockPlaceholderResolver(String... pairs) {
if (pairs.length % 2 == 1) {
throw new IllegalArgumentException("size must be even, it is a set of key=value pairs");
}
@ -394,7 +386,7 @@ class PlaceholderParserTests {
return resolver;
}
void verifyPlaceholderResolutions(PlaceholderResolver mock, String... placeholders) {
private static void verifyPlaceholderResolutions(PlaceholderResolver mock, String... placeholders) {
InOrder ordered = inOrder(mock);
for (String placeholder : placeholders) {
ordered.verify(mock).resolvePlaceholder(placeholder);