Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
55 changes: 55 additions & 0 deletions STEPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -2711,6 +2712,60 @@ Given the render cache has been cleared

</details>

## 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.
> <br/><br/>
> 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).
> <br/><br/>
> 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.
> <br/><br/>
> 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.
> <br/><br/>
> 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);
> }
> ```
> <br/><br/>
> Soft dependency: if the consuming context also uses `RestTrait`, the
> `$restHeaders` array is updated so standalone REST requests receive the
> same signal.
> <br/><br/>
> 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"
> ```
> <br/><br/>
> 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)
Expand Down
13 changes: 13 additions & 0 deletions scripts/provision.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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:<name> 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

Expand Down
184 changes: 184 additions & 0 deletions src/Drupal/ConfigOverrideTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<?php

declare(strict_types=1);

namespace DrevOps\BehatSteps\Drupal;

use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Behat\Hook\Scope\BeforeStepScope;
use Behat\Hook\BeforeScenario;
use Behat\Hook\BeforeStep;
use Behat\Mink\Driver\Selenium2Driver;

/**
* 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:
* @code
* 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);
* }
* @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<int, string>
*/
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.
*
* 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;
}

// 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 === []) {
// 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;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

$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);
}

/**
* 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', '');
}
}

}
2 changes: 2 additions & 0 deletions tests/behat/bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -58,6 +59,7 @@ class FeatureContext extends DrupalContext {
use BigPipeTrait;
use BlockTrait;
use CacheTrait;
use ConfigOverrideTrait;
use ContentBlockTrait;
use ContentTrait;
use CookieTrait;
Expand Down
67 changes: 67 additions & 0 deletions tests/behat/features/drupal_config_override.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
@config-override
Feature: Check that ConfigOverrideTrait works
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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

# 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"
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"
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,17 @@ 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'

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'
Loading