Verified Commit d9a1c6e8 authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3300639 by amateescu, acrazyanimal, smustgrave: Improve and add...

Issue #3300639 by amateescu, acrazyanimal, smustgrave: Improve and add explicit test coverage for the workspace conflict validator
parent 5f6c6ec6
Loading
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -21,6 +21,6 @@ class EntityWorkspaceConflictConstraint extends SymfonyConstraint {
   *
   * @var string
   */
  public $message = 'The content is being edited in the %label workspace. As a result, your changes cannot be saved.';
  public $message = 'The content is being edited in the @label workspace. As a result, your changes cannot be saved.';

}
+15 −62
Original line number Diff line number Diff line
@@ -6,62 +6,22 @@
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\workspaces\WorkspaceAssociationInterface;
use Drupal\workspaces\WorkspaceManagerInterface;
use Drupal\workspaces\WorkspaceRepositoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

/**
 * Validates the EntityWorkspaceConflict constraint.
 */
class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The workspace manager service.
   *
   * @var \Drupal\workspaces\WorkspaceManagerInterface
   */
  protected $workspaceManager;

  /**
   * The workspace association service.
 *
   * @var \Drupal\workspaces\WorkspaceAssociationInterface
 * @internal
 */
  protected $workspaceAssociation;
class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {

  /**
   * The workspace repository service.
   *
   * @var \Drupal\workspaces\WorkspaceRepositoryInterface
   */
  protected $workspaceRepository;

  /**
   * Constructs an EntityUntranslatableFieldsConstraintValidator object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager service.
   * @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
   *   The workspace manager service.
   * @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association
   *   The workspace association service.
   * @param \Drupal\workspaces\WorkspaceRepositoryInterface $workspace_repository
   *   The Workspace repository service.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association, WorkspaceRepositoryInterface $workspace_repository) {
    $this->entityTypeManager = $entity_type_manager;
    $this->workspaceManager = $workspace_manager;
    $this->workspaceAssociation = $workspace_association;
    $this->workspaceRepository = $workspace_repository;
  }
  public function __construct(
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    protected readonly WorkspaceManagerInterface $workspaceManager,
    protected readonly WorkspaceAssociationInterface $workspaceAssociation,
  ) {}

  /**
   * {@inheritdoc}
@@ -71,7 +31,6 @@ public static function create(ContainerInterface $container) {
      $container->get('entity_type.manager'),
      $container->get('workspaces.manager'),
      $container->get('workspaces.association'),
      $container->get('workspaces.repository')
    );
  }

@@ -83,22 +42,16 @@ public function validate($entity, Constraint $constraint): void {
    if (isset($entity) && !$entity->isNew()) {
      $active_workspace = $this->workspaceManager->getActiveWorkspace();

      // Get the latest revision of the entity in order to check if it's being
      // edited in a different workspace.
      $latest_revision = $this->workspaceManager->executeOutsideWorkspace(function () use ($entity) {
        /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
        $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
        return $storage->loadRevision($storage->getLatestRevisionId($entity->id()));
      });

      // If the latest revision of the entity is tracked in a workspace, it can
      // only be edited in that workspace or one of its descendants.
      if ($latest_revision_workspace = $latest_revision->workspace->entity) {
        $descendants_and_self = $this->workspaceRepository->getDescendantsAndSelf($latest_revision_workspace->id());
      // If the entity is tracked in a workspace, it can only be edited in
      // that workspace or one of its descendants.
      if ($tracking_workspace_ids = $this->workspaceAssociation->getEntityTrackingWorkspaceIds($entity, TRUE)) {
        if (!$active_workspace || !in_array($active_workspace->id(), $tracking_workspace_ids, TRUE)) {
          $first_tracking_workspace_id = reset($tracking_workspace_ids);
          $workspace = $this->entityTypeManager->getStorage('workspace')
            ->load($first_tracking_workspace_id);

        if (!$active_workspace || !in_array($active_workspace->id(), $descendants_and_self, TRUE)) {
          $this->context->buildViolation($constraint->message)
            ->setParameter('%label', $latest_revision_workspace->label())
            ->setParameter('@label', $workspace->label())
            ->addViolation();
        }
      }
+26 −6
Original line number Diff line number Diff line
@@ -304,13 +304,33 @@ public function getAssociatedInitialRevisions(string $workspace_id, string $enti
  /**
   * {@inheritdoc}
   */
  public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity) {
    $query = $this->database->select(static::TABLE)
      ->fields(static::TABLE, ['workspace'])
      ->condition('target_entity_type_id', $entity->getEntityTypeId())
      ->condition('target_entity_id', $entity->id());
  public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity, bool $latest_revision = FALSE) {
    $query = $this->database->select(static::TABLE, 'wa')
      ->fields('wa', ['workspace'])
      ->condition('[wa].[target_entity_type_id]', $entity->getEntityTypeId())
      ->condition('[wa].[target_entity_id]', $entity->id());

    // Use a self-join to get only the workspaces in which the latest revision
    // of the entity is tracked.
    if ($latest_revision) {
      $inner_select = $this->database->select(static::TABLE, 'wai')
        ->condition('[wai].[target_entity_type_id]', $entity->getEntityTypeId())
        ->condition('[wai].[target_entity_id]', $entity->id());
      $inner_select->addExpression('MAX([wai].[target_entity_revision_id])', 'max_revision_id');

      $query->join($inner_select, 'waj', '[wa].[target_entity_revision_id] = [waj].[max_revision_id]');
    }

    $result = $query->execute()->fetchCol();

    // Return early if the entity is not tracked in any workspace.
    if (empty($result)) {
      return [];
    }

    return $query->execute()->fetchCol();
    // Return workspace IDs sorted in tree order.
    $tree = $this->workspaceRepository->loadTree();
    return array_keys(array_intersect_key($tree, array_flip($result)));
  }

  /**
+4 −1
Original line number Diff line number Diff line
@@ -96,12 +96,15 @@ public function getAssociatedInitialRevisions(string $workspace_id, string $enti
   *
   * @param \Drupal\Core\Entity\RevisionableInterface $entity
   *   An entity object.
   * @param bool $latest_revision
   *   (optional) Whether to return only the workspaces in which the latest
   *   revision of the entity is tracked. Defaults to FALSE.
   *
   * @return string[]
   *   An array of workspace IDs where the given entity is tracked, or an empty
   *   array if it is not tracked anywhere.
   */
  public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity);
  public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity, bool $latest_revision = FALSE);

  /**
   * Triggers clean-up operations after publishing a workspace.
+148 −0
Original line number Diff line number Diff line
<?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',
    'path_alias',
    '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());
  }

}