From 3b3ddb1317644c1f7cc0cf51a2ee6fbaa6f99795 Mon Sep 17 00:00:00 2001 From: Ksiona Date: Thu, 14 May 2026 14:03:28 +0400 Subject: [PATCH 1/4] fix: documentation mismatch --- core-context-propagation-quarkus/README.md | 22 +++++++++------------- core-context-propagation/README.md | 2 +- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/core-context-propagation-quarkus/README.md b/core-context-propagation-quarkus/README.md index 198fa4944..7d2a2f34c 100644 --- a/core-context-propagation-quarkus/README.md +++ b/core-context-propagation-quarkus/README.md @@ -29,6 +29,7 @@ Design overview: [context-propagation diagram](./design.png) - [Allowed headers](#allowed-headers) - [API version](#api-version) - [X-Request-Id](#x-request-id) + - [X-Channel-Request-Id](#x-channel-request-id) - [X-Version](#x-version) - [X-Version-Name](#x-version-name) - [X-Nc-Client-Ip](#x-nc-client-ip) @@ -130,7 +131,7 @@ String xRequestId = xRequestIdContextObject.getRequestId(); Propagates and allows to get `X-Channel-Request-Id` value. If an incoming request does not contain the `X-Channel-Request-Id` header then a random value is not generated and the value defaults to placeholder "-". This context is **blocked by default** and will not be propagated to outgoing requests. -**Default behavior:** `X-Channel-Request-Id` is NOT propagated to outgoing responses. +**Default behavior:** `X-Channel-Request-Id` is NOT propagated to outgoing requests. **Enabling propagation:** To allow `X-Channel-Request-Id` to be propagated to outgoing requests, remove it from the blacklist using one of the following methods: @@ -140,23 +141,18 @@ blacklist using one of the following methods: HEADERS_BLOCKED= ``` -2. **Via system property:** -```text --Dheaders.blocked= -``` - -3. **Via application.properties (Quarkus):** +2. **Via application.properties (Quarkus):** ```properties -headers.blocked= +quarkus.headers.blocked ``` -**`headers.blocked` rules and limitations** +**`quarkus.headers.blocked` rules and limitations** -- Source priority: system property `headers.blocked` overrides environment variable `HEADERS_BLOCKED`. +- Source priority: system property `quarkus.headers.blocked` overrides environment variable `HEADERS_BLOCKED`. - Default when not configured at all: `X-Channel-Request-Id` is blocked. -- Explicit empty value (`headers.blocked=` / `HEADERS_BLOCKED=`): blacklist is empty (nothing is blocked). -- Explicit non-empty value with valid headers (for example `headers.blocked=Some-Header`): only listed headers are blocked. -- `X-Request-Id` is non-blockable: if it is listed in `headers.blocked`/`HEADERS_BLOCKED`, it is ignored. +- Explicit empty value (`quarkus.headers.blocked=` / `HEADERS_BLOCKED=`): blacklist is empty (nothing is blocked). +- Explicit non-empty value with valid headers (for example `quarkus.headers.blocked=Some-Header`): only listed headers are blocked. +- `X-Request-Id` is non-blockable: if it is listed in `quarkus.headers.blocked`/`HEADERS_BLOCKED`, it is ignored. - If configured value contains only non-blockable entries (for example only `X-Request-Id`), default block is applied and `X-Channel-Request-Id` remains blocked. **MDC Integration:** The channel request ID is automatically stored in MDC under the key `x_channel_request_id` for use in diff --git a/core-context-propagation/README.md b/core-context-propagation/README.md index c7a8c6301..40f550846 100644 --- a/core-context-propagation/README.md +++ b/core-context-propagation/README.md @@ -145,7 +145,7 @@ Access: Propagates and allows to get `X-Channel-Request-Id` value. If an incoming request does not contain the `X-Channel-Request-Id` header then a random value is not generated and the value defaults to placeholder "-". This context is **blocked by default** and will not be propagated to outgoing requests. -**Default behavior:** `X-Channel-Request-Id` is NOT propagated to outgoing responses. +**Default behavior:** `X-Channel-Request-Id` is NOT propagated to outgoing requests. **Enabling propagation:** To allow `X-Channel-Request-Id` to be propagated to outgoing requests, remove it from the blacklist using one of the following methods: From 3bba46267d00c0ac668e057e16e9026d0f1cdb75 Mon Sep 17 00:00:00 2001 From: Ksiona Date: Thu, 14 May 2026 16:00:46 +0400 Subject: [PATCH 2/4] fix: documentation mismatch --- core-context-propagation-quarkus/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core-context-propagation-quarkus/README.md b/core-context-propagation-quarkus/README.md index 7d2a2f34c..d156f5320 100644 --- a/core-context-propagation-quarkus/README.md +++ b/core-context-propagation-quarkus/README.md @@ -143,7 +143,7 @@ HEADERS_BLOCKED= 2. **Via application.properties (Quarkus):** ```properties -quarkus.headers.blocked +quarkus.headers.blocked= ``` **`quarkus.headers.blocked` rules and limitations** From 39c9234130cd30172a850d26eb31527fa3157d93 Mon Sep 17 00:00:00 2001 From: Ksiona Date: Thu, 14 May 2026 16:29:47 +0400 Subject: [PATCH 3/4] fix: documentation mismatch --- core-context-propagation-quarkus/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core-context-propagation-quarkus/README.md b/core-context-propagation-quarkus/README.md index d156f5320..6ab77d25c 100644 --- a/core-context-propagation-quarkus/README.md +++ b/core-context-propagation-quarkus/README.md @@ -84,7 +84,7 @@ String acceptLanguage = acceptLanguageContextObject.getAcceptedLanguages(); #### Allowed headers Allows propagating any specified headers. To set a list of headers you should put either -`HEADERS_ALLOWED` environment or set the `headers.allowed` property. Property has more precedence than env. +`HEADERS_ALLOWED` environment or set the `quarkus.headers.allowed` property. Property has more precedence than env. Access: @@ -94,10 +94,10 @@ Map allowedHeaders = allowedHeadersContextObject.getHeaders(); ``` You just need to specify a list of headers in `application.properties` -in the `headers.allowed` property. For example: +in the `quarkus.headers.allowed` property. For example: ```properties -headers.allowed=myheader1,myheader2,... +quarkus.headers.allowed=myheader1,myheader2,... ``` Otherwise, you need to take care that this parameter is in System#property or environment. From e384b69ec8d85d2066fd67f541942a3ed1603ef3 Mon Sep 17 00:00:00 2001 From: Ksiona Date: Fri, 15 May 2026 23:54:41 +0400 Subject: [PATCH 4/4] fix: empty value is equal to "not set" for quarkus. --- core-context-propagation-quarkus/README.md | 4 +- .../allowedheaders/HeadersAllowedConfig.java | 32 ++++++++- .../HeadersAllowedRecorder.java | 5 +- .../allowedheaders/RawStringConverter.java | 10 +++ .../HeadersAllowedConfigTest.java | 9 ++- .../src/test/resources/application.properties | 2 + core-context-propagation/README.md | 4 +- .../HeaderPropagationConfiguration.java | 24 ++++--- .../HeaderPropagationConfigurationTest.java | 34 ++++++++-- .../context-propagation-spring-common/pom.xml | 6 ++ ...ContextProviderConfigurationEmptyTest.java | 52 +++++++++++++++ ...xtProviderConfigurationFromEnvVarTest.java | 66 +++++++++++++++++++ ...roviderConfigurationNotConfiguredTest.java | 48 ++++++++++++++ ...iderConfigurationOnlyNonBlockableTest.java | 54 +++++++++++++++ ...extProviderConfigurationWithValueTest.java | 52 +++++++++++++++ 15 files changed, 378 insertions(+), 24 deletions(-) create mode 100644 core-context-propagation-quarkus/framework-contexts/runtime/src/main/java/com/netcracker/cloud/framework/quarkus/contexts/allowedheaders/RawStringConverter.java create mode 100644 core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationEmptyTest.java create mode 100644 core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationFromEnvVarTest.java create mode 100644 core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationNotConfiguredTest.java create mode 100644 core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationOnlyNonBlockableTest.java create mode 100644 core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationWithValueTest.java diff --git a/core-context-propagation-quarkus/README.md b/core-context-propagation-quarkus/README.md index 6ab77d25c..4be422ff0 100644 --- a/core-context-propagation-quarkus/README.md +++ b/core-context-propagation-quarkus/README.md @@ -152,8 +152,8 @@ quarkus.headers.blocked= - Default when not configured at all: `X-Channel-Request-Id` is blocked. - Explicit empty value (`quarkus.headers.blocked=` / `HEADERS_BLOCKED=`): blacklist is empty (nothing is blocked). - Explicit non-empty value with valid headers (for example `quarkus.headers.blocked=Some-Header`): only listed headers are blocked. -- `X-Request-Id` is non-blockable: if it is listed in `quarkus.headers.blocked`/`HEADERS_BLOCKED`, it is ignored. -- If configured value contains only non-blockable entries (for example only `X-Request-Id`), default block is applied and `X-Channel-Request-Id` remains blocked. +- `X-Request-Id` is non-blockable: if it is listed in `quarkus.headers.blocked`/`HEADERS_BLOCKED`, it is silently dropped from the configured list. +- If the configured value contains only non-blockable entries (for example only `X-Request-Id`), the resulting blocked list is **empty** — the default is **not** restored. Any explicit configuration (even one that effectively blocks nothing) is treated as the user's deliberate override of the default. **MDC Integration:** The channel request ID is automatically stored in MDC under the key `x_channel_request_id` for use in logging. diff --git a/core-context-propagation-quarkus/framework-contexts/runtime/src/main/java/com/netcracker/cloud/framework/quarkus/contexts/allowedheaders/HeadersAllowedConfig.java b/core-context-propagation-quarkus/framework-contexts/runtime/src/main/java/com/netcracker/cloud/framework/quarkus/contexts/allowedheaders/HeadersAllowedConfig.java index ff956700c..c59ddce43 100644 --- a/core-context-propagation-quarkus/framework-contexts/runtime/src/main/java/com/netcracker/cloud/framework/quarkus/contexts/allowedheaders/HeadersAllowedConfig.java +++ b/core-context-propagation-quarkus/framework-contexts/runtime/src/main/java/com/netcracker/cloud/framework/quarkus/contexts/allowedheaders/HeadersAllowedConfig.java @@ -3,6 +3,8 @@ import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithConverter; +import io.smallrye.config.WithDefault; import io.smallrye.config.WithName; import java.util.Optional; @@ -10,15 +12,41 @@ @ConfigMapping(prefix = "quarkus") @ConfigRoot(phase = ConfigPhase.RUN_TIME) public interface HeadersAllowedConfig { + + /** + * Sentinel value used to distinguish "property not set" from "property explicitly set to empty". + * Starts with a NUL character so users cannot accidentally type it. + */ + String UNSET = "\0__unset__"; + /** - * Allowed headers to propagate in contexts + * Allowed headers to propagate in contexts. */ @WithName("headers.allowed") Optional allowedHeaders(); /** * Blocked headers for context propagation. X-Channel-Request-Id is blocked by default. + *

+ * Three distinct states are supported: + *

    + *
  • property not set — value equals {@link #UNSET}; the default blocked list applies.
  • + *
  • property set to empty — value equals "" (empty string); the default is erased.
  • + *
  • property set to a value — value is the raw configured string.
  • + *
+ * The custom {@link RawStringConverter} is required so that an empty value is preserved + * instead of being collapsed to {@code null} by SmallRye's default String converter. */ @WithName("headers.blocked") - Optional blockedHeaders(); + @WithConverter(RawStringConverter.class) + @WithDefault(UNSET) + String blockedHeaders(); + + /** + * @return {@code true} if the user explicitly configured {@code quarkus.headers.blocked} + * (including to an empty value); {@code false} if the property was not set at all. + */ + default boolean isBlockedHeadersSet() { + return !UNSET.equals(blockedHeaders()); + } } diff --git a/core-context-propagation-quarkus/framework-contexts/runtime/src/main/java/com/netcracker/cloud/framework/quarkus/contexts/allowedheaders/HeadersAllowedRecorder.java b/core-context-propagation-quarkus/framework-contexts/runtime/src/main/java/com/netcracker/cloud/framework/quarkus/contexts/allowedheaders/HeadersAllowedRecorder.java index e795f9919..99cec85df 100644 --- a/core-context-propagation-quarkus/framework-contexts/runtime/src/main/java/com/netcracker/cloud/framework/quarkus/contexts/allowedheaders/HeadersAllowedRecorder.java +++ b/core-context-propagation-quarkus/framework-contexts/runtime/src/main/java/com/netcracker/cloud/framework/quarkus/contexts/allowedheaders/HeadersAllowedRecorder.java @@ -9,6 +9,9 @@ public class HeadersAllowedRecorder { public void setAllowedHeadersToSystemProperty() { HeadersAllowedConfig allowedConfig = Arc.container().instance(HeadersAllowedConfig.class).get(); allowedConfig.allowedHeaders().ifPresent(allowedHeaders -> System.setProperty("headers.allowed", allowedHeaders)); - allowedConfig.blockedHeaders().ifPresent(blockedHeaders -> System.setProperty("headers.blocked", blockedHeaders)); + + if (allowedConfig.isBlockedHeadersSet()) { + System.setProperty("headers.blocked", allowedConfig.blockedHeaders()); + } } } diff --git a/core-context-propagation-quarkus/framework-contexts/runtime/src/main/java/com/netcracker/cloud/framework/quarkus/contexts/allowedheaders/RawStringConverter.java b/core-context-propagation-quarkus/framework-contexts/runtime/src/main/java/com/netcracker/cloud/framework/quarkus/contexts/allowedheaders/RawStringConverter.java new file mode 100644 index 000000000..12ac744ff --- /dev/null +++ b/core-context-propagation-quarkus/framework-contexts/runtime/src/main/java/com/netcracker/cloud/framework/quarkus/contexts/allowedheaders/RawStringConverter.java @@ -0,0 +1,10 @@ +package com.netcracker.cloud.framework.quarkus.contexts.allowedheaders; + +import org.eclipse.microprofile.config.spi.Converter; + +public class RawStringConverter implements Converter { + @Override + public String convert(String value) { + return value; + } +} \ No newline at end of file diff --git a/core-context-propagation-quarkus/integration-tests/context-propagation-reactive-test/src/test/java/com/netcracker/cloud/framework/quarkus/contexts/allowedheaders/HeadersAllowedConfigTest.java b/core-context-propagation-quarkus/integration-tests/context-propagation-reactive-test/src/test/java/com/netcracker/cloud/framework/quarkus/contexts/allowedheaders/HeadersAllowedConfigTest.java index 0d5618251..fcece6b33 100644 --- a/core-context-propagation-quarkus/integration-tests/context-propagation-reactive-test/src/test/java/com/netcracker/cloud/framework/quarkus/contexts/allowedheaders/HeadersAllowedConfigTest.java +++ b/core-context-propagation-quarkus/integration-tests/context-propagation-reactive-test/src/test/java/com/netcracker/cloud/framework/quarkus/contexts/allowedheaders/HeadersAllowedConfigTest.java @@ -10,6 +10,7 @@ @QuarkusTest class HeadersAllowedConfigTest { + @Inject HeadersAllowedConfig headersAllowedConfig; @@ -21,5 +22,11 @@ void shouldReadHeadersAllowedFromProperty() { assertEquals("test-quarkus.headers.allowed", value.get()); } + @Test + void shouldReadHeadersBlockedAsExplicitlyEmpty() { + assertTrue(headersAllowedConfig.isBlockedHeadersSet(), + "quarkus.headers.blocked must be considered explicitly set"); + assertEquals("", headersAllowedConfig.blockedHeaders(), + "quarkus.headers.blocked must be preserved as an empty string"); + } } - diff --git a/core-context-propagation-quarkus/integration-tests/context-propagation-reactive-test/src/test/resources/application.properties b/core-context-propagation-quarkus/integration-tests/context-propagation-reactive-test/src/test/resources/application.properties index 6fecc5866..209ea103f 100644 --- a/core-context-propagation-quarkus/integration-tests/context-propagation-reactive-test/src/test/resources/application.properties +++ b/core-context-propagation-quarkus/integration-tests/context-propagation-reactive-test/src/test/resources/application.properties @@ -4,4 +4,6 @@ quarkus.devservices.enabled=false quarkus.rest-client.my-client.url=http://localhost:${quarkus.http.test-port} quarkus.headers.allowed=test-quarkus.headers.allowed +quarkus.headers.blocked= headers.allowed=test-headers.allowed +headers.blocked=test-headers.blocked diff --git a/core-context-propagation/README.md b/core-context-propagation/README.md index 40f550846..2e968cb92 100644 --- a/core-context-propagation/README.md +++ b/core-context-propagation/README.md @@ -171,8 +171,8 @@ headers.blocked= - Default when not configured at all: `X-Channel-Request-Id` is blocked. - Explicit empty value (`headers.blocked=` / `HEADERS_BLOCKED=`): blacklist is empty (nothing is blocked). - Explicit non-empty value with valid headers (for example `headers.blocked=Some-Header`): only listed headers are blocked. -- `X-Request-Id` is non-blockable: if it is listed in `headers.blocked`/`HEADERS_BLOCKED`, it is ignored. -- If configured value contains only non-blockable entries (for example only `X-Request-Id`), default block is applied and `X-Channel-Request-Id` remains blocked. +- `X-Request-Id` is non-blockable: if it is listed in `headers.blocked`/`HEADERS_BLOCKED`, it is silently dropped from the configured list. +- If the configured value contains only non-blockable entries (for example only `X-Request-Id`), the resulting blocked list is **empty** — the default is **not** restored. Any explicit configuration (even one that effectively blocks nothing) is treated as the user's deliberate override of the default. **MDC Integration:** The `X-Channel-Request-Id` is automatically integrated with SLF4J's Mapped Diagnostic Context (MDC) for seamless logging. diff --git a/core-context-propagation/framework-contexts/src/main/java/com/netcracker/cloud/framework/contexts/allowedheaders/HeaderPropagationConfiguration.java b/core-context-propagation/framework-contexts/src/main/java/com/netcracker/cloud/framework/contexts/allowedheaders/HeaderPropagationConfiguration.java index aa9de6028..e3d9a301a 100644 --- a/core-context-propagation/framework-contexts/src/main/java/com/netcracker/cloud/framework/contexts/allowedheaders/HeaderPropagationConfiguration.java +++ b/core-context-propagation/framework-contexts/src/main/java/com/netcracker/cloud/framework/contexts/allowedheaders/HeaderPropagationConfiguration.java @@ -69,23 +69,29 @@ private static List readBlockedHeaders() { String envValue = System.getenv(HEADERS_BLOCKED_ENV); boolean envSpecified = envValue != null; - String blockedHeaders = propertySpecified + // No source set at all → fall back to the built-in default. + if (!propertySpecified && !envSpecified) { + return DEFAULT_BLOCKED_HEADERS; + } + + // Property wins over env when both are set. + String raw = propertySpecified ? System.getProperty(HEADERS_BLOCKED_PROPERTY) : envValue; - boolean anySourceSpecified = propertySpecified || envSpecified; - - if (blockedHeaders == null || blockedHeaders.isBlank()) { - return anySourceSpecified ? Collections.emptyList() : DEFAULT_BLOCKED_HEADERS; + // Source is set but empty/blank → explicit "erase the default". + if (raw == null || raw.isBlank()) { + return Collections.emptyList(); } - - List configured = Arrays.stream(blockedHeaders.split(",")) + + // Source is set with a value → parse, trim, drop non-blockable entries. + // If everything filters out, we still respect the user's explicit override + // and return an empty list — we do NOT silently restore the default. + return Arrays.stream(raw.split(",")) .map(String::trim) .filter(s -> !s.isEmpty()) .filter(s -> NON_BLOCKABLE_HEADERS.stream() .noneMatch(s::equalsIgnoreCase)) .toList(); - - return configured.isEmpty() ? DEFAULT_BLOCKED_HEADERS : configured; } } diff --git a/core-context-propagation/framework-contexts/src/test/java/com/netcracker/cloud/framework/contexts/allowedheaders/HeaderPropagationConfigurationTest.java b/core-context-propagation/framework-contexts/src/test/java/com/netcracker/cloud/framework/contexts/allowedheaders/HeaderPropagationConfigurationTest.java index cf5944adf..fed20c41a 100644 --- a/core-context-propagation/framework-contexts/src/test/java/com/netcracker/cloud/framework/contexts/allowedheaders/HeaderPropagationConfigurationTest.java +++ b/core-context-propagation/framework-contexts/src/test/java/com/netcracker/cloud/framework/contexts/allowedheaders/HeaderPropagationConfigurationTest.java @@ -67,31 +67,51 @@ void shouldNotBlacklistXChannelRequestIdWhenBlockedHeadersExplicitlyEmpty() { @Test void shouldNotBlacklistXChannelRequestIdWhenOtherHeadersExplicitlyBlocked() { + // The property is explicitly set to a non-blockable header; the default + // blocked list must NOT silently kick in. System.setProperty(HeaderPropagationConfiguration.HEADERS_BLOCKED_PROPERTY, "X-Request-Id"); HeaderPropagationConfiguration.resetCache(); Assertions.assertFalse(HeaderPropagationConfiguration.isBlacklisted("X-Request-Id")); - Assertions.assertTrue(HeaderPropagationConfiguration.isBlacklisted("X-Channel-Request-Id")); + Assertions.assertFalse(HeaderPropagationConfiguration.isBlacklisted("X-Channel-Request-Id")); } @Test - void shouldApplyDefaultBlacklistWhenOnlyNonBlockableHeadersConfigured() { + void shouldReturnEmptyListWhenOnlyNonBlockableHeadersConfigured() { System.setProperty(HeaderPropagationConfiguration.HEADERS_BLOCKED_PROPERTY, "X-Request-Id, x-request-id"); HeaderPropagationConfiguration.resetCache(); Assertions.assertFalse(HeaderPropagationConfiguration.isBlacklisted("X-Request-Id")); - Assertions.assertTrue(HeaderPropagationConfiguration.isBlacklisted("X-Channel-Request-Id")); - Assertions.assertEquals(HeaderPropagationConfiguration.DEFAULT_BLOCKED_HEADERS, - HeaderPropagationConfiguration.blockedHeaders()); + Assertions.assertFalse(HeaderPropagationConfiguration.isBlacklisted("X-Channel-Request-Id")); + Assertions.assertTrue(HeaderPropagationConfiguration.blockedHeaders().isEmpty()); } @Test - void shouldBlacklistHeaderByEnvWhenPropertyNotSet() throws Exception { + void shouldReadEnvWhenPropertyNotSet() throws Exception { + // Env value is read when no system property is set. The configured value + // is only X-Request-Id (non-blockable), so the resulting list must be empty — + // we deliberately do NOT fall back to the default blocked list here. environmentVariables.set(HeaderPropagationConfiguration.HEADERS_BLOCKED_ENV, "X-Request-Id"); try { HeaderPropagationConfiguration.resetCache(); Assertions.assertFalse(HeaderPropagationConfiguration.isBlacklisted("X-Request-Id")); - Assertions.assertTrue(HeaderPropagationConfiguration.isBlacklisted("X-Channel-Request-Id")); + Assertions.assertFalse(HeaderPropagationConfiguration.isBlacklisted("X-Channel-Request-Id")); + Assertions.assertTrue(HeaderPropagationConfiguration.blockedHeaders().isEmpty()); + } finally { + environmentVariables.remove(HeaderPropagationConfiguration.HEADERS_BLOCKED_ENV); + HeaderPropagationConfiguration.resetCache(); + } + } + + @Test + void shouldBlacklistHeaderByEnvWhenPropertyNotSet() throws Exception { + // Sanity check that env-sourced configuration actually drives the blocked list + // when the system property is absent. + environmentVariables.set(HeaderPropagationConfiguration.HEADERS_BLOCKED_ENV, "Custom-Header"); + try { + HeaderPropagationConfiguration.resetCache(); + Assertions.assertTrue(HeaderPropagationConfiguration.isBlacklisted("Custom-Header")); + Assertions.assertFalse(HeaderPropagationConfiguration.isBlacklisted("X-Channel-Request-Id")); } finally { environmentVariables.remove(HeaderPropagationConfiguration.HEADERS_BLOCKED_ENV); HeaderPropagationConfiguration.resetCache(); diff --git a/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/pom.xml b/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/pom.xml index 3694ec174..c208c3358 100644 --- a/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/pom.xml +++ b/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/pom.xml @@ -56,6 +56,12 @@ spring-test test + + uk.org.webcompere + system-stubs-jupiter + 2.1.8 + test + diff --git a/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationEmptyTest.java b/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationEmptyTest.java new file mode 100644 index 000000000..ce5a5721b --- /dev/null +++ b/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationEmptyTest.java @@ -0,0 +1,52 @@ +package com.netcracker.cloud.context.propagation.spring.common.configuration; + +import com.netcracker.cloud.framework.contexts.allowedheaders.HeaderPropagationConfiguration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Spring integration scenario: {@code headers.blocked=} (set explicitly to empty). + * Verifies that {@link SpringContextProviderConfiguration#init()} propagates the + * empty value to the system property, which downstream code interprets as + * "erase the default blocked list". + */ +@SpringJUnitConfig(classes = SpringContextProviderConfiguration.class) +@TestPropertySource(properties = { + "headers.allowed=custom-header", + "headers.blocked=" +}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +class SpringContextProviderConfigurationEmptyTest { + + @BeforeAll + static void setup() { + System.clearProperty("headers.blocked"); + HeaderPropagationConfiguration.resetCache(); + } + + @AfterAll + static void teardown() { + System.clearProperty("headers.blocked"); + HeaderPropagationConfiguration.resetCache(); + } + + @Test + void shouldSetEmptySystemPropertyAndEraseDefaultBlockedList() { + assertEquals("", System.getProperty("headers.blocked"), + "Spring init() must propagate explicit empty value to the system property"); + + HeaderPropagationConfiguration.resetCache(); + assertTrue(HeaderPropagationConfiguration.blockedHeaders().isEmpty(), + "explicit empty value must erase the default blocked list"); + assertFalse(HeaderPropagationConfiguration.isBlacklisted("X-Channel-Request-Id"), + "X-Channel-Request-Id must no longer be blocked"); + } +} diff --git a/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationFromEnvVarTest.java b/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationFromEnvVarTest.java new file mode 100644 index 000000000..96851f99c --- /dev/null +++ b/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationFromEnvVarTest.java @@ -0,0 +1,66 @@ +package com.netcracker.cloud.context.propagation.spring.common.configuration; + +import com.netcracker.cloud.framework.contexts.allowedheaders.HeaderPropagationConfiguration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; +import uk.org.webcompere.systemstubs.jupiter.SystemStub; +import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Spring integration scenario: {@code headers.blocked} is configured only via the + * {@code HEADERS_BLOCKED} environment variable. Verifies that Spring's relaxed + * binding picks up the env var as the {@code headers.blocked} property, that + * {@link SpringContextProviderConfiguration#init()} propagates it to the system + * property, and that the downstream blocked list reflects the env-sourced value. + * + *

{@link SystemStubsExtension} must be registered before {@link SpringExtension} + * so that the env var is set before Spring's {@code SystemEnvironmentPropertySource} + * is consulted during context initialization.

+ */ +@ExtendWith({SystemStubsExtension.class, SpringExtension.class}) +@ContextConfiguration(classes = SpringContextProviderConfiguration.class) +@TestPropertySource(properties = { + // headers.blocked deliberately not declared here — it must come from the env var + "headers.allowed=custom-header" +}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +class SpringContextProviderConfigurationFromEnvVarTest { + + @SystemStub + static EnvironmentVariables envVars = new EnvironmentVariables("HEADERS_BLOCKED", "Custom-Header"); + + @BeforeAll + static void setup() { + System.clearProperty("headers.blocked"); + HeaderPropagationConfiguration.resetCache(); + } + + @AfterAll + static void teardown() { + System.clearProperty("headers.blocked"); + HeaderPropagationConfiguration.resetCache(); + } + + @Test + void shouldReadHeadersBlockedFromEnvVar() { + assertEquals("Custom-Header", System.getProperty("headers.blocked"), + "Spring init() must propagate env-sourced value to the system property"); + + HeaderPropagationConfiguration.resetCache(); + assertTrue(HeaderPropagationConfiguration.isBlacklisted("Custom-Header"), + "env-sourced header must be blocked"); + assertFalse(HeaderPropagationConfiguration.isBlacklisted("X-Channel-Request-Id"), + "explicit env-sourced configuration overrides the default"); + } +} diff --git a/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationNotConfiguredTest.java b/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationNotConfiguredTest.java new file mode 100644 index 000000000..664859f6a --- /dev/null +++ b/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationNotConfiguredTest.java @@ -0,0 +1,48 @@ +package com.netcracker.cloud.context.propagation.spring.common.configuration; + +import com.netcracker.cloud.framework.contexts.allowedheaders.HeaderPropagationConfiguration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Spring integration scenario: {@code headers.blocked} is not set anywhere. + * Verifies that {@link SpringContextProviderConfiguration#init()} does NOT touch + * the {@code headers.blocked} system property, so downstream code falls back to + * the built-in default blocked list (which contains {@code X-Channel-Request-Id}). + */ +@SpringJUnitConfig(classes = SpringContextProviderConfiguration.class) +@TestPropertySource(properties = { + "headers.allowed=custom-header" +}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +class SpringContextProviderConfigurationNotConfiguredTest { + + @BeforeAll + static void setup() { + System.clearProperty("headers.blocked"); + HeaderPropagationConfiguration.resetCache(); + } + + @AfterAll + static void teardown() { + System.clearProperty("headers.blocked"); + HeaderPropagationConfiguration.resetCache(); + } + + @Test + void shouldNotSetSystemPropertyAndApplyDefaultBlockedList() { + assertNull(System.getProperty("headers.blocked"), + "headers.blocked must remain unset when no source configures it"); + + HeaderPropagationConfiguration.resetCache(); + assertTrue(HeaderPropagationConfiguration.isBlacklisted("X-Channel-Request-Id"), + "default blocked list must apply when nothing is configured"); + } +} diff --git a/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationOnlyNonBlockableTest.java b/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationOnlyNonBlockableTest.java new file mode 100644 index 000000000..e97020668 --- /dev/null +++ b/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationOnlyNonBlockableTest.java @@ -0,0 +1,54 @@ +package com.netcracker.cloud.context.propagation.spring.common.configuration; + +import com.netcracker.cloud.framework.contexts.allowedheaders.HeaderPropagationConfiguration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Spring integration scenario: {@code headers.blocked=X-Request-Id} — the configured + * value consists exclusively of a non-blockable header. The resulting blocked list + * must be empty, NOT the built-in default. This locks in the behavior change made + * to {@link HeaderPropagationConfiguration} (no silent fallback to default when the + * user explicitly configured something). + */ +@SpringJUnitConfig(classes = SpringContextProviderConfiguration.class) +@TestPropertySource(properties = { + "headers.allowed=custom-header", + "headers.blocked=X-Request-Id" +}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +class SpringContextProviderConfigurationOnlyNonBlockableTest { + + @BeforeAll + static void setup() { + System.clearProperty("headers.blocked"); + HeaderPropagationConfiguration.resetCache(); + } + + @AfterAll + static void teardown() { + System.clearProperty("headers.blocked"); + HeaderPropagationConfiguration.resetCache(); + } + + @Test + void shouldRespectExplicitOverrideEvenWhenItFiltersToEmpty() { + assertEquals("X-Request-Id", System.getProperty("headers.blocked")); + + HeaderPropagationConfiguration.resetCache(); + assertTrue(HeaderPropagationConfiguration.blockedHeaders().isEmpty(), + "the resulting blocked list must be empty — the default must NOT be restored"); + assertFalse(HeaderPropagationConfiguration.isBlacklisted("X-Request-Id"), + "X-Request-Id is non-blockable and must never be blocked"); + assertFalse(HeaderPropagationConfiguration.isBlacklisted("X-Channel-Request-Id"), + "explicit (even if filter-emptied) configuration overrides the default"); + } +} diff --git a/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationWithValueTest.java b/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationWithValueTest.java new file mode 100644 index 000000000..45480ca94 --- /dev/null +++ b/core-context-propagation/spring-context-aggregator/context-propagation-spring-common/src/test/java/com/netcracker/cloud/context/propagation/spring/common/configuration/SpringContextProviderConfigurationWithValueTest.java @@ -0,0 +1,52 @@ +package com.netcracker.cloud.context.propagation.spring.common.configuration; + +import com.netcracker.cloud.framework.contexts.allowedheaders.HeaderPropagationConfiguration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Spring integration scenario: {@code headers.blocked} is set to a concrete blockable header. + * Verifies that the listed header is blocked and the default's + * {@code X-Channel-Request-Id} entry no longer applies. + */ +@SpringJUnitConfig(classes = SpringContextProviderConfiguration.class) +@TestPropertySource(properties = { + "headers.allowed=custom-header", + "headers.blocked=Custom-Header" +}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +class SpringContextProviderConfigurationWithValueTest { + + @BeforeAll + static void setup() { + System.clearProperty("headers.blocked"); + HeaderPropagationConfiguration.resetCache(); + } + + @AfterAll + static void teardown() { + System.clearProperty("headers.blocked"); + HeaderPropagationConfiguration.resetCache(); + } + + @Test + void shouldSetSystemPropertyAndBlockConfiguredHeader() { + assertEquals("Custom-Header", System.getProperty("headers.blocked")); + + HeaderPropagationConfiguration.resetCache(); + assertTrue(HeaderPropagationConfiguration.isBlacklisted("Custom-Header"), + "configured header must be blocked"); + assertTrue(HeaderPropagationConfiguration.isBlacklisted("custom-header"), + "blocking must be case-insensitive"); + assertFalse(HeaderPropagationConfiguration.isBlacklisted("X-Channel-Request-Id"), + "explicit configuration overrides the default blocked list"); + } +}