From c81c884a99b77f662a05e53d0305696b629942a1 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Fri, 10 Apr 2026 18:57:31 +1000 Subject: [PATCH 1/3] [#571] Added ConfigOverrideTrait to disable Drupal config overrides via tags. --- README.md | 1 + STEPS.md | 55 +++++++ src/Drupal/ConfigOverrideTrait.php | 142 ++++++++++++++++++ tests/behat/bootstrap/FeatureContext.php | 2 + .../features/drupal_config_override.feature | 48 ++++++ .../mysite_core/mysite_core.routing.yml | 7 + .../src/Controller/TestContent.php | 16 ++ .../mysite_core/mysite_core.routing.yml | 7 + .../src/Controller/TestContent.php | 16 ++ 9 files changed, 294 insertions(+) create mode 100644 src/Drupal/ConfigOverrideTrait.php create mode 100644 tests/behat/features/drupal_config_override.feature diff --git a/README.md b/README.md index f1146c4d..da11a398 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ from the community. | [Drupal\BigPipeTrait](STEPS.md#drupalbigpipetrait) | Bypass Drupal BigPipe when rendering pages. | | [Drupal\BlockTrait](STEPS.md#drupalblocktrait) | Manage Drupal blocks. | | [Drupal\CacheTrait](STEPS.md#drupalcachetrait) | Invalidate specific Drupal caches from within a scenario. | +| [Drupal\ConfigOverrideTrait](STEPS.md#drupalconfigoverridetrait) | Disable Drupal config overrides from settings.php during a scenario. | | [Drupal\ContentBlockTrait](STEPS.md#drupalcontentblocktrait) | Manage Drupal content blocks. | | [Drupal\ContentTrait](STEPS.md#drupalcontenttrait) | Manage Drupal content with workflow and moderation support. | | [Drupal\DraggableviewsTrait](STEPS.md#drupaldraggableviewstrait) | Order items in the Drupal Draggable Views. | diff --git a/STEPS.md b/STEPS.md index b63e5e60..de96a3f5 100644 --- a/STEPS.md +++ b/STEPS.md @@ -30,6 +30,7 @@ | [Drupal\BigPipeTrait](#drupalbigpipetrait) | Bypass Drupal BigPipe when rendering pages. | | [Drupal\BlockTrait](#drupalblocktrait) | Manage Drupal blocks. | | [Drupal\CacheTrait](#drupalcachetrait) | Invalidate specific Drupal caches from within a scenario. | +| [Drupal\ConfigOverrideTrait](#drupalconfigoverridetrait) | Disable Drupal config overrides from settings.php during a scenario. | | [Drupal\ContentBlockTrait](#drupalcontentblocktrait) | Manage Drupal content blocks. | | [Drupal\ContentTrait](#drupalcontenttrait) | Manage Drupal content with workflow and moderation support. | | [Drupal\DraggableviewsTrait](#drupaldraggableviewstrait) | Order items in the Drupal Draggable Views. | @@ -2711,6 +2712,60 @@ Given the render cache has been cleared +## Drupal\ConfigOverrideTrait + +[Source](src/Drupal/ConfigOverrideTrait.php), [Example](tests/behat/features/drupal_config_override.feature) + +> Disable Drupal config overrides from settings.php during a scenario. +>

+> Config overrides set in `settings.php` replace the stored configuration at +> runtime. They cannot be disabled from the Behat process because tests run +> in a separate process from the system under test (SUT). +>

+> This trait signals the SUT - through a request header, a `$_SERVER` entry +> and an environment variable - that specific config objects should be read +> from their original (unoverridden) values. The SUT is responsible for +> reading that signal and calling `ImmutableConfig::getOriginal()` instead of +> `ImmutableConfig::get()` for the listed config names. +>

+> Activated by adding `@disable-config-override:CONFIG_NAME` tags to a +> feature or scenario. Multiple tags are combined into a comma-separated +> list. Runs on every step because some steps reset headers set earlier in +> the scenario. +>

+> Limitations: +> - Cannot be used with Selenium/JavaScript drivers (the underlying driver +> does not expose request headers). +> - The SUT must implement support for the `X-Config-No-Override` header, +> the `HTTP_X_CONFIG_NO_OVERRIDE` `$_SERVER` entry or the matching +> environment variable. An example implementation: +> ``` +> public function getConfigValue(string $name, string $key): mixed { +> $config = $this->configFactory->get($name); +> $header = $_SERVER['HTTP_X_CONFIG_NO_OVERRIDE'] ?? getenv('HTTP_X_CONFIG_NO_OVERRIDE') ?: ''; +> if (in_array($name, array_map('trim', explode(',', $header)), TRUE)) { +> return $config->getOriginal($key, FALSE); +> } +> return $config->get($key); +> } +> ``` +>

+> Soft dependency: if the consuming context also uses `RestTrait`, the +> `$restHeaders` array is updated so standalone REST requests receive the +> same signal. +>

+> Example: +> ``` +> @api @disable-config-override:system.site @disable-config-override:myconfig.settings +> Scenario: Render the page with original config values +> When I visit "/" +> Then the response should contain "Original site name" +> ``` +>

+> Skip processing with tags: `@behat-steps-skip:configOverrideBeforeScenario` +> and `@behat-steps-skip:configOverrideBeforeStep`. + + ## Drupal\ContentBlockTrait [Source](src/Drupal/ContentBlockTrait.php), [Example](tests/behat/features/drupal_content_block.feature) diff --git a/src/Drupal/ConfigOverrideTrait.php b/src/Drupal/ConfigOverrideTrait.php new file mode 100644 index 00000000..fa1656d1 --- /dev/null +++ b/src/Drupal/ConfigOverrideTrait.php @@ -0,0 +1,142 @@ +configFactory->get($name); + * $header = $_SERVER['HTTP_X_CONFIG_NO_OVERRIDE'] ?? getenv('HTTP_X_CONFIG_NO_OVERRIDE') ?: ''; + * if (in_array($name, array_map('trim', explode(',', $header)), TRUE)) { + * return $config->getOriginal($key, FALSE); + * } + * return $config->get($key); + * } + * @endcode + * + * Soft dependency: if the consuming context also uses `RestTrait`, the + * `$restHeaders` array is updated so standalone REST requests receive the + * same signal. + * + * Example: + * @code + * @api @disable-config-override:system.site @disable-config-override:myconfig.settings + * Scenario: Render the page with original config values + * When I visit "/" + * Then the response should contain "Original site name" + * @endcode + * + * Skip processing with tags: `@behat-steps-skip:configOverrideBeforeScenario` + * and `@behat-steps-skip:configOverrideBeforeStep`. + */ +trait ConfigOverrideTrait { + + /** + * Config names parsed from `@disable-config-override:*` tags. + * + * @var array + */ + protected array $configOverrideDisabledNames = []; + + /** + * Whether the `BeforeStep` hook should be skipped for this scenario. + */ + protected bool $configOverrideSkipBeforeStep = FALSE; + + /** + * Collect `@disable-config-override:*` tags for the current scenario. + */ + #[BeforeScenario] + public function configOverrideBeforeScenario(BeforeScenarioScope $scope): void { + $this->configOverrideDisabledNames = []; + $this->configOverrideSkipBeforeStep = FALSE; + + if ($scope->getScenario()->hasTag('behat-steps-skip:' . __FUNCTION__)) { + return; + } + + // BeforeStep scope does not have access to scenario tags, so resolve the + // skip flag here. + if ($scope->getScenario()->hasTag('behat-steps-skip:configOverrideBeforeStep')) { + $this->configOverrideSkipBeforeStep = TRUE; + } + + $tags = array_unique(array_merge($scope->getFeature()->getTags(), $scope->getScenario()->getTags())); + $prefix = 'disable-config-override:'; + foreach ($tags as $tag) { + if (str_starts_with($tag, $prefix)) { + $name = substr($tag, strlen($prefix)); + if ($name !== '' && !in_array($name, $this->configOverrideDisabledNames, TRUE)) { + $this->configOverrideDisabledNames[] = $name; + } + } + } + } + + /** + * Apply the `X-Config-No-Override` signal before every step. + * + * This runs on every step because some steps reset headers set earlier in + * the scenario (for example, Drupal Extension login steps). + */ + #[BeforeStep] + public function configOverrideBeforeStep(BeforeStepScope $scope): void { + if ($this->configOverrideSkipBeforeStep || $this->configOverrideDisabledNames === []) { + return; + } + + $value = implode(',', $this->configOverrideDisabledNames); + + // Set request header on the Mink driver for BrowserKit-based sessions. + // Selenium-based drivers cannot set request headers - skip silently. + $driver = $this->getSession()->getDriver(); + if (!$driver instanceof Selenium2Driver) { + $driver->setRequestHeader('X-Config-No-Override', $value); + } + + // Soft dependency on RestTrait: propagate to the standalone REST client + // when RestTrait is also used by the consuming context. + // @phpstan-ignore-next-line function.alreadyNarrowedType + if (property_exists($this, 'restHeaders')) { + $this->restHeaders['X-Config-No-Override'] = $value; + } + + // For SUTs accessed via direct code invocation within the same process. + $_SERVER['HTTP_X_CONFIG_NO_OVERRIDE'] = $value; + + // For SUTs accessed via Drush subprocesses. + putenv('HTTP_X_CONFIG_NO_OVERRIDE=' . $value); + } + +} diff --git a/tests/behat/bootstrap/FeatureContext.php b/tests/behat/bootstrap/FeatureContext.php index 9cbaa1c3..fe3f3cc4 100644 --- a/tests/behat/bootstrap/FeatureContext.php +++ b/tests/behat/bootstrap/FeatureContext.php @@ -12,6 +12,7 @@ use DrevOps\BehatSteps\Drupal\BigPipeTrait; use DrevOps\BehatSteps\Drupal\BlockTrait; use DrevOps\BehatSteps\Drupal\CacheTrait; +use DrevOps\BehatSteps\Drupal\ConfigOverrideTrait; use DrevOps\BehatSteps\Drupal\ContentBlockTrait; use DrevOps\BehatSteps\Drupal\ContentTrait; use DrevOps\BehatSteps\Drupal\DraggableviewsTrait; @@ -58,6 +59,7 @@ class FeatureContext extends DrupalContext { use BigPipeTrait; use BlockTrait; use CacheTrait; + use ConfigOverrideTrait; use ContentBlockTrait; use ContentTrait; use CookieTrait; diff --git a/tests/behat/features/drupal_config_override.feature b/tests/behat/features/drupal_config_override.feature new file mode 100644 index 00000000..369ab6cf --- /dev/null +++ b/tests/behat/features/drupal_config_override.feature @@ -0,0 +1,48 @@ +Feature: Check that ConfigOverrideTrait works + As Behat Steps library developer + I want to provide a way to disable Drupal config overrides for a scenario + So that users can test original config values without redeploying the SUT + + @api + Scenario: Visiting a page without the tag sends no X-Config-No-Override header + When I visit "/mysite_core/test-config-no-override-header" + Then the response status code should be 200 + And the response should not contain "system.site" + + @api @disable-config-override:system.site + Scenario: A single @disable-config-override tag sets the X-Config-No-Override header + When I visit "/mysite_core/test-config-no-override-header" + Then the response status code should be 200 + And the response should contain "system.site" + + @api @disable-config-override:system.site @disable-config-override:myconfig.settings + Scenario: Multiple @disable-config-override tags set a comma-separated X-Config-No-Override header + When I visit "/mysite_core/test-config-no-override-header" + Then the response status code should be 200 + And the response should contain "system.site,myconfig.settings" + + @api @disable-config-override:system.site + Scenario: The X-Config-No-Override header survives a login step that resets headers + Given users: + | name | mail | roles | status | + | test_user | test_user@example.com | administrator | 1 | + When I am logged in as "test_user" + And I visit "/mysite_core/test-config-no-override-header" + Then the response status code should be 200 + And the response should contain "system.site" + + @api @disable-config-override:system.site @behat-steps-skip:configOverrideBeforeScenario + Scenario: The @behat-steps-skip:configOverrideBeforeScenario tag bypasses the trait entirely + When I visit "/mysite_core/test-config-no-override-header" + Then the response status code should be 200 + And the response should not contain "system.site" + + @api @disable-config-override:system.site @behat-steps-skip:configOverrideBeforeStep + Scenario: The @behat-steps-skip:configOverrideBeforeStep tag keeps tag parsing but skips header propagation + Given users: + | name | mail | roles | status | + | test_user2 | test_user2@example.com | administrator | 1 | + When I am logged in as "test_user2" + And I visit "/mysite_core/test-config-no-override-header" + Then the response status code should be 200 + And the response should not contain "system.site" diff --git a/tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/mysite_core.routing.yml b/tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/mysite_core.routing.yml index 33413491..e6e91ba1 100644 --- a/tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/mysite_core.routing.yml +++ b/tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/mysite_core.routing.yml @@ -13,3 +13,10 @@ mysite_core.test_time: _access: 'TRUE' defaults: _controller: '\Drupal\mysite_core\Controller\TestContent::testTime' + +mysite_core.test_config_no_override_header: + path: '/mysite_core/test-config-no-override-header' + requirements: + _access: 'TRUE' + defaults: + _controller: '\Drupal\mysite_core\Controller\TestContent::testConfigNoOverrideHeader' diff --git a/tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/src/Controller/TestContent.php b/tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/src/Controller/TestContent.php index 801e226d..325df2e3 100644 --- a/tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/src/Controller/TestContent.php +++ b/tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/src/Controller/TestContent.php @@ -7,6 +7,7 @@ use Drupal\Core\Controller\ControllerBase; use Drupal\mysite_core\Time\TimeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** @@ -49,4 +50,19 @@ public function testTime(): Response { ]); } + /** + * Echoes the value of the X-Config-No-Override request header. + * + * Used by ConfigOverrideTrait tests to verify that the Behat process + * successfully signals the SUT. + */ + public function testConfigNoOverrideHeader(Request $request): Response { + $value = (string) $request->headers->get('X-Config-No-Override', ''); + + return new Response($value, 200, [ + 'Content-Type' => 'text/plain', + 'Cache-Control' => 'no-cache, no-store, must-revalidate', + ]); + } + } diff --git a/tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/mysite_core.routing.yml b/tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/mysite_core.routing.yml index 33413491..e6e91ba1 100644 --- a/tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/mysite_core.routing.yml +++ b/tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/mysite_core.routing.yml @@ -13,3 +13,10 @@ mysite_core.test_time: _access: 'TRUE' defaults: _controller: '\Drupal\mysite_core\Controller\TestContent::testTime' + +mysite_core.test_config_no_override_header: + path: '/mysite_core/test-config-no-override-header' + requirements: + _access: 'TRUE' + defaults: + _controller: '\Drupal\mysite_core\Controller\TestContent::testConfigNoOverrideHeader' diff --git a/tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/src/Controller/TestContent.php b/tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/src/Controller/TestContent.php index 801e226d..325df2e3 100644 --- a/tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/src/Controller/TestContent.php +++ b/tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/src/Controller/TestContent.php @@ -7,6 +7,7 @@ use Drupal\Core\Controller\ControllerBase; use Drupal\mysite_core\Time\TimeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** @@ -49,4 +50,19 @@ public function testTime(): Response { ]); } + /** + * Echoes the value of the X-Config-No-Override request header. + * + * Used by ConfigOverrideTrait tests to verify that the Behat process + * successfully signals the SUT. + */ + public function testConfigNoOverrideHeader(Request $request): Response { + $value = (string) $request->headers->get('X-Config-No-Override', ''); + + return new Response($value, 200, [ + 'Content-Type' => 'text/plain', + 'Cache-Control' => 'no-cache, no-store, must-revalidate', + ]); + } + } From 60e6cc438b70714cc788d658135898f50b481b5a Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Fri, 10 Apr 2026 19:30:25 +1000 Subject: [PATCH 2/3] [#571] Added end-to-end fixture proving settings.php overrides are bypassed. --- scripts/provision.sh | 13 +++++++++++ .../features/drupal_config_override.feature | 18 +++++++++++++++ .../mysite_core/mysite_core.routing.yml | 7 ++++++ .../src/Controller/TestContent.php | 23 +++++++++++++++++++ .../mysite_core/mysite_core.routing.yml | 7 ++++++ .../src/Controller/TestContent.php | 23 +++++++++++++++++++ 6 files changed, 91 insertions(+) diff --git a/scripts/provision.sh b/scripts/provision.sh index 96b4f0d4..16ae8f2f 100755 --- a/scripts/provision.sh +++ b/scripts/provision.sh @@ -74,6 +74,19 @@ composer run-script post-install-cmd echo " > Installing Drupal site." /usr/bin/env PHP_OPTIONS='-d sendmail_path=/bin/true' /app/build/vendor/bin/drush -r /app/build/web si standard -y --db-url=mysql://drupal:drupal@mariadb/drupal --account-name=admin --account-pass=admin install_configure_form.enable_update_status_module=NULL install_configure_form.enable_update_status_emails=NULL --uri=http://nginx +echo " > Appending fixture \$config overrides to settings.php for ConfigOverrideTrait tests." +chmod 666 /app/build/web/sites/default/settings.php +cat >> /app/build/web/sites/default/settings.php <<'PHP' + +// Fixture config overrides used by ConfigOverrideTrait tests. These mimic +// environment-specific overrides that a real site would set in settings.php, +// so tests can verify that @disable-config-override: tags let the SUT +// read the stored (original) values via ImmutableConfig::getOriginal(). +$config['system.site']['name'] = 'Overridden Site Name'; +$config['system.site']['slogan'] = 'Overridden Slogan'; +PHP +chmod 444 /app/build/web/sites/default/settings.php + echo " > Running post-install commands defined in the composer.json for each specific fixture." composer run-script drupal-post-install diff --git a/tests/behat/features/drupal_config_override.feature b/tests/behat/features/drupal_config_override.feature index 369ab6cf..182db8a3 100644 --- a/tests/behat/features/drupal_config_override.feature +++ b/tests/behat/features/drupal_config_override.feature @@ -3,6 +3,24 @@ Feature: Check that ConfigOverrideTrait works I want to provide a way to disable Drupal config overrides for a scenario So that users can test original config values without redeploying the SUT + # The fixture site has the following overrides in settings.php: + # $config['system.site']['name'] = 'Overridden Site Name'; + # $config['system.site']['slogan'] = 'Overridden Slogan'; + # The stored (original) name is 'Drush Site-Install'. + + @api + Scenario: Without the tag, the SUT serves the settings.php-overridden config value + When I visit "/mysite_core/test-config-system-site-name" + Then the response status code should be 200 + And the response should contain "Overridden Site Name" + + @api @disable-config-override:system.site + Scenario: With @disable-config-override, the SUT serves the original stored config value + When I visit "/mysite_core/test-config-system-site-name" + Then the response status code should be 200 + And the response should contain "Drush Site-Install" + And the response should not contain "Overridden Site Name" + @api Scenario: Visiting a page without the tag sends no X-Config-No-Override header When I visit "/mysite_core/test-config-no-override-header" diff --git a/tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/mysite_core.routing.yml b/tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/mysite_core.routing.yml index e6e91ba1..eac0974e 100644 --- a/tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/mysite_core.routing.yml +++ b/tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/mysite_core.routing.yml @@ -20,3 +20,10 @@ mysite_core.test_config_no_override_header: _access: 'TRUE' defaults: _controller: '\Drupal\mysite_core\Controller\TestContent::testConfigNoOverrideHeader' + +mysite_core.test_config_system_site_name: + path: '/mysite_core/test-config-system-site-name' + requirements: + _access: 'TRUE' + defaults: + _controller: '\Drupal\mysite_core\Controller\TestContent::testConfigSystemSiteName' diff --git a/tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/src/Controller/TestContent.php b/tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/src/Controller/TestContent.php index 325df2e3..3db4fac7 100644 --- a/tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/src/Controller/TestContent.php +++ b/tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/src/Controller/TestContent.php @@ -65,4 +65,27 @@ public function testConfigNoOverrideHeader(Request $request): Response { ]); } + /** + * Returns the system.site name, honoring X-Config-No-Override. + * + * Demonstrates the SUT-side implementation expected by + * ConfigOverrideTrait: when the request header lists a config name, the + * original (un-overridden) value is returned via + * ImmutableConfig::getOriginal(). + */ + public function testConfigSystemSiteName(Request $request): Response { + $config = $this->config('system.site'); + $header = (string) $request->headers->get('X-Config-No-Override', ''); + $disabled = array_filter(array_map(trim(...), explode(',', $header))); + + $value = in_array('system.site', $disabled, TRUE) + ? (string) $config->getOriginal('name', FALSE) + : (string) $config->get('name'); + + return new Response($value, 200, [ + 'Content-Type' => 'text/plain', + 'Cache-Control' => 'no-cache, no-store, must-revalidate', + ]); + } + } diff --git a/tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/mysite_core.routing.yml b/tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/mysite_core.routing.yml index e6e91ba1..eac0974e 100644 --- a/tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/mysite_core.routing.yml +++ b/tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/mysite_core.routing.yml @@ -20,3 +20,10 @@ mysite_core.test_config_no_override_header: _access: 'TRUE' defaults: _controller: '\Drupal\mysite_core\Controller\TestContent::testConfigNoOverrideHeader' + +mysite_core.test_config_system_site_name: + path: '/mysite_core/test-config-system-site-name' + requirements: + _access: 'TRUE' + defaults: + _controller: '\Drupal\mysite_core\Controller\TestContent::testConfigSystemSiteName' diff --git a/tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/src/Controller/TestContent.php b/tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/src/Controller/TestContent.php index 325df2e3..3db4fac7 100644 --- a/tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/src/Controller/TestContent.php +++ b/tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/src/Controller/TestContent.php @@ -65,4 +65,27 @@ public function testConfigNoOverrideHeader(Request $request): Response { ]); } + /** + * Returns the system.site name, honoring X-Config-No-Override. + * + * Demonstrates the SUT-side implementation expected by + * ConfigOverrideTrait: when the request header lists a config name, the + * original (un-overridden) value is returned via + * ImmutableConfig::getOriginal(). + */ + public function testConfigSystemSiteName(Request $request): Response { + $config = $this->config('system.site'); + $header = (string) $request->headers->get('X-Config-No-Override', ''); + $disabled = array_filter(array_map(trim(...), explode(',', $header))); + + $value = in_array('system.site', $disabled, TRUE) + ? (string) $config->getOriginal('name', FALSE) + : (string) $config->get('name'); + + return new Response($value, 200, [ + 'Content-Type' => 'text/plain', + 'Cache-Control' => 'no-cache, no-store, must-revalidate', + ]); + } + } From 5ce23f45b1439042fa2ec9b1c5333ac16bf78a57 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Fri, 10 Apr 2026 19:45:32 +1000 Subject: [PATCH 3/3] Addressed code review: prevented state bleed and added feature tag. --- src/Drupal/ConfigOverrideTrait.php | 42 +++++++++++++++++++ .../features/drupal_config_override.feature | 1 + 2 files changed, 43 insertions(+) diff --git a/src/Drupal/ConfigOverrideTrait.php b/src/Drupal/ConfigOverrideTrait.php index fa1656d1..ca7efd4e 100644 --- a/src/Drupal/ConfigOverrideTrait.php +++ b/src/Drupal/ConfigOverrideTrait.php @@ -76,11 +76,16 @@ trait ConfigOverrideTrait { /** * Collect `@disable-config-override:*` tags for the current scenario. + * + * Always clears any propagated signal from a previous scenario first so + * state never bleeds between scenarios - even when this hook is bypassed + * via `@behat-steps-skip:configOverrideBeforeScenario`. */ #[BeforeScenario] public function configOverrideBeforeScenario(BeforeScenarioScope $scope): void { $this->configOverrideDisabledNames = []; $this->configOverrideSkipBeforeStep = FALSE; + $this->configOverrideClearSignal(); if ($scope->getScenario()->hasTag('behat-steps-skip:' . __FUNCTION__)) { return; @@ -113,6 +118,9 @@ public function configOverrideBeforeScenario(BeforeScenarioScope $scope): void { #[BeforeStep] public function configOverrideBeforeStep(BeforeStepScope $scope): void { if ($this->configOverrideSkipBeforeStep || $this->configOverrideDisabledNames === []) { + // Nothing to propagate - make sure the driver-level header is also + // cleared so a previously-set value does not survive into this step. + $this->configOverrideClearDriverHeader(); return; } @@ -139,4 +147,38 @@ public function configOverrideBeforeStep(BeforeStepScope $scope): void { putenv('HTTP_X_CONFIG_NO_OVERRIDE=' . $value); } + /** + * Clear the process-level and REST-level X-Config-No-Override signal. + * + * Driver-level request headers are cleared separately in the BeforeStep + * hook because the session is not guaranteed to be started at the point + * BeforeScenario runs. + */ + protected function configOverrideClearSignal(): void { + unset($_SERVER['HTTP_X_CONFIG_NO_OVERRIDE']); + putenv('HTTP_X_CONFIG_NO_OVERRIDE'); + + // @phpstan-ignore-next-line function.alreadyNarrowedType + if (property_exists($this, 'restHeaders')) { + unset($this->restHeaders['X-Config-No-Override']); + } + } + + /** + * Clear the driver-level X-Config-No-Override request header. + */ + protected function configOverrideClearDriverHeader(): void { + try { + $driver = $this->getSession()->getDriver(); + } + // @codeCoverageIgnoreStart + catch (\Exception) { + return; + } + // @codeCoverageIgnoreEnd + if (!$driver instanceof Selenium2Driver) { + $driver->setRequestHeader('X-Config-No-Override', ''); + } + } + } diff --git a/tests/behat/features/drupal_config_override.feature b/tests/behat/features/drupal_config_override.feature index 182db8a3..69a1f3a5 100644 --- a/tests/behat/features/drupal_config_override.feature +++ b/tests/behat/features/drupal_config_override.feature @@ -1,3 +1,4 @@ +@config-override Feature: Check that ConfigOverrideTrait works As Behat Steps library developer I want to provide a way to disable Drupal config overrides for a scenario