diff --git a/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java b/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java index 42faac60483..514052e47c6 100644 --- a/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-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. @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Properties; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.core.convert.ConverterNotFoundException; @@ -38,18 +39,15 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; */ class PropertySourcesPropertyResolverTests { - private Properties testProperties; + private final Properties testProperties = new Properties(); - private MutablePropertySources propertySources; + private final MutablePropertySources propertySources = new MutablePropertySources(); - private PropertySourcesPropertyResolver propertyResolver; + private final PropertySourcesPropertyResolver propertyResolver = new PropertySourcesPropertyResolver(propertySources); @BeforeEach void setUp() { - propertySources = new MutablePropertySources(); - propertyResolver = new PropertySourcesPropertyResolver(propertySources); - testProperties = new Properties(); propertySources.addFirst(new PropertiesPropertySource("testProperties", testProperties)); } @@ -77,14 +75,12 @@ class PropertySourcesPropertyResolverTests { @Test void getProperty_propertySourceSearchOrderIsFIFO() { - MutablePropertySources sources = new MutablePropertySources(); - PropertyResolver resolver = new PropertySourcesPropertyResolver(sources); - sources.addFirst(new MockPropertySource("ps1").withProperty("pName", "ps1Value")); - assertThat(resolver.getProperty("pName")).isEqualTo("ps1Value"); - sources.addFirst(new MockPropertySource("ps2").withProperty("pName", "ps2Value")); - assertThat(resolver.getProperty("pName")).isEqualTo("ps2Value"); - sources.addFirst(new MockPropertySource("ps3").withProperty("pName", "ps3Value")); - assertThat(resolver.getProperty("pName")).isEqualTo("ps3Value"); + propertySources.addFirst(new MockPropertySource("ps1").withProperty("pName", "ps1Value")); + assertThat(propertyResolver.getProperty("pName")).isEqualTo("ps1Value"); + propertySources.addFirst(new MockPropertySource("ps2").withProperty("pName", "ps2Value")); + assertThat(propertyResolver.getProperty("pName")).isEqualTo("ps2Value"); + propertySources.addFirst(new MockPropertySource("ps3").withProperty("pName", "ps3Value")); + assertThat(propertyResolver.getProperty("pName")).isEqualTo("ps3Value"); } @Test @@ -115,8 +111,8 @@ class PropertySourcesPropertyResolverTests { class TestType { } - assertThatExceptionOfType(ConverterNotFoundException.class).isThrownBy(() -> - propertyResolver.getProperty("foo", TestType.class)); + assertThatExceptionOfType(ConverterNotFoundException.class) + .isThrownBy(() -> propertyResolver.getProperty("foo", TestType.class)); } @Test @@ -127,7 +123,6 @@ class PropertySourcesPropertyResolverTests { HashMap map = new HashMap<>(); map.put(key, value1); // before construction - MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MapPropertySource("testProperties", map)); PropertyResolver propertyResolver = new PropertySourcesPropertyResolver(propertySources); assertThat(propertyResolver.getProperty(key)).isEqualTo(value1); @@ -138,7 +133,6 @@ class PropertySourcesPropertyResolverTests { @Test void getProperty_doesNotCache_addNewKeyPostConstruction() { HashMap map = new HashMap<>(); - MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MapPropertySource("testProperties", map)); PropertyResolver propertyResolver = new PropertySourcesPropertyResolver(propertySources); assertThat(propertyResolver.getProperty("foo")).isNull(); @@ -148,10 +142,9 @@ class PropertySourcesPropertyResolverTests { @Test void getPropertySources_replacePropertySource() { - propertySources = new MutablePropertySources(); - propertyResolver = new PropertySourcesPropertyResolver(propertySources); propertySources.addLast(new MockPropertySource("local").withProperty("foo", "localValue")); propertySources.addLast(new MockPropertySource("system").withProperty("foo", "systemValue")); + assertThat(propertySources).hasSize(3); // 'local' was added first so has precedence assertThat(propertyResolver.getProperty("foo")).isEqualTo("localValue"); @@ -162,7 +155,7 @@ class PropertySourcesPropertyResolverTests { // 'system' now has precedence assertThat(propertyResolver.getProperty("foo")).isEqualTo("newValue"); - assertThat(propertySources).hasSize(2); + assertThat(propertySources).hasSize(3); } @Test @@ -170,81 +163,65 @@ class PropertySourcesPropertyResolverTests { testProperties.put("exists", "xyz"); assertThat(propertyResolver.getRequiredProperty("exists")).isEqualTo("xyz"); - assertThatIllegalStateException().isThrownBy(() -> - propertyResolver.getRequiredProperty("bogus")); + assertThatIllegalStateException().isThrownBy(() -> propertyResolver.getRequiredProperty("bogus")); } @Test void getRequiredProperty_withStringArrayConversion() { testProperties.put("exists", "abc,123"); - assertThat(propertyResolver.getRequiredProperty("exists", String[].class)).isEqualTo(new String[] { "abc", "123" }); + assertThat(propertyResolver.getRequiredProperty("exists", String[].class)).containsExactly("abc", "123"); - assertThatIllegalStateException().isThrownBy(() -> - propertyResolver.getRequiredProperty("bogus", String[].class)); + assertThatIllegalStateException().isThrownBy(() -> propertyResolver.getRequiredProperty("bogus", String[].class)); } @Test void resolvePlaceholders() { - MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); - PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); - assertThat(resolver.resolvePlaceholders("Replace this ${key}")).isEqualTo("Replace this value"); + assertThat(propertyResolver.resolvePlaceholders("Replace this ${key}")).isEqualTo("Replace this value"); } @Test void resolvePlaceholders_withUnresolvable() { - MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); - PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); - assertThat(resolver.resolvePlaceholders("Replace this ${key} plus ${unknown}")) + assertThat(propertyResolver.resolvePlaceholders("Replace this ${key} plus ${unknown}")) .isEqualTo("Replace this value plus ${unknown}"); } @Test void resolvePlaceholders_withDefaultValue() { - MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); - PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); - assertThat(resolver.resolvePlaceholders("Replace this ${key} plus ${unknown:defaultValue}")) + assertThat(propertyResolver.resolvePlaceholders("Replace this ${key} plus ${unknown:defaultValue}")) .isEqualTo("Replace this value plus defaultValue"); } @Test void resolvePlaceholders_withNullInput() { - assertThatIllegalArgumentException().isThrownBy(() -> - new PropertySourcesPropertyResolver(new MutablePropertySources()).resolvePlaceholders(null)); + assertThatIllegalArgumentException().isThrownBy(() -> propertyResolver.resolvePlaceholders(null)); } @Test void resolveRequiredPlaceholders() { - MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); - PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); - assertThat(resolver.resolveRequiredPlaceholders("Replace this ${key}")).isEqualTo("Replace this value"); + assertThat(propertyResolver.resolveRequiredPlaceholders("Replace this ${key}")).isEqualTo("Replace this value"); } @Test void resolveRequiredPlaceholders_withUnresolvable() { - MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); - PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); - assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> - resolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown}")); + assertThatExceptionOfType(PlaceholderResolutionException.class) + .isThrownBy(() -> propertyResolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown}")); } @Test void resolveRequiredPlaceholders_withDefaultValue() { - MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); - PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); - assertThat(resolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown:defaultValue}")) + assertThat(propertyResolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown:defaultValue}")) .isEqualTo("Replace this value plus defaultValue"); } @Test void resolveRequiredPlaceholders_withNullInput() { - assertThatIllegalArgumentException().isThrownBy(() -> - new PropertySourcesPropertyResolver(new MutablePropertySources()).resolveRequiredPlaceholders(null)); + assertThatIllegalArgumentException().isThrownBy(() -> propertyResolver.resolveRequiredPlaceholders(null)); } @Test @@ -256,17 +233,17 @@ class PropertySourcesPropertyResolverTests { propertyResolver.setRequiredProperties("foo", "bar"); // neither foo nor bar properties are present -> validating should throw - assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy( - propertyResolver::validateRequiredProperties) - .withMessage("The following properties were declared as required " + - "but could not be resolved: [foo, bar]"); + assertThatExceptionOfType(MissingRequiredPropertiesException.class) + .isThrownBy(propertyResolver::validateRequiredProperties) + .withMessage("The following properties were declared as required " + + "but could not be resolved: [foo, bar]"); // add foo property -> validation should fail only on missing 'bar' property testProperties.put("foo", "fooValue"); - assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy( - propertyResolver::validateRequiredProperties) - .withMessage("The following properties were declared as required " + - "but could not be resolved: [bar]"); + assertThatExceptionOfType(MissingRequiredPropertiesException.class) + .isThrownBy(propertyResolver::validateRequiredProperties) + .withMessage("The following properties were declared as required " + + "but could not be resolved: [bar]"); // add bar property -> validation should pass, even with an empty string value testProperties.put("bar", ""); @@ -291,13 +268,13 @@ class PropertySourcesPropertyResolverTests { assertThat(pr.getProperty("p2")).isEqualTo("v2"); assertThat(pr.getProperty("p3")).isEqualTo("v1:v2"); assertThat(pr.getProperty("p4")).isEqualTo("v1:v2"); - assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> - pr.getProperty("p5")) - .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); + assertThatExceptionOfType(PlaceholderResolutionException.class) + .isThrownBy(() -> pr.getProperty("p5")) + .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); assertThat(pr.getProperty("p6")).isEqualTo("v1:v2:def"); - assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> - pr.getProperty("pL")) - .withMessageContaining("Circular"); + assertThatExceptionOfType(PlaceholderResolutionException.class) + .isThrownBy(() -> pr.getProperty("pL")) + .withMessageContaining("Circular"); } @Test @@ -349,9 +326,9 @@ class PropertySourcesPropertyResolverTests { // placeholders nested within the value of "p4" are unresolvable and cause an // exception by default - assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> - pr.getProperty("p4")) - .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); + assertThatExceptionOfType(PlaceholderResolutionException.class) + .isThrownBy(() -> pr.getProperty("p4")) + .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); // relax the treatment of unresolvable nested placeholders pr.setIgnoreUnresolvableNestedPlaceholders(true); @@ -361,9 +338,58 @@ class PropertySourcesPropertyResolverTests { // resolve[Nested]Placeholders methods behave as usual regardless the value of // ignoreUnresolvableNestedPlaceholders assertThat(pr.resolvePlaceholders("${p1}:${p2}:${bogus}")).isEqualTo("v1:v2:${bogus}"); - assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> - pr.resolveRequiredPlaceholders("${p1}:${p2}:${bogus}")) - .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); + assertThatExceptionOfType(PlaceholderResolutionException.class) + .isThrownBy(() -> pr.resolveRequiredPlaceholders("${p1}:${p2}:${bogus}")) + .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); + } + + + @Nested + class EscapedPlaceholderTests { + + @Test // gh-34720 + void escapedPlaceholdersAreNotEvaluated() { + testProperties.put("prop1", "value1"); + testProperties.put("prop2", "value2\\${prop1}"); + + assertThat(propertyResolver.getProperty("prop2")).isEqualTo("value2${prop1}"); + } + + @Test // gh-34720 + void escapedPlaceholdersAreNotEvaluatedWithCharSequenceValues() { + testProperties.put("prop1", "value1"); + testProperties.put("prop2", new StringBuilder("value2\\${prop1}")); + + assertThat(propertyResolver.getProperty("prop2")).isEqualTo("value2${prop1}"); + } + + @Test // gh-34720 + void multipleEscapedPlaceholdersArePreserved() { + testProperties.put("prop1", "value1"); + testProperties.put("prop2", "value2"); + testProperties.put("complex", "start\\${prop1}middle\\${prop2}end"); + + assertThat(propertyResolver.getProperty("complex")).isEqualTo("start${prop1}middle${prop2}end"); + } + + @Test // gh-34720 + void doubleBackslashesAreProcessedCorrectly() { + testProperties.put("prop1", "value1"); + testProperties.put("doubleEscaped", "value2\\\\${prop1}"); + + assertThat(propertyResolver.getProperty("doubleEscaped")).isEqualTo("value2\\${prop1}"); + } + + @Test // gh-34720 + void escapedPlaceholdersInNestedPropertiesAreNotEvaluated() { + testProperties.put("p1", "v1"); + testProperties.put("p2", "v2"); + testProperties.put("escaped", "prefix-\\${p1}"); + testProperties.put("nested", "${escaped}-${p2}"); + + assertThat(propertyResolver.getProperty("nested")).isEqualTo("prefix-${p1}-v2"); + } + } }