Skip to content

Commit 155dbf5

Browse files
committed
fix: resolve properties in second-level dependencies when property and dependency are split
1 parent 4806299 commit 155dbf5

7 files changed

Lines changed: 256 additions & 15 deletions

File tree

core/flamingock-core/src/main/java/io/flamingock/internal/core/context/AbstractContextResolver.java renamed to core/flamingock-core/src/main/java/io/flamingock/internal/core/context/AbstractSimpleContextResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import java.util.Optional;
2424
import java.util.function.Supplier;
2525

26-
public abstract class AbstractContextResolver implements ContextResolver {
26+
public abstract class AbstractSimpleContextResolver implements ContextResolver {
2727

2828
@Override
2929
public Optional<Dependency> getDependency(Class<?> type) {

core/flamingock-core/src/main/java/io/flamingock/internal/core/context/PriorityContextResolver.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public Optional<Dependency> getDependency(String name) {
7878
*/
7979
@Override
8080
public Optional<String> getProperty(String key) {
81-
return getDependencyValue(key, String.class);
81+
return getPropertyAs(key, String.class);
8282
}
8383

8484
/**
@@ -92,6 +92,11 @@ public Optional<String> getProperty(String key) {
9292
*/
9393
@Override
9494
public <T> Optional<T> getPropertyAs(String key, Class<T> type) {
95-
return getDependencyValue(key, type);
95+
Optional<T> dependencyValue = getDependencyValue(key, type);
96+
if (dependencyValue.isPresent()) {
97+
return dependencyValue;
98+
} else {
99+
return baseContext.getPropertyAs(key, type);
100+
}
96101
}
97102
}

core/flamingock-core/src/main/java/io/flamingock/internal/core/context/SimpleContext.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
import java.util.Optional;
4545
import java.util.UUID;
4646

47-
public class SimpleContext extends AbstractContextResolver implements Context {
47+
public class SimpleContext extends AbstractSimpleContextResolver implements Context {
4848

4949
private final Map<String, Dependency> dependenciesByName;
5050
private final Map<Class<?>, Dependency> dependenciesByExactType;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2026 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.internal.core.context;
17+
18+
import org.junit.jupiter.api.DisplayName;
19+
import org.junit.jupiter.api.Test;
20+
21+
import java.util.Optional;
22+
23+
import static org.junit.jupiter.api.Assertions.assertEquals;
24+
import static org.junit.jupiter.api.Assertions.assertTrue;
25+
26+
/**
27+
* Tests proving that {@link PriorityContextResolver} fails to delegate property resolution
28+
* to the base context when that context keeps dependencies and properties in separate stacks.
29+
* <p>
30+
* The bug: {@code getPropertyAs()} routes through {@code getDependencyValue(name, type)} which calls
31+
* {@code getDependency(name)}, never reaching the base context's {@code getProperty()}/{@code getPropertyAs()}.
32+
* This works for {@link SimpleContext} (which stores properties as dependencies) but breaks for any
33+
* {@code ContextResolver} that separates the two (like {@code SpringbootDependencyContext}).
34+
*/
35+
class PriorityContextResolverPropertyTest {
36+
37+
@Test
38+
@DisplayName("Should resolve property from base context when property is in a separate stack (not a dependency)")
39+
void shouldResolvePropertyFromBaseContext_whenPropertyInSeparateStack() {
40+
// Given: a base context that stores properties separately from dependencies
41+
SplitStackContextResolver splitStackContext = new SplitStackContextResolver();
42+
splitStackContext.addProperty("timeout", "5000");
43+
44+
// When: wrapped in PriorityContextResolver as the base context
45+
PriorityContextResolver priorityResolver = new PriorityContextResolver(new SimpleContext(), splitStackContext);
46+
47+
// Then: the property should be resolvable
48+
Optional<String> result = priorityResolver.getProperty("timeout");
49+
assertTrue(result.isPresent(), "Property 'timeout' should be present but was empty");
50+
assertEquals("5000", result.get());
51+
}
52+
53+
@Test
54+
@DisplayName("Should resolve both dependency and property from base context when both exist in separate stacks")
55+
void shouldResolvePropertyFromBaseContext_whenDependencyAlsoExists() {
56+
// Given: a base context with both a dependency (bean) and a property in separate stacks
57+
SplitStackContextResolver splitStackContext = new SplitStackContextResolver();
58+
splitStackContext.addDependency("myService", Runnable.class, (Runnable) () -> {});
59+
splitStackContext.addProperty("timeout", "5000");
60+
61+
PriorityContextResolver priorityResolver = new PriorityContextResolver(new SimpleContext(), splitStackContext);
62+
63+
// Sanity: the dependency IS resolvable through the priority context
64+
assertTrue(priorityResolver.getDependency(Runnable.class).isPresent(),
65+
"Dependency should be resolvable through PriorityContextResolver");
66+
67+
// Then: the property should also be resolvable
68+
Optional<String> result = priorityResolver.getProperty("timeout");
69+
assertTrue(result.isPresent(), "Property 'timeout' should be present but was empty");
70+
assertEquals("5000", result.get());
71+
}
72+
73+
@Test
74+
@DisplayName("Should resolve property from priority context when set via SimpleContext.setProperty (control test)")
75+
void shouldResolvePropertyFromPriorityContext_whenPropertySetAsProperty() {
76+
// Given: a SimpleContext (priority) with a property set directly
77+
// In SimpleContext, setProperty stores it as a dependency, so getDependencyValue works
78+
SimpleContext simpleContext = new SimpleContext();
79+
simpleContext.setProperty("timeout", "5000");
80+
81+
PriorityContextResolver priorityResolver = new PriorityContextResolver(simpleContext, new SimpleContext());
82+
83+
// Then: this DOES work because SimpleContext stores properties as dependencies
84+
Optional<String> result = priorityResolver.getProperty("timeout");
85+
assertTrue(result.isPresent(), "Property 'timeout' should be present");
86+
assertEquals("5000", result.get());
87+
}
88+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2026 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.internal.core.context;
17+
18+
import io.flamingock.internal.common.core.context.ContextResolver;
19+
import io.flamingock.internal.common.core.context.Dependency;
20+
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import java.util.Optional;
24+
25+
/**
26+
* A test helper {@link ContextResolver} that keeps dependencies and properties in separate stacks,
27+
* simulating the pattern used by framework integrations (Spring Boot, Quarkus, etc.) where beans
28+
* come from one source and configuration properties from another.
29+
* <p>
30+
* This is intentionally NOT backed by {@link AbstractSimpleContextResolver} or {@link SimpleContext},
31+
* so that properties stored via {@link #addProperty} are NOT resolvable through {@code getDependency()}.
32+
*/
33+
class SplitStackContextResolver implements ContextResolver {
34+
35+
private final Map<String, Dependency> dependenciesByName = new HashMap<>();
36+
private final Map<Class<?>, Dependency> dependenciesByType = new HashMap<>();
37+
private final Map<String, String> properties = new HashMap<>();
38+
39+
void addDependency(String name, Class<?> type, Object instance) {
40+
Dependency dep = new Dependency(name, type, instance);
41+
dependenciesByName.put(name, dep);
42+
dependenciesByType.put(type, dep);
43+
}
44+
45+
void addProperty(String key, String value) {
46+
properties.put(key, value);
47+
}
48+
49+
@Override
50+
public Optional<Dependency> getDependency(Class<?> type) {
51+
return Optional.ofNullable(dependenciesByType.get(type));
52+
}
53+
54+
@Override
55+
public Optional<Dependency> getDependency(String name) {
56+
return Optional.ofNullable(dependenciesByName.get(name));
57+
}
58+
59+
@Override
60+
public Optional<String> getProperty(String key) {
61+
return Optional.ofNullable(properties.get(key));
62+
}
63+
64+
@Override
65+
public <T> Optional<T> getPropertyAs(String key, Class<T> type) {
66+
String value = properties.get(key);
67+
if (value == null) {
68+
return Optional.empty();
69+
}
70+
return Optional.of(type.cast(value));
71+
}
72+
}

platform-plugins/flamingock-springboot-integration/src/main/java/io/flamingock/springboot/SpringbootDependencyContext.java

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public Optional<Dependency> getDependency(String name) {
8383
*/
8484
@Override
8585
public Optional<String> getProperty(String key) {
86-
return Optional.ofNullable(environment.getProperty(flamingockKey(key)));
86+
return Optional.ofNullable(environment.getProperty(key));
8787
}
8888

8989
/**
@@ -97,16 +97,8 @@ public Optional<String> getProperty(String key) {
9797
*/
9898
@Override
9999
public <T> Optional<T> getPropertyAs(String key, Class<T> type) {
100-
return Optional.ofNullable(environment.getProperty(flamingockKey(key), type));
100+
return Optional.ofNullable(environment.getProperty(key, type));
101101
}
102102

103-
/**
104-
* Adds the {@code flamingock.} namespace prefix to the property key.
105-
*
106-
* @param key the raw property key
107-
* @return the fully qualified key with the {@code flamingock.} prefix
108-
*/
109-
private static String flamingockKey(String key) {
110-
return "flamingock." + key;
111-
}
103+
112104
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2026 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.springboot;
17+
18+
import io.flamingock.internal.core.context.PriorityContextResolver;
19+
import io.flamingock.internal.core.context.SimpleContext;
20+
import org.junit.jupiter.api.DisplayName;
21+
import org.junit.jupiter.api.Test;
22+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
23+
import org.springframework.context.annotation.Bean;
24+
import org.springframework.context.annotation.Configuration;
25+
26+
import java.util.Optional;
27+
28+
import static org.junit.jupiter.api.Assertions.assertEquals;
29+
import static org.junit.jupiter.api.Assertions.assertTrue;
30+
31+
/**
32+
* Tests proving that {@link PriorityContextResolver} fails to resolve Spring Environment properties
33+
* through {@link SpringbootDependencyContext}, because it routes property resolution through the
34+
* dependency path ({@code getDependencyValue} → {@code getDependency}) instead of delegating to
35+
* the context's {@code getProperty()}/{@code getPropertyAs()}.
36+
*/
37+
class SpringbootPropertyResolutionTest {
38+
39+
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
40+
.withPropertyValues("flamingock.timeout=5000");
41+
42+
@Test
43+
@DisplayName("Should resolve Spring property through PriorityContextResolver")
44+
void shouldResolveSpringPropertyThroughPriorityContext() {
45+
contextRunner.run(ctx -> {
46+
SpringbootDependencyContext springbootContext = new SpringbootDependencyContext(ctx);
47+
48+
// Sanity: direct call to SpringbootDependencyContext works
49+
Optional<String> directResult = springbootContext.getProperty("flamingock.timeout");
50+
assertTrue(directResult.isPresent(), "Direct getProperty on SpringbootDependencyContext should work");
51+
assertEquals("5000", directResult.get());
52+
53+
// Bug: when wrapped in PriorityContextResolver, property resolution breaks
54+
PriorityContextResolver priorityResolver = new PriorityContextResolver(new SimpleContext(), springbootContext);
55+
Optional<String> result = priorityResolver.getProperty("flamingock.timeout");
56+
57+
assertTrue(result.isPresent(), "Property 'timeout' should be resolvable through PriorityContextResolver");
58+
assertEquals("5000", result.get());
59+
});
60+
}
61+
62+
@Test
63+
@DisplayName("Should resolve Spring bean through PriorityContextResolver (control test)")
64+
void shouldResolveSpringBeanThroughPriorityContext() {
65+
contextRunner
66+
.withUserConfiguration(BeanConfiguration.class)
67+
.run(ctx -> {
68+
SpringbootDependencyContext springbootContext = new SpringbootDependencyContext(ctx);
69+
PriorityContextResolver priorityResolver = new PriorityContextResolver(new SimpleContext(), springbootContext);
70+
71+
// Bean resolution DOES work through PriorityContextResolver
72+
assertTrue(priorityResolver.getDependency(Runnable.class).isPresent(),
73+
"Spring bean should be resolvable through PriorityContextResolver");
74+
});
75+
}
76+
77+
@Configuration
78+
static class BeanConfiguration {
79+
@Bean
80+
public Runnable testService() {
81+
return () -> {};
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)