Testing and debugging¶
Test a site-aware module against real sites, hostnames and per-site roles, and see what Sites resolved for a page.
Base classes and traits¶
Sites ships them under tests/src, namespace Drupal\Tests\sites. A module
that has Sites in its codebase can extend them.
| Class or trait | Gives you |
|---|---|
Kernel\SitesKernelTestBase |
Sites and Sites content with their schemas installed, and the three traits below. Declare only the modules you add. |
Functional\SitesFunctionalTestBase |
BrowserTestBase with SiteCreationTrait, the stark theme, and helpers for several hostnames, the page cache and members. |
Traits\SiteCreationTrait |
createSiteEntity(), createSiteOfferingRoles(), assignContentToSite(), loadSiteEntity(), refreshSitePlugins(). |
Traits\SiteContextTrait |
setCurrentSite() and withSite(). |
Traits\SiteSchemaInstallTrait |
installSiteSchemas(): the site, configuration override and role assignment schemas, and the membership schema when Sites content is on. |
createSiteEntity($id, $settings, $values) creates a site with the hostname
<id>.example.com; $settings is keyed by setting plugin, then by setting key
(Site settings). It refreshes the site plugin
caches; call refreshSitePlugins() yourself after changing a site entity in
another way.
Kernel tests¶
setCurrentSite() puts the test where a request on the site's hostname is:
the site is negotiated and active. withSite() switches to another site for
a callback, as site_switcher does, and switches back even when an assertion
fails. Until a site is set, the current site is not ready: it answers the
hub, and configuration overrides and query scoping stay off. Call
setCurrentSite('default') when the test needs the hub.
#[Group('my_module')]
#[RunTestsInSeparateProcesses]
final class MyModuleScopingTest extends SitesKernelTestBase {
protected static $modules = ['node', 'field', 'path', 'my_module'];
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('node');
$this->installSchema('node', ['node_access']);
$this->installConfig(['system']);
// Bundles first: a site reads them when its definition is built.
NodeType::create(['type' => 'page', 'name' => 'Page'])->save();
}
public function testQueryFollowsTheSite(): void {
$alpha = $this->createSiteEntity('alpha', ['site_filtering' => ['filter_views' => TRUE]]);
$node = Node::create(['type' => 'page', 'title' => 'Alpha news']);
$node->save();
$this->assignContentToSite($node, $alpha, TRUE);
$this->setCurrentSite('alpha');
$ids = \Drupal::entityQuery('node')->accessCheck(TRUE)->execute();
$this->assertSame([(string) $node->id()], array_values($ids));
}
}
EntityQueryScopingTest in Sites is the complete version.
In a kernel test, the first user created is user 1, who passes every access
check: create it before the users you test with. The entity access handlers
remember their answers for the whole test: reset them with
resetCache() after changing a role assignment.
Per-site role access needs functional tests¶
The access policy adds the permissions of a per-site role to the account of the current request. A kernel test that checks access for another account, or switches accounts, does not take that path and can pass or fail wrongly. Test what a per-site role allows with real requests, logged in as a member that holds only the per-site role. A user with Administer sites passes every site gate and proves nothing. Kernel tests are fine for permissions of global roles and for membership rules.
#[Group('my_module')]
#[RunTestsInSeparateProcesses]
final class SiteEditorAccessTest extends SitesFunctionalTestBase {
protected static $modules = ['node', 'sites_content'];
protected function memberPermissions(): array {
return ['access content'];
}
public function testEditorEditsTheirSiteOnly(): void {
$this->drupalCreateContentType(['type' => 'page']);
Role::create(['id' => 'site_editor', 'label' => 'Site editor'])
->grantPermission('edit any page content')
->save();
$alpha = $this->createSiteOfferingRoles('alpha', ['site_editor']);
$beta = $this->createSiteOfferingRoles('beta', ['site_editor']);
$alpha_page = $this->drupalCreateNode(['type' => 'page']);
$this->assignContentToSite($alpha_page, $alpha, TRUE);
$beta_page = $this->drupalCreateNode(['type' => 'page']);
$this->assignContentToSite($beta_page, $beta, TRUE);
$this->drupalLogin($this->memberWithRole('alpha', 'site_editor'));
$this->drupalGet('node/' . $alpha_page->id() . '/edit');
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet('node/' . $beta_page->id() . '/edit');
$this->assertSession()->statusCodeEquals(403);
}
}
memberWithRole() creates a user with memberPermissions(), makes it a
member of the site and grants the role there. SiteRoleJourneyTest in Sites
walks a site manager and a site editor through the hub and a site's hostname.
Hostnames in functional tests¶
Every request of a functional test goes to the test site's own URL.
drupalGetOnHost($host, $path) sends it with a Host header, so Sites
negotiates the site of that hostname and no DNS entry is needed. Pass the
host on every call; NULL is the hub. A redirect to a site's hostname
cannot be followed, since the hostname does not resolve: send such a request
through getHttpClient() with allow_redirects off, as
SiteRoleJourneyTest::rawStatusOnHost() does.
public function testPageCachePerHost(): void {
$this->enableAnonymousPageCache();
$path = 'node/' . $this->alphaPage->id();
$this->drupalGetOnHost('alpha.example.com', $path);
$this->assertPageCache('MISS');
$this->drupalGetOnHost('alpha.example.com', $path);
$this->assertPageCache('HIT');
$this->drupalGetOnHost('beta.example.com', $path);
$this->assertSession()->responseHeaderNotEquals('X-Drupal-Cache', 'HIT');
}
HostResolutionIsolationTest in Sites has this test with its setup.
Sites devel¶
Sites devel adds a Sites debug information block to the content region of every page. It needs Devel and Block plugin view builder, and you do not place it. For the request it dumps:
- the sites that claim the route, each with its definition, its resolved
settings and its
config_overridessetting; sensitive settings are redacted; - the active site;
- the route name and parameters;
- the languages;
- the path aliases and redirects of the route's entity, with Sites path alias and Redirect.
The block shows to users with the View the Sites debug block permission
(use sites_devel). In development mode it shows to every user who is
logged in, and Devel's dump() and kint() output does too, without
Access developer information. Development mode is the
sites.development container parameter, false by default. Turn it on in
the services.yml of a development environment and rebuild the container:
parameters:
sites.development: true
Anonymous visitors never see the block without the permission. Do not enable Sites devel in production.
drush sites:info acme prints a site's resolved settings and configuration
overrides on the command line; see Drush commands.