From b9e3c3ad225c3a25f9571747e60af6adfd5b2d2c Mon Sep 17 00:00:00 2001 From: gortiz-dotcms Date: Wed, 20 May 2026 09:47:59 -0300 Subject: [PATCH 1/4] fix: Feature flag boolean parsing error (#35551) --- .../dot-custom-event-handler.service.ts | 2 +- .../api/v1/system/ConfigurationResource.java | 70 ++++- .../v1/system/ConfigurationResourceTest.java | 293 ++++++++++++++++++ 3 files changed, 361 insertions(+), 4 deletions(-) create mode 100644 dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java diff --git a/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.ts b/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.ts index fb9201231344..2fc6ce788da0 100644 --- a/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.ts +++ b/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.ts @@ -58,7 +58,7 @@ export class DotCustomEventHandlerService { .getKeys([FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED]) .subscribe((response) => { const contentEditorFeatureFlag = - response[FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED] === 'true'; + response[FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED] === true; if (!this.handlers) { this.handlers = { diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java index dc8dc61f8c0f..70679ac90cc9 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java @@ -30,6 +30,7 @@ import com.dotcms.rest.WebResource; import com.dotmarketing.business.Role; import com.dotmarketing.util.Config; +import com.dotmarketing.util.Logger; import org.glassfish.jersey.server.JSONP; import com.dotcms.rest.ResponseEntityView; import io.swagger.v3.oas.annotations.tags.Tag; @@ -58,8 +59,31 @@ public class ConfigurationResource implements Serializable { private final ConfigurationHelper helper; + private final WebResource webResource; private static final String REPORT_ISSUE_INCLUDE_USER_PII = "REPORT_ISSUE_INCLUDE_USER_PII"; + /** + * Feature flag keys in WHITE_LIST that must be serialised as native JSON booleans. + * All other WHITE_LIST entries (strings, numbers, lists) are left as-is. + */ + private static final Set BOOLEAN_FEATURE_FLAGS = ImmutableSet.of( + FeatureFlagName.FEATURE_FLAG_EXPERIMENTS, + FeatureFlagName.DOTFAVORITEPAGE_FEATURE_ENABLE, + FeatureFlagName.FEATURE_FLAG_TEMPLATE_BUILDER_2, + FeatureFlagName.FEATURE_FLAG_SEO_IMPROVEMENTS, + FeatureFlagName.FEATURE_FLAG_SEO_PAGE_TOOLS, + FeatureFlagName.FEATURE_FLAG_EDIT_URL_CONTENT_MAP, + FeatureFlagName.FEATURE_FLAG_NEW_BINARY_FIELD, + FeatureFlagName.FEATURE_FLAG_ANNOUNCEMENTS, + FeatureFlagName.FEATURE_FLAG_NEW_EDIT_PAGE, + FeatureFlagName.FEATURE_FLAG_UVE_PREVIEW_MODE, + FeatureFlagName.FEATURE_FLAG_UVE_TOGGLE_LOCK, + FeatureFlagName.FEATURE_FLAG_UVE_STYLE_EDITOR, + FeatureFlagName.FEATURE_FLAG_PAGE_SCANNER, + FeatureFlagName.FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION, + FeatureFlagName.FEATURE_FLAG_NEW_BLOCK_EDITOR, + "CONTENT_EDITOR2_ENABLED"); // FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED + private static final Set WHITE_LIST = ImmutableSet.copyOf( Config.getStringArrayProperty("CONFIGURATION_WHITE_LIST", new String[] {"EMAIL_SYSTEM_ADDRESS", "WYSIWYG_IMAGE_URL_PATTERN", "CHARSET","CONTENT_PALETTE_HIDDEN_CONTENT_TYPES", "DEFAULT_CONTAINER", @@ -83,6 +107,15 @@ private boolean isOnBlackList(final String key) { */ public ConfigurationResource() { this.helper = ConfigurationHelper.INSTANCE; + this.webResource = new WebResource(); + } + + /** + * Test constructor — allows injecting a mock {@link WebResource}. + */ + ConfigurationResource(final WebResource webResource) { + this.helper = ConfigurationHelper.INSTANCE; + this.webResource = webResource; } /** @@ -100,10 +133,9 @@ public ConfigurationResource() { @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) public final Response getConfigVariables(@Context final HttpServletRequest request, @Context final HttpServletResponse response, - @QueryParam("keys") final String keysQuery) - throws IOException { + @QueryParam("keys") final String keysQuery) { - new WebResource.InitBuilder(request, response) + new WebResource.InitBuilder(webResource) .requiredBackendUser(true) .requestAndResponse(request, response) .rejectWhenNoUser(true) @@ -147,9 +179,41 @@ private Object recoveryFromConfig (final String key) { return Config.getIntProperty(key.replace("number:", StringPool.BLANK), 0); } + if (BOOLEAN_FEATURE_FLAGS.contains(key)) { + return parseBooleanFlag(key); + } + return Config.getStringProperty(key, "NOT_FOUND"); } + /** + * Resolves a feature flag property to a native boolean, or the sentinel "NOT_FOUND" + * when the key is not defined anywhere (no .properties entry, no DOT_* env override). + * Accepted truthy values (case-insensitive, whitespace-trimmed): "true", "1". + * Accepted falsy values: "false", "0", "". + * Unrecognised values are logged as WARN and resolve to false. + */ + private static Object parseBooleanFlag(final String key) { + final String rawValue = Config.getStringProperty(key, null); + if (rawValue == null) { + return "NOT_FOUND"; + } + final String normalized = rawValue.trim().toLowerCase(Locale.ROOT); + switch (normalized) { + case "true": + case "1": + return Boolean.TRUE; + case "false": + case "0": + case "": + return Boolean.FALSE; + default: + Logger.warn(ConfigurationResource.class, + "Feature flag '" + key + "' has unrecognized value '" + rawValue + "'; treating as false."); + return Boolean.FALSE; + } + } + /** * Returns the list of system properties that are set through the dotCMS * configuration files. diff --git a/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java b/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java new file mode 100644 index 000000000000..8db45c514c95 --- /dev/null +++ b/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java @@ -0,0 +1,293 @@ +package com.dotcms.rest.api.v1.system; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import com.dotcms.featureflag.FeatureFlagName; +import com.dotcms.rest.InitDataObject; +import com.dotcms.rest.ResponseEntityView; +import com.dotcms.rest.WebResource; +import com.dotmarketing.util.Config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.Response; +import java.util.Map; + +/** + * Unit tests for {@link ConfigurationResource}. + * + *

This class is the single home for all unit-level assertions about + * {@code ConfigurationResource}. Add new test methods here as new behaviour + * is introduced; + * + *

Integration-level tests (requiring a running dotCMS instance) live in + * {@code dotcms-integration/.../ConfigurationResourceTest.java}. + * + */ +public class ConfigurationResourceTest { + + private static final String FLAG = FeatureFlagName.FEATURE_FLAG_UVE_TOGGLE_LOCK; + private static final String UNDEFINED_FLAG = FeatureFlagName.FEATURE_FLAG_UVE_STYLE_EDITOR; + private static final String NON_FLAG_KEY = "EMAIL_SYSTEM_ADDRESS"; + private static final String UNLISTED_KEY = "SOME_INTERNAL_SECRET"; + + private WebResource webResource; + private ConfigurationResource resource; + private HttpServletRequest request; + private HttpServletResponse response; + + // ── Setup / teardown ────────────────────────────────────────────────────── + + @BeforeEach + void setUp() { + webResource = mock(WebResource.class); + resource = new ConfigurationResource(webResource); + request = mock(HttpServletRequest.class); + response = mock(HttpServletResponse.class); + + final InitDataObject initData = mock(InitDataObject.class); + when(webResource.init(any(WebResource.InitBuilder.class))).thenReturn(initData); + } + + // ── Truthy variants (AC: "true", "True", "TRUE", " true ", "1" → true) ── + + /** + * Method to test: {@link ConfigurationResource#getConfigVariables} + * Given scenario: A boolean feature flag is set to lowercase {@code "true"}. + * Expected result: Response contains native boolean {@code true} (not the string). + */ + @Test + void getConfigVariables_flagSetToLowercaseTrue_returnsNativeBooleanTrue() { + try (MockedStatic config = mockStatic(Config.class)) { + config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("true"); + + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + + assertEquals(Boolean.TRUE, entity.get(FLAG)); + } + } + + /** + * Method to test: {@link ConfigurationResource#getConfigVariables} + * Given scenario: A boolean feature flag is set to capitalized {@code "True"} — + * the customer-reported reproduction case. + * Expected result: Response contains native boolean {@code true}. + */ + @Test + void getConfigVariables_flagSetToCapitalizedTrue_returnsNativeBooleanTrue() { + try (MockedStatic config = mockStatic(Config.class)) { + config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("True"); + + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + + assertEquals(Boolean.TRUE, entity.get(FLAG)); + } + } + + /** + * Method to test: {@link ConfigurationResource#getConfigVariables} + * Given scenario: A boolean feature flag is set to all-caps {@code "TRUE"}. + * Expected result: Response contains native boolean {@code true}. + */ + @Test + void getConfigVariables_flagSetToUppercaseTrue_returnsNativeBooleanTrue() { + try (MockedStatic config = mockStatic(Config.class)) { + config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("TRUE"); + + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + + assertEquals(Boolean.TRUE, entity.get(FLAG)); + } + } + + /** + * Method to test: {@link ConfigurationResource#getConfigVariables} + * Given scenario: A boolean feature flag is set to {@code " true "} with surrounding whitespace. + * Expected result: Response contains native boolean {@code true} — whitespace is trimmed. + */ + @Test + void getConfigVariables_flagSetToTrueWithWhitespace_returnsNativeBooleanTrue() { + try (MockedStatic config = mockStatic(Config.class)) { + config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn(" true "); + + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + + assertEquals(Boolean.TRUE, entity.get(FLAG)); + } + } + + /** + * Method to test: {@link ConfigurationResource#getConfigVariables} + * Given scenario: A boolean feature flag is set to the numeric shorthand {@code "1"}. + * Expected result: Response contains native boolean {@code true}. + */ + @Test + void getConfigVariables_flagSetToNumericOne_returnsNativeBooleanTrue() { + try (MockedStatic config = mockStatic(Config.class)) { + config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("1"); + + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + + assertEquals(Boolean.TRUE, entity.get(FLAG)); + } + } + + // ── Falsy variants (AC: "false", "False", "FALSE", " false ", "0", "" → false) ── + + /** + * Method to test: {@link ConfigurationResource#getConfigVariables} + * Given scenario: A boolean feature flag is set to lowercase {@code "false"}. + * Expected result: Response contains native boolean {@code false}. + */ + @Test + void getConfigVariables_flagSetToLowercaseFalse_returnsNativeBooleanFalse() { + try (MockedStatic config = mockStatic(Config.class)) { + config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("false"); + + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + + assertEquals(Boolean.FALSE, entity.get(FLAG)); + } + } + + /** + * Method to test: {@link ConfigurationResource#getConfigVariables} + * Given scenario: A boolean feature flag is set to all-caps {@code "FALSE"}. + * Expected result: Response contains native boolean {@code false}. + */ + @Test + void getConfigVariables_flagSetToUppercaseFalse_returnsNativeBooleanFalse() { + try (MockedStatic config = mockStatic(Config.class)) { + config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("FALSE"); + + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + + assertEquals(Boolean.FALSE, entity.get(FLAG)); + } + } + + /** + * Method to test: {@link ConfigurationResource#getConfigVariables} + * Given scenario: A boolean feature flag is set to the numeric shorthand {@code "0"}. + * Expected result: Response contains native boolean {@code false}. + */ + @Test + void getConfigVariables_flagSetToNumericZero_returnsNativeBooleanFalse() { + try (MockedStatic config = mockStatic(Config.class)) { + config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("0"); + + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + + assertEquals(Boolean.FALSE, entity.get(FLAG)); + } + } + + /** + * Method to test: {@link ConfigurationResource#getConfigVariables} + * Given scenario: A boolean feature flag is set to an empty string. + * Expected result: Response contains native boolean {@code false}. + */ + @Test + void getConfigVariables_flagSetToEmptyString_returnsNativeBooleanFalse() { + try (MockedStatic config = mockStatic(Config.class)) { + config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn(""); + + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + + assertEquals(Boolean.FALSE, entity.get(FLAG)); + } + } + + // ── NOT_FOUND sentinel ──────────────────────────────────────────────────── + + /** + * Method to test: {@link ConfigurationResource#getConfigVariables} + * Given scenario: A boolean feature flag key is not defined anywhere on the server + * ({@code Config.getStringProperty} returns {@code null}). + * Expected result: Response contains the literal string {@code "NOT_FOUND"}, never a boolean — + * the frontend uses this sentinel to apply its own enabled-by-default opt-out logic. + */ + @Test + void getConfigVariables_flagNotDefined_returnsNotFoundSentinel() { + try (MockedStatic config = mockStatic(Config.class)) { + config.when(() -> Config.getStringProperty(UNDEFINED_FLAG, null)).thenReturn(null); + + final Map entity = entityMap( + resource.getConfigVariables(request, response, UNDEFINED_FLAG)); + + assertEquals("NOT_FOUND", entity.get(UNDEFINED_FLAG)); + } + } + + // ── Unrecognised value ──────────────────────────────────────────────────── + + /** + * Method to test: {@link ConfigurationResource#getConfigVariables} + * Given scenario: A boolean feature flag has an unrecognized value such as {@code "enabled"}. + * Expected result: Response contains native boolean {@code false} — safe default preserved. + */ + @Test + void getConfigVariables_flagSetToUnrecognizedValue_returnsNativeBooleanFalse() { + try (MockedStatic config = mockStatic(Config.class)) { + config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("enabled"); + + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + + assertEquals(Boolean.FALSE, entity.get(FLAG)); + } + } + + // ── Whitelist filtering ─────────────────────────────────────────────────── + + /** + * Method to test: {@link ConfigurationResource#getConfigVariables} + * Given scenario: The caller requests a key that is not on the whitelist. + * Expected result: The key is absent from the response — it is silently excluded. + */ + @Test + void getConfigVariables_keyNotOnWhitelist_isExcludedFromResponse() { + final Map entity = entityMap( + resource.getConfigVariables(request, response, UNLISTED_KEY)); + + assertFalse(entity.containsKey(UNLISTED_KEY)); + } + + // ── Non-boolean whitelisted key ─────────────────────────────────────────── + + /** + * Method to test: {@link ConfigurationResource#getConfigVariables} + * Given scenario: The caller requests a whitelisted key that is not a boolean feature flag + * (e.g. {@code EMAIL_SYSTEM_ADDRESS}). + * Expected result: The raw string value is returned unchanged — boolean coercion is not applied + * to non-flag keys. + */ + @Test + void getConfigVariables_nonFlagWhitelistedKey_returnsRawStringValue() { + final String testAddress = "admin@example.com"; + try (MockedStatic config = mockStatic(Config.class)) { + config.when(() -> Config.getStringProperty(NON_FLAG_KEY, "NOT_FOUND")).thenReturn(testAddress); + + final Map entity = entityMap( + resource.getConfigVariables(request, response, NON_FLAG_KEY)); + + assertEquals(testAddress, entity.get(NON_FLAG_KEY)); + } + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + @SuppressWarnings("unchecked") + private static Map entityMap(final Response response) { + return (Map) ((ResponseEntityView) response.getEntity()).getEntity(); + } +} From c3d3dd6e68d4dbbdde1aa219e81b887f73a77326 Mon Sep 17 00:00:00 2001 From: gortiz-dotcms Date: Wed, 20 May 2026 10:04:07 -0300 Subject: [PATCH 2/4] =?UTF-8?q?refactor(rest-api):=20address=20PR=20review?= =?UTF-8?q?=20=E2=80=94=20eliminate=20hardcoded=20flags,=20fix=20Logger,?= =?UTF-8?q?=20harden=20test=20(#35551)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add FeatureFlagName.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED constant and replace two hardcoded "CONTENT_EDITOR2_ENABLED" literals in ConfigurationResource - Add maintenance warning to BOOLEAN_FEATURE_FLAGS Javadoc explaining the dual-list rule - Use Logger.warn lambda form to avoid eager string concatenation when WARN is filtered - Make dot-custom-event-handler tolerant of both boolean true and legacy string 'true' to eliminate rollback-window regression (M-3 concern) - Replace fragile SOME_INTERNAL_SECRET test key with a UUID-shaped string that can never be accidentally added to WHITE_LIST Co-Authored-By: Claude Sonnet 4.6 --- .../dot-custom-event-handler.service.ts | 6 ++++-- .../java/com/dotcms/featureflag/FeatureFlagName.java | 7 +++++++ .../rest/api/v1/system/ConfigurationResource.java | 11 ++++++++--- .../rest/api/v1/system/ConfigurationResourceTest.java | 3 ++- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.ts b/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.ts index 2fc6ce788da0..0c21edc81c50 100644 --- a/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.ts +++ b/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.ts @@ -57,8 +57,10 @@ export class DotCustomEventHandlerService { this.dotPropertiesService .getKeys([FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED]) .subscribe((response) => { - const contentEditorFeatureFlag = - response[FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED] === true; + // Accept native boolean true (current backend) or the legacy string 'true' + // (N-1 backend during a rollback window). + const val = response[FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED]; + const contentEditorFeatureFlag = val === true || val === 'true'; if (!this.handlers) { this.handlers = { diff --git a/dotCMS/src/main/java/com/dotcms/featureflag/FeatureFlagName.java b/dotCMS/src/main/java/com/dotcms/featureflag/FeatureFlagName.java index b7d1fc0d4a45..3181ca7caf01 100644 --- a/dotCMS/src/main/java/com/dotcms/featureflag/FeatureFlagName.java +++ b/dotCMS/src/main/java/com/dotcms/featureflag/FeatureFlagName.java @@ -64,4 +64,11 @@ public interface FeatureFlagName { String FEATURE_FLAG_OPEN_SEARCH_PHASE = "FEATURE_FLAG_OPEN_SEARCH_PHASE"; String FEATURE_FLAG_NEW_BLOCK_EDITOR = "FEATURE_FLAG_NEW_BLOCK_EDITOR"; + + /** + * Enables the new content editor (Edit Content v2). + * Also checked in content-type metadata to opt individual types out. + * Frontend equivalent: {@code FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED}. + */ + String FEATURE_FLAG_CONTENT_EDITOR2_ENABLED = "CONTENT_EDITOR2_ENABLED"; } diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java index 70679ac90cc9..8ab8b400a0ff 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java @@ -65,6 +65,11 @@ public class ConfigurationResource implements Serializable { /** * Feature flag keys in WHITE_LIST that must be serialised as native JSON booleans. * All other WHITE_LIST entries (strings, numbers, lists) are left as-is. + * + *

Maintenance rule: every key added here MUST also be present in WHITE_LIST, + * and every frontend caller that reads the key via {@code getKeys()} must compare with + * {@code === true} (boolean), not {@code === 'true'} (string). Adding a key here without + * updating both lists and all callers reproduces the exact bug this class was written to fix. */ private static final Set BOOLEAN_FEATURE_FLAGS = ImmutableSet.of( FeatureFlagName.FEATURE_FLAG_EXPERIMENTS, @@ -82,14 +87,14 @@ public class ConfigurationResource implements Serializable { FeatureFlagName.FEATURE_FLAG_PAGE_SCANNER, FeatureFlagName.FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION, FeatureFlagName.FEATURE_FLAG_NEW_BLOCK_EDITOR, - "CONTENT_EDITOR2_ENABLED"); // FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED + FeatureFlagName.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED); private static final Set WHITE_LIST = ImmutableSet.copyOf( Config.getStringArrayProperty("CONFIGURATION_WHITE_LIST", new String[] {"EMAIL_SYSTEM_ADDRESS", "WYSIWYG_IMAGE_URL_PATTERN", "CHARSET","CONTENT_PALETTE_HIDDEN_CONTENT_TYPES", "DEFAULT_CONTAINER", FeatureFlagName.FEATURE_FLAG_EXPERIMENTS, FeatureFlagName.DOTFAVORITEPAGE_FEATURE_ENABLE, FeatureFlagName.FEATURE_FLAG_TEMPLATE_BUILDER_2, "SHOW_VIDEO_THUMBNAIL", "EXPERIMENTS_MIN_DURATION", "EXPERIMENTS_MAX_DURATION", "EXPERIMENTS_DEFAULT_DURATION", FeatureFlagName.FEATURE_FLAG_SEO_IMPROVEMENTS, - FeatureFlagName.FEATURE_FLAG_SEO_PAGE_TOOLS, FeatureFlagName.FEATURE_FLAG_EDIT_URL_CONTENT_MAP, "CONTENT_EDITOR2_ENABLED", "CONTENT_EDITOR2_CONTENT_TYPE", + FeatureFlagName.FEATURE_FLAG_SEO_PAGE_TOOLS, FeatureFlagName.FEATURE_FLAG_EDIT_URL_CONTENT_MAP, FeatureFlagName.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED, "CONTENT_EDITOR2_CONTENT_TYPE", FeatureFlagName.FEATURE_FLAG_NEW_BINARY_FIELD, FeatureFlagName.FEATURE_FLAG_ANNOUNCEMENTS, FeatureFlagName.FEATURE_FLAG_NEW_EDIT_PAGE, FeatureFlagName.FEATURE_FLAG_UVE_PREVIEW_MODE, FeatureFlagName.FEATURE_FLAG_UVE_TOGGLE_LOCK, FeatureFlagName.FEATURE_FLAG_UVE_STYLE_EDITOR, FeatureFlagName.FEATURE_FLAG_PAGE_SCANNER, @@ -209,7 +214,7 @@ private static Object parseBooleanFlag(final String key) { return Boolean.FALSE; default: Logger.warn(ConfigurationResource.class, - "Feature flag '" + key + "' has unrecognized value '" + rawValue + "'; treating as false."); + () -> "Feature flag '" + key + "' has unrecognized value '" + rawValue + "'; treating as false."); return Boolean.FALSE; } } diff --git a/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java b/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java index 8db45c514c95..9d1c25cfd75f 100644 --- a/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java +++ b/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java @@ -39,7 +39,8 @@ public class ConfigurationResourceTest { private static final String FLAG = FeatureFlagName.FEATURE_FLAG_UVE_TOGGLE_LOCK; private static final String UNDEFINED_FLAG = FeatureFlagName.FEATURE_FLAG_UVE_STYLE_EDITOR; private static final String NON_FLAG_KEY = "EMAIL_SYSTEM_ADDRESS"; - private static final String UNLISTED_KEY = "SOME_INTERNAL_SECRET"; + // A key guaranteed never to appear in WHITE_LIST (fixed UUID-shaped string). + private static final String UNLISTED_KEY = "TEST_KEY_THAT_WILL_NEVER_BE_WHITELISTED_a1b2c3d4"; private WebResource webResource; private ConfigurationResource resource; From 30f7359b8211203627db1e6b6566a93f696ccf40 Mon Sep 17 00:00:00 2001 From: gortiz-dotcms Date: Wed, 20 May 2026 10:44:41 -0300 Subject: [PATCH 3/4] fix(test): fix ConfigurationResourceTest NPE from JVMInfoResource static init (#35551) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mockStatic(Config.class) inside each try block was intercepting Config.getStringProperty called by JVMInfoResource's static initializer (Pattern.compile) and returning null for the unstubbed call, causing Pattern.compile(null) → NullPointerException. Fix: open the Config mock at @BeforeEach/@AfterEach level with a default answer that returns the caller-supplied default for any unstubbed getStringProperty call, preventing JVMInfoResource from receiving null when it first loads. Co-Authored-By: Claude Sonnet 4.6 --- .../v1/system/ConfigurationResourceTest.java | 127 +++++++++--------- 1 file changed, 62 insertions(+), 65 deletions(-) diff --git a/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java b/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java index 9d1c25cfd75f..744ad07fe726 100644 --- a/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java +++ b/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java @@ -2,8 +2,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; @@ -14,6 +14,7 @@ import com.dotcms.rest.WebResource; import com.dotmarketing.util.Config; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; @@ -28,11 +29,10 @@ * *

This class is the single home for all unit-level assertions about * {@code ConfigurationResource}. Add new test methods here as new behaviour - * is introduced; + * is introduced. * *

Integration-level tests (requiring a running dotCMS instance) live in * {@code dotcms-integration/.../ConfigurationResourceTest.java}. - * */ public class ConfigurationResourceTest { @@ -46,11 +46,27 @@ public class ConfigurationResourceTest { private ConfigurationResource resource; private HttpServletRequest request; private HttpServletResponse response; + private MockedStatic mockedConfig; // ── Setup / teardown ────────────────────────────────────────────────────── @BeforeEach void setUp() { + // Open the Config mock BEFORE constructing ConfigurationResource so that + // any static initializer that calls Config (WHITE_LIST, and + // JVMInfoResource.obfuscatePattern via isOnBlackList) receives a safe + // default instead of null. Without this, Pattern.compile(null) inside + // JVMInfoResource throws NullPointerException on first class load. + // + // Default answer: return the caller-supplied default (second argument) + // for any getStringProperty / getStringArrayProperty call that is not + // individually stubbed by a test. + mockedConfig = mockStatic(Config.class); + mockedConfig.when(() -> Config.getStringProperty(anyString(), anyString())) + .thenAnswer(inv -> inv.getArgument(1)); + mockedConfig.when(() -> Config.getStringArrayProperty(anyString(), any(String[].class))) + .thenAnswer(inv -> inv.getArgument(1)); + webResource = mock(WebResource.class); resource = new ConfigurationResource(webResource); request = mock(HttpServletRequest.class); @@ -60,6 +76,11 @@ void setUp() { when(webResource.init(any(WebResource.InitBuilder.class))).thenReturn(initData); } + @AfterEach + void tearDown() { + mockedConfig.close(); + } + // ── Truthy variants (AC: "true", "True", "TRUE", " true ", "1" → true) ── /** @@ -69,13 +90,11 @@ void setUp() { */ @Test void getConfigVariables_flagSetToLowercaseTrue_returnsNativeBooleanTrue() { - try (MockedStatic config = mockStatic(Config.class)) { - config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("true"); + mockedConfig.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("true"); - final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.TRUE, entity.get(FLAG)); - } + assertEquals(Boolean.TRUE, entity.get(FLAG)); } /** @@ -86,13 +105,11 @@ void getConfigVariables_flagSetToLowercaseTrue_returnsNativeBooleanTrue() { */ @Test void getConfigVariables_flagSetToCapitalizedTrue_returnsNativeBooleanTrue() { - try (MockedStatic config = mockStatic(Config.class)) { - config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("True"); + mockedConfig.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("True"); - final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.TRUE, entity.get(FLAG)); - } + assertEquals(Boolean.TRUE, entity.get(FLAG)); } /** @@ -102,13 +119,11 @@ void getConfigVariables_flagSetToCapitalizedTrue_returnsNativeBooleanTrue() { */ @Test void getConfigVariables_flagSetToUppercaseTrue_returnsNativeBooleanTrue() { - try (MockedStatic config = mockStatic(Config.class)) { - config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("TRUE"); + mockedConfig.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("TRUE"); - final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.TRUE, entity.get(FLAG)); - } + assertEquals(Boolean.TRUE, entity.get(FLAG)); } /** @@ -118,13 +133,11 @@ void getConfigVariables_flagSetToUppercaseTrue_returnsNativeBooleanTrue() { */ @Test void getConfigVariables_flagSetToTrueWithWhitespace_returnsNativeBooleanTrue() { - try (MockedStatic config = mockStatic(Config.class)) { - config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn(" true "); + mockedConfig.when(() -> Config.getStringProperty(FLAG, null)).thenReturn(" true "); - final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.TRUE, entity.get(FLAG)); - } + assertEquals(Boolean.TRUE, entity.get(FLAG)); } /** @@ -134,13 +147,11 @@ void getConfigVariables_flagSetToTrueWithWhitespace_returnsNativeBooleanTrue() { */ @Test void getConfigVariables_flagSetToNumericOne_returnsNativeBooleanTrue() { - try (MockedStatic config = mockStatic(Config.class)) { - config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("1"); + mockedConfig.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("1"); - final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.TRUE, entity.get(FLAG)); - } + assertEquals(Boolean.TRUE, entity.get(FLAG)); } // ── Falsy variants (AC: "false", "False", "FALSE", " false ", "0", "" → false) ── @@ -152,13 +163,11 @@ void getConfigVariables_flagSetToNumericOne_returnsNativeBooleanTrue() { */ @Test void getConfigVariables_flagSetToLowercaseFalse_returnsNativeBooleanFalse() { - try (MockedStatic config = mockStatic(Config.class)) { - config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("false"); + mockedConfig.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("false"); - final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.FALSE, entity.get(FLAG)); - } + assertEquals(Boolean.FALSE, entity.get(FLAG)); } /** @@ -168,13 +177,11 @@ void getConfigVariables_flagSetToLowercaseFalse_returnsNativeBooleanFalse() { */ @Test void getConfigVariables_flagSetToUppercaseFalse_returnsNativeBooleanFalse() { - try (MockedStatic config = mockStatic(Config.class)) { - config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("FALSE"); + mockedConfig.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("FALSE"); - final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.FALSE, entity.get(FLAG)); - } + assertEquals(Boolean.FALSE, entity.get(FLAG)); } /** @@ -184,13 +191,11 @@ void getConfigVariables_flagSetToUppercaseFalse_returnsNativeBooleanFalse() { */ @Test void getConfigVariables_flagSetToNumericZero_returnsNativeBooleanFalse() { - try (MockedStatic config = mockStatic(Config.class)) { - config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("0"); + mockedConfig.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("0"); - final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.FALSE, entity.get(FLAG)); - } + assertEquals(Boolean.FALSE, entity.get(FLAG)); } /** @@ -200,13 +205,11 @@ void getConfigVariables_flagSetToNumericZero_returnsNativeBooleanFalse() { */ @Test void getConfigVariables_flagSetToEmptyString_returnsNativeBooleanFalse() { - try (MockedStatic config = mockStatic(Config.class)) { - config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn(""); + mockedConfig.when(() -> Config.getStringProperty(FLAG, null)).thenReturn(""); - final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.FALSE, entity.get(FLAG)); - } + assertEquals(Boolean.FALSE, entity.get(FLAG)); } // ── NOT_FOUND sentinel ──────────────────────────────────────────────────── @@ -220,14 +223,12 @@ void getConfigVariables_flagSetToEmptyString_returnsNativeBooleanFalse() { */ @Test void getConfigVariables_flagNotDefined_returnsNotFoundSentinel() { - try (MockedStatic config = mockStatic(Config.class)) { - config.when(() -> Config.getStringProperty(UNDEFINED_FLAG, null)).thenReturn(null); + mockedConfig.when(() -> Config.getStringProperty(UNDEFINED_FLAG, null)).thenReturn(null); - final Map entity = entityMap( - resource.getConfigVariables(request, response, UNDEFINED_FLAG)); + final Map entity = entityMap( + resource.getConfigVariables(request, response, UNDEFINED_FLAG)); - assertEquals("NOT_FOUND", entity.get(UNDEFINED_FLAG)); - } + assertEquals("NOT_FOUND", entity.get(UNDEFINED_FLAG)); } // ── Unrecognised value ──────────────────────────────────────────────────── @@ -239,13 +240,11 @@ void getConfigVariables_flagNotDefined_returnsNotFoundSentinel() { */ @Test void getConfigVariables_flagSetToUnrecognizedValue_returnsNativeBooleanFalse() { - try (MockedStatic config = mockStatic(Config.class)) { - config.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("enabled"); + mockedConfig.when(() -> Config.getStringProperty(FLAG, null)).thenReturn("enabled"); - final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); + final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.FALSE, entity.get(FLAG)); - } + assertEquals(Boolean.FALSE, entity.get(FLAG)); } // ── Whitelist filtering ─────────────────────────────────────────────────── @@ -275,14 +274,12 @@ void getConfigVariables_keyNotOnWhitelist_isExcludedFromResponse() { @Test void getConfigVariables_nonFlagWhitelistedKey_returnsRawStringValue() { final String testAddress = "admin@example.com"; - try (MockedStatic config = mockStatic(Config.class)) { - config.when(() -> Config.getStringProperty(NON_FLAG_KEY, "NOT_FOUND")).thenReturn(testAddress); + mockedConfig.when(() -> Config.getStringProperty(NON_FLAG_KEY, "NOT_FOUND")).thenReturn(testAddress); - final Map entity = entityMap( - resource.getConfigVariables(request, response, NON_FLAG_KEY)); + final Map entity = entityMap( + resource.getConfigVariables(request, response, NON_FLAG_KEY)); - assertEquals(testAddress, entity.get(NON_FLAG_KEY)); - } + assertEquals(testAddress, entity.get(NON_FLAG_KEY)); } // ── Helpers ─────────────────────────────────────────────────────────────── From a2db5489e4cda07946b36512a7f3d243c8a9a92c Mon Sep 17 00:00:00 2001 From: gortiz-dotcms Date: Wed, 20 May 2026 12:03:12 -0300 Subject: [PATCH 4/4] fix(rest-api): normalise flag values to lowercase strings, not native booleans (#35551) Changing the wire format from string to boolean broke the existing Postman contract tests (which assert typeof value === 'string') and introduced an M-3 rollback risk for any consumer built against the original string-typed contract. parseBooleanFlag now returns the canonical lowercase strings "true" / "false" / "NOT_FOUND" instead of Boolean.TRUE / Boolean.FALSE. This fully fixes the original bug (case-insensitive handling of "True", "TRUE", "1", etc.) without changing the API contract that existing callers depend on. Unit test expectations updated to match the string wire format. Co-Authored-By: Claude Sonnet 4.6 --- .../api/v1/system/ConfigurationResource.java | 28 +++++++------ .../v1/system/ConfigurationResourceTest.java | 40 +++++++++---------- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java index 8ab8b400a0ff..4fe91d4212ac 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java @@ -66,10 +66,10 @@ public class ConfigurationResource implements Serializable { * Feature flag keys in WHITE_LIST that must be serialised as native JSON booleans. * All other WHITE_LIST entries (strings, numbers, lists) are left as-is. * - *

Maintenance rule: every key added here MUST also be present in WHITE_LIST, - * and every frontend caller that reads the key via {@code getKeys()} must compare with - * {@code === true} (boolean), not {@code === 'true'} (string). Adding a key here without - * updating both lists and all callers reproduces the exact bug this class was written to fix. + *

Maintenance rule: every key added here MUST also be present in WHITE_LIST. + * The wire format is the normalised lowercase string {@code "true"} or {@code "false"} — + * frontend callers should compare with {@code === 'true'}. Adding a key here without + * also adding it to WHITE_LIST will silently exclude it from the response. */ private static final Set BOOLEAN_FEATURE_FLAGS = ImmutableSet.of( FeatureFlagName.FEATURE_FLAG_EXPERIMENTS, @@ -192,11 +192,15 @@ private Object recoveryFromConfig (final String key) { } /** - * Resolves a feature flag property to a native boolean, or the sentinel "NOT_FOUND" - * when the key is not defined anywhere (no .properties entry, no DOT_* env override). - * Accepted truthy values (case-insensitive, whitespace-trimmed): "true", "1". - * Accepted falsy values: "false", "0", "". - * Unrecognised values are logged as WARN and resolve to false. + * Normalises a feature flag property to the canonical lowercase string {@code "true"} or + * {@code "false"}, preserving the existing string wire format so that consumers built + * against the pre-existing contract continue to work without changes. + * Returns the sentinel {@code "NOT_FOUND"} when the key is not defined anywhere + * (no .properties entry, no DOT_* env override). + * + *

Accepted truthy values (case-insensitive, whitespace-trimmed): {@code "true"}, {@code "1"}. + * Accepted falsy values: {@code "false"}, {@code "0"}, {@code ""}. + * Unrecognised values are logged as WARN and normalise to {@code "false"}. */ private static Object parseBooleanFlag(final String key) { final String rawValue = Config.getStringProperty(key, null); @@ -207,15 +211,15 @@ private static Object parseBooleanFlag(final String key) { switch (normalized) { case "true": case "1": - return Boolean.TRUE; + return "true"; case "false": case "0": case "": - return Boolean.FALSE; + return "false"; default: Logger.warn(ConfigurationResource.class, () -> "Feature flag '" + key + "' has unrecognized value '" + rawValue + "'; treating as false."); - return Boolean.FALSE; + return "false"; } } diff --git a/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java b/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java index 744ad07fe726..a35acb808003 100644 --- a/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java +++ b/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java @@ -86,7 +86,7 @@ void tearDown() { /** * Method to test: {@link ConfigurationResource#getConfigVariables} * Given scenario: A boolean feature flag is set to lowercase {@code "true"}. - * Expected result: Response contains native boolean {@code true} (not the string). + * Expected result: Response contains the normalised string {@code "true"}. */ @Test void getConfigVariables_flagSetToLowercaseTrue_returnsNativeBooleanTrue() { @@ -94,14 +94,14 @@ void getConfigVariables_flagSetToLowercaseTrue_returnsNativeBooleanTrue() { final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.TRUE, entity.get(FLAG)); + assertEquals("true", entity.get(FLAG)); } /** * Method to test: {@link ConfigurationResource#getConfigVariables} * Given scenario: A boolean feature flag is set to capitalized {@code "True"} — * the customer-reported reproduction case. - * Expected result: Response contains native boolean {@code true}. + * Expected result: Response contains the normalised string {@code "true"}. */ @Test void getConfigVariables_flagSetToCapitalizedTrue_returnsNativeBooleanTrue() { @@ -109,13 +109,13 @@ void getConfigVariables_flagSetToCapitalizedTrue_returnsNativeBooleanTrue() { final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.TRUE, entity.get(FLAG)); + assertEquals("true", entity.get(FLAG)); } /** * Method to test: {@link ConfigurationResource#getConfigVariables} * Given scenario: A boolean feature flag is set to all-caps {@code "TRUE"}. - * Expected result: Response contains native boolean {@code true}. + * Expected result: Response contains the normalised string {@code "true"}. */ @Test void getConfigVariables_flagSetToUppercaseTrue_returnsNativeBooleanTrue() { @@ -123,13 +123,13 @@ void getConfigVariables_flagSetToUppercaseTrue_returnsNativeBooleanTrue() { final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.TRUE, entity.get(FLAG)); + assertEquals("true", entity.get(FLAG)); } /** * Method to test: {@link ConfigurationResource#getConfigVariables} * Given scenario: A boolean feature flag is set to {@code " true "} with surrounding whitespace. - * Expected result: Response contains native boolean {@code true} — whitespace is trimmed. + * Expected result: Response contains the normalised string {@code "true"} — whitespace is trimmed. */ @Test void getConfigVariables_flagSetToTrueWithWhitespace_returnsNativeBooleanTrue() { @@ -137,13 +137,13 @@ void getConfigVariables_flagSetToTrueWithWhitespace_returnsNativeBooleanTrue() { final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.TRUE, entity.get(FLAG)); + assertEquals("true", entity.get(FLAG)); } /** * Method to test: {@link ConfigurationResource#getConfigVariables} * Given scenario: A boolean feature flag is set to the numeric shorthand {@code "1"}. - * Expected result: Response contains native boolean {@code true}. + * Expected result: Response contains the normalised string {@code "true"}. */ @Test void getConfigVariables_flagSetToNumericOne_returnsNativeBooleanTrue() { @@ -151,7 +151,7 @@ void getConfigVariables_flagSetToNumericOne_returnsNativeBooleanTrue() { final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.TRUE, entity.get(FLAG)); + assertEquals("true", entity.get(FLAG)); } // ── Falsy variants (AC: "false", "False", "FALSE", " false ", "0", "" → false) ── @@ -159,7 +159,7 @@ void getConfigVariables_flagSetToNumericOne_returnsNativeBooleanTrue() { /** * Method to test: {@link ConfigurationResource#getConfigVariables} * Given scenario: A boolean feature flag is set to lowercase {@code "false"}. - * Expected result: Response contains native boolean {@code false}. + * Expected result: Response contains the normalised string {@code "false"}. */ @Test void getConfigVariables_flagSetToLowercaseFalse_returnsNativeBooleanFalse() { @@ -167,13 +167,13 @@ void getConfigVariables_flagSetToLowercaseFalse_returnsNativeBooleanFalse() { final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.FALSE, entity.get(FLAG)); + assertEquals("false", entity.get(FLAG)); } /** * Method to test: {@link ConfigurationResource#getConfigVariables} * Given scenario: A boolean feature flag is set to all-caps {@code "FALSE"}. - * Expected result: Response contains native boolean {@code false}. + * Expected result: Response contains the normalised string {@code "false"}. */ @Test void getConfigVariables_flagSetToUppercaseFalse_returnsNativeBooleanFalse() { @@ -181,13 +181,13 @@ void getConfigVariables_flagSetToUppercaseFalse_returnsNativeBooleanFalse() { final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.FALSE, entity.get(FLAG)); + assertEquals("false", entity.get(FLAG)); } /** * Method to test: {@link ConfigurationResource#getConfigVariables} * Given scenario: A boolean feature flag is set to the numeric shorthand {@code "0"}. - * Expected result: Response contains native boolean {@code false}. + * Expected result: Response contains the normalised string {@code "false"}. */ @Test void getConfigVariables_flagSetToNumericZero_returnsNativeBooleanFalse() { @@ -195,13 +195,13 @@ void getConfigVariables_flagSetToNumericZero_returnsNativeBooleanFalse() { final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.FALSE, entity.get(FLAG)); + assertEquals("false", entity.get(FLAG)); } /** * Method to test: {@link ConfigurationResource#getConfigVariables} * Given scenario: A boolean feature flag is set to an empty string. - * Expected result: Response contains native boolean {@code false}. + * Expected result: Response contains the normalised string {@code "false"}. */ @Test void getConfigVariables_flagSetToEmptyString_returnsNativeBooleanFalse() { @@ -209,7 +209,7 @@ void getConfigVariables_flagSetToEmptyString_returnsNativeBooleanFalse() { final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.FALSE, entity.get(FLAG)); + assertEquals("false", entity.get(FLAG)); } // ── NOT_FOUND sentinel ──────────────────────────────────────────────────── @@ -236,7 +236,7 @@ void getConfigVariables_flagNotDefined_returnsNotFoundSentinel() { /** * Method to test: {@link ConfigurationResource#getConfigVariables} * Given scenario: A boolean feature flag has an unrecognized value such as {@code "enabled"}. - * Expected result: Response contains native boolean {@code false} — safe default preserved. + * Expected result: Response contains the normalised string {@code "false"} — safe default preserved. */ @Test void getConfigVariables_flagSetToUnrecognizedValue_returnsNativeBooleanFalse() { @@ -244,7 +244,7 @@ void getConfigVariables_flagSetToUnrecognizedValue_returnsNativeBooleanFalse() { final Map entity = entityMap(resource.getConfigVariables(request, response, FLAG)); - assertEquals(Boolean.FALSE, entity.get(FLAG)); + assertEquals("false", entity.get(FLAG)); } // ── Whitelist filtering ───────────────────────────────────────────────────