Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,90 @@
<?php
// phpcs:ignoreFile
use Drupal\Core\Database\Database;
use Drupal\Core\Entity\EntityTypeInterface;
$connection = Database::getConnection();
// Set the schema version.
$connection->merge('key_value')
->fields([
'value' => 'i:10000;',
'name' => 'workspaces',
'collection' => 'system.schema',
])
->condition('collection', 'system.schema')
->condition('name', 'workspaces')
->execute();
// Update core.extension.
$extensions = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.extension')
->execute()
->fetchField();
$extensions = unserialize($extensions);
$extensions['module']['workspaces'] = 0;
$connection->update('config')
->fields(['data' => serialize($extensions)])
->condition('collection', '')
->condition('name', 'core.extension')
->execute();
// Add all workspaces_removed_post_updates() as existing updates.
require_once __DIR__ . '/../../../../workspaces/workspaces.post_update.php';
$existing_updates = $connection->select('key_value')
->fields('key_value', ['value'])
->condition('collection', 'post_update')
->condition('name', 'existing_updates')
->execute()
->fetchField();
$existing_updates = unserialize($existing_updates);
$existing_updates = array_merge(
$existing_updates,
array_keys(workspaces_removed_post_updates())
);
$connection->update('key_value')
->fields(['value' => serialize($existing_updates)])
->condition('collection', 'post_update')
->condition('name', 'existing_updates')
->execute();
// Create the 'workspace_association' table.
$spec = [
'description' => 'Stores the association between entity revisions and their workspace.',
'fields' => [
'workspace' => [
'type' => 'varchar_ascii',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The workspace ID.',
],
'target_entity_type_id' => [
'type' => 'varchar_ascii',
'length' => EntityTypeInterface::ID_MAX_LENGTH,
'not null' => TRUE,
'default' => '',
'description' => 'The ID of the associated entity type.',
],
'target_entity_id' => [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The ID of the associated entity.',
],
'target_entity_revision_id' => [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The revision ID of the associated entity.',
],
],
'indexes' => [
'target_entity_revision_id' => ['target_entity_revision_id'],
],
'primary key' => ['workspace', 'target_entity_type_id', 'target_entity_id'],
];
$connection->schema()->createTable('workspace_association', $spec);

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Drupal\workspace_access_test\Hook;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for workspace_access_test.
*/
class WorkspaceAccessTestHooks {
/**
* Implements hook_ENTITY_TYPE_access() for the 'workspace' entity type.
*/
#[Hook('workspace_access')]
public function workspaceAccess(EntityInterface $entity, $operation, AccountInterface $account): AccessResultInterface {
return \Drupal::state()->get("workspace_access_test.result.{$operation}", AccessResult::neutral());
}
}

View File

@ -0,0 +1,7 @@
name: 'Workspace Access Test'
type: module
description: 'Provides supporting code for testing access for workspaces.'
package: Testing
version: VERSION
dependencies:
- drupal:workspaces

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Drupal\workspace_update_test\Negotiator;
use Drupal\workspaces\Entity\Workspace;
use Drupal\workspaces\Negotiator\WorkspaceIdNegotiatorInterface;
use Drupal\workspaces\Negotiator\WorkspaceNegotiatorInterface;
use Drupal\workspaces\WorkspaceInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Defines a workspace negotiator used for testing.
*/
class TestWorkspaceNegotiator implements WorkspaceNegotiatorInterface, WorkspaceIdNegotiatorInterface {
/**
* {@inheritdoc}
*/
public function applies(Request $request) {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getActiveWorkspaceId(Request $request): ?string {
return 'test';
}
/**
* {@inheritdoc}
*/
public function getActiveWorkspace(Request $request) {
return Workspace::load($this->getActiveWorkspaceId($request));
}
/**
* {@inheritdoc}
*/
public function setActiveWorkspace(WorkspaceInterface $workspace) {
// Nothing to do here.
}
/**
* {@inheritdoc}
*/
public function unsetActiveWorkspace() {
// Nothing to do here.
}
}

View File

@ -0,0 +1,7 @@
name: 'Workspace Update Test'
type: module
description: 'Provides supporting code for testing workspaces during database updates.'
package: Testing
version: VERSION
dependencies:
- drupal:workspaces

View File

@ -0,0 +1,15 @@
<?php
/**
* @file
* Post update functions for the Workspace Update Test module.
*/
declare(strict_types=1);
/**
* Checks the active workspace during database updates.
*/
function workspace_update_test_post_update_check_active_workspace(): void {
\Drupal::state()->set('workspace_update_test.has_active_workspace', \Drupal::service('workspaces.manager')->hasActiveWorkspace());
}

View File

@ -0,0 +1,5 @@
services:
workspace_update_test.negotiator.test:
class: Drupal\workspace_update_test\Negotiator\TestWorkspaceNegotiator
tags:
- { name: workspace_negotiator, priority: 0 }

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Drupal\workspaces_test\Entity;
use Drupal\Core\Entity\Attribute\ContentEntityType;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\entity_test\Entity\EntityTestMulRevPub;
/**
* Defines the test entity class.
*/
#[ContentEntityType(
id: 'entity_test_mulrevpub_string_id',
label: new TranslatableMarkup('Test entity - revisions, data table, and published interface'),
base_table: 'entity_test_mulrevpub_string_id',
data_table: 'entity_test_mulrevpub_string_id_property_data',
revision_table: 'entity_test_mulrevpub_string_id_revision',
revision_data_table: 'entity_test_mulrevpub_string_id_property_revision',
admin_permission: 'administer entity_test content',
translatable: TRUE,
entity_keys: [
'id' => 'id',
'uuid' => 'uuid',
'bundle' => 'type',
'revision' => 'revision_id',
'label' => 'name',
'langcode' => 'langcode',
'published' => 'status',
]
)]
class EntityTestMulRevPubStringId extends EntityTestMulRevPub {
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields['id'] = BaseFieldDefinition::create('string')
->setLabel(t('ID'))
->setDescription(t('The ID of the test entity.'))
->setReadOnly(TRUE)
// In order to work around the InnoDB 191 character limit on utf8mb4
// primary keys, we set the character set for the field to ASCII.
->setSetting('is_ascii', TRUE);
return $fields;
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Drupal\workspaces_test;
use Drupal\Core\Entity\EntityInterface;
use Drupal\workspaces\Entity\Handler\DefaultWorkspaceHandler;
/**
* Provides a custom workspace handler for testing purposes.
*/
class EntityTestRevPubWorkspaceHandler extends DefaultWorkspaceHandler {
/**
* {@inheritdoc}
*/
public function isEntitySupported(EntityInterface $entity): bool {
return $entity->bundle() !== 'ignored_bundle';
}
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Drupal\workspaces_test\Form;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceSafeFormInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\Url;
use Drupal\workspaces\WorkspaceManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form for testing the active workspace.
*
* @internal
*/
class ActiveWorkspaceTestForm extends FormBase implements WorkspaceSafeFormInterface {
/**
* The workspace manager.
*/
protected WorkspaceManagerInterface $workspaceManager;
/**
* The test key-value store.
*/
protected KeyValueStoreInterface $keyValue;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->workspaceManager = $container->get('workspaces.manager');
$instance->keyValue = $container->get('keyvalue')->get('ws_test');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'active_workspace_test_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
$form['test'] = [
'#type' => 'textfield',
'#ajax' => [
'url' => Url::fromRoute('workspaces_test.get_form'),
'callback' => function () {
$this->keyValue->set('ajax_test_active_workspace', $this->workspaceManager->getActiveWorkspace()->id());
return new AjaxResponse();
},
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$this->keyValue->set('form_test_active_workspace', $this->workspaceManager->getActiveWorkspace()->id());
}
}

View File

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Drupal\workspaces_test\Hook;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
/**
* Hook implementations for workspaces_test.
*/
class WorkspacesTestHooks {
public function __construct(
#[Autowire(service: 'keyvalue')]
protected readonly KeyValueFactoryInterface $keyValueFactory,
) {}
/**
* Implements hook_entity_type_alter().
*/
#[Hook('entity_type_alter')]
public function entityTypeAlter(array &$entity_types) : void {
$state = \Drupal::state();
// Allow all entity types to have their definition changed dynamically for
// testing purposes.
foreach ($entity_types as $entity_type_id => $entity_type) {
$entity_types[$entity_type_id] = $state->get("{$entity_type_id}.entity_type", $entity_types[$entity_type_id]);
}
}
/**
* Implements hook_ENTITY_TYPE_translation_create() for 'entity_test_mulrevpub'.
*/
#[Hook('entity_test_mulrevpub_translation_create')]
public function entityTranslationCreate(): void {
/** @var \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager */
$workspace_manager = \Drupal::service('workspaces.manager');
$this->keyValueFactory->get('ws_test')->set('workspace_was_active', $workspace_manager->hasActiveWorkspace());
}
/**
* Implements hook_entity_create().
*/
#[Hook('entity_create')]
public function entityCreate(EntityInterface $entity): void {
$this->incrementHookCount('hook_entity_create', $entity);
}
/**
* Implements hook_entity_presave().
*/
#[Hook('entity_presave')]
public function entityPresave(EntityInterface $entity): void {
$this->incrementHookCount('hook_entity_presave', $entity);
}
/**
* Implements hook_entity_insert().
*/
#[Hook('entity_insert')]
public function entityInsert(EntityInterface $entity): void {
$this->incrementHookCount('hook_entity_insert', $entity);
}
/**
* Implements hook_entity_update().
*/
#[Hook('entity_update')]
public function entityUpdate(EntityInterface $entity): void {
$this->incrementHookCount('hook_entity_update', $entity);
}
/**
* Increments the invocation count for a specific entity hook.
*
* @param string $hook_name
* The name of the hook being invoked (e.g., 'hook_entity_create').
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object involved in the hook.
*/
protected function incrementHookCount(string $hook_name, EntityInterface $entity): void {
$key = $entity->getEntityTypeId() . '.' . $hook_name . '.count';
$count = $this->keyValueFactory->get('ws_test')->get($key, 0);
$this->keyValueFactory->get('ws_test')->set($key, $count + 1);
}
}

View File

@ -0,0 +1,8 @@
name: 'Workspace Test'
type: module
description: 'Provides supporting code for testing workspaces.'
package: Testing
version: VERSION
dependencies:
- drupal:entity_test
- drupal:workspaces

View File

@ -0,0 +1,7 @@
workspaces_test.get_form:
path: '/active-workspace-test-form'
defaults:
_title: 'Active Workspace Test Form'
_form: '\Drupal\workspaces_test\Form\ActiveWorkspaceTestForm'
requirements:
_access: 'TRUE'

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for workspaces.
*
* @group workspaces
*/
class GenericTest extends GenericModuleTestBase {
/**
* {@inheritdoc}
*/
protected function preUninstallSteps(): void {
$storage = \Drupal::entityTypeManager()->getStorage('workspace');
$workspaces = $storage->loadMultiple();
$storage->delete($workspaces);
}
}

View File

@ -0,0 +1,361 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\content_translation\Traits\ContentTranslationTestTrait;
use Drupal\Tests\WaitTerminateTestTrait;
/**
* Tests path aliases with workspaces.
*
* @group path
* @group workspaces
*/
class PathWorkspacesTest extends BrowserTestBase {
use ContentTranslationTestTrait;
use WorkspaceTestUtilities;
use WaitTerminateTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'content_translation',
'node',
'path',
'workspaces',
'workspaces_ui',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
static::createLanguageFromLangcode('ro');
$this->rebuildContainer();
// Create a content type.
$this->drupalCreateContentType([
'name' => 'article',
'type' => 'article',
]);
$permissions = [
'administer languages',
'administer nodes',
'administer url aliases',
'administer workspaces',
'create article content',
'create content translations',
'edit any article content',
'translate any entity',
];
$this->drupalLogin($this->drupalCreateUser($permissions));
// Enable URL language detection and selection.
$edit = ['language_interface[enabled][language-url]' => 1];
$this->drupalGet('admin/config/regional/language/detection');
$this->submitForm($edit, 'Save settings');
// Enable translation for article node.
static::enableContentTranslation('node', 'article');
$this->setupWorkspaceSwitcherBlock();
// The \Drupal\path_alias\AliasPrefixList service performs cache clears
// after Drupal has flushed the response to the client. We use
// WaitTerminateTestTrait to wait for Drupal to do this before continuing.
$this->setWaitForTerminate();
}
/**
* Tests path aliases with workspaces.
*/
public function testPathAliases(): void {
// Create a published node in Live, without an alias.
$node = $this->drupalCreateNode([
'type' => 'article',
'status' => TRUE,
]);
// Activate a workspace and create an alias for the node.
$stage = $this->createAndActivateWorkspaceThroughUi('Stage', 'stage');
$edit = [
'path[0][alias]' => '/' . $this->randomMachineName(),
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
// Check that the node can be accessed in Stage with the given alias.
$path = $edit['path[0][alias]'];
$this->assertAccessiblePaths([$path]);
// Check that the 'preload-paths' cache includes the active workspace ID in
// the cache key.
$this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:stage:/node/1'));
$this->assertFalse(\Drupal::cache('data')->get('preload-paths:/node/1'));
// Check that the alias can not be accessed in Live.
$this->switchToLive();
$this->assertNotAccessiblePaths([$path]);
$this->assertFalse(\Drupal::cache('data')->get('preload-paths:/node/1'));
// Publish the workspace and check that the alias can be accessed in Live.
$stage->publish();
$this->assertAccessiblePaths([$path]);
$this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:/node/1'));
}
/**
* Tests path aliases with workspaces and user switching.
*/
public function testPathAliasesUserSwitch(): void {
// Create a published node in Live, without an alias.
$node = $this->drupalCreateNode([
'type' => 'article',
'status' => TRUE,
]);
// Activate a workspace and create an alias for the node.
$stage = $this->createAndActivateWorkspaceThroughUi('Stage', 'stage');
$edit = [
'path[0][alias]' => '/' . $this->randomMachineName(),
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
// Check that the node can be accessed in Stage with the given alias.
$path = $edit['path[0][alias]'];
$this->assertAccessiblePaths([$path]);
// Check that the 'preload-paths' cache includes the active workspace ID in
// the cache key.
$this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:stage:/node/1'));
$this->assertFalse(\Drupal::cache('data')->get('preload-paths:/node/1'));
// Check that the alias can not be accessed in Live, by logging out without
// an explicit switch.
$this->drupalLogout();
$this->assertNotAccessiblePaths([$path]);
$this->assertFalse(\Drupal::cache('data')->get('preload-paths:/node/1'));
// Publish the workspace and check that the alias can be accessed in Live.
$this->drupalLogin($this->rootUser);
$stage->publish();
$this->drupalLogout();
$this->assertAccessiblePaths([$path]);
$this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:/node/1'));
}
/**
* Tests path aliases with workspaces for translatable nodes.
*/
public function testPathAliasesWithTranslation(): void {
$stage = $this->createWorkspaceThroughUi('Stage', 'stage');
// Create one node with a random alias.
$default_node = $this->drupalCreateNode([
'type' => 'article',
'langcode' => 'en',
'status' => TRUE,
'path' => '/' . $this->randomMachineName(),
]);
// Add published translation with another alias.
$this->drupalGet('node/' . $default_node->id());
$this->drupalGet('node/' . $default_node->id() . '/translations');
$this->clickLink('Add');
$edit_translation = [
'body[0][value]' => $this->randomMachineName(),
'status[value]' => TRUE,
'path[0][alias]' => '/' . $this->randomMachineName(),
];
$this->submitForm($edit_translation, 'Save (this translation)');
// Confirm that the alias works.
$this->drupalGet('ro' . $edit_translation['path[0][alias]']);
$this->assertSession()->pageTextContains($edit_translation['body[0][value]']);
$default_path = $default_node->path->alias;
$translation_path = 'ro' . $edit_translation['path[0][alias]'];
$this->assertAccessiblePaths([$default_path, $translation_path]);
// Verify the default alias is available in the live workspace.
$this->assertAccessiblePaths([$default_path]);
$this->switchToWorkspace($stage);
$this->assertAccessiblePaths([$default_path, $translation_path]);
// Create a workspace-specific revision for the translation with a new path
// alias.
$edit_new_translation_draft_with_alias = [
'path[0][alias]' => '/' . $this->randomMachineName(),
];
$this->drupalGet('ro/node/' . $default_node->id() . '/edit');
$this->submitForm($edit_new_translation_draft_with_alias, 'Save (this translation)');
$stage_translation_path = 'ro' . $edit_new_translation_draft_with_alias['path[0][alias]'];
// The new alias of the translation should be available in Stage, but not
// available in Live.
$this->assertAccessiblePaths([$default_path, $stage_translation_path]);
// Check that the previous (Live) path alias no longer works.
$this->assertNotAccessiblePaths([$translation_path]);
// Switch out of Stage and check that the initial path aliases still work.
$this->switchToLive();
$this->assertAccessiblePaths([$default_path, $translation_path]);
$this->assertNotAccessiblePaths([$stage_translation_path]);
// Switch back to Stage.
$this->switchToWorkspace($stage);
// Create new workspace-specific revision for translation without changing
// the path alias.
$edit_new_translation_draft = [
'body[0][value]' => $this->randomMachineName(),
];
$this->drupalGet('ro/node/' . $default_node->id() . '/edit');
$this->submitForm($edit_new_translation_draft, 'Save (this translation)');
// Confirm that the new draft revision was created.
$this->assertSession()->pageTextContains($edit_new_translation_draft['body[0][value]']);
// Switch out of Stage and check that the initial path aliases still work.
$this->switchToLive();
$this->assertAccessiblePaths([$default_path, $translation_path]);
$this->assertNotAccessiblePaths([$stage_translation_path]);
// Switch back to Stage.
$this->switchToWorkspace($stage);
$this->assertAccessiblePaths([$default_path, $stage_translation_path]);
$this->assertNotAccessiblePaths([$translation_path]);
// Create a new workspace-specific revision for translation with path alias
// from the original language's default revision.
$edit_new_translation_draft_with_defaults_alias = [
'path[0][alias]' => $default_node->path->alias,
];
$this->drupalGet('ro/node/' . $default_node->id() . '/edit');
$this->submitForm($edit_new_translation_draft_with_defaults_alias, 'Save (this translation)');
// Switch out of Stage and check that the initial path aliases still work.
$this->switchToLive();
$this->assertAccessiblePaths([$default_path, $translation_path]);
$this->assertNotAccessiblePaths([$stage_translation_path]);
// Check that only one path alias (the original one) is available in Stage.
$this->switchToWorkspace($stage);
$this->assertAccessiblePaths([$default_path]);
$this->assertNotAccessiblePaths([$translation_path, $stage_translation_path]);
// Create new workspace-specific revision for translation with a deleted
// (empty) path alias.
$edit_new_translation_draft_empty_alias = [
'body[0][value]' => $this->randomMachineName(),
'path[0][alias]' => '',
];
$this->drupalGet('ro/node/' . $default_node->id() . '/edit');
$this->submitForm($edit_new_translation_draft_empty_alias, 'Save (this translation)');
// Check that only one path alias (the original one) is available now.
$this->switchToLive();
$this->assertAccessiblePaths([$default_path, $translation_path]);
$this->assertNotAccessiblePaths([$stage_translation_path]);
$this->switchToWorkspace($stage);
$this->assertAccessiblePaths([$default_path]);
$this->assertNotAccessiblePaths([$translation_path, $stage_translation_path]);
// Create a new workspace-specific revision for the translation with a new
// path alias.
$edit_new_translation = [
'body[0][value]' => $this->randomMachineName(),
'path[0][alias]' => '/' . $this->randomMachineName(),
];
$this->drupalGet('ro/node/' . $default_node->id() . '/edit');
$this->submitForm($edit_new_translation, 'Save (this translation)');
// Confirm that the new revision was created.
$this->assertSession()->pageTextContains($edit_new_translation['body[0][value]']);
$this->assertSession()->addressEquals('ro' . $edit_new_translation['path[0][alias]']);
// Check that only the new path alias of the translation can be accessed.
$new_stage_translation_path = 'ro' . $edit_new_translation['path[0][alias]'];
$this->assertAccessiblePaths([$default_path, $new_stage_translation_path]);
$this->assertNotAccessiblePaths([$stage_translation_path]);
// Switch out of Stage and check that none of the workspace-specific path
// aliases can be accessed.
$this->switchToLive();
$this->assertAccessiblePaths([$default_path, $translation_path]);
$this->assertNotAccessiblePaths([$stage_translation_path, $new_stage_translation_path]);
// Publish Stage and check that its path alias for the translation can be
// accessed.
$stage->publish();
$this->assertAccessiblePaths([$default_path, $new_stage_translation_path]);
$this->assertNotAccessiblePaths([$stage_translation_path]);
// Switch back to Stage.
$this->switchToWorkspace($stage);
// Edit the path alias to set its language to "Not specified".
$alias_edit_path = "admin/config/search/path/edit/{$default_node->id()}";
$this->drupalGet($alias_edit_path);
// Set the alias language to "Not specified".
$edit = [
'langcode[0][value]' => 'und',
];
$this->submitForm($edit, 'Save');
// Verify the path alias is still available in the Stage workspace.
$this->assertAccessiblePaths([$default_path]);
}
/**
* Helper callback to verify paths are responding with status 200.
*
* @param string[] $paths
* An array of paths to check for.
*
* @internal
*/
protected function assertAccessiblePaths(array $paths): void {
foreach ($paths as $path) {
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(200);
}
}
/**
* Helper callback to verify paths are responding with status 404.
*
* @param string[] $paths
* An array of paths to check for.
*
* @internal
*/
protected function assertNotAccessiblePaths(array $paths): void {
foreach ($paths as $path) {
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(404);
}
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* Test workspace entities for unauthenticated JSON requests.
*
* @group workspaces
*/
class WorkspaceJsonAnonTest extends WorkspaceResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* Test workspace entities for JSON requests via basic auth.
*
* @group workspaces
*/
class WorkspaceJsonBasicAuthTest extends WorkspaceResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* Test workspace entities for JSON requests with cookie authentication.
*
* @group workspaces
*/
class WorkspaceJsonCookieTest extends WorkspaceResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional\Rest;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\user\Entity\User;
use Drupal\workspaces\Entity\Workspace;
/**
* Base class for workspace EntityResource tests.
*/
abstract class WorkspaceResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['workspaces'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'workspace';
/**
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [
'changed' => NULL,
];
/**
* {@inheritdoc}
*/
protected static $uniqueFieldNames = [
'id',
];
/**
* {@inheritdoc}
*/
protected static $firstCreatedEntityId = 'running_on_faith';
/**
* {@inheritdoc}
*/
protected static $secondCreatedEntityId = 'running_on_faith_2';
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
switch ($method) {
case 'GET':
$this->grantPermissionsToTestedRole(['view any workspace']);
break;
case 'POST':
$this->grantPermissionsToTestedRole(['create workspace']);
break;
case 'PATCH':
$this->grantPermissionsToTestedRole(['edit any workspace']);
break;
case 'DELETE':
$this->grantPermissionsToTestedRole(['delete any workspace']);
break;
}
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
$workspace = Workspace::create([
'id' => 'layla',
'label' => 'Layla',
]);
$workspace->save();
return $workspace;
}
/**
* {@inheritdoc}
*/
protected function createAnotherEntity() {
$workspace = $this->entity->createDuplicate();
$workspace->id = 'layla_dupe';
$workspace->label = 'Layla_dupe';
$workspace->save();
return $workspace;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
$author = User::load($this->entity->getOwnerId());
return [
'created' => [
[
'value' => (new \DateTime())->setTimestamp((int) $this->entity->getCreatedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
'format' => \DateTime::RFC3339,
],
],
'changed' => [
[
'value' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
'format' => \DateTime::RFC3339,
],
],
'id' => [
[
'value' => 'layla',
],
],
'label' => [
[
'value' => 'Layla',
],
],
'revision_id' => [
[
'value' => 1,
],
],
'parent' => [],
'uid' => [
[
'target_id' => (int) $author->id(),
'target_type' => 'user',
'target_uuid' => $author->uuid(),
'url' => base_path() . 'user/' . $author->id(),
],
],
'uuid' => [
[
'value' => $this->entity->uuid(),
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
return [
'id' => [
[
'value' => static::$firstCreatedEntityId,
],
],
'label' => [
[
'value' => 'Running on faith',
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPatchEntity() {
return array_diff_key($this->getNormalizedPostEntity(), ['id' => TRUE]);
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
switch ($method) {
case 'GET':
return "The 'view any workspace' permission is required.";
case 'POST':
return "The following permissions are required: 'administer workspaces' OR 'create workspace'.";
case 'PATCH':
return "The 'edit any workspace' permission is required.";
case 'DELETE':
return "The 'delete any workspace' permission is required.";
}
return parent::getExpectedUnauthorizedAccessMessage($method);
}
/**
* {@inheritdoc}
*/
protected function getModifiedEntityForPostTesting() {
$modified = parent::getModifiedEntityForPostTesting();
// Even though the field type of the workspace ID is 'string', it acts as a
// machine name through a custom constraint, so we need to ensure that we
// generate a proper random value for it.
// @see \Drupal\workspaces\Entity\Workspace::baseFieldDefinitions()
$modified['id'] = [$this->randomMachineName()];
return $modified;
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* Test workspace entities for unauthenticated XML requests.
*
* @group workspaces
*/
class WorkspaceXmlAnonTest extends WorkspaceResourceTestBase {
use AnonResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* Test workspace entities for XML requests with cookie authentication.
*
* @group workspaces
*/
class WorkspaceXmlBasicAuthTest extends WorkspaceResourceTestBase {
use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* Test workspace entities for XML requests.
*
* @group workspaces
*/
class WorkspaceXmlCookieTest extends WorkspaceResourceTestBase {
use CookieResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional\Update;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* Tests the update path for string IDs in workspace_association.
*
* @group workspaces
*/
class WorkspaceAssociationStringIdsUpdatePathTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected $checkEntityFieldDefinitionUpdates = FALSE;
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles(): void {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-10.3.0.bare.standard.php.gz',
__DIR__ . '/../../../fixtures/update/workspaces.php',
];
}
/**
* Tests the update path for string IDs in workspace_association.
*/
public function testRunUpdates(): void {
$schema = \Drupal::database()->schema();
$find_primary_key_columns = new \ReflectionMethod(get_class($schema), 'findPrimaryKeyColumns');
$this->assertFalse($schema->fieldExists('workspace_association', 'target_entity_id_string'));
$primary_key_columns = ['workspace', 'target_entity_type_id', 'target_entity_id'];
$this->assertEquals($primary_key_columns, $find_primary_key_columns->invoke($schema, 'workspace_association'));
$this->runUpdates();
$this->assertTrue($schema->fieldExists('workspace_association', 'target_entity_id_string'));
$primary_key_columns = ['workspace', 'target_entity_type_id', 'target_entity_id', 'target_entity_id_string'];
$this->assertEquals($primary_key_columns, $find_primary_key_columns->invoke($schema, 'workspace_association'));
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional\UpdateSystem;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\UpdatePathTestTrait;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests that there is no active workspace during database updates.
*
* @group workspaces
* @group Update
*/
class ActiveWorkspaceUpdateTest extends BrowserTestBase {
use UpdatePathTestTrait;
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['workspaces'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpCurrentUser([], ['view any workspace']);
$this->container->get('module_installer')->install(['workspace_update_test']);
$this->rebuildContainer();
// Ensure the workspace_update_test_post_update_check_active_workspace()
// update runs.
$existing_updates = \Drupal::keyValue('post_update')->get('existing_updates', []);
$index = array_search('workspace_update_test_post_update_check_active_workspace', $existing_updates);
unset($existing_updates[$index]);
\Drupal::keyValue('post_update')->set('existing_updates', $existing_updates);
// Create a valid workspace that can be used for testing.
Workspace::create(['id' => 'test', 'label' => 'Test'])->save();
}
/**
* Tests that there is no active workspace during database updates.
*/
public function testActiveWorkspaceDuringUpdate(): void {
/** @var \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager */
$workspace_manager = \Drupal::service('workspaces.manager');
// Check that we have an active workspace before running the updates.
$this->assertTrue($workspace_manager->hasActiveWorkspace());
$this->assertEquals('test', $workspace_manager->getActiveWorkspace()->id());
$this->runUpdates();
// Check that we didn't have an active workspace while running the updates.
// @see workspace_update_test_post_update_check_active_workspace()
$this->assertFalse(\Drupal::state()->get('workspace_update_test.has_active_workspace'));
// Check that we have an active workspace after running the updates.
$workspace_manager = \Drupal::service('workspaces.manager');
$this->assertTrue($workspace_manager->hasActiveWorkspace());
$this->assertEquals('test', $workspace_manager->getActiveWorkspace()->id());
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
// cspell:ignore ditka
/**
* Tests access bypass permission controls on workspaces.
*
* @group workspaces
*/
class WorkspaceBypassTest extends BrowserTestBase {
use WorkspaceTestUtilities;
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'user', 'block', 'workspaces', 'workspaces_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Verifies that a user can edit anything in a workspace they own.
*/
public function testBypassOwnWorkspace(): void {
$permissions = [
'create workspace',
'edit own workspace',
'view own workspace',
'bypass entity access own workspace',
];
$this->createContentType(['type' => 'test', 'label' => 'Test']);
$this->setupWorkspaceSwitcherBlock();
$coach = $this->drupalCreateUser(array_merge($permissions, ['create test content']));
// Login as a limited-access user and create a workspace.
$this->drupalLogin($coach);
$bears = $this->createAndActivateWorkspaceThroughUi('Bears', 'bears');
// Now create a node in the Bears workspace, as the owner of that workspace.
$coach_bears_node = $this->createNodeThroughUi('Ditka Bears node', 'test');
$coach_bears_node_id = $coach_bears_node->id();
// Editing both nodes should be possible.
$this->drupalGet('/node/' . $coach_bears_node_id . '/edit');
$this->assertSession()->statusCodeEquals(200);
// Create a new user that should be able to edit anything in the Bears
// workspace.
$this->switchToLive();
$lombardi = $this->drupalCreateUser(array_merge($permissions, ['view any workspace']));
$this->drupalLogin($lombardi);
$this->switchToWorkspace($bears);
// Editor 2 has the bypass permission but does not own the workspace and so,
// should not be able to create and edit any node.
$this->drupalGet('/node/' . $coach_bears_node_id . '/edit');
$this->assertSession()->statusCodeEquals(403);
}
}

View File

@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\workspaces\Entity\Workspace;
use Drupal\workspaces\WorkspaceCacheContext;
/**
* Tests the workspace cache context.
*
* @group workspaces
* @group Cache
*/
class WorkspaceCacheContextTest extends BrowserTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'node', 'workspaces', 'workspaces_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the 'workspace' cache context.
*/
public function testWorkspaceCacheContext(): void {
$renderer = \Drupal::service('renderer');
$cache_contexts_manager = \Drupal::service("cache_contexts_manager");
/** @var \Drupal\Core\Cache\VariationCacheFactoryInterface $variation_cache_factory */
$variation_cache_factory = $this->container->get('variation_cache_factory');
// Check that the 'workspace' cache context is present when the module is
// installed.
$this->drupalGet('<front>');
$this->assertCacheContext('workspace');
$cache_context = new WorkspaceCacheContext(\Drupal::service('workspaces.manager'));
$this->assertSame('live', $cache_context->getContext());
// Create a node and check that its render array contains the proper cache
// context.
$this->drupalCreateContentType(['type' => 'page']);
$node = $this->createNode();
// Get a fully built entity view render array.
$build = \Drupal::entityTypeManager()->getViewBuilder('node')->view($node, 'full');
// Render it so the default cache contexts are applied.
$renderer->renderRoot($build);
$this->assertContains('workspace', $build['#cache']['contexts']);
$context_tokens = $cache_contexts_manager->convertTokensToKeys($build['#cache']['contexts'])->getKeys();
$this->assertContains('[workspace]=live', $context_tokens);
// Test that a cache entry is created.
$cache_bin = $variation_cache_factory->get($build['#cache']['bin']);
$this->assertInstanceOf(\stdClass::class, $cache_bin->get($build['#cache']['keys'], CacheableMetadata::createFromRenderArray($build)));
// Switch to the test workspace and check that the correct workspace cache
// context is used.
$test_user = $this->drupalCreateUser(['view any workspace']);
$this->drupalLogin($test_user);
$vultures = Workspace::create([
'id' => 'vultures',
'label' => 'Vultures',
]);
$vultures->save();
$workspace_manager = \Drupal::service('workspaces.manager');
$workspace_manager->setActiveWorkspace($vultures);
$cache_context = new WorkspaceCacheContext($workspace_manager);
$this->assertSame('vultures', $cache_context->getContext());
$build = \Drupal::entityTypeManager()->getViewBuilder('node')->view($node, 'full');
// Render it so the default cache contexts are applied.
$renderer->renderRoot($build);
$this->assertContains('workspace', $build['#cache']['contexts']);
$context_tokens = $cache_contexts_manager->convertTokensToKeys($build['#cache']['contexts'])->getKeys();
$this->assertContains('[workspace]=vultures', $context_tokens);
// Test that a cache entry is created.
$cache_bin = $variation_cache_factory->get($build['#cache']['bin']);
$this->assertInstanceOf(\stdClass::class, $cache_bin->get($build['#cache']['keys'], CacheableMetadata::createFromRenderArray($build)));
}
}

View File

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests concurrent edits in different workspaces.
*
* @group workspaces
*/
class WorkspaceConcurrentEditingTest extends BrowserTestBase {
use WorkspaceTestUtilities;
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'node', 'workspaces', 'workspaces_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests editing a node in multiple workspaces.
*/
public function testConcurrentEditing(): void {
// Create a test node.
$this->createContentType(['type' => 'test', 'label' => 'Test']);
$this->setupWorkspaceSwitcherBlock();
$permissions = [
'create workspace',
'edit own workspace',
'view own workspace',
'create test content',
'edit own test content',
];
$mayer = $this->drupalCreateUser($permissions);
$this->drupalLogin($mayer);
$test_node = $this->createNodeThroughUi('Test node', 'test');
// Check that the user can edit the node.
$page = $this->getSession()->getPage();
$page->hasField('title[0][value]');
// Create two workspaces.
$vultures = $this->createWorkspaceThroughUi('Vultures', 'vultures');
$gravity = $this->createWorkspaceThroughUi('Gravity', 'gravity');
// Edit the node in workspace 'vultures'.
$this->switchToWorkspace($vultures);
$this->drupalGet('/node/' . $test_node->id() . '/edit');
$page = $this->getSession()->getPage();
$page->fillField('Title', 'Test node - override');
$page->findButton('Save')->click();
// Check that the user can still edit the node in the same workspace.
$this->drupalGet('/node/' . $test_node->id() . '/edit');
$page = $this->getSession()->getPage();
$this->assertTrue($page->hasField('title[0][value]'));
// Switch to a different workspace and check that the user can not edit the
// node anymore.
$this->switchToWorkspace($gravity);
$this->drupalGet('/node/' . $test_node->id() . '/edit');
$page = $this->getSession()->getPage();
$this->assertFalse($page->hasField('title[0][value]'));
$page->hasContent('The content is being edited in the Vultures workspace. As a result, your changes cannot be saved.');
// Check that the node fails validation for API calls.
$violations = $test_node->validate();
$this->assertCount(1, $violations);
$this->assertEquals('The content is being edited in the Vultures workspace. As a result, your changes cannot be saved.', $violations->get(0)->getMessage());
// Switch to the Live version of the site and check that the user still can
// not edit the node.
$this->switchToLive();
$this->drupalGet('/node/' . $test_node->id() . '/edit');
$page = $this->getSession()->getPage();
$this->assertFalse($page->hasField('title[0][value]'));
$page->hasContent('The content is being edited in the Vultures workspace. As a result, your changes cannot be saved.');
// Check that the node fails validation for API calls.
$violations = $test_node->validate();
$this->assertCount(1, $violations);
$this->assertEquals('The content is being edited in the Vultures workspace. As a result, your changes cannot be saved.', $violations->get(0)->getMessage());
// Publish the changes from the 'Vultures' workspace and check that the node
// can be edited again in other workspaces.
$vultures->publish();
$this->switchToWorkspace($gravity);
$this->drupalGet('/node/' . $test_node->id() . '/edit');
$page = $this->getSession()->getPage();
$this->assertTrue($page->hasField('title[0][value]'));
}
}

View File

@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests entity deletions with workspaces.
*
* @group workspaces
*/
class WorkspaceEntityDeleteTest extends BrowserTestBase {
use WorkspaceTestUtilities;
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'node', 'user', 'workspaces', 'workspaces_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createContentType(['type' => 'article', 'label' => 'Article']);
$this->setupWorkspaceSwitcherBlock();
}
/**
* Test entity deletion with workspaces.
*/
public function testEntityDelete(): void {
$assert_session = $this->assertSession();
$permissions = [
'administer workspaces',
'create workspace',
'access content overview',
'administer nodes',
'create article content',
'edit own article content',
'delete own article content',
'view own unpublished content',
];
$editor = $this->drupalCreateUser($permissions);
$this->drupalLogin($editor);
// Create a Dev workspace as a child of Stage.
$stage = $this->createWorkspaceThroughUi('Stage', 'stage');
$dev = $this->createWorkspaceThroughUi('Dev', 'dev', 'stage');
// Create a published and an unpublished node in Live.
$published_live = $this->createNodeThroughUi('Test 1 published - live', 'article');
$unpublished_live = $this->createNodeThroughUi('Test 2 unpublished - live', 'article', FALSE);
// Create a published and an unpublished node in Stage.
$this->switchToWorkspace($stage);
$published_stage = $this->createNodeThroughUi('Test 3 published - stage', 'article');
$unpublished_stage = $this->createNodeThroughUi('Test 4 unpublished - stage', 'article', FALSE);
// Check that the Live nodes (both published and unpublished) can not be
// deleted, while the Stage nodes can be.
$this->drupalGet('admin/content');
$assert_session->linkByHrefNotExists($published_live->toUrl('delete-form')->toString());
$assert_session->linkByHrefNotExists($unpublished_live->toUrl('delete-form')->toString());
$assert_session->linkByHrefExists($published_stage->toUrl('delete-form')->toString());
$assert_session->linkByHrefExists($unpublished_stage->toUrl('delete-form')->toString());
// Switch to Dev and check which nodes can be deleted.
$this->switchToWorkspace($dev);
$this->drupalGet('admin/content');
// The two Live nodes have the same deletable status as they had in Stage.
$assert_session->linkByHrefNotExists($published_live->toUrl('delete-form')->toString());
$assert_session->linkByHrefNotExists($unpublished_live->toUrl('delete-form')->toString());
// The two Stage nodes should not be deletable in a child workspace (Dev).
$assert_session->linkByHrefNotExists($published_stage->toUrl('delete-form')->toString());
$assert_session->linkByHrefNotExists($unpublished_stage->toUrl('delete-form')->toString());
// Add a new revision for each node and check that their 'deletable' status
// remains unchanged.
$this->switchToWorkspace($stage);
$this->drupalGet($published_live->toUrl('edit-form')->toString());
$this->submitForm([], 'Save');
$this->drupalGet($unpublished_live->toUrl('edit-form')->toString());
$this->submitForm([], 'Save');
$this->drupalGet($published_stage->toUrl('edit-form')->toString());
$this->submitForm([], 'Save');
$this->drupalGet($unpublished_stage->toUrl('edit-form')->toString());
$this->submitForm([], 'Save');
$this->drupalGet('admin/content');
$assert_session->linkByHrefNotExists($published_live->toUrl('delete-form')->toString());
$assert_session->linkByHrefNotExists($unpublished_live->toUrl('delete-form')->toString());
$assert_session->linkByHrefExists($published_stage->toUrl('delete-form')->toString());
$assert_session->linkByHrefExists($unpublished_stage->toUrl('delete-form')->toString());
// Publish the Stage workspace and check that no entity can be deleted
// anymore in Stage nor Dev.
$stage->publish();
$this->drupalGet('admin/content');
$assert_session->linkByHrefNotExists($published_live->toUrl('delete-form')->toString());
$assert_session->linkByHrefNotExists($unpublished_live->toUrl('delete-form')->toString());
$assert_session->linkByHrefNotExists($published_stage->toUrl('delete-form')->toString());
$assert_session->linkByHrefNotExists($unpublished_stage->toUrl('delete-form')->toString());
$this->switchToWorkspace($dev);
$this->drupalGet('admin/content');
$assert_session->linkByHrefNotExists($published_live->toUrl('delete-form')->toString());
$assert_session->linkByHrefNotExists($unpublished_live->toUrl('delete-form')->toString());
$assert_session->linkByHrefNotExists($published_stage->toUrl('delete-form')->toString());
$assert_session->linkByHrefNotExists($unpublished_stage->toUrl('delete-form')->toString());
}
/**
* Test node deletion with workspaces and the 'bypass node access' permission.
*/
public function testNodeDeleteWithBypassAccessPermission(): void {
$assert_session = $this->assertSession();
$permissions = [
'administer workspaces',
'create workspace',
'access content overview',
'bypass node access',
];
$editor = $this->drupalCreateUser($permissions);
$this->drupalLogin($editor);
// Create a published node in Live.
$published_live = $this->createNodeThroughUi('Test 1 published - live', 'article');
$this->createAndActivateWorkspaceThroughUi('Stage', 'stage');
// A user with the 'bypass node access' permission will be able to see the
// 'Delete' operation button, but it shouldn't be able to perform the
// deletion.
$this->drupalGet('admin/content');
$assert_session->linkByHrefExists($published_live->toUrl('delete-form')->toString());
$this->clickLink('Delete');
$assert_session->pageTextContains('This content item can only be deleted in the Live workspace.');
$assert_session->buttonNotExists('Delete');
$this->drupalGet($published_live->toUrl('delete-form')->toString());
$assert_session->pageTextContains('This content item can only be deleted in the Live workspace.');
$assert_session->buttonNotExists('Delete');
// Go back to Live and check that the delete form is not affected by the
// workspace delete protection.
$this->switchToLive();
$this->drupalGet($published_live->toUrl('delete-form')->toString());
$assert_session->pageTextNotContains('This content item can only be deleted in the Live workspace.');
$assert_session->buttonExists('Delete');
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests Workspaces form validation.
*
* @group workspaces
*/
class WorkspaceFormValidationTest extends BrowserTestBase {
use WorkspaceTestUtilities;
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'form_test', 'workspaces'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser(['administer workspaces']));
$this->setupWorkspaceSwitcherBlock();
}
/**
* Tests partial form validation through #limit_validation_errors.
*/
public function testValidateLimitErrors(): void {
$this->createAndActivateWorkspaceThroughUi();
$edit = [
'test' => 'test1',
'test_numeric_index[0]' => 'test2',
'test_substring[foo]' => 'test3',
];
$path = 'form-test/limit-validation-errors';
// Submit the form by pressing all the 'Partial validate' buttons.
$this->drupalGet($path);
$this->submitForm($edit, 'Partial validate');
$this->assertSession()->pageTextContains('This form can only be submitted in the default workspace.');
$this->drupalGet($path);
$this->submitForm($edit, 'Partial validate (numeric index)');
$this->assertSession()->pageTextContains('This form can only be submitted in the default workspace.');
$this->drupalGet($path);
$this->submitForm($edit, 'Partial validate (substring)');
$this->assertSession()->pageTextContains('This form can only be submitted in the default workspace.');
// Now test full form validation.
$this->drupalGet($path);
$this->submitForm($edit, 'Full validate');
$this->assertSession()->pageTextContains('This form can only be submitted in the default workspace.');
}
}

View File

@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests workspace integration for custom menu links.
*
* @group workspaces
* @group menu_link_content
*/
class WorkspaceMenuLinkContentIntegrationTest extends BrowserTestBase {
use WorkspaceTestUtilities;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'menu_link_content',
'menu_ui',
'node',
'workspaces',
'workspaces_ui',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$permissions = [
'access administration pages',
'administer menu',
'administer site configuration',
'administer workspaces',
];
$this->drupalLogin($this->drupalCreateUser($permissions));
$this->drupalPlaceBlock('system_menu_block:main');
}
/**
* Tests custom menu links in non-default workspaces.
*/
public function testWorkspacesWithCustomMenuLinks(): void {
$stage = $this->createWorkspaceThroughUi('Stage', 'stage');
$this->setupWorkspaceSwitcherBlock();
$default_title = 'default';
$default_link = '#live';
// Add a new menu link in Live.
$this->drupalGet('admin/structure/menu/manage/main/add');
$this->submitForm([
'title[0][value]' => $default_title,
'link[0][uri]' => $default_link,
], 'Save');
$menu_links = \Drupal::entityTypeManager()
->getStorage('menu_link_content')
->loadByProperties(['title' => $default_title]);
$menu_link = reset($menu_links);
$pending_title = 'pending';
$pending_link = 'http://example.com';
// Change the menu link in 'stage' and check that the updated values are
// visible in that workspace.
$this->switchToWorkspace($stage);
$this->drupalGet("admin/structure/menu/item/{$menu_link->id()}/edit");
$this->submitForm([
'title[0][value]' => $pending_title,
'link[0][uri]' => $pending_link,
], 'Save');
$this->drupalGet('');
$assert_session = $this->assertSession();
$assert_session->linkExists($pending_title);
$assert_session->linkByHrefExists($pending_link);
// Add a new menu link in the Stage workspace.
$this->drupalGet('admin/structure/menu/manage/main/add');
$this->submitForm([
'title[0][value]' => 'stage link',
'link[0][uri]' => '#stage',
], 'Save');
$this->drupalGet('');
$assert_session->linkExists('stage link');
$assert_session->linkByHrefExists('#stage');
// Switch back to the Live workspace and check that the menu link has the
// default values.
$this->switchToLive();
$this->drupalGet('');
$assert_session->linkExists($default_title);
$assert_session->linkByHrefExists($default_link);
$assert_session->linkNotExists($pending_title);
$assert_session->linkByHrefNotExists($pending_link);
$assert_session->linkNotExists('stage link');
$assert_session->linkByHrefNotExists('#stage');
// Publish the workspace and check that the menu link has been updated.
$stage->publish();
$this->drupalGet('');
$assert_session->linkNotExists($default_title);
$assert_session->linkByHrefNotExists($default_link);
$assert_session->linkExists($pending_title);
$assert_session->linkByHrefExists($pending_link);
$assert_session->linkExists('stage link');
$assert_session->linkByHrefExists('#stage');
}
}

View File

@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests permission controls on workspaces.
*
* @group workspaces
*/
class WorkspacePermissionsTest extends BrowserTestBase {
use WorkspaceTestUtilities;
/**
* {@inheritdoc}
*/
protected static $modules = ['workspaces', 'workspaces_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Verifies that a user can create but not edit a workspace.
*/
public function testCreateWorkspace(): void {
$editor = $this->drupalCreateUser([
'access administration pages',
'administer site configuration',
'create workspace',
]);
// Login as a limited-access user and create a workspace.
$this->drupalLogin($editor);
$this->createWorkspaceThroughUi('Bears', 'bears');
// Now edit that same workspace; We shouldn't be able to do so, since
// we don't have edit permissions.
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $etm */
$etm = \Drupal::service('entity_type.manager');
/** @var \Drupal\workspaces\WorkspaceInterface $bears */
$entity_list = $etm->getStorage('workspace')->loadByProperties(['label' => 'Bears']);
$bears = current($entity_list);
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/edit");
$this->assertSession()->statusCodeEquals(403);
}
/**
* Verifies that a user can create and edit only their own workspace.
*/
public function testEditOwnWorkspace(): void {
$permissions = [
'access administration pages',
'administer site configuration',
'create workspace',
'edit own workspace',
];
$editor1 = $this->drupalCreateUser($permissions);
// Login as a limited-access user and create a workspace.
$this->drupalLogin($editor1);
$this->createWorkspaceThroughUi('Bears', 'bears');
// Now edit that same workspace; We should be able to do so.
$bears = Workspace::load('bears');
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/edit");
$this->assertSession()->statusCodeEquals(200);
$page = $this->getSession()->getPage();
$page->fillField('label', 'Bears again');
$page->fillField('id', 'bears');
$page->findButton('Save')->click();
$page->hasContent('Bears again (bears)');
// Now login as a different user and ensure they don't have edit access,
// and vice versa.
$editor2 = $this->drupalCreateUser($permissions);
$this->drupalLogin($editor2);
$this->createWorkspaceThroughUi('Packers', 'packers');
$packers = Workspace::load('packers');
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$packers->id()}/edit");
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/edit");
$this->assertSession()->statusCodeEquals(403);
}
/**
* Verifies that a user can edit any workspace.
*/
public function testEditAnyWorkspace(): void {
$permissions = [
'access administration pages',
'administer site configuration',
'create workspace',
'edit own workspace',
];
$editor1 = $this->drupalCreateUser($permissions);
// Login as a limited-access user and create a workspace.
$this->drupalLogin($editor1);
$this->createWorkspaceThroughUi('Bears', 'bears');
// Now edit that same workspace; We should be able to do so.
$bears = Workspace::load('bears');
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/edit");
$this->assertSession()->statusCodeEquals(200);
$page = $this->getSession()->getPage();
$page->fillField('label', 'Bears again');
$page->fillField('id', 'bears');
$page->findButton('Save')->click();
$page->hasContent('Bears again (bears)');
// Now login as a different user and ensure they don't have edit access,
// and vice versa.
$admin = $this->drupalCreateUser(array_merge($permissions, ['edit any workspace']));
$this->drupalLogin($admin);
$this->createWorkspaceThroughUi('Packers', 'packers');
$packers = Workspace::load('packers');
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$packers->id()}/edit");
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/edit");
$this->assertSession()->statusCodeEquals(200);
}
/**
* Verifies that a user can create and delete only their own workspace.
*/
public function testDeleteOwnWorkspace(): void {
$permissions = [
'access administration pages',
'administer site configuration',
'create workspace',
'delete own workspace',
];
$editor1 = $this->drupalCreateUser($permissions);
// Login as a limited-access user and create a workspace.
$this->drupalLogin($editor1);
$bears = $this->createWorkspaceThroughUi('Bears', 'bears');
// Now try to delete that same workspace; We should be able to do so.
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/delete");
$this->assertSession()->statusCodeEquals(200);
// Now login as a different user and ensure they don't have edit access,
// and vice versa.
$editor2 = $this->drupalCreateUser($permissions);
$this->drupalLogin($editor2);
$packers = $this->createWorkspaceThroughUi('Packers', 'packers');
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$packers->id()}/delete");
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/delete");
$this->assertSession()->statusCodeEquals(403);
}
/**
* Verifies that a user can delete any workspace.
*/
public function testDeleteAnyWorkspace(): void {
$permissions = [
'access administration pages',
'administer site configuration',
'create workspace',
'delete own workspace',
];
$editor1 = $this->drupalCreateUser($permissions);
// Login as a limited-access user and create a workspace.
$this->drupalLogin($editor1);
$bears = $this->createWorkspaceThroughUi('Bears', 'bears');
// Now edit that same workspace; We should be able to do so.
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/delete");
$this->assertSession()->statusCodeEquals(200);
// Now login as a different user and ensure they have delete access on both
// workspaces.
$admin = $this->drupalCreateUser(array_merge($permissions, ['delete any workspace']));
$this->drupalLogin($admin);
$packers = $this->createWorkspaceThroughUi('Packers', 'packers');
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$packers->id()}/delete");
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/delete");
$this->assertSession()->statusCodeEquals(200);
}
}

View File

@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional;
use Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests workspace switching functionality.
*
* @group workspaces
*/
class WorkspaceSwitcherTest extends BrowserTestBase {
use AssertPageCacheContextsAndTagsTrait;
use WorkspaceTestUtilities;
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'dynamic_page_cache',
'node',
'toolbar',
'workspaces',
'workspaces_ui',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$permissions = [
'create workspace',
'edit own workspace',
'view own workspace',
'bypass entity access own workspace',
];
$this->setupWorkspaceSwitcherBlock();
$mayer = $this->drupalCreateUser($permissions);
$this->drupalLogin($mayer);
$this->createWorkspaceThroughUi('Vultures', 'vultures');
$this->createWorkspaceThroughUi('Gravity', 'gravity');
}
/**
* Tests switching workspace via the switcher block and admin page.
*/
public function testSwitchingWorkspaces(): void {
$vultures = Workspace::load('vultures');
$gravity = Workspace::load('gravity');
$this->switchToWorkspace($vultures);
// Confirm the block shows on the front page.
$this->drupalGet('<front>');
$page = $this->getSession()->getPage();
$this->assertTrue($page->hasContent('Workspace switcher'));
$this->drupalGet('/admin/config/workflow/workspaces/manage/' . $gravity->id() . '/activate');
$this->assertSession()->statusCodeEquals(200);
$page = $this->getSession()->getPage();
$page->findButton('Confirm')->click();
// Check that WorkspaceCacheContext provides the cache context used to
// support its functionality.
$this->assertCacheContext('session');
$page->findLink($gravity->label());
}
/**
* Tests switching workspace via a query parameter.
*/
public function testQueryParameterNegotiator(): void {
$web_assert = $this->assertSession();
// Initially the default workspace should be active.
$web_assert->elementContains('css', '#block-workspace-switcher', 'None');
// When adding a query parameter the workspace will be switched.
$current_user_url = \Drupal::currentUser()->getAccount()->toUrl();
$this->drupalGet($current_user_url, ['query' => ['workspace' => 'vultures']]);
$web_assert->elementContains('css', '#block-workspace-switcher', 'Vultures');
// The workspace switching via query parameter should persist.
$this->drupalGet($current_user_url);
$web_assert->elementContains('css', '#block-workspace-switcher', 'Vultures');
// Check that WorkspaceCacheContext provides the cache context used to
// support its functionality.
$this->assertCacheContext('session');
}
/**
* Tests that the toolbar workspace switcher doesn't disable the page cache.
*/
public function testToolbarSwitcherDynamicPageCache(): void {
$node_type = $this->drupalCreateContentType();
$node = $this->drupalCreateNode(['type' => $node_type->id()]);
$this->drupalLogin($this->drupalCreateUser([
'access toolbar',
'view any workspace',
]));
$this->drupalGet($node->toUrl());
$this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'MISS');
// Reload the page, it should be cached now.
$this->drupalGet($node->toUrl());
$this->assertSession()->elementExists('css', '.workspaces-toolbar-tab');
$this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'HIT');
}
}

View File

@ -0,0 +1,381 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
/**
* Test the workspace entity.
*
* @group workspaces
*/
class WorkspaceTest extends BrowserTestBase {
use WorkspaceTestUtilities;
use ContentTypeCreationTrait;
use TaxonomyTestTrait;
use FieldUiTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'field_ui',
'node',
'taxonomy',
'toolbar',
'user',
'workspaces',
'workspaces_ui',
'workspaces_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A test user.
*
* @var \Drupal\user\Entity\User
*/
protected $editor1;
/**
* A test user.
*
* @var \Drupal\user\Entity\User
*/
protected $editor2;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$permissions = [
'access administration pages',
'administer site configuration',
'create workspace',
'edit own workspace',
'edit any workspace',
'view any workspace',
'view own workspace',
'access toolbar',
];
$this->editor1 = $this->drupalCreateUser($permissions);
$this->editor2 = $this->drupalCreateUser($permissions);
$this->drupalPlaceBlock('local_actions_block');
}
/**
* Tests creating a workspace with special characters.
*/
public function testSpecialCharacters(): void {
$this->drupalLogin($this->editor1);
$page = $this->getSession()->getPage();
// Test a valid workspace name.
$this->createAndActivateWorkspaceThroughUi('Workspace 1', 'workspace_1');
$this->assertSession()->elementTextContains('css', '.workspaces-toolbar-tab', 'Workspace 1');
// Test and invalid workspace name.
$this->drupalGet('/admin/config/workflow/workspaces/add');
$this->assertSession()->statusCodeEquals(200);
$page->fillField('label', 'workspace2');
$page->fillField('id', 'A!"£%^&*{}#~@?');
$page->findButton('Save')->click();
$page->hasContent("This value is not valid");
}
/**
* Tests that the toolbar correctly shows the active workspace.
*/
public function testWorkspaceToolbar(): void {
$this->drupalLogin($this->editor1);
$this->drupalGet('/admin/config/workflow/workspaces/add');
$this->submitForm([
'id' => 'test_workspace',
'label' => 'Test workspace',
], 'Save');
// Activate the test workspace.
$this->drupalGet('/admin/config/workflow/workspaces/manage/test_workspace/activate');
$this->submitForm([], 'Confirm');
$this->drupalGet('<front>');
$page = $this->getSession()->getPage();
// Toolbar should show the correct label.
$this->assertTrue($page->hasLink('Test workspace'));
// Change the workspace label.
$this->drupalGet('/admin/config/workflow/workspaces/manage/test_workspace/edit');
$this->submitForm(['label' => 'New name'], 'Save');
$this->drupalGet('<front>');
$page = $this->getSession()->getPage();
// Toolbar should show the new label.
$this->assertTrue($page->hasLink('New name'));
}
/**
* Tests changing the owner of a workspace.
*/
public function testWorkspaceOwner(): void {
$this->drupalLogin($this->editor1);
$this->drupalGet('/admin/config/workflow/workspaces/add');
$this->submitForm([
'id' => 'test_workspace',
'label' => 'Test workspace',
], 'Save');
$storage = \Drupal::entityTypeManager()->getStorage('workspace');
$test_workspace = $storage->load('test_workspace');
$this->assertEquals($this->editor1->id(), $test_workspace->getOwnerId());
$this->drupalGet('/admin/config/workflow/workspaces/manage/test_workspace/edit');
$this->submitForm(['uid[0][target_id]' => $this->editor2->getAccountName()], 'Save');
$test_workspace = $storage->loadUnchanged('test_workspace');
$this->assertEquals($this->editor2->id(), $test_workspace->getOwnerId());
}
/**
* Tests that editing a workspace creates a new revision.
*/
public function testWorkspaceFormRevisions(): void {
$this->drupalLogin($this->editor1);
$storage = \Drupal::entityTypeManager()->getStorage('workspace');
$this->createWorkspaceThroughUi('Stage', 'stage');
// The current 'stage' workspace entity should be revision 1.
$stage_workspace = $storage->load('stage');
$this->assertEquals('1', $stage_workspace->getRevisionId());
// Re-save the 'stage' workspace via the UI to create revision 2.
$this->drupalGet($stage_workspace->toUrl('edit-form')->toString());
$this->submitForm([], 'Save');
$stage_workspace = $storage->loadUnchanged('stage');
$this->assertEquals('2', $stage_workspace->getRevisionId());
}
/**
* Tests the manage workspace page.
*/
public function testWorkspaceManagePage(): void {
$this->drupalCreateContentType(['type' => 'test', 'label' => 'Test']);
$permissions = [
'administer taxonomy',
'administer workspaces',
'create test content',
'delete any test content',
];
$this->drupalLogin($this->drupalCreateUser($permissions));
$this->setupWorkspaceSwitcherBlock();
$assert_session = $this->assertSession();
$vocabulary = $this->createVocabulary();
$test_1 = $this->createWorkspaceThroughUi('Test 1', 'test_1');
$test_2 = $this->createWorkspaceThroughUi('Test 2', 'test_2');
$this->switchToWorkspace($test_1);
// Check that the 'test_1' workspace doesn't contain any changes initially.
$this->drupalGet($test_1->toUrl()->toString());
$assert_session->pageTextContains('This workspace has no changes.');
// Check that the 'Switch to this workspace' action link is not displayed on
// the manage page of the currently active workspace.
$assert_session->linkNotExists('Switch to this workspace');
$this->drupalGet($test_2->toUrl()->toString());
$assert_session->linkExists('Switch to this workspace');
// Create some test content.
$this->createNodeThroughUi('Node 1', 'test');
$this->createNodeThroughUi('Node 2', 'test');
$edit = [
'name[0][value]' => 'Term 1',
];
$this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary->id() . '/add');
$this->submitForm($edit, 'Save');
$this->drupalGet($test_1->toUrl()->toString());
$assert_session->pageTextContains('2 content items, 1 taxonomy term');
$assert_session->linkExists('Node 1');
$assert_session->linkExists('Node 2');
$assert_session->linkExists('Term 1');
// Create 50 more nodes to test the pagination.
for ($i = 3; $i < 53; $i++) {
$this->createNodeThroughUi('Node ' . $i, 'test');
}
$this->drupalGet($test_1->toUrl()->toString());
$assert_session->pageTextContains('52 content items');
$assert_session->pageTextContains('1 taxonomy term');
$assert_session->linkExists('Node 52');
$assert_session->linkExists('Node 3');
$assert_session->linkNotExists('Term 1');
$this->drupalGet($test_1->toUrl()->toString(), ['query' => ['page' => '1']]);
$assert_session->linkExists('Node 1');
$assert_session->linkExists('Node 2');
$assert_session->linkExists('Term 1');
}
/**
* Tests adding new fields to workspace entities.
*/
public function testWorkspaceFieldUi(): void {
$user = $this->drupalCreateUser([
'administer workspaces',
'access administration pages',
'administer site configuration',
'administer workspace fields',
'administer workspace display',
'administer workspace form display',
]);
$this->drupalLogin($user);
$this->drupalGet('admin/config/workflow/workspaces/fields');
$this->assertSession()->statusCodeEquals(200);
// Create a new filed.
$field_name = $this->randomMachineName();
$field_label = $this->randomMachineName();
$this->fieldUIAddNewField('admin/config/workflow/workspaces', $field_name, $field_label, 'string');
// Check that the field is displayed on the manage form display page.
$this->drupalGet('admin/config/workflow/workspaces/form-display');
$this->assertSession()->pageTextContains($field_label);
// Check that the field is displayed on the manage display page.
$this->drupalGet('admin/config/workflow/workspaces/display');
$this->assertSession()->pageTextContains($field_label);
}
/**
* Verifies that a workspace with existing content may be deleted.
*/
public function testDeleteWorkspaceWithExistingContent(): void {
$this->createContentType(['type' => 'test', 'label' => 'Test']);
// Login and create a workspace.
$permissions = [
'administer workspaces',
'create test content',
'delete any test content',
];
$this->drupalLogin($this->drupalCreateUser($permissions));
$this->createAndActivateWorkspaceThroughUi('May 4', 'may_4');
// Create a node in the workspace.
$this->createNodeThroughUi('A mayfly flies / In May or June', 'test');
// Delete the workspace.
$this->drupalGet('/admin/config/workflow/workspaces/manage/may_4/delete');
$this->assertSession()->statusCodeEquals(200);
$page = $this->getSession()->getPage();
$page->findButton('Delete')->click();
$page->hasContent('The workspace May 4 has been deleted.');
}
/**
* Tests the Workspaces listing UI.
*/
public function testWorkspaceList(): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
// Login and create a workspace.
$this->drupalLogin($this->editor1);
$this->createWorkspaceThroughUi('Summer event', 'summer_event');
// Check that Live is the current active workspace.
$this->drupalGet('/admin/config/workflow/workspaces');
$this->assertSession()->statusCodeEquals(200);
$active_workspace_row = $page->find('css', '.active-workspace');
$this->assertTrue($active_workspace_row->hasClass('active-workspace--default'));
$this->assertEquals('Live', $active_workspace_row->find('css', 'td:first-of-type')->getText());
// The 'Switch to Live' operation is not shown when 'Live' is the active
// workspace.
$assert_session->linkNotExists('Switch to Live');
// Switch to another workspace and check that it has been marked as active.
$page->clickLink('Switch to Summer event');
$page->pressButton('Confirm');
$active_workspace_row = $page->find('css', '.active-workspace');
$this->assertTrue($active_workspace_row->hasClass('active-workspace--not-default'));
$this->assertEquals('Summer event', $active_workspace_row->find('css', 'td:first-of-type')->getText());
// 'Live' is no longer the active workspace, so it's 'Switch to Live'
// operation should be visible now.
$assert_session->linkExists('Switch to Live');
// Delete any of the workspace owners and visit workspaces listing.
$this->drupalLogin($this->editor2);
user_cancel([], $this->editor1->id(), 'user_cancel_reassign');
$user = \Drupal::service('entity_type.manager')->getStorage('user')->load($this->editor1->id());
$user->delete();
$this->drupalGet('/admin/config/workflow/workspaces');
$this->assertSession()->pageTextContains('Summer event');
$summer_event_workspace_row = $page->find('css', 'table tbody tr:nth-of-type(2)');
$this->assertEquals('N/A', $summer_event_workspace_row->find('css', 'td:nth-of-type(2)')->getText());
}
/**
* Verifies that a workspace can be published.
*/
public function testPublishWorkspace(): void {
$this->createContentType(['type' => 'test', 'label' => 'Test']);
$permissions = [
'administer workspaces',
'create test content',
];
$this->drupalLogin($this->drupalCreateUser($permissions));
$this->drupalGet('/admin/config/workflow/workspaces/add');
$this->submitForm([
'id' => 'test_workspace',
'label' => 'Test workspace',
], 'Save');
// Activate the test workspace.
$this->drupalGet('/admin/config/workflow/workspaces/manage/test_workspace/activate');
$this->submitForm([], 'Confirm');
$this->drupalGet('/admin/config/workflow/workspaces/manage/test_workspace/publish');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('There are no changes that can be published from Test workspace to Live.');
// Create a node in the workspace.
$this->drupalGet('/node/add/test');
$this->assertEquals(1, \Drupal::keyValue('ws_test')->get('node.hook_entity_create.count'));
$this->submitForm(['title[0][value]' => 'Test node'], 'Save');
$this->drupalGet('/admin/config/workflow/workspaces/manage/test_workspace/publish');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('There is 1 item that can be published from Test workspace to Live');
$this->getSession()->getPage()->pressButton('Publish 1 item to Live');
$this->assertSession()->pageTextContains('Successful publication.');
}
}

View File

@ -0,0 +1,235 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Tests\block\Traits\BlockCreationTrait;
use Drupal\workspaces\Entity\Handler\IgnoredWorkspaceHandler;
use Drupal\workspaces\Entity\Workspace;
use Drupal\workspaces\WorkspaceInterface;
/**
* Utility methods for use in BrowserTestBase tests.
*
* This trait will not work if not used in a child of BrowserTestBase.
*/
trait WorkspaceTestUtilities {
use BlockCreationTrait;
/**
* Signifies that the switcher block is configured.
*
* @var bool
*/
protected $switcherBlockConfigured = FALSE;
/**
* Loads a single entity by its label.
*
* The UI approach to creating an entity doesn't make it easy to know what
* the ID is, so this lets us make paths for an entity after it's created.
*
* @param string $type
* The type of entity to load.
* @param string $label
* The label of the entity to load.
*
* @return \Drupal\Core\Entity\EntityInterface
* The entity.
*/
protected function getOneEntityByLabel($type, $label): EntityInterface {
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
$entity_type_manager = \Drupal::service('entity_type.manager');
$property = $entity_type_manager->getDefinition($type)->getKey('label');
$entity_list = $entity_type_manager->getStorage($type)->loadByProperties([$property => $label]);
$entity = current($entity_list);
if (!$entity) {
$this->fail("No {$type} entity named {$label} found.");
}
return $entity;
}
/**
* Creates and activates a new Workspace through the UI.
*
* @param string|null $label
* The label of the workspace to create.
* @param string|null $id
* The ID of the workspace to create.
* @param string $parent
* (optional) The ID of the parent workspace. Defaults to '_none'.
*
* @return \Drupal\workspaces\WorkspaceInterface
* The workspace that was just created.
*/
protected function createAndActivateWorkspaceThroughUi(?string $label = NULL, ?string $id = NULL, string $parent = '_none'): WorkspaceInterface {
$id ??= $this->randomMachineName();
$label ??= $this->randomString();
$this->drupalGet('/admin/config/workflow/workspaces/add');
$this->submitForm([
'id' => $id,
'label' => $label,
'parent' => $parent,
], 'Save and switch');
$this->getSession()->getPage()->hasContent("$label ($id)");
// Keep the test runner in sync with the system under test.
$workspace = Workspace::load($id);
\Drupal::service('workspaces.manager')->setActiveWorkspace($workspace);
return $workspace;
}
/**
* Creates a new Workspace through the UI.
*
* @param string|null $label
* The label of the workspace to create.
* @param string|null $id
* The ID of the workspace to create.
* @param string $parent
* (optional) The ID of the parent workspace. Defaults to '_none'.
*
* @return \Drupal\workspaces\WorkspaceInterface
* The workspace that was just created.
*/
protected function createWorkspaceThroughUi(?string $label = NULL, ?string $id = NULL, string $parent = '_none') {
$id ??= $this->randomMachineName();
$label ??= $this->randomString();
$this->drupalGet('/admin/config/workflow/workspaces/add');
$this->submitForm([
'id' => $id,
'label' => $label,
'parent' => $parent,
], 'Save');
$this->getSession()->getPage()->hasContent("$label ($id)");
return Workspace::load($id);
}
/**
* Adds the workspace switcher block to the site.
*
* This is necessary for switchToWorkspace() to function correctly.
*/
protected function setupWorkspaceSwitcherBlock() {
// Add the block to the sidebar.
$this->placeBlock('workspace_switcher', [
'id' => 'workspace_switcher',
'region' => 'sidebar_first',
'label' => 'Workspace switcher',
]);
$this->drupalGet('<front>');
$this->switcherBlockConfigured = TRUE;
}
/**
* Sets a given workspace as "active" for subsequent requests.
*
* This assumes that the switcher block has already been setup by calling
* setupWorkspaceSwitcherBlock().
*
* @param \Drupal\workspaces\WorkspaceInterface $workspace
* The workspace to set active.
*/
protected function switchToWorkspace(WorkspaceInterface $workspace) {
$this->assertTrue($this->switcherBlockConfigured, 'This test was not written correctly: you must call setupWorkspaceSwitcherBlock() before switchToWorkspace()');
/** @var \Drupal\Tests\WebAssert $session */
$session = $this->assertSession();
$session->buttonExists('Activate');
$this->submitForm(['workspace_id' => $workspace->id()], 'Activate');
$session->pageTextContains($workspace->label() . ' is now the active workspace.');
// Keep the test runner in sync with the system under test.
\Drupal::service('workspaces.manager')->setActiveWorkspace($workspace);
}
/**
* Switches to the live version of the site for subsequent requests.
*
* This assumes that the switcher block has already been setup by calling
* setupWorkspaceSwitcherBlock().
*/
protected function switchToLive() {
/** @var \Drupal\Tests\WebAssert $session */
$session = $this->assertSession();
$this->submitForm([], 'Switch to Live');
$session->pageTextContains('You are now viewing the live version of the site.');
// Keep the test runner in sync with the system under test.
\Drupal::service('workspaces.manager')->switchToLive();
}
/**
* Creates a node by "clicking" buttons.
*
* @param string $label
* The label of the Node to create.
* @param string $bundle
* The bundle of the Node to create.
* @param bool $publish
* The publishing status to set.
*
* @return \Drupal\node\NodeInterface
* The Node that was just created.
*
* @throws \Behat\Mink\Exception\ElementNotFoundException
*/
protected function createNodeThroughUi($label, $bundle, $publish = TRUE) {
$this->drupalGet('/node/add/' . $bundle);
/** @var \Behat\Mink\Session $session */
$session = $this->getSession();
$this->assertSession()->statusCodeEquals(200);
/** @var \Behat\Mink\Element\DocumentElement $page */
$page = $session->getPage();
$page->fillField('Title', $label);
if ($publish) {
$page->findButton('Save')->click();
}
else {
$page->uncheckField('Published');
$page->findButton('Save')->click();
}
$session->getPage()->hasContent("{$label} has been created");
return $this->getOneEntityByLabel('node', $label);
}
/**
* Determine if the content list has an entity's label.
*
* This assertion can be used to validate a particular entity exists in the
* current workspace.
*/
protected function isLabelInContentOverview($label) {
$this->drupalGet('/admin/content');
$session = $this->getSession();
$this->assertSession()->statusCodeEquals(200);
$page = $session->getPage();
return $page->hasContent($label);
}
/**
* Marks an entity type as ignored in a workspace.
*
* @param string $entity_type_id
* The entity type ID.
*/
protected function ignoreEntityType(string $entity_type_id): void {
$entity_type = clone \Drupal::entityTypeManager()->getDefinition($entity_type_id);
$entity_type->setHandlerClass('workspace', IgnoredWorkspaceHandler::class);
\Drupal::state()->set("$entity_type_id.entity_type", $entity_type);
\Drupal::entityTypeManager()->clearCachedDefinitions();
}
}

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests permission controls on workspaces.
*
* @group workspaces
*/
class WorkspaceViewTest extends BrowserTestBase {
use WorkspaceTestUtilities;
/**
* {@inheritdoc}
*/
protected static $modules = ['workspaces', 'workspaces_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Verifies that a user can view their own workspace.
*/
public function testViewOwnWorkspace(): void {
$permissions = [
'access administration pages',
'administer site configuration',
'create workspace',
'edit own workspace',
'view own workspace',
];
$editor1 = $this->drupalCreateUser($permissions);
// Login as a limited-access user and create a workspace.
$this->drupalLogin($editor1);
$this->createWorkspaceThroughUi('Bears', 'bears');
$bears = Workspace::load('bears');
// Now login as a different user and create a workspace.
$editor2 = $this->drupalCreateUser($permissions);
$this->drupalLogin($editor2);
$this->createWorkspaceThroughUi('Packers', 'packers');
$packers = Workspace::load('packers');
// Load the activate form for the Bears workspace. It should fail because
// the workspace belongs to someone else.
$this->drupalGet("admin/config/workflow/workspaces/manage/{$bears->id()}/activate");
$this->assertSession()->statusCodeEquals(403);
// But editor 2 should be able to activate the Packers workspace.
$this->drupalGet("admin/config/workflow/workspaces/manage/{$packers->id()}/activate");
$this->assertSession()->statusCodeEquals(200);
}
/**
* Verifies that a user can view any workspace.
*/
public function testViewAnyWorkspace(): void {
$permissions = [
'access administration pages',
'administer site configuration',
'create workspace',
'edit own workspace',
'view any workspace',
];
$editor1 = $this->drupalCreateUser($permissions);
// Login as a limited-access user and create a workspace.
$this->drupalLogin($editor1);
$this->createWorkspaceThroughUi('Bears', 'bears');
$bears = Workspace::load('bears');
// Now login as a different user and create a workspace.
$editor2 = $this->drupalCreateUser($permissions);
$this->drupalLogin($editor2);
$this->createWorkspaceThroughUi('Packers', 'packers');
$packers = Workspace::load('packers');
// Load the activate form for the Bears workspace. This user should be
// able to see both workspaces because of the "view any" permission.
$this->drupalGet("admin/config/workflow/workspaces/manage/{$bears->id()}/activate");
$this->assertSession()->statusCodeEquals(200);
// But editor 2 should be able to activate the Packers workspace.
$this->drupalGet("admin/config/workflow/workspaces/manage/{$packers->id()}/activate");
$this->assertSession()->statusCodeEquals(200);
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional;
use Drupal\Tests\views\Functional\BulkFormTest;
/**
* Tests the views bulk form in a workspace.
*
* @group views
* @group workspaces
*/
class WorkspaceViewsBulkFormTest extends BulkFormTest {
use WorkspaceTestUtilities;
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'workspaces', 'workspaces_ui', 'workspaces_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Override the user created in the parent method to add workspaces access.
$admin_user = $this->drupalCreateUser([
'administer nodes',
'administer workspaces',
'edit any page content',
'delete any page content',
]);
$this->drupalLogin($admin_user);
// Ensure that all the test methods are executed in the context of a
// workspace.
$this->setupWorkspaceSwitcherBlock();
$this->createAndActivateWorkspaceThroughUi('Test workspace', 'test');
}
/**
* Tests the Workspaces view bulk form integration.
*/
public function testBulkForm(): void {
// Ignore entity types that are not being tested, in order to fully re-use
// the parent test method.
$this->ignoreEntityType('view');
parent::testBulkForm();
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Tests uninstalling the Workspaces module.
*
* @group workspaces
*/
class WorkspacesUninstallTest extends BrowserTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['workspaces', 'node', 'workspaces_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$permissions = [
'administer workspaces',
'administer modules',
];
$this->drupalLogin($this->drupalCreateUser($permissions));
}
/**
* Tests deleting workspace entities and uninstalling Workspaces module.
*/
public function testUninstallingWorkspace(): void {
$this->createContentType(['type' => 'article']);
$this->drupalGet('admin/modules/uninstall');
$this->submitForm(['uninstall[workspaces_ui]' => TRUE], 'Uninstall');
$this->submitForm([], 'Uninstall');
$this->submitForm(['uninstall[workspaces]' => TRUE], 'Uninstall');
$this->submitForm([], 'Uninstall');
$session = $this->assertSession();
$session->pageTextContains('The selected modules have been uninstalled.');
$session->pageTextNotContains('Workspaces');
$this->assertFalse(\Drupal::database()->schema()->fieldExists('node_revision', 'workspace'));
// Verify that the revision metadata key has been removed.
$this->rebuildContainer();
$entity_type = \Drupal::entityDefinitionUpdateManager()->getEntityType('node');
$revision_metadata_keys = $entity_type->get('revision_metadata_keys');
$this->assertArrayNotHasKey('workspace', $revision_metadata_keys);
}
}

View File

@ -0,0 +1,250 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\FunctionalJavascript;
use Drupal\Tests\layout_builder\FunctionalJavascript\InlineBlockTestBase;
use Drupal\Tests\system\Traits\OffCanvasTestTrait;
use Drupal\Tests\workspaces\Functional\WorkspaceTestUtilities;
use Drupal\user\UserInterface;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests for layout editing in workspaces.
*
* @group layout_builder
* @group workspaces
* @group #slow
*/
class WorkspacesLayoutBuilderIntegrationTest extends InlineBlockTestBase {
use OffCanvasTestTrait;
use WorkspaceTestUtilities;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* The default user that is getting logged in during setup.
*/
protected UserInterface $defaultUser;
/**
* {@inheritdoc}
*/
protected static $modules = [
'field_ui',
'workspaces',
'workspaces_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->defaultUser = $this->drupalCreateUser([
'access contextual links',
'configure any layout',
'administer node display',
'administer node fields',
'create and edit custom blocks',
'administer blocks',
'administer content types',
'administer workspaces',
'view any workspace',
'administer site configuration',
'administer nodes',
'bypass node access',
]);
$this->drupalLogin($this->defaultUser);
$this->setupWorkspaceSwitcherBlock();
Workspace::create(['id' => 'stage', 'label' => 'Stage'])->save();
// Enable layout builder.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
$this->submitForm([
'layout[enabled]' => TRUE,
'layout[allow_custom]' => TRUE,
], 'Save');
$this->clickLink('Manage layout');
$this->assertSession()->addressEquals(static::FIELD_UI_PREFIX . '/display/default/layout');
// Add a basic block with the body field set.
$this->addInlineBlockToLayout('Block title', 'The DEFAULT block body');
$this->assertSaveLayout();
}
/**
* Tests changing a layout/blocks inside a workspace.
*/
public function testBlocksInWorkspaces(): void {
$assert_session = $this->assertSession();
$this->drupalGet('node/1');
$assert_session->pageTextContains('The DEFAULT block body');
$this->drupalGet('node/2');
$assert_session->pageTextContains('The DEFAULT block body');
$stage = Workspace::load('stage');
$this->switchToWorkspace($stage);
// Confirm the block can be edited.
$this->drupalGet('node/1/layout');
$new_block_body = 'The NEW block body';
$this->configureInlineBlock('The DEFAULT block body', $new_block_body);
$this->assertSaveLayout();
$this->drupalGet('node/1');
$assert_session->pageTextContains($new_block_body);
$assert_session->pageTextNotContains('The DEFAULT block body');
$this->drupalGet('node/2');
// Node 2 should use default layout.
$assert_session->pageTextContains('The DEFAULT block body');
$assert_session->pageTextNotContains($new_block_body);
// Switch back to the live workspace and verify that the changes are not
// visible there.
$this->switchToLive();
$this->drupalGet('node/1');
$assert_session->pageTextNotContains($new_block_body);
$assert_session->pageTextContains('The DEFAULT block body');
$this->switchToWorkspace($stage);
// Add a basic block with the body field set.
$this->drupalGet('node/1/layout');
$second_block_body = 'The 2nd block body';
$this->addInlineBlockToLayout('2nd Block title', $second_block_body);
$this->assertSaveLayout();
$this->drupalGet('node/1');
$assert_session->pageTextContains($second_block_body);
$this->drupalGet('node/2');
// Node 2 should use default layout.
$assert_session->pageTextContains('The DEFAULT block body');
$assert_session->pageTextNotContains($new_block_body);
$assert_session->pageTextNotContains($second_block_body);
// Switch back to the live workspace and verify that the new added block is
// not visible there.
$this->switchToLive();
$this->drupalGet('node/1');
$assert_session->pageTextNotContains($second_block_body);
$assert_session->pageTextContains('The DEFAULT block body');
// Check the concurrent editing protection on the Layout Builder form.
$this->drupalGet('/node/1/layout');
$assert_session->pageTextContains('The content is being edited in the Stage workspace. As a result, your changes cannot be saved.');
$stage->publish();
$this->drupalGet('node/1');
$assert_session->pageTextNotContains('The DEFAULT block body');
$assert_session->pageTextContains($new_block_body);
$assert_session->pageTextContains($second_block_body);
}
/**
* Tests that blocks can be deleted inside workspaces.
*/
public function testBlockDeletionInWorkspaces(): void {
$assert_session = $this->assertSession();
$stage = Workspace::load('stage');
$this->switchToWorkspace($stage);
$this->drupalGet('node/1/layout');
$workspace_block_content = 'The WORKSPACE block body';
$this->addInlineBlockToLayout('Workspace block title', $workspace_block_content);
$this->assertSaveLayout();
$this->drupalGet('node/1');
$assert_session->pageTextContains('The DEFAULT block body');
$assert_session->pageTextContains($workspace_block_content);
$this->switchToLive();
$assert_session->pageTextNotContains($workspace_block_content);
$this->switchToWorkspace($stage);
$this->drupalGet('node/1/layout');
$this->removeInlineBlockFromLayout(static::INLINE_BLOCK_LOCATOR . ' ~ ' . static::INLINE_BLOCK_LOCATOR);
$this->assertSaveLayout();
$this->drupalGet('node/1');
$assert_session->pageTextContains('The DEFAULT block body');
$assert_session->pageTextNotContains($workspace_block_content);
$this->drupalGet('node/1/layout');
$this->removeInlineBlockFromLayout();
$this->assertSaveLayout();
$this->drupalGet('node/1');
$assert_session->pageTextNotContains('The DEFAULT block body');
$assert_session->pageTextNotContains($workspace_block_content);
$this->switchToLive();
$this->drupalGet('node/1');
$assert_session->pageTextContains('The DEFAULT block body');
$stage->publish();
$this->drupalGet('node/1');
$assert_session->pageTextNotContains('The DEFAULT block body');
$assert_session->pageTextNotContains($workspace_block_content);
}
/**
* Tests workspace specific layout tempstore data.
*
* @covers \Drupal\workspaces\WorkspacesLayoutTempstoreRepository::getKey
*/
public function testWorkspacesLayoutTempstore(): void {
$assert_session = $this->assertSession();
$this->drupalGet('node/1');
$assert_session->pageTextContains('The DEFAULT block body');
$second_user = $this->drupalCreateUser([
'access contextual links',
'configure any layout',
'administer node display',
'administer node fields',
'create and edit custom blocks',
'administer blocks',
'administer content types',
'administer workspaces',
'view any workspace',
'administer site configuration',
'administer nodes',
'bypass node access',
]);
$stage = Workspace::load('stage');
$this->switchToWorkspace($stage);
// Confirm the block can be edited.
$this->drupalGet('node/1/layout');
$workspace_block_body = 'The WS block body';
$this->configureInlineBlock('The DEFAULT block body', $workspace_block_body);
// Switch to another user and check the layout edit page in the live
// workspace and verify that the changes are not visible there. Switching
// the user automatically switches the workspace to Live.
$this->drupalLogin($second_user);
$this->drupalGet('node/1/layout');
$assert_session->pageTextNotContains($workspace_block_body);
$assert_session->pageTextContains('The DEFAULT block body');
$live_block_body = 'Live edit block body';
$this->configureInlineBlock('The DEFAULT block body', $live_block_body);
$assert_session->pageTextContains($live_block_body);
$assert_session->pageTextNotContains('The DEFAULT block body');
$this->drupalGet('node/1');
$assert_session->pageTextNotContains($live_block_body);
$assert_session->pageTextContains('The DEFAULT block body');
$this->drupalLogin($this->defaultUser);
$this->switchToWorkspace($stage);
$this->drupalGet('node/1/layout');
$assert_session->pageTextContains($workspace_block_body);
$assert_session->pageTextNotContains($live_block_body);
$assert_session->pageTextNotContains('The DEFAULT block body');
$this->drupalGet('node/1');
$assert_session->pageTextNotContains($workspace_block_body);
$assert_session->pageTextContains('The DEFAULT block body');
}
}

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\FunctionalJavascript;
use Drupal\Tests\media_library\FunctionalJavascript\EntityReferenceWidgetTest;
use Drupal\user\UserInterface;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests the Media library entity reference widget in a workspace.
*
* @group workspaces
*/
class WorkspacesMediaLibraryIntegrationTest extends EntityReferenceWidgetTest {
/**
* {@inheritdoc}
*/
protected static $modules = [
'workspaces',
];
/**
* An array of test methods that are not relevant for workspaces.
*/
const SKIP_METHODS = [
// This test does not assert anything that can be workspace-specific.
'testFocusNotAppliedWithoutSelectionChange',
// This test does not assert anything that can be workspace-specific.
'testRequiredMediaField',
// This test tries to edit an entity in Live after it has been edited in a
// workspace, which is not currently possible.
'testWidgetPreview',
];
/**
* {@inheritdoc}
*/
public function setUp(): void {
if (in_array($this->name(), static::SKIP_METHODS, TRUE)) {
$this->markTestSkipped('Irrelevant for this test');
}
parent::setUp();
// Ensure that all the test methods are executed in the context of a
// workspace.
$workspace = Workspace::create(['id' => 'test', 'label' => 'Test']);
$workspace->save();
\Drupal::service('workspaces.manager')->setActiveWorkspace($workspace);
}
/**
* {@inheritdoc}
*/
protected function drupalCreateUser(array $permissions = [], $name = NULL, $admin = FALSE, array $values = []): UserInterface|false {
// Ensure that users and roles are managed outside a workspace context.
return \Drupal::service('workspaces.manager')->executeOutsideWorkspace(function () use ($permissions, $name, $admin, $values) {
$permissions = array_merge($permissions, [
'view any workspace',
]);
return parent::drupalCreateUser($permissions, $name, $admin, $values);
});
}
}

View File

@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\entity_test\Entity\EntityTestMulRevPub;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* @coversDefaultClass \Drupal\workspaces\Plugin\Validation\Constraint\EntityReferenceSupportedNewEntitiesConstraintValidator
* @group workspaces
*/
class EntityReferenceSupportedNewEntitiesConstraintValidatorTest extends KernelTestBase {
use UserCreationTrait;
use WorkspaceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'workspaces',
'entity_test',
];
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManager
*/
protected EntityTypeManager $entityTypeManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->createUser();
$fields['supported_reference'] = BaseFieldDefinition::create('entity_reference')->setSetting('target_type', 'entity_test_mulrevpub');
$fields['unsupported_reference'] = BaseFieldDefinition::create('entity_reference')->setSetting('target_type', 'entity_test');
$this->container->get('state')->set('entity_test_mulrevpub.additional_base_field_definitions', $fields);
$this->installEntitySchema('entity_test_mulrevpub');
$this->initializeWorkspacesModule();
}
/**
* @covers ::validate
*/
public function testNewEntitiesAllowedInDefaultWorkspace(): void {
$entity = EntityTestMulRevPub::create([
'unsupported_reference' => [
'entity' => EntityTest::create([]),
],
'supported_reference' => [
'entity' => EntityTest::create([]),
],
]);
$this->assertCount(0, $entity->validate());
}
/**
* @covers ::validate
*/
public function testNewEntitiesForbiddenInNonDefaultWorkspace(): void {
$this->switchToWorkspace('stage');
$entity = EntityTestMulRevPub::create([
'unsupported_reference' => [
'entity' => EntityTest::create([]),
],
'supported_reference' => [
'entity' => EntityTestMulRevPub::create([]),
],
]);
$violations = $entity->validate();
$this->assertCount(1, $violations);
$this->assertEquals('Test entity entities can only be created in the default workspace.', $violations[0]->getMessage());
}
}

View File

@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\entity_test\Entity\EntityTestMulRevPub;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\workspaces\Entity\Workspace;
/**
* @coversDefaultClass \Drupal\workspaces\Plugin\Validation\Constraint\EntityWorkspaceConflictConstraintValidator
* @group workspaces
*/
class EntityWorkspaceConflictConstraintValidatorTest extends KernelTestBase {
use UserCreationTrait;
use WorkspaceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'entity_test',
'system',
'user',
'workspaces',
];
/**
* The entity type manager.
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entityTypeManager = \Drupal::entityTypeManager();
$this->installSchema('workspaces', ['workspace_association']);
$this->installEntitySchema('entity_test_mulrevpub');
$this->installEntitySchema('workspace');
$this->installEntitySchema('user');
$this->createUser();
}
/**
* @covers ::validate
*/
public function testNewEntitiesAllowedInDefaultWorkspace(): void {
// Create two top-level workspaces and a second-level one.
$stage = Workspace::create(['id' => 'stage', 'label' => 'Stage']);
$stage->save();
$dev = Workspace::create(['id' => 'dev', 'label' => 'Dev', 'parent' => 'stage']);
$dev->save();
$other = Workspace::create(['id' => 'other', 'label' => 'Other']);
$other->save();
// Create an entity in Live, and check that the validation is skipped.
$entity = EntityTestMulRevPub::create();
$this->assertCount(0, $entity->validate());
$entity->save();
$entity = $this->reloadEntity($entity);
$this->assertCount(0, $entity->validate());
// Edit the entity in Stage.
$this->switchToWorkspace('stage');
$entity->save();
$entity = $this->reloadEntity($entity);
$this->assertCount(0, $entity->validate());
$expected_message = 'The content is being edited in the Stage workspace. As a result, your changes cannot be saved.';
// Check that the entity can no longer be edited in Live.
$this->switchToLive();
$entity = $this->reloadEntity($entity);
$violations = $entity->validate();
$this->assertCount(1, $violations);
$this->assertSame($expected_message, (string) $violations->get(0)->getMessage());
// Check that the entity can no longer be edited in another top-level
// workspace.
$this->switchToWorkspace('other');
$entity = $this->reloadEntity($entity);
$violations = $entity->validate();
$this->assertCount(1, $violations);
$this->assertSame($expected_message, (string) $violations->get(0)->getMessage());
// Check that the entity can still be edited in a sub-workspace of Stage.
$this->switchToWorkspace('dev');
$entity = $this->reloadEntity($entity);
$this->assertCount(0, $entity->validate());
// Edit the entity in Dev.
$this->switchToWorkspace('dev');
$entity->save();
$entity = $this->reloadEntity($entity);
$this->assertCount(0, $entity->validate());
$expected_message = 'The content is being edited in the Dev workspace. As a result, your changes cannot be saved.';
// Check that the entity can no longer be edited in Live.
$this->switchToLive();
$entity = $this->reloadEntity($entity);
$violations = $entity->validate();
$this->assertCount(1, $violations);
$this->assertSame($expected_message, (string) $violations->get(0)->getMessage());
// Check that the entity can no longer be edited in the parent workspace.
$this->switchToWorkspace('stage');
$entity = $this->reloadEntity($entity);
$violations = $entity->validate();
$this->assertCount(1, $violations);
$this->assertSame($expected_message, (string) $violations->get(0)->getMessage());
// Check that the entity can no longer be edited in another top-level
// workspace.
$this->switchToWorkspace('other');
$entity = $this->reloadEntity($entity);
$violations = $entity->validate();
$this->assertCount(1, $violations);
$this->assertSame($expected_message, (string) $violations->get(0)->getMessage());
}
/**
* Reloads the given entity from the storage and returns it.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to be reloaded.
*
* @return \Drupal\Core\Entity\EntityInterface
* The reloaded entity.
*/
protected function reloadEntity(EntityInterface $entity): EntityInterface {
$storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
$storage->resetCache([$entity->id()]);
return $storage->load($entity->id());
}
}

View File

@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\Core\Access\AccessResult;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests access on workspaces.
*
* @group workspaces
*/
class WorkspaceAccessTest extends KernelTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'user',
'system',
'workspaces',
'workspace_access_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installSchema('workspaces', ['workspace_association']);
$this->installEntitySchema('workspace');
$this->installEntitySchema('user');
// User 1.
$this->createUser();
}
/**
* Tests cases for testWorkspaceAccess().
*
* @return array
* An array of operations and permissions to test with.
*/
public static function operationCases() {
return [
['create', 'administer workspaces'],
['create', 'create workspace'],
['view', 'administer workspaces'],
['view', 'view any workspace'],
['view', 'view own workspace'],
['update', 'administer workspaces'],
['update', 'edit any workspace'],
['update', 'edit own workspace'],
['delete', 'administer workspaces'],
['delete', 'delete any workspace'],
['delete', 'delete own workspace'],
];
}
/**
* Verifies all workspace roles have the correct access for the operation.
*
* @param string $operation
* The operation to test with.
* @param string $permission
* The permission to test with.
*
* @dataProvider operationCases
*/
public function testWorkspaceAccess($operation, $permission): void {
$user = $this->createUser();
$this->setCurrentUser($user);
$workspace = Workspace::create(['id' => 'oak']);
$workspace->save();
$this->assertFalse($workspace->access($operation, $user));
\Drupal::entityTypeManager()->getAccessControlHandler('workspace')->resetCache();
$role = $this->createRole([$permission]);
$user->addRole($role);
$this->assertTrue($workspace->access($operation, $user));
}
/**
* Tests workspace publishing access.
*/
public function testPublishWorkspaceAccess(): void {
$user = $this->createUser([
'view own workspace',
'edit own workspace',
]);
$this->setCurrentUser($user);
$workspace = Workspace::create(['id' => 'stage']);
$workspace->save();
// Check that, by default, an admin user is allowed to publish a workspace.
$this->assertTrue($workspace->access('publish'));
// Simulate an external factor which decides that a workspace can not be
// published.
\Drupal::state()->set('workspace_access_test.result.publish', AccessResult::forbidden());
\Drupal::entityTypeManager()->getAccessControlHandler('workspace')->resetCache();
$this->assertFalse($workspace->access('publish'));
}
/**
* @covers \Drupal\workspaces\Plugin\EntityReferenceSelection\WorkspaceSelection::getReferenceableEntities
*/
public function testWorkspaceSelection(): void {
$own_permission_user = $this->createUser(['view own workspace']);
$any_permission_user = $this->createUser(['view any workspace']);
$admin_permission_user = $this->createUser(['administer workspaces']);
// Create the following workspace hierarchy:
// - top1 ($own_permission_user)
// --- child1_1 ($own_permission_user)
// --- child1_2 ($any_permission_user)
// ----- child1_2_1 ($any_permission_user)
// - top2 ($admin_permission_user)
// --- child2_1 ($admin_permission_user)
$created_time = \Drupal::time()->getCurrentTime();
Workspace::create([
'uid' => $own_permission_user->id(),
'id' => 'top1',
'label' => 'top1',
'created' => ++$created_time,
])->save();
Workspace::create([
'uid' => $own_permission_user->id(),
'id' => 'child1_1',
'parent' => 'top1',
'label' => 'child1_1',
'created' => ++$created_time,
])->save();
Workspace::create([
'uid' => $any_permission_user->id(),
'id' => 'child1_2',
'parent' => 'top1',
'label' => 'child1_2',
'created' => ++$created_time,
])->save();
Workspace::create([
'uid' => $any_permission_user->id(),
'id' => 'child1_2_1',
'parent' => 'child1_2',
'label' => 'child1_2_1',
'created' => ++$created_time,
])->save();
Workspace::create([
'uid' => $admin_permission_user->id(),
'id' => 'top2',
'label' => 'top2',
'created' => ++$created_time,
])->save();
Workspace::create([
'uid' => $admin_permission_user->id(),
'id' => 'child2_1',
'parent' => 'top2',
'label' => 'child2_1',
'created' => ++$created_time,
])->save();
/** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $selection_handler */
$selection_handler = \Drupal::service('plugin.manager.entity_reference_selection')->getInstance([
'target_type' => 'workspace',
'handler' => 'default',
'sort' => [
'field' => 'created',
'direction' => 'asc',
],
]);
// The $own_permission_user should only be allowed to reference 'top1' and
// 'child1_1'.
$this->setCurrentUser($own_permission_user);
$expected = [
'top1',
'child1_1',
];
$this->assertEquals($expected, array_keys($selection_handler->getReferenceableEntities()['workspace']));
$this->assertEquals($expected, array_keys($selection_handler->getReferenceableEntities(NULL, 'CONTAINS', 3)['workspace']));
$expected = [
'top1',
];
$this->assertEquals($expected, array_keys($selection_handler->getReferenceableEntities('top')['workspace']));
// The $any_permission_user and $admin_permission_user should be allowed to
// reference any workspace.
$expected_all = [
'top1',
'child1_1',
'child1_2',
'child1_2_1',
'top2',
'child2_1',
];
$expected_3 = [
'top1',
'child1_1',
'child1_2',
];
$expected_top = [
'top1',
'top2',
];
$this->setCurrentUser($any_permission_user);
$this->assertEquals($expected_all, array_keys($selection_handler->getReferenceableEntities()['workspace']));
$this->assertEquals($expected_3, array_keys($selection_handler->getReferenceableEntities(NULL, 'CONTAINS', 3)['workspace']));
$this->assertEquals($expected_top, array_keys($selection_handler->getReferenceableEntities('top')['workspace']));
$this->setCurrentUser($admin_permission_user);
$this->assertEquals($expected_all, array_keys($selection_handler->getReferenceableEntities()['workspace']));
$this->assertEquals($expected_3, array_keys($selection_handler->getReferenceableEntities(NULL, 'CONTAINS', 3)['workspace']));
$this->assertEquals($expected_top, array_keys($selection_handler->getReferenceableEntities('top')['workspace']));
}
/**
* @covers \Drupal\workspaces\Plugin\Block\WorkspaceSwitcherBlock::blockAccess
*/
public function testWorkspaceSwitcherBlock(): void {
$own_permission_user = $this->createUser(['view own workspace']);
$any_permission_user = $this->createUser(['view any workspace']);
$admin_permission_user = $this->createUser(['administer workspaces']);
$access_content_user = $this->createUser(['access content']);
$no_permission_user = $this->createUser();
/** @var \Drupal\Core\Block\BlockManagerInterface $block_manager */
$block_manager = \Drupal::service('plugin.manager.block');
/** @var \Drupal\Core\Block\BlockPluginInterface $switcher_block */
$switcher_block = $block_manager->createInstance('workspace_switcher');
$this->assertTrue($switcher_block->access($own_permission_user));
$this->assertTrue($switcher_block->access($any_permission_user));
$this->assertTrue($switcher_block->access($admin_permission_user));
$this->assertFalse($switcher_block->access($access_content_user));
$this->assertFalse($switcher_block->access($no_permission_user));
}
}

View File

@ -0,0 +1,254 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests workspace associations.
*
* @coversDefaultClass \Drupal\workspaces\WorkspaceAssociation
*
* @group workspaces
*/
class WorkspaceAssociationTest extends KernelTestBase {
use UserCreationTrait;
use WorkspaceTestTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* {@inheritdoc}
*/
protected static $modules = [
'entity_test',
'user',
'system',
'workspaces',
'workspaces_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entityTypeManager = \Drupal::entityTypeManager();
$this->installEntitySchema('entity_test_mulrevpub');
$this->installEntitySchema('entity_test_mulrevpub_string_id');
$this->installEntitySchema('user');
$this->installEntitySchema('workspace');
$this->installConfig(['system']);
$this->installSchema('workspaces', ['workspace_association']);
$permissions = array_intersect([
'administer nodes',
'create workspace',
'edit any workspace',
'view any workspace',
], array_keys($this->container->get('user.permissions')->getPermissions()));
$this->setCurrentUser($this->createUser($permissions));
$this->workspaces['stage'] = Workspace::create(['id' => 'stage', 'label' => 'Stage']);
$this->workspaces['stage']->save();
$this->workspaces['dev'] = Workspace::create(['id' => 'dev', 'parent' => 'stage', 'label' => 'Dev']);
$this->workspaces['dev']->save();
}
/**
* Tests the revisions tracked by a workspace.
*
* @param string $entity_type_id
* The ID of the entity type to test.
* @param array $entity_values
* An array of values for the entities created in this test.
*
* @covers ::getTrackedEntities
* @covers ::getAssociatedRevisions
*
* @dataProvider getEntityTypeIds
*/
public function testWorkspaceAssociation(string $entity_type_id, array $entity_values): void {
$entity_1 = $this->createEntity($entity_type_id, $entity_values[1]);
$this->createEntity($entity_type_id, $entity_values[2]);
// Edit one of the existing nodes in 'stage'.
$this->switchToWorkspace('stage');
$entity_1->set('name', 'Test entity 1 - stage - published');
$entity_1->setPublished();
// This creates rev. 3.
$entity_1->save();
// Generate content with the following structure:
// Stage:
// - Test entity 3 - stage - unpublished (rev. 4)
// - Test entity 4 - stage - published (rev. 5 and 6)
$this->createEntity($entity_type_id, $entity_values[3]);
$this->createEntity($entity_type_id, $entity_values[4]);
$expected_latest_revisions = [
'stage' => [3, 4, 6],
];
$expected_all_revisions = [
'stage' => [3, 4, 5, 6],
];
$expected_initial_revisions = [
'stage' => [4, 5],
];
$this->assertWorkspaceAssociations($entity_type_id, $expected_latest_revisions, $expected_all_revisions, $expected_initial_revisions);
// Dev:
// - Test entity 1 - stage - published (rev. 3)
// - Test entity 3 - stage - unpublished (rev. 4)
// - Test entity 4 - stage - published (rev. 5 and 6)
// - Test entity 5 - dev - unpublished (rev. 7)
// - Test entity 6 - dev - published (rev. 8 and 9)
$this->switchToWorkspace('dev');
$this->createEntity($entity_type_id, $entity_values[5]);
$this->createEntity($entity_type_id, $entity_values[6]);
$expected_latest_revisions += [
'dev' => [3, 4, 6, 7, 9],
];
// Revisions 3, 4, 5 and 6 that were created in the parent 'stage' workspace
// are also considered as being part of the child 'dev' workspace.
$expected_all_revisions += [
'dev' => [3, 4, 5, 6, 7, 8, 9],
];
$expected_initial_revisions += [
'dev' => [7, 8],
];
$this->assertWorkspaceAssociations($entity_type_id, $expected_latest_revisions, $expected_all_revisions, $expected_initial_revisions);
// Merge 'dev' into 'stage' and check the workspace associations.
/** @var \Drupal\workspaces\WorkspaceMergerInterface $workspace_merger */
$workspace_merger = \Drupal::service('workspaces.operation_factory')->getMerger($this->workspaces['dev'], $this->workspaces['stage']);
$workspace_merger->merge();
// The latest revisions from 'dev' are now tracked in 'stage'.
$expected_latest_revisions['stage'] = $expected_latest_revisions['dev'];
// Two revisions (8 and 9) were created for 'Test article 6', but only the
// latest one (9) is being merged into 'stage'.
$expected_all_revisions['stage'] = [3, 4, 5, 6, 7, 9];
// Revision 7 was both an initial and latest revision in 'dev', so it is now
// considered an initial revision in 'stage'.
$expected_initial_revisions['stage'] = [4, 5, 7];
// Which leaves revision 8 as the only remaining initial revision in 'dev'.
$expected_initial_revisions['dev'] = [8];
$this->assertWorkspaceAssociations($entity_type_id, $expected_latest_revisions, $expected_all_revisions, $expected_initial_revisions);
// Publish 'stage' and check the workspace associations.
/** @var \Drupal\workspaces\WorkspacePublisherInterface $workspace_publisher */
$workspace_publisher = \Drupal::service('workspaces.operation_factory')->getPublisher($this->workspaces['stage']);
$workspace_publisher->publish();
$expected_revisions['stage'] = $expected_revisions['dev'] = [];
$this->assertWorkspaceAssociations($entity_type_id, $expected_revisions, $expected_revisions, $expected_revisions);
}
/**
* The data provider for ::testWorkspaceAssociation().
*/
public static function getEntityTypeIds(): array {
return [
[
'entity_type_id' => 'entity_test_mulrevpub',
'entity_values' => [
1 => ['name' => 'Test entity 1 - live - unpublished', 'status' => FALSE],
2 => ['name' => 'Test entity 2 - live - published', 'status' => TRUE],
3 => ['name' => 'Test entity 3 - stage - unpublished', 'status' => FALSE],
4 => ['name' => 'Test entity 4 - stage - published', 'status' => TRUE],
5 => ['name' => 'Test entity 5 - dev - unpublished', 'status' => FALSE],
6 => ['name' => 'Test entity 6 - dev - published', 'status' => TRUE],
],
],
[
'entity_type_id' => 'entity_test_mulrevpub_string_id',
'entity_values' => [
1 => ['id' => 'test_1', 'name' => 'Test entity 1 - live - unpublished', 'status' => FALSE],
2 => ['id' => 'test_2', 'name' => 'Test entity 2 - live - published', 'status' => TRUE],
3 => ['id' => 'test_3', 'name' => 'Test entity 3 - stage - unpublished', 'status' => FALSE],
4 => ['id' => 'test_4', 'name' => 'Test entity 4 - stage - published', 'status' => TRUE],
5 => ['id' => 'test_5', 'name' => 'Test entity 5 - dev - unpublished', 'status' => FALSE],
6 => ['id' => 'test_6', 'name' => 'Test entity 6 - dev - published', 'status' => TRUE],
],
],
];
}
/**
* Tests the count of revisions returned for tracked entities listing.
*
* @covers ::getTrackedEntitiesForListing
*/
public function testWorkspaceAssociationForListing(): void {
$this->switchToWorkspace($this->workspaces['stage']->id());
$entity_type_id = 'entity_test_mulrevpub';
for ($i = 1; $i <= 51; ++$i) {
$this->createEntity($entity_type_id, ['name' => "Test entity {$i}"]);
}
/** @var \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association */
$workspace_association = \Drupal::service('workspaces.association');
// The default behavior uses a pager with 50 items per page.
$tracked_items = $workspace_association->getTrackedEntitiesForListing($this->workspaces['stage']->id());
$this->assertEquals(50, count($tracked_items[$entity_type_id]));
// Verifies that all items are returned, not broken into pages.
$tracked_items_no_pager = $workspace_association->getTrackedEntitiesForListing($this->workspaces['stage']->id(), NULL, FALSE);
$this->assertEquals(51, count($tracked_items_no_pager[$entity_type_id]));
}
/**
* Checks the workspace associations for a test scenario.
*
* @param string $entity_type_id
* The ID of the entity type that is being tested.
* @param array $expected_latest_revisions
* An array of expected values for the latest tracked revisions.
* @param array $expected_all_revisions
* An array of expected values for all the tracked revisions.
* @param array $expected_initial_revisions
* An array of expected values for the initial revisions, i.e. for the
* entities that were created in the specified workspace.
*/
protected function assertWorkspaceAssociations($entity_type_id, array $expected_latest_revisions, array $expected_all_revisions, array $expected_initial_revisions): void {
$workspace_association = \Drupal::service('workspaces.association');
foreach ($expected_latest_revisions as $workspace_id => $expected_tracked_revision_ids) {
$tracked_entities = $workspace_association->getTrackedEntities($workspace_id, $entity_type_id);
$tracked_revision_ids = $tracked_entities[$entity_type_id] ?? [];
$this->assertEquals($expected_tracked_revision_ids, array_keys($tracked_revision_ids));
}
foreach ($expected_all_revisions as $workspace_id => $expected_all_revision_ids) {
$all_associated_revisions = $workspace_association->getAssociatedRevisions($workspace_id, $entity_type_id);
$this->assertEquals($expected_all_revision_ids, array_keys($all_associated_revisions));
}
foreach ($expected_initial_revisions as $workspace_id => $expected_initial_revision_ids) {
$initial_revisions = $workspace_association->getAssociatedInitialRevisions($workspace_id, $entity_type_id);
$this->assertEquals($expected_initial_revision_ids, array_keys($initial_revisions));
}
}
}

View File

@ -0,0 +1,319 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests CRUD operations for workspaces.
*
* @group workspaces
*/
class WorkspaceCRUDTest extends KernelTestBase {
use UserCreationTrait;
use NodeCreationTrait;
use ContentTypeCreationTrait;
use WorkspaceTestTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The workspace replication manager.
*
* @var \Drupal\workspaces\WorkspaceManagerInterface
*/
protected $workspaceManager;
/**
* {@inheritdoc}
*/
protected static $modules = [
'user',
'system',
'workspaces',
'field',
'filter',
'node',
'text',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpCurrentUser();
$this->installSchema('node', ['node_access']);
$this->installEntitySchema('workspace');
$this->installSchema('workspaces', ['workspace_association']);
$this->installEntitySchema('node');
$this->installConfig(['filter', 'node', 'system']);
$this->createContentType(['type' => 'page']);
$this->entityTypeManager = \Drupal::entityTypeManager();
$this->state = \Drupal::state();
$this->workspaceManager = \Drupal::service('workspaces.manager');
}
/**
* Tests the deletion of workspaces.
*/
public function testDeletingWorkspaces(): void {
$admin = $this->createUser([
'administer nodes',
'create workspace',
'view any workspace',
'edit any workspace',
'delete any workspace',
]);
$this->setCurrentUser($admin);
/** @var \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association */
$workspace_association = \Drupal::service('workspaces.association');
// Create a workspace with a very small number of associated node revisions.
$workspace_1 = Workspace::create([
'id' => 'gibbon',
'label' => 'Gibbon',
]);
$workspace_1->save();
$this->workspaceManager->setActiveWorkspace($workspace_1);
$workspace_1_node_1 = $this->createNode(['status' => FALSE]);
$workspace_1_node_2 = $this->createNode(['status' => FALSE]);
// Check that the workspace tracks the initial revisions for both nodes.
$initial_revisions = $workspace_association->getAssociatedInitialRevisions($workspace_1->id(), 'node');
$this->assertCount(2, $initial_revisions);
for ($i = 0; $i < 4; $i++) {
$workspace_1_node_1->setNewRevision(TRUE);
$workspace_1_node_1->save();
$workspace_1_node_2->setNewRevision(TRUE);
$workspace_1_node_2->save();
}
// The workspace should now track 2 nodes.
$tracked_entities = $workspace_association->getTrackedEntities($workspace_1->id());
$this->assertCount(2, $tracked_entities['node']);
// Since all the revisions were created inside a workspace, including the
// default one, 'workspace_1' should be tracking all 10 revisions.
$associated_revisions = $workspace_association->getAssociatedRevisions($workspace_1->id(), 'node');
$this->assertCount(10, $associated_revisions);
// Check that we are allowed to delete the workspace.
$this->assertTrue($workspace_1->access('delete', $admin));
// Delete the workspace and check that all the workspace_association
// entities and all the node revisions have been deleted as well.
$workspace_1->delete();
// There are no more tracked entities in 'workspace_1'.
$tracked_entities = $workspace_association->getTrackedEntities($workspace_1->id());
$this->assertEmpty($tracked_entities);
// There are no more revisions associated with 'workspace_1'.
$associated_revisions = $workspace_association->getAssociatedRevisions($workspace_1->id(), 'node');
$this->assertCount(0, $associated_revisions);
// Create another workspace, this time with a larger number of associated
// node revisions so we can test the batch purge process.
$workspace_2 = Workspace::create([
'id' => 'baboon',
'label' => 'Baboon',
]);
$workspace_2->save();
$this->workspaceManager->setActiveWorkspace($workspace_2);
$workspace_2_node_1 = $this->createNode(['status' => FALSE]);
for ($i = 0; $i < 59; $i++) {
$workspace_2_node_1->setNewRevision(TRUE);
$workspace_2_node_1->save();
}
// Now there is one entity tracked in 'workspace_2'.
$tracked_entities = $workspace_association->getTrackedEntities($workspace_2->id());
$this->assertCount(1, $tracked_entities['node']);
// All 60 are associated with 'workspace_2'.
$associated_revisions = $workspace_association->getAssociatedRevisions($workspace_2->id(), 'node', [$workspace_2_node_1->id()]);
$this->assertCount(60, $associated_revisions);
// Delete the workspace and check that we still have 10 revision left to
// delete.
$workspace_2->delete();
$associated_revisions = $workspace_association->getAssociatedRevisions($workspace_2->id(), 'node', [$workspace_2_node_1->id()]);
$this->assertCount(10, $associated_revisions);
$workspace_deleted = \Drupal::state()->get('workspace.deleted');
$this->assertCount(1, $workspace_deleted);
// Check that we can not create another workspace with the same ID while its
// data purging is not finished.
$workspace_3 = Workspace::create([
'id' => 'baboon',
'label' => 'Baboon',
]);
$violations = $workspace_3->validate();
$this->assertCount(1, $violations);
$this->assertEquals('A workspace with this ID has been deleted but data still exists for it.', $violations[0]->getMessage());
// Running cron should delete the remaining data as well as the workspace ID
// from the "workspace.delete" state entry.
\Drupal::service('cron')->run();
// Check that the actual node revisions were deleted as well.
$node_storage = $this->entityTypeManager->getStorage('node');
$this->assertEmpty($node_storage->loadMultipleRevisions(array_keys($associated_revisions)));
// 'workspace_2 'is empty now.
$associated_revisions = $workspace_association->getAssociatedRevisions($workspace_2->id(), 'node', [$workspace_2_node_1->id()]);
$this->assertCount(0, $associated_revisions);
$tracked_entities = $workspace_association->getTrackedEntities($workspace_2->id());
$this->assertCount(0, $tracked_entities);
$workspace_deleted = \Drupal::state()->get('workspace.deleted');
$this->assertCount(0, $workspace_deleted);
// Check that the deleted workspace is no longer active.
$this->assertFalse($this->workspaceManager->hasActiveWorkspace());
}
/**
* Tests that deleting a workspace keeps its already published content.
*/
public function testDeletingPublishedWorkspace(): void {
$admin = $this->createUser([
'administer nodes',
'create workspace',
'view own workspace',
'edit own workspace',
'delete own workspace',
]);
$this->setCurrentUser($admin);
$live_workspace = Workspace::create([
'id' => 'live',
'label' => 'Live',
]);
$live_workspace->save();
$workspace = Workspace::create([
'id' => 'stage',
'label' => 'Stage',
]);
$workspace->save();
$this->workspaceManager->setActiveWorkspace($workspace);
// Create a new node in the 'stage' workspace
$node = $this->createNode(['status' => TRUE]);
// Create an additional workspace-specific revision for the node.
$node->setNewRevision(TRUE);
$node->save();
// The node should have 3 revisions now: a default and 2 pending ones.
$revisions = $this->entityTypeManager->getStorage('node')->loadMultipleRevisions([1, 2, 3]);
$this->assertCount(3, $revisions);
$this->assertTrue($revisions[1]->isDefaultRevision());
$this->assertFalse($revisions[2]->isDefaultRevision());
$this->assertFalse($revisions[3]->isDefaultRevision());
// Publish the workspace, which should mark revision 3 as the default one
// and keep revision 2 as a 'source' draft revision.
$workspace->publish();
$revisions = $this->entityTypeManager->getStorage('node')->loadMultipleRevisions([1, 2, 3]);
$this->assertFalse($revisions[1]->isDefaultRevision());
$this->assertFalse($revisions[2]->isDefaultRevision());
$this->assertTrue($revisions[3]->isDefaultRevision());
// Create two new workspace-revisions for the node.
$node->setNewRevision(TRUE);
$node->save();
$node->setNewRevision(TRUE);
$node->save();
// The node should now have 5 revisions.
$revisions = $this->entityTypeManager->getStorage('node')->loadMultipleRevisions([1, 2, 3, 4, 5]);
$this->assertFalse($revisions[1]->isDefaultRevision());
$this->assertFalse($revisions[2]->isDefaultRevision());
$this->assertTrue($revisions[3]->isDefaultRevision());
$this->assertFalse($revisions[4]->isDefaultRevision());
$this->assertFalse($revisions[5]->isDefaultRevision());
// Delete the workspace and check that only the two new pending revisions
// were deleted by the workspace purging process.
$workspace->delete();
$revisions = $this->entityTypeManager->getStorage('node')->loadMultipleRevisions([1, 2, 3, 4, 5]);
$this->assertCount(3, $revisions);
$this->assertFalse($revisions[1]->isDefaultRevision());
$this->assertFalse($revisions[2]->isDefaultRevision());
$this->assertTrue($revisions[3]->isDefaultRevision());
$this->assertFalse(isset($revisions[4]));
$this->assertFalse(isset($revisions[5]));
}
/**
* Tests that a workspace with children can not be deleted.
*/
public function testDeletingWorkspaceWithChildren(): void {
$stage = Workspace::create(['id' => 'stage', 'label' => 'Stage']);
$stage->save();
$dev = Workspace::create(['id' => 'dev', 'label' => 'Dev', 'parent' => 'stage']);
$dev->save();
// Check that a workspace which has children can not be deleted.
try {
$stage->delete();
$this->fail('The Stage workspace has children and should not be deletable.');
}
catch (EntityStorageException $e) {
$this->assertEquals('The Stage workspace can not be deleted because it has child workspaces.', $e->getMessage());
$this->assertNotNull(Workspace::load('stage'));
}
// Check that if we delete its child first, the parent workspace can also be
// deleted.
$dev->delete();
$stage->delete();
$this->assertNull(Workspace::load('dev'));
$this->assertNull(Workspace::load('stage'));
}
/**
* Tests loading the workspace tree when there are no workspaces available.
*/
public function testEmptyWorkspaceTree(): void {
$tree = \Drupal::service('workspaces.repository')->loadTree();
$this->assertSame([], $tree);
}
}

View File

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests entity translations with workspaces.
*
* @group workspaces
*/
class WorkspaceContentTranslationTest extends KernelTestBase {
use UserCreationTrait;
use WorkspaceTestTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_translation',
'entity_test',
'language',
'user',
'workspaces',
'workspaces_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entityTypeManager = \Drupal::entityTypeManager();
$this->installEntitySchema('entity_test_mulrevpub');
$this->installEntitySchema('user');
$this->installEntitySchema('workspace');
$this->installConfig(['language', 'content_translation']);
$this->installSchema('workspaces', ['workspace_association']);
$language = ConfigurableLanguage::createFromLangcode('ro');
$language->save();
$this->container->get('content_translation.manager')
->setEnabled('entity_test_mulrevpub', 'entity_test_mulrevpub', TRUE);
Workspace::create(['id' => 'stage', 'label' => 'Stage'])->save();
}
/**
* Tests translations created in a workspace.
*
* @covers \Drupal\workspaces\Hook\EntityOperations::entityTranslationInsert
*/
public function testTranslations(): void {
$storage = $this->entityTypeManager->getStorage('entity_test_mulrevpub');
// Create two untranslated nodes in Live, a published and an unpublished
// one.
$entity_published = $storage->create(['name' => 'live - 1 - published', 'status' => TRUE]);
$entity_published->save();
$entity_unpublished = $storage->create(['name' => 'live - 2 - unpublished', 'status' => FALSE]);
$entity_unpublished->save();
// Activate the Stage workspace and add translations.
$this->switchToWorkspace('stage');
// Add a translation for each entity.
$entity_published->addTranslation('ro', ['name' => 'live - 1 - published - RO']);
$entity_published->save();
// Test that the default revision translation is created in a WS.
$this->assertTrue(\Drupal::keyValue('ws_test')->get('workspace_was_active'));
$entity_unpublished->addTranslation('ro', ['name' => 'live - 2 - unpublished - RO']);
$entity_unpublished->save();
// Both 'EN' and 'RO' translations are published in Stage.
$entity_published = $storage->loadUnchanged($entity_published->id());
$this->assertTrue($entity_published->isPublished());
$this->assertEquals('live - 1 - published', $entity_published->get('name')->value);
$translation = $entity_published->getTranslation('ro');
$this->assertTrue($translation->isPublished());
$this->assertEquals('live - 1 - published - RO', $translation->get('name')->value);
// Both 'EN' and 'RO' translations are unpublished in Stage.
$entity_unpublished = $storage->loadUnchanged($entity_unpublished->id());
$this->assertFalse($entity_unpublished->isPublished());
$this->assertEquals('live - 2 - unpublished', $entity_unpublished->get('name')->value);
$translation = $entity_unpublished->getTranslation('ro');
$this->assertEquals('live - 2 - unpublished - RO', $translation->get('name')->value);
$this->assertTrue($translation->isPublished());
// Switch to Live and check the translations.
$this->switchToLive();
// The 'EN' translation is still published in Live, but the 'RO' one is
// unpublished.
$entity_published = $storage->loadUnchanged($entity_published->id());
$this->assertTrue($entity_published->isPublished());
$this->assertEquals('live - 1 - published', $entity_published->get('name')->value);
$translation = $entity_published->getTranslation('ro');
$this->assertFalse($translation->isPublished());
$this->assertEquals('live - 1 - published - RO', $translation->get('name')->value);
// Both 'EN' and 'RO' translations are unpublished in Live.
$entity_unpublished = $storage->loadUnchanged($entity_unpublished->id());
$this->assertFalse($entity_unpublished->isPublished());
$this->assertEquals('live - 2 - unpublished', $entity_unpublished->get('name')->value);
$translation = $entity_unpublished->getTranslation('ro');
$this->assertFalse($translation->isPublished());
$this->assertEquals('live - 2 - unpublished - RO', $translation->get('name')->value);
}
}

View File

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests entity deletions with workspaces.
*
* @group workspaces
*/
class WorkspaceEntityDeleteTest extends KernelTestBase {
use UserCreationTrait;
use NodeCreationTrait;
use ContentTypeCreationTrait;
use WorkspaceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'field',
'filter',
'node',
'system',
'text',
'user',
'workspaces',
];
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entityTypeManager = \Drupal::entityTypeManager();
$this->workspaceManager = \Drupal::service('workspaces.manager');
$this->installEntitySchema('node');
$this->installEntitySchema('workspace');
$this->installSchema('node', ['node_access']);
$this->installSchema('workspaces', ['workspace_association']);
$this->installConfig(['filter', 'node', 'system']);
$this->createContentType(['type' => 'page']);
$this->setUpCurrentUser([], [
'access content',
'create page content',
'edit any page content',
'delete any page content',
'create workspace',
'view any workspace',
'edit any workspace',
'delete any workspace',
]);
}
/**
* Test entity deletion in a workspace.
*/
public function testEntityDeletion(): void {
/** @var \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association */
$workspace_association = \Drupal::service('workspaces.association');
$storage = $this->entityTypeManager->getStorage('node');
$published_live = $this->createNode(['title' => 'Test 1 published - live', 'type' => 'page']);
$unpublished_live = $this->createNode(['title' => 'Test 2 unpublished - live', 'type' => 'page', 'status' => FALSE]);
// Create a published and an unpublished node in Stage.
Workspace::create(['id' => 'stage', 'label' => 'Stage'])->save();
$this->switchToWorkspace('stage');
$published_stage = $this->createNode(['title' => 'Test 3 published - stage', 'type' => 'page']);
$unpublished_stage = $this->createNode([
'title' => 'Test 4 unpublished - stage',
'type' => 'page',
'status' => FALSE,
]);
$this->assertEquals(['node' => [4 => 3, 5 => 4]], $workspace_association->getTrackedEntities('stage', 'node'));
$this->assertTrue($published_stage->access('delete'));
$this->assertTrue($unpublished_stage->access('delete'));
// While the Stage workspace is active, check that the nodes created in
// Stage can be deleted, while the ones created in Live can not be deleted.
$published_stage->delete();
$this->assertEquals(['node' => [5 => 4]], $workspace_association->getTrackedEntities('stage', 'node'));
$unpublished_stage->delete();
$this->assertEmpty($workspace_association->getTrackedEntities('stage', 'node'));
$this->assertEmpty($storage->loadMultiple([$published_stage->id(), $unpublished_stage->id()]));
$this->expectExceptionMessage('This content item can only be deleted in the Live workspace.');
$this->assertFalse($published_live->access('delete'));
$this->assertFalse($unpublished_live->access('delete'));
$published_live->delete();
$unpublished_live->delete();
$this->assertNotEmpty($storage->loadMultiple([$published_live->id(), $unpublished_live->id()]));
}
}

View File

@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests the entity repository integration for workspaces.
*
* @group workspaces
*/
class WorkspaceEntityRepositoryTest extends KernelTestBase {
use UserCreationTrait;
use WorkspaceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'entity_test',
'language',
'system',
'user',
'workspaces',
];
/**
* The entity type manager.
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The entity repository.
*/
protected EntityRepositoryInterface $entityRepository;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entityTypeManager = $this->container->get('entity_type.manager');
$this->entityRepository = $this->container->get('entity.repository');
$this->installEntitySchema('entity_test_revpub');
$this->installEntitySchema('entity_test_mulrevpub');
$this->installEntitySchema('workspace');
$this->installSchema('workspaces', ['workspace_association']);
$this->installConfig(['system', 'language']);
ConfigurableLanguage::createFromLangcode('ro')
->setWeight(1)
->save();
Workspace::create(['id' => 'ham', 'label' => 'Ham'])->save();
Workspace::create(['id' => 'cheese', 'label' => 'Cheese'])->save();
}
/**
* Tests retrieving active variants in a workspace.
*
* @covers \Drupal\Core\Entity\EntityRepository::getActive
* @covers \Drupal\Core\Entity\EntityRepository::getActiveMultiple
*/
public function testGetActive(): void {
$en_contexts = ['langcode' => 'en'];
$ro_contexts = ['langcode' => 'ro'];
// Check that the correct active variant is returned for a non-translatable
// revisionable entity.
$entity_type_id = 'entity_test_revpub';
$storage = $this->entityTypeManager->getStorage($entity_type_id);
$values = ['name' => $this->randomString()];
$entity = $storage->create($values);
$storage->save($entity);
// Create revisions in two workspaces, then another one in Live.
$this->switchToWorkspace('ham');
$ham_revision = $storage->createRevision($entity);
$storage->save($ham_revision);
$active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $en_contexts);
$this->assertSame($ham_revision->getLoadedRevisionId(), $active->getLoadedRevisionId());
// Check that the active variant in Live is still the default revision.
$this->switchToLive();
$active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $en_contexts);
$this->assertSame($entity->getLoadedRevisionId(), $active->getLoadedRevisionId());
$this->switchToWorkspace('cheese');
$cheese_revision = $storage->createRevision($entity);
$storage->save($cheese_revision);
$active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $en_contexts);
$this->assertSame($cheese_revision->getLoadedRevisionId(), $active->getLoadedRevisionId());
$this->switchToLive();
$live_revision = $storage->createRevision($entity);
$storage->save($live_revision);
$active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $en_contexts);
$this->assertSame($live_revision->getLoadedRevisionId(), $active->getLoadedRevisionId());
// Switch back to the two workspaces and check that workspace-specific
// revision are returned even when there's a newer revision in Live.
$this->switchToWorkspace('ham');
$active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $en_contexts);
$this->assertSame($ham_revision->getLoadedRevisionId(), $active->getLoadedRevisionId());
$this->switchToWorkspace('cheese');
$active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $en_contexts);
$this->assertSame($cheese_revision->getLoadedRevisionId(), $active->getLoadedRevisionId());
// Check that a revision created in a workspace does not leak into other
// workspaces.
$entity_2 = $storage->create(['name' => $this->randomString()]);
$storage->save($entity_2);
// Create a new revision in a workspace.
$this->switchToWorkspace('ham');
$ham_revision = $storage->createRevision($entity_2);
$storage->save($ham_revision);
$active = $this->entityRepository->getActive($entity_type_id, $entity_2->id(), $en_contexts);
$this->assertSame($ham_revision->getLoadedRevisionId(), $active->getLoadedRevisionId());
// Check that the default revision is returned in another workspace.
$this->switchToWorkspace('cheese');
$active = $this->entityRepository->getActive($entity_type_id, $entity_2->id(), $en_contexts);
$this->assertSame($entity_2->getLoadedRevisionId(), $active->getLoadedRevisionId());
// Check that the correct active variant is returned for a translatable and
// revisionable entity.
$this->switchToLive();
$entity_type_id = 'entity_test_mulrevpub';
$storage = $this->entityTypeManager->getStorage($entity_type_id);
$values = ['name' => $this->randomString()];
$initial_revision = $storage->create($values);
$storage->save($initial_revision);
$revision_translation = $initial_revision->addTranslation('ro', $values);
$revision_translation = $storage->createRevision($revision_translation);
$storage->save($revision_translation);
// Add a translation in a workspace.
$this->switchToWorkspace('ham');
$ham_revision_ro = $storage->createRevision($revision_translation);
$storage->save($ham_revision_ro);
$active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $ro_contexts);
$this->assertSame($ham_revision_ro->getLoadedRevisionId(), $active->getLoadedRevisionId());
$this->assertSame($ham_revision_ro->language()->getId(), $active->language()->getId());
// Add a new translation in another workspace.
$this->switchToWorkspace('cheese');
$cheese_revision_ro = $storage->createRevision($revision_translation);
$storage->save($cheese_revision_ro);
$active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $ro_contexts);
$this->assertSame($cheese_revision_ro->getLoadedRevisionId(), $active->getLoadedRevisionId());
$this->assertSame($cheese_revision_ro->language()->getId(), $active->language()->getId());
// Add a new translations in Live.
$this->switchToLive();
$live_revision_ro = $storage->createRevision($revision_translation);
$storage->save($live_revision_ro);
$active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $ro_contexts);
$this->assertSame($live_revision_ro->getLoadedRevisionId(), $active->getLoadedRevisionId());
$this->assertSame($live_revision_ro->language()->getId(), $active->language()->getId());
$this->switchToWorkspace('ham');
$active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $ro_contexts);
$this->assertSame($ham_revision_ro->getLoadedRevisionId(), $active->getLoadedRevisionId());
$this->assertSame($ham_revision_ro->language()->getId(), $active->language()->getId());
$this->switchToWorkspace('cheese');
$active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $ro_contexts);
$this->assertSame($cheese_revision_ro->getLoadedRevisionId(), $active->getLoadedRevisionId());
$this->assertSame($cheese_revision_ro->language()->getId(), $active->language()->getId());
}
}

View File

@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormState;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\workspaces\Entity\Workspace;
use Drupal\workspaces_test\Form\ActiveWorkspaceTestForm;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests form persistence for the active workspace.
*
* @group workspaces
*/
class WorkspaceFormPersistenceTest extends KernelTestBase {
use UserCreationTrait;
use WorkspaceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'workspaces',
'workspaces_test',
];
/**
* The entity type manager.
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The form builder.
*/
protected FormBuilderInterface $formBuilder;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entityTypeManager = \Drupal::entityTypeManager();
$this->formBuilder = \Drupal::formBuilder();
$this->installEntitySchema('user');
$this->installEntitySchema('workspace');
Workspace::create(['id' => 'ham', 'label' => 'Ham'])->save();
Workspace::create(['id' => 'cheese', 'label' => 'Cheese'])->save();
$this->setCurrentUser($this->createUser([
'view any workspace',
]));
}
/**
* Tests that the active workspace is persisted throughout a form's lifecycle.
*/
public function testFormPersistence(): void {
$form_arg = ActiveWorkspaceTestForm::class;
$this->switchToWorkspace('ham');
$form_state_1 = new FormState();
$form_1 = $this->formBuilder->buildForm($form_arg, $form_state_1);
$this->switchToWorkspace('cheese');
$form_state_2 = new FormState();
$this->formBuilder->buildForm($form_arg, $form_state_2);
// Submit the second form and check the workspace in which it was submitted.
$this->formBuilder->submitForm($form_arg, $form_state_2);
$this->assertSame('cheese', $this->keyValue->get('ws_test')->get('form_test_active_workspace'));
// Submit the first form and check the workspace in which it was submitted.
$this->formBuilder->submitForm($form_arg, $form_state_1);
$this->assertSame('ham', $this->keyValue->get('ws_test')->get('form_test_active_workspace'));
// Reset the workspace manager service to simulate a new request and check
// that the second workspace is still active.
\Drupal::getContainer()->set('workspaces.manager', NULL);
$this->assertSame('cheese', \Drupal::service('workspaces.manager')->getActiveWorkspace()->id());
// Reset the workspace manager service again to prepare for a new request.
\Drupal::getContainer()->set('workspaces.manager', NULL);
$request = Request::create(
$form_1['test']['#ajax']['url']->toString(),
'POST',
[
MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax',
] + $form_1['test']['#attached']['drupalSettings']['ajax'][$form_1['test']['#id']]['submit'],
);
\Drupal::service('http_kernel')->handle($request);
$form_state_1->setTriggeringElement($form_1['test']);
\Drupal::service('form_ajax_response_builder')->buildResponse($request, $form_1, $form_state_1, []);
// Check that the AJAX callback is executed in the initial workspace of its
// parent form.
$this->assertSame('ham', $this->keyValue->get('ws_test')->get('ajax_test_active_workspace'));
// Reset the workspace manager service again and check that the AJAX request
// didn't change the persisted workspace.
\Drupal::getContainer()->set('workspaces.manager', NULL);
\Drupal::requestStack()->pop();
$this->assertSame('cheese', \Drupal::service('workspaces.manager')->getActiveWorkspace()->id());
}
}

View File

@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\workspaces\Entity\Handler\IgnoredWorkspaceHandler;
use Drupal\workspaces\Entity\Workspace;
use Drupal\workspaces_test\EntityTestRevPubWorkspaceHandler;
/**
* Tests the workspace information service.
*
* @coversDefaultClass \Drupal\workspaces\WorkspaceInformation
*
* @group workspaces
*/
class WorkspaceInformationTest extends KernelTestBase {
use UserCreationTrait;
use WorkspaceTestTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The workspace information service.
*
* @var \Drupal\wse\Core\WorkspaceInformationInterface
*/
protected $workspaceInformation;
/**
* The state store.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* {@inheritdoc}
*/
protected static $modules = [
'entity_test',
'user',
'workspaces',
'workspaces_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entityTypeManager = \Drupal::entityTypeManager();
$this->workspaceInformation = \Drupal::service('workspaces.information');
$this->state = \Drupal::state();
$this->installEntitySchema('entity_test');
$this->installEntitySchema('entity_test_rev');
$this->installEntitySchema('entity_test_revpub');
$this->installEntitySchema('workspace');
$this->installSchema('workspaces', ['workspace_association']);
// Create a new workspace and activate it.
Workspace::create(['id' => 'stage', 'label' => 'Stage'])->save();
$this->switchToWorkspace('stage');
}
/**
* Tests fully supported entity types.
*/
public function testSupportedEntityTypes(): void {
// Check a supported entity type.
$entity = $this->entityTypeManager->getStorage('entity_test_revpub')->create();
$this->assertTrue($this->workspaceInformation->isEntitySupported($entity));
$this->assertTrue($this->workspaceInformation->isEntityTypeSupported($entity->getEntityType()));
$this->assertFalse($this->workspaceInformation->isEntityIgnored($entity));
$this->assertFalse($this->workspaceInformation->isEntityTypeIgnored($entity->getEntityType()));
// Check that supported entity types are tracked in a workspace. This entity
// is published by default, so the second revision will be tracked.
$entity->save();
$this->assertWorkspaceAssociation(['stage' => [2]], 'entity_test_revpub');
}
/**
* Tests an entity type with a custom workspace handler.
*/
public function testCustomSupportEntityTypes(): void {
$entity_type = clone $this->entityTypeManager->getDefinition('entity_test_revpub');
$entity_type->setHandlerClass('workspace', EntityTestRevPubWorkspaceHandler::class);
$this->state->set('entity_test_revpub.entity_type', $entity_type);
$this->entityTypeManager->clearCachedDefinitions();
$entity = $this->entityTypeManager->getStorage('entity_test_revpub')->create([
'type' => 'supported_bundle',
]);
$this->assertTrue($this->workspaceInformation->isEntitySupported($entity));
$this->assertTrue($this->workspaceInformation->isEntityTypeSupported($entity->getEntityType()));
$this->assertFalse($this->workspaceInformation->isEntityIgnored($entity));
$this->assertFalse($this->workspaceInformation->isEntityTypeIgnored($entity->getEntityType()));
// Check that supported entity types are tracked in a workspace. This entity
// is published by default, so the second revision will be tracked.
$entity->save();
$this->assertWorkspaceAssociation(['stage' => [2]], 'entity_test_revpub');
$entity = $this->entityTypeManager->getStorage('entity_test_revpub')->create([
'type' => 'ignored_bundle',
]);
$this->assertFalse($this->workspaceInformation->isEntitySupported($entity));
$this->assertTrue($this->workspaceInformation->isEntityTypeSupported($entity->getEntityType()));
$this->assertTrue($this->workspaceInformation->isEntityIgnored($entity));
$this->assertFalse($this->workspaceInformation->isEntityTypeIgnored($entity->getEntityType()));
// Check that an ignored entity can be saved, but won't be tracked.
$entity->save();
$this->assertWorkspaceAssociation(['stage' => [2]], 'entity_test_revpub');
}
/**
* Tests ignored entity types.
*/
public function testIgnoredEntityTypes(): void {
$entity_type = clone $this->entityTypeManager->getDefinition('entity_test_rev');
$entity_type->setHandlerClass('workspace', IgnoredWorkspaceHandler::class);
$this->state->set('entity_test_rev.entity_type', $entity_type);
$this->entityTypeManager->clearCachedDefinitions();
// Check an ignored entity type. CRUD operations for an ignored entity type
// are allowed in a workspace, but their revisions are not tracked.
$entity = $this->entityTypeManager->getStorage('entity_test_rev')->create();
$this->assertTrue($this->workspaceInformation->isEntityIgnored($entity));
$this->assertTrue($this->workspaceInformation->isEntityTypeIgnored($entity->getEntityType()));
$this->assertFalse($this->workspaceInformation->isEntitySupported($entity));
$this->assertFalse($this->workspaceInformation->isEntityTypeSupported($entity->getEntityType()));
// Check that ignored entity types are not tracked in a workspace.
$entity->save();
$this->assertWorkspaceAssociation(['stage' => []], 'entity_test_rev');
}
/**
* Tests unsupported entity types.
*/
public function testUnsupportedEntityTypes(): void {
// Check an unsupported entity type.
$entity_test = $this->entityTypeManager->getDefinition('entity_test');
$this->assertFalse($entity_test->hasHandlerClass('workspace'));
$entity = $this->entityTypeManager->getStorage('entity_test')->create();
$this->assertFalse($this->workspaceInformation->isEntitySupported($entity));
$this->assertFalse($this->workspaceInformation->isEntityTypeSupported($entity_test));
$this->assertFalse($this->workspaceInformation->isEntityIgnored($entity));
$this->assertFalse($this->workspaceInformation->isEntityTypeIgnored($entity_test));
// Check that unsupported entity types can not be saved in a workspace.
$this->expectException(EntityStorageException::class);
$this->expectExceptionMessage('The "entity_test" entity type can only be saved in the default workspace.');
$entity->save();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,214 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;
use Drupal\Tests\user\Traits\UserCreationTrait;
// cspell:ignore differring
/**
* Tests workspace merging.
*
* @coversDefaultClass \Drupal\workspaces\WorkspaceMerger
*
* @group workspaces
*/
class WorkspaceMergerTest extends KernelTestBase {
use ContentTypeCreationTrait;
use NodeCreationTrait;
use UserCreationTrait;
use WorkspaceTestTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* An array of nodes created before installing the Workspaces module.
*
* @var \Drupal\node\NodeInterface[]
*/
protected $nodes = [];
/**
* {@inheritdoc}
*/
protected static $modules = [
'field',
'filter',
'node',
'text',
'user',
'system',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entityTypeManager = \Drupal::entityTypeManager();
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installConfig(['filter', 'node', 'system']);
$this->installSchema('node', ['node_access']);
$this->createContentType(['type' => 'article']);
$this->setCurrentUser($this->createUser(['administer nodes']));
}
/**
* Tests workspace merging.
*
* @covers ::merge
* @covers ::getNumberOfChangesOnSource
* @covers ::getNumberOfChangesOnTarget
* @covers ::getDifferringRevisionIdsOnSource
* @covers ::getDifferringRevisionIdsOnTarget
*/
public function testWorkspaceMerger(): void {
$this->initializeWorkspacesModule();
$this->createWorkspaceHierarchy();
// Generate content in the workspace hierarchy with the following structure:
// Live:
// - Test article 1 - live
//
// Stage:
// - Test article 2 - stage
//
// Dev:
// - Test article 2 - stage
// - Test article 3 - dev
//
// Local 1:
// - Test article 2 - stage
// - Test article 3 - dev
// - Test article 4 - local_1
//
// Local 2:
// - Test article 2 - stage
// - Test article 3 - dev
//
// Note that the contents of each workspace are inherited automatically in
// each of its descendants.
$this->createNode(['title' => 'Test article 1 - live', 'type' => 'article']);
// This creates revisions 2 and 3. Revision 2 is an unpublished default
// revision (which is also available in Live), and revision 3 is a published
// pending revision that is available in Stage and all its descendants.
$this->switchToWorkspace('stage');
$this->createNode(['title' => 'Test article 2 - stage', 'type' => 'article']);
$expected_workspace_association = [
'stage' => [3],
'dev' => [3],
'local_1' => [3],
'local_2' => [3],
'qa' => [],
];
$this->assertWorkspaceAssociation($expected_workspace_association, 'node');
// Create the second test article in Dev. This creates revisions 4 and 5.
// Revision 4 is default and unpublished, and revision 5 is now being
// tracked in Dev and its descendants.
$this->switchToWorkspace('dev');
$this->createNode(['title' => 'Test article 3 - dev', 'type' => 'article']);
$expected_workspace_association = [
'stage' => [3],
'dev' => [3, 5],
'local_1' => [3, 5],
'local_2' => [3, 5],
'qa' => [],
];
$this->assertWorkspaceAssociation($expected_workspace_association, 'node');
// Create the third article in Local 1. This creates revisions 6 and 7.
// Revision 6 is default and unpublished, and revision 7 is now being
// tracked in the Local 1.
$this->switchToWorkspace('local_1');
$this->createNode(['title' => 'Test article 4 - local_1', 'type' => 'article']);
$expected_workspace_association = [
'stage' => [3],
'dev' => [3, 5],
'local_1' => [3, 5, 7],
'local_2' => [3, 5],
'qa' => [],
];
$this->assertWorkspaceAssociation($expected_workspace_association, 'node');
/** @var \Drupal\workspaces\WorkspaceMergerInterface $workspace_merger */
$workspace_merger = \Drupal::service('workspaces.operation_factory')->getMerger($this->workspaces['local_1'], $this->workspaces['dev']);
// Check that there is no content in Dev that's not also in Local 1.
$this->assertEmpty($workspace_merger->getDifferringRevisionIdsOnTarget());
$this->assertEquals(0, $workspace_merger->getNumberOfChangesOnTarget());
// Check that there is only one node in Local 1 that's not available in Dev,
// revision 7 created above for the fourth test article.
$expected = [
'node' => [7 => 4],
];
$this->assertEquals($expected, $workspace_merger->getDifferringRevisionIdsOnSource());
$this->assertEquals(1, $workspace_merger->getNumberOfChangesOnSource());
// Merge the contents of Local 1 into Dev, and check that Dev, Local 1 and
// Local 2 have the same content.
$workspace_merger->merge();
$this->assertEmpty($workspace_merger->getDifferringRevisionIdsOnTarget());
$this->assertEquals(0, $workspace_merger->getNumberOfChangesOnTarget());
$this->assertEmpty($workspace_merger->getDifferringRevisionIdsOnSource());
$this->assertEquals(0, $workspace_merger->getNumberOfChangesOnSource());
$this->switchToWorkspace('dev');
$expected_workspace_association = [
'stage' => [3],
'dev' => [3, 5, 7],
'local_1' => [3, 5, 7],
'local_2' => [3, 5, 7],
'qa' => [],
];
$this->assertWorkspaceAssociation($expected_workspace_association, 'node');
$workspace_merger = \Drupal::service('workspaces.operation_factory')->getMerger($this->workspaces['local_1'], $this->workspaces['stage']);
// Check that there is no content in Stage that's not also in Local 1.
$this->assertEmpty($workspace_merger->getDifferringRevisionIdsOnTarget());
$this->assertEquals(0, $workspace_merger->getNumberOfChangesOnTarget());
// Check that the difference between Local 1 and Stage are the two revisions
// for 'Test article 3 - dev' and 'Test article 4 - local_1'.
$expected = [
'node' => [
5 => 3,
7 => 4,
],
];
$this->assertEquals($expected, $workspace_merger->getDifferringRevisionIdsOnSource());
$this->assertEquals(2, $workspace_merger->getNumberOfChangesOnSource());
// Check that Local 1 can not be merged directly into Stage, since it can
// only be merged into its direct parent.
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The contents of a workspace can only be merged into its parent workspace.');
$workspace_merger->merge();
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\workspaces\Entity\Workspace;
use Drupal\workspaces\Form\WorkspacePublishForm;
use Drupal\workspaces\WorkspaceOperationFactory;
use Drupal\workspaces\WorkspacePublisherInterface;
use Psr\Log\LoggerInterface;
/**
* @coversDefaultClass \Drupal\workspaces\Form\WorkspacePublishForm
* @group workspaces
*/
class WorkspacePublishFormTest extends KernelTestBase {
/**
* @covers ::submitForm
*/
public function testSubmitFormWithException(): void {
/** @var \Drupal\Core\Messenger\MessengerInterface $messenger */
$messenger = \Drupal::service('messenger');
$workspaceOperationFactory = $this->createMock(WorkspaceOperationFactory::class);
$entityTypeManager = $this->createMock(EntityTypeManagerInterface::class);
$logger = $this->createMock(LoggerInterface::class);
/** @var \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory */
$loggerFactory = \Drupal::service('logger.factory');
$loggerFactory->addLogger($logger);
$workspace = $this->createMock(Workspace::class);
$workspacePublisher = $this->createMock(WorkspacePublisherInterface::class);
$workspace
->expects($this->any())
->method('label');
$workspace
->expects($this->once())
->method('publish')
->willThrowException(new \Exception('Unexpected error'));
$workspaceOperationFactory
->expects($this->once())
->method('getPublisher')
->willReturn($workspacePublisher);
$workspacePublisher
->expects($this->once())
->method('getTargetLabel');
$publishForm = new WorkspacePublishForm(
$workspaceOperationFactory,
$entityTypeManager
);
$form = [];
$formState = new FormState();
$publishForm->buildForm($form, $formState, $workspace);
$logger
->expects($this->once())
->method('log')
->with(RfcLogLevel::ERROR, 'Unexpected error');
$publishForm->submitForm($form, $formState);
$messages = $messenger->messagesByType(MessengerInterface::TYPE_ERROR);
$this->assertCount(1, $messages);
$this->assertEquals('Publication failed. All errors have been logged.', $messages[0]);
}
}

View File

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\Component\Utility\Crypt;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests the query parameter workspace negotiator.
*
* @coversDefaultClass \Drupal\workspaces\Negotiator\QueryParameterWorkspaceNegotiator
* @group workspaces
*/
class WorkspaceQueryParameterNegotiatorTest extends KernelTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'user',
'system',
'workspaces',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('workspace');
$this->installSchema('workspaces', ['workspace_association']);
// Create a new workspace for testing.
Workspace::create(['id' => 'stage', 'label' => 'Stage'])->save();
$this->setCurrentUser($this->createUser(['administer workspaces']));
// Reset the internal state of the workspace manager so that checking for an
// active workspace in the test is not influenced by previous actions.
\Drupal::getContainer()->set('workspaces.manager', NULL);
}
/**
* @covers ::getActiveWorkspaceId
* @dataProvider providerTestWorkspaceQueryParameter
*/
public function testWorkspaceQueryParameter(?string $workspace, ?string $token, ?string $negotiated_workspace, bool $has_active_workspace): void {
// We can't access the settings service in the data provider method, so we
// generate a good token here.
if ($token === 'good_token') {
$hash_salt = $this->container->get('settings')->get('hash_salt');
$token = substr(Crypt::hmacBase64($workspace, $hash_salt), 0, 8);
}
$request = \Drupal::request();
$request->query->set('workspace', $workspace);
$request->query->set('token', $token);
/** @var \Drupal\workspaces\Negotiator\QueryParameterWorkspaceNegotiator $negotiator */
$negotiator = $this->container->get('workspaces.negotiator.query_parameter');
$this->assertSame($negotiated_workspace, $negotiator->getActiveWorkspaceId($request));
$this->assertSame($has_active_workspace, \Drupal::service('workspaces.manager')->hasActiveWorkspace());
}
/**
* Data provider for testWorkspaceQueryParameter.
*/
public static function providerTestWorkspaceQueryParameter(): array {
return [
'no workspace, no token' => [
'workspace' => NULL,
'token' => NULL,
'negotiated_workspace' => NULL,
'has_active_workspace' => FALSE,
],
'fake workspace, no token' => [
'workspace' => 'fake_id',
'token' => NULL,
'negotiated_workspace' => NULL,
'has_active_workspace' => FALSE,
],
'fake workspace, fake token' => [
'workspace' => 'fake_id',
'token' => 'fake_token',
'negotiated_workspace' => NULL,
'has_active_workspace' => FALSE,
],
'good workspace, fake token' => [
'workspace' => 'stage',
'token' => 'fake_token',
'negotiated_workspace' => NULL,
'has_active_workspace' => FALSE,
],
// The fake workspace will be accepted by the negotiator in this case, but
// the workspace manager will try to load and check access for it, and
// won't set it as the active workspace. Note that "fake" can also mean a
// workspace that existed at some point, then it was deleted and the user
// is just accessing a stale link.
'fake workspace, good token' => [
'workspace' => 'fake_id',
'token' => 'good_token',
'negotiated_workspace' => 'fake_id',
'has_active_workspace' => FALSE,
],
'good workspace, good token' => [
'workspace' => 'stage',
'token' => 'good_token',
'negotiated_workspace' => 'stage',
'has_active_workspace' => TRUE,
],
];
}
}

View File

@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\Core\Entity\EntityInterface;
use Drupal\workspaces\Entity\Handler\IgnoredWorkspaceHandler;
use Drupal\workspaces\Entity\Workspace;
/**
* A trait with common workspaces testing functionality.
*/
trait WorkspaceTestTrait {
/**
* The workspaces manager.
*
* @var \Drupal\workspaces\WorkspaceManagerInterface
*/
protected $workspaceManager;
/**
* An array of test workspaces, keyed by workspace ID.
*
* @var \Drupal\workspaces\WorkspaceInterface[]
*/
protected $workspaces = [];
/**
* Enables the Workspaces module and creates two workspaces.
*/
protected function initializeWorkspacesModule() {
// Enable the Workspaces module here instead of the static::$modules array
// so we can test it with default content.
$this->enableModules(['workspaces']);
$this->container = \Drupal::getContainer();
$this->entityTypeManager = \Drupal::entityTypeManager();
$this->workspaceManager = \Drupal::service('workspaces.manager');
$this->installEntitySchema('workspace');
$this->installSchema('workspaces', ['workspace_association']);
// Install the entity schema for supported entity types to ensure that the
// 'workspace' revision metadata field gets created.
foreach (array_keys(\Drupal::service('workspaces.information')->getSupportedEntityTypes()) as $entity_type_id) {
$this->installEntitySchema($entity_type_id);
}
// Create two workspaces by default, 'live' and 'stage'.
$this->workspaces['live'] = Workspace::create(['id' => 'live', 'label' => 'Live']);
$this->workspaces['live']->save();
$this->workspaces['stage'] = Workspace::create(['id' => 'stage', 'label' => 'Stage']);
$this->workspaces['stage']->save();
$permissions = array_intersect([
'administer nodes',
'create workspace',
'edit any workspace',
'view any workspace',
], array_keys($this->container->get('user.permissions')->getPermissions()));
$this->setCurrentUser($this->createUser($permissions));
}
/**
* Sets a given workspace as active.
*
* @param string $workspace_id
* The ID of the workspace to switch to.
*/
protected function switchToWorkspace($workspace_id) {
// Switch the test runner's context to the specified workspace.
$workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
\Drupal::service('workspaces.manager')->setActiveWorkspace($workspace);
}
/**
* Switches the test runner's context to Live.
*/
protected function switchToLive(): void {
\Drupal::service('workspaces.manager')->switchToLive();
}
/**
* Creates a test workspace hierarchy.
*
* The full hierarchy including the default workspaces 'live' and 'stage' is:
*
* live
* - stage
* - dev
* - local_1
* - local_2
* - qa
*/
protected function createWorkspaceHierarchy() {
$this->workspaces['dev'] = Workspace::create(['id' => 'dev', 'parent' => 'stage', 'label' => 'dev']);
$this->workspaces['dev']->save();
$this->workspaces['local_1'] = Workspace::create(['id' => 'local_1', 'parent' => 'dev', 'label' => 'local_1']);
$this->workspaces['local_1']->save();
$this->workspaces['local_2'] = Workspace::create(['id' => 'local_2', 'parent' => 'dev', 'label' => 'local_2']);
$this->workspaces['local_2']->save();
$this->workspaces['qa'] = Workspace::create(['id' => 'qa', 'parent' => 'live', 'label' => 'qa']);
$this->workspaces['qa']->save();
}
/**
* Checks the workspace_association records for a test scenario.
*
* @param array $expected
* An array of expected values, as defined in ::testWorkspaces().
* @param string $entity_type_id
* The ID of the entity type that is being tested.
*/
protected function assertWorkspaceAssociation(array $expected, $entity_type_id) {
/** @var \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association */
$workspace_association = \Drupal::service('workspaces.association');
foreach ($expected as $workspace_id => $expected_tracked_revision_ids) {
$tracked_entities = $workspace_association->getTrackedEntities($workspace_id, $entity_type_id);
$tracked_revision_ids = $tracked_entities[$entity_type_id] ?? [];
$this->assertEquals($expected_tracked_revision_ids, array_keys($tracked_revision_ids));
}
}
/**
* Returns all the revisions which are not associated with any workspace.
*
* @param string $entity_type_id
* An entity type ID to find revisions for.
* @param int[]|string[]|null $entity_ids
* (optional) An array of entity IDs to filter the results by. Defaults to
* NULL.
*
* @return array
* An array of entity IDs, keyed by revision IDs.
*/
protected function getUnassociatedRevisions($entity_type_id, $entity_ids = NULL) {
$entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
$query = \Drupal::entityTypeManager()
->getStorage($entity_type_id)
->getQuery()
->allRevisions()
->accessCheck(FALSE)
->notExists($entity_type->get('revision_metadata_keys')['workspace']);
if ($entity_ids) {
$query->condition($entity_type->getKey('id'), $entity_ids, 'IN');
}
return $query->execute();
}
/**
* Marks an entity type as ignored in a workspace.
*
* @param string $entity_type_id
* The entity type ID.
*/
protected function ignoreEntityType(string $entity_type_id): void {
$entity_type = clone \Drupal::entityTypeManager()->getDefinition($entity_type_id);
$entity_type->setHandlerClass('workspace', IgnoredWorkspaceHandler::class);
\Drupal::state()->set("$entity_type_id.entity_type", $entity_type);
\Drupal::entityTypeManager()->clearCachedDefinitions();
}
/**
* Creates an entity.
*
* @param string $entity_type_id
* The entity type ID.
* @param array $values
* An array of values for the entity.
*
* @return \Drupal\Core\Entity\EntityInterface
* The created entity.
*/
protected function createEntity(string $entity_type_id, array $values = []): EntityInterface {
$storage = \Drupal::entityTypeManager()->getStorage($entity_type_id);
$entity = $storage->create($values);
$entity->save();
return $entity;
}
}

View File

@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormState;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
use Drupal\views\Views;
use Drupal\views_ui\ViewUI;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests the views integration for workspaces.
*
* @group views
* @group workspaces
*/
class WorkspaceViewsIntegrationTest extends ViewsKernelTestBase {
use ContentTypeCreationTrait;
use EntityReferenceFieldCreationTrait;
use NodeCreationTrait;
use UserCreationTrait;
use WorkspaceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_translation',
'entity_test',
'field',
'filter',
'node',
'language',
'text',
'views_ui',
'workspaces',
];
/**
* The entity type manager.
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* Creation timestamp that should be incremented for each new entity.
*/
protected int $createdTimestamp = 0;
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE): void {
parent::setUp(FALSE);
$this->entityTypeManager = \Drupal::entityTypeManager();
$this->installEntitySchema('entity_test_mulrevpub');
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installEntitySchema('workspace');
$this->installConfig(['filter', 'node', 'system', 'language', 'content_translation']);
$this->installSchema('node', ['node_access']);
$this->installSchema('workspaces', ['workspace_association']);
$language = ConfigurableLanguage::createFromLangcode('ro');
$language->save();
$this->createContentType(['type' => 'page']);
$this->container->get('content_translation.manager')->setEnabled('node', 'page', TRUE);
// Create an entity reference field, in order to test relationship queries.
FieldStorageConfig::create([
'entity_type' => 'node',
'type' => 'entity_reference',
'field_name' => 'field_reference',
'settings' => [
'target_type' => 'entity_test_mulrevpub',
],
])->save();
FieldConfig::create([
'entity_type' => 'node',
'bundle' => 'page',
'field_name' => 'field_reference',
])->save();
}
/**
* Tests workspace query alter for views.
*
* @covers \Drupal\workspaces\Hook\ViewsOperations::alterQueryForEntityType
* @covers \Drupal\workspaces\Hook\ViewsOperations::getRevisionTableJoin
*/
public function testViewsQueryAlter(): void {
// Create a test entity and two nodes.
$test_entity = \Drupal::entityTypeManager()
->getStorage('entity_test_mulrevpub')
->create(['name' => 'test entity - live']);
$test_entity->save();
$node_1 = $this->createNode([
'title' => 'node - live - 1',
'body' => 'node 1',
'created' => $this->createdTimestamp++,
'field_reference' => $test_entity->id(),
]);
$node_2 = $this->createNode([
'title' => 'node - live - 2',
'body' => 'node 2',
'created' => $this->createdTimestamp++,
]);
// Create a new workspace and activate it.
Workspace::create(['id' => 'stage', 'label' => 'Stage'])->save();
$this->switchToWorkspace('stage');
$view = Views::getView('frontpage');
// Add a filter on a field that is stored in a dedicated table in order to
// test field joins with extra conditions (e.g. 'deleted' and 'langcode').
$view->setDisplay('page_1');
$filters = $view->displayHandlers->get('page_1')->getOption('filters');
$view->displayHandlers->get('page_1')->overrideOption('filters', $filters + [
'body_value' => [
'id' => 'body_value',
'table' => 'node__body',
'field' => 'body_value',
'operator' => 'not empty',
'plugin_id' => 'string',
],
]);
$view->execute();
$expected = [
['nid' => $node_2->id()],
['nid' => $node_1->id()],
];
$this->assertIdenticalResultset($view, $expected, ['nid' => 'nid']);
// Add a filter on a field from a relationship, in order to test field
// joins with extra conditions (e.g. 'deleted' and 'langcode').
$view->destroy();
$view->setDisplay('page_1');
$view->displayHandlers->get('page_1')->overrideOption('relationships', [
'field_reference' => [
'id' => 'field_reference',
'table' => 'node__field_reference',
'field' => 'field_reference',
'required' => FALSE,
],
]);
$view->displayHandlers->get('page_1')->overrideOption('filters', $filters + [
'name' => [
'id' => 'name',
'table' => 'entity_test_mulrevpub_property_data',
'field' => 'name',
'operator' => 'not empty',
'relationship' => 'field_reference',
],
]);
$view->execute();
$expected = [
['nid' => $node_1->id()],
];
$this->assertIdenticalResultset($view, $expected, ['nid' => 'nid']);
}
/**
* Tests creating a view of workspace entities.
*
* @see \Drupal\views\Plugin\views\wizard\WizardPluginBase
*/
public function testCreateWorkspaceView(): void {
$wizard = \Drupal::service('plugin.manager.views.wizard')->createInstance('standard:workspace', []);
$form = [];
$form_state = new FormState();
$form = $wizard->buildForm($form, $form_state);
$random_id = $this->randomMachineName();
$random_label = $this->randomMachineName();
$form_state->setValues([
'id' => $random_id,
'label' => $random_label,
'base_table' => 'workspace',
]);
$wizard->validateView($form, $form_state);
$view = $wizard->createView($form, $form_state);
$this->assertInstanceOf(ViewUI::class, $view);
$this->assertEquals($random_id, $view->get('id'));
$this->assertEquals($random_label, $view->get('label'));
$this->assertEquals('workspace', $view->get('base_table'));
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Tests\file\Kernel\FileItemTest;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests using entity fields of the file field type in a workspace.
*
* @group workspaces
*/
class WorkspacesFileItemTest extends FileItemTest {
use UserCreationTrait;
use WorkspaceTestTrait;
/**
* The entity type manager.
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* {@inheritdoc}
*/
protected static $modules = [
'file',
'workspaces',
'workspaces_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entityTypeManager = \Drupal::entityTypeManager();
$this->installEntitySchema('workspace');
$this->installSchema('workspaces', ['workspace_association']);
// Create a new workspace and activate it.
Workspace::create(['id' => 'stage', 'label' => 'Stage'])->save();
$this->switchToWorkspace('stage');
}
/**
* {@inheritdoc}
*/
public function testFileItem(): void {
// Ignore entity types that are not being tested, in order to fully re-use
// the parent test method.
$this->ignoreEntityType('entity_test');
$this->ignoreEntityType('entity_view_display');
parent::testFileItem();
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Unit;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Drupal\Tests\UnitTestCase;
use Drupal\workspaces\Access\ActiveWorkspaceCheck;
use Drupal\workspaces\WorkspaceManagerInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Routing\Route;
/**
* @coversDefaultClass \Drupal\workspaces\Access\ActiveWorkspaceCheck
*
* @group workspaces
* @group Access
*/
class ActiveWorkspaceCheckTest extends UnitTestCase {
/**
* The dependency injection container.
*
* @var \Symfony\Component\DependencyInjection\ContainerBuilder
*/
protected $container;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->container = new ContainerBuilder();
$cache_contexts_manager = $this->prophesize(CacheContextsManager::class);
$cache_contexts_manager->assertValidTokens()->willReturn(TRUE);
$cache_contexts_manager->reveal();
$this->container->set('cache_contexts_manager', $cache_contexts_manager);
\Drupal::setContainer($this->container);
}
/**
* Provides data for the testAccess method.
*
* @return array
* An array of test data.
*/
public static function providerTestAccess() {
return [
[[], FALSE, FALSE],
[[], TRUE, FALSE],
[['_has_active_workspace' => 'TRUE'], TRUE, TRUE, ['workspace']],
[['_has_active_workspace' => 'TRUE'], FALSE, FALSE, ['workspace']],
[['_has_active_workspace' => 'FALSE'], TRUE, FALSE, ['workspace']],
[['_has_active_workspace' => 'FALSE'], FALSE, TRUE, ['workspace']],
];
}
/**
* @covers ::access
* @dataProvider providerTestAccess
*/
public function testAccess($requirements, $has_active_workspace, $access, array $contexts = []): void {
$route = new Route('', [], $requirements);
$workspace_manager = $this->prophesize(WorkspaceManagerInterface::class);
$workspace_manager->hasActiveWorkspace()->willReturn($has_active_workspace);
$access_check = new ActiveWorkspaceCheck($workspace_manager->reveal());
$access_result = AccessResult::allowedIf($access)->addCacheContexts($contexts);
$this->assertEquals($access_result, $access_check->access($route));
}
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Unit;
use Drupal\Core\Routing\CacheableRouteProviderInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\workspaces\EventSubscriber\WorkspaceRequestSubscriber;
use Drupal\workspaces\WorkspaceInterface;
use Drupal\workspaces\WorkspaceManagerInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
/**
* @coversDefaultClass \Drupal\workspaces\EventSubscriber\WorkspaceRequestSubscriber
*
* @group workspaces
*/
class WorkspaceRequestSubscriberTest extends UnitTestCase {
/**
* The workspace manager.
*
* @var \Drupal\workspaces\WorkspaceManagerInterface
*/
protected $workspaceManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->workspaceManager = $this->prophesize(WorkspaceManagerInterface::class);
$active_workspace = $this->prophesize(WorkspaceInterface::class);
$active_workspace->id()->willReturn('test');
$this->workspaceManager->getActiveWorkspace()->willReturn($active_workspace->reveal());
$this->workspaceManager->hasActiveWorkspace()->willReturn(TRUE);
}
/**
* @covers ::onKernelRequest
*/
public function testOnKernelRequestWithCacheableRouteProvider(): void {
$route_provider = $this->prophesize(CacheableRouteProviderInterface::class);
$route_provider->addExtraCacheKeyPart('workspace', 'test')->shouldBeCalled();
// Check that WorkspaceRequestSubscriber::onKernelRequest() calls
// addExtraCacheKeyPart() on a route provider that implements
// CacheableRouteProviderInterface.
$workspace_request_subscriber = new WorkspaceRequestSubscriber($route_provider->reveal(), $this->workspaceManager->reveal());
$event = $this->prophesize(RequestEvent::class)->reveal();
$this->assertNull($workspace_request_subscriber->onKernelRequest($event));
}
/**
* @covers ::onKernelRequest
*/
public function testOnKernelRequestWithoutCacheableRouteProvider(): void {
$route_provider = $this->prophesize(RouteProviderInterface::class);
// Check that WorkspaceRequestSubscriber::onKernelRequest() doesn't call
// addExtraCacheKeyPart() on a route provider that does not implement
// CacheableRouteProviderInterface.
$workspace_request_subscriber = new WorkspaceRequestSubscriber($route_provider->reveal(), $this->workspaceManager->reveal());
$event = $this->prophesize(RequestEvent::class)->reveal();
$this->assertNull($workspace_request_subscriber->onKernelRequest($event));
}
}