Initial Drupal 11 with DDEV setup
This commit is contained in:
		
							
								
								
									
										90
									
								
								web/core/modules/workspaces/tests/fixtures/update/workspaces.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								web/core/modules/workspaces/tests/fixtures/update/workspaces.php
									
									
									
									
										vendored
									
									
										Normal 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);
 | 
			
		||||
@ -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());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
@ -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.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
@ -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());
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,5 @@
 | 
			
		||||
services:
 | 
			
		||||
  workspace_update_test.negotiator.test:
 | 
			
		||||
    class: Drupal\workspace_update_test\Negotiator\TestWorkspaceNegotiator
 | 
			
		||||
    tags:
 | 
			
		||||
      - { name: workspace_negotiator, priority: 0 }
 | 
			
		||||
@ -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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
@ -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'
 | 
			
		||||
@ -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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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';
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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';
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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';
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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';
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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';
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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';
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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)));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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]'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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()]));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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
											
										
									
								
							@ -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();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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,
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user