Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
142 changes: 142 additions & 0 deletions src/Drupal/ConfigOverrideTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?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.
*/
#[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;
}
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);
}

}
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
66 changes: 66 additions & 0 deletions tests/behat/features/drupal_config_override.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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'
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -49,4 +50,42 @@ 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',
]);
}

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

}
Loading