EntityStateChangeValidationTest.php

Same filename in other branches
  1. 8.9.x core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php
  2. 10 core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php
  3. 11.x core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php

Namespace

Drupal\Tests\content_moderation\Kernel

File

core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php

View source
<?php

namespace Drupal\Tests\content_moderation\Kernel;

use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
use Drupal\Tests\user\Traits\UserCreationTrait;

/**
 * @coversDefaultClass \Drupal\content_moderation\Plugin\Validation\Constraint\ModerationStateConstraintValidator
 * @group content_moderation
 */
class EntityStateChangeValidationTest extends KernelTestBase {
    use ContentModerationTestTrait;
    use UserCreationTrait;
    
    /**
     * {@inheritdoc}
     */
    protected static $modules = [
        'node',
        'content_moderation',
        'user',
        'system',
        'language',
        'content_translation',
        'workflows',
    ];
    
    /**
     * An admin user.
     *
     * @var \Drupal\Core\Session\AccountInterface
     */
    protected $adminUser;
    
    /**
     * {@inheritdoc}
     */
    protected function setUp() : void {
        parent::setUp();
        $this->installSchema('node', 'node_access');
        $this->installEntitySchema('node');
        $this->installEntitySchema('user');
        $this->installEntitySchema('content_moderation_state');
        $this->installConfig('content_moderation');
        $this->installSchema('system', [
            'sequences',
        ]);
        $this->adminUser = $this->createUser(array_keys($this->container
            ->get('user.permissions')
            ->getPermissions()));
    }
    
    /**
     * Tests valid transitions.
     *
     * @covers ::validate
     */
    public function testValidTransition() {
        $this->setCurrentUser($this->adminUser);
        $node_type = NodeType::create([
            'type' => 'example',
        ]);
        $node_type->save();
        $workflow = $this->createEditorialWorkflow();
        $workflow->getTypePlugin()
            ->addEntityTypeAndBundle('node', 'example');
        $workflow->save();
        $node = Node::create([
            'type' => 'example',
            'title' => 'Test title',
        ]);
        $node->moderation_state->value = 'draft';
        $node->save();
        $node->moderation_state->value = 'published';
        $this->assertCount(0, $node->validate());
        $node->save();
        $this->assertEquals('published', $node->moderation_state->value);
    }
    
    /**
     * Tests invalid transitions.
     *
     * @covers ::validate
     */
    public function testInvalidTransition() {
        $this->setCurrentUser($this->adminUser);
        $node_type = NodeType::create([
            'type' => 'example',
        ]);
        $node_type->save();
        $workflow = $this->createEditorialWorkflow();
        $workflow->getTypePlugin()
            ->addEntityTypeAndBundle('node', 'example');
        $workflow->save();
        $node = Node::create([
            'type' => 'example',
            'title' => 'Test title',
        ]);
        $node->moderation_state->value = 'draft';
        $node->save();
        $node->moderation_state->value = 'archived';
        $violations = $node->validate();
        $this->assertCount(1, $violations);
        $this->assertEquals('Invalid state transition from <em class="placeholder">Draft</em> to <em class="placeholder">Archived</em>', $violations->get(0)
            ->getMessage());
    }
    
    /**
     * Tests validation with an invalid state.
     */
    public function testInvalidState() {
        $node_type = NodeType::create([
            'type' => 'example',
        ]);
        $node_type->save();
        $workflow = $this->createEditorialWorkflow();
        $workflow->getTypePlugin()
            ->addEntityTypeAndBundle('node', 'example');
        $workflow->save();
        $node = Node::create([
            'type' => 'example',
            'title' => 'Test title',
        ]);
        $node->moderation_state->value = 'invalid_state';
        $violations = $node->validate();
        $this->assertCount(1, $violations);
        $this->assertEquals('State <em class="placeholder">invalid_state</em> does not exist on <em class="placeholder">Editorial</em> workflow', $violations->get(0)
            ->getMessage());
    }
    
    /**
     * Tests validation with no initial state or an invalid state.
     */
    public function testInvalidStateWithoutExisting() {
        $this->setCurrentUser($this->adminUser);
        // Create content without moderation enabled for the content type.
        $node_type = NodeType::create([
            'type' => 'example',
        ]);
        $node_type->save();
        $node = Node::create([
            'type' => 'example',
            'title' => 'Test title',
        ]);
        $node->save();
        // Enable moderation to test validation on existing content, with no
        // explicit state.
        $workflow = $this->createEditorialWorkflow();
        $workflow->getTypePlugin()
            ->addState('deleted_state', 'Deleted state');
        $workflow->getTypePlugin()
            ->addEntityTypeAndBundle('node', 'example');
        $workflow->save();
        // Validate the invalid state.
        $node = Node::load($node->id());
        $node->moderation_state->value = 'invalid_state';
        $violations = $node->validate();
        $this->assertCount(1, $violations);
        // Assign the node to a state we're going to delete.
        $node->moderation_state->value = 'deleted_state';
        $node->save();
        // Delete the state so $node->original contains an invalid state when
        // validating.
        $workflow->getTypePlugin()
            ->deleteState('deleted_state');
        $workflow->save();
        // When there is an invalid state, the content will revert to "draft". This
        // will allow a draft to draft transition.
        $node->moderation_state->value = 'draft';
        $violations = $node->validate();
        $this->assertCount(0, $violations);
        // This will disallow a draft to archived transition.
        $node->moderation_state->value = 'archived';
        $violations = $node->validate();
        $this->assertCount(1, $violations);
    }
    
    /**
     * Tests state transition validation with multiple languages.
     */
    public function testInvalidStateMultilingual() {
        $this->setCurrentUser($this->adminUser);
        ConfigurableLanguage::createFromLangcode('fr')->save();
        $node_type = NodeType::create([
            'type' => 'example',
        ]);
        $node_type->save();
        $workflow = $this->createEditorialWorkflow();
        $workflow->getTypePlugin()
            ->addEntityTypeAndBundle('node', 'example');
        $workflow->save();
        $node = Node::create([
            'type' => 'example',
            'title' => 'English Published Node',
            'langcode' => 'en',
            'moderation_state' => 'published',
        ]);
        $node->save();
        $node_fr = $node->addTranslation('fr', $node->toArray());
        $node_fr->setTitle('French Published Node');
        $node_fr->save();
        $this->assertEquals('published', $node_fr->moderation_state->value);
        // Create a pending revision of the original node.
        $node->moderation_state = 'draft';
        $node->setNewRevision(TRUE);
        $node->isDefaultRevision(FALSE);
        $node->save();
        // For the pending english revision, there should be a violation from draft
        // to archived.
        $node->moderation_state = 'archived';
        $violations = $node->validate();
        $this->assertCount(1, $violations);
        $this->assertEquals('Invalid state transition from <em class="placeholder">Draft</em> to <em class="placeholder">Archived</em>', $violations->get(0)
            ->getMessage());
        // From the default french published revision, there should be none.
        $node_fr = Node::load($node->id())
            ->getTranslation('fr');
        $this->assertEquals('published', $node_fr->moderation_state->value);
        $node_fr->moderation_state = 'archived';
        $violations = $node_fr->validate();
        $this->assertCount(0, $violations);
        // From the latest french revision, there should also be no violation.
        $node_fr = Node::load($node->id())
            ->getTranslation('fr');
        $this->assertEquals('published', $node_fr->moderation_state->value);
        $node_fr->moderation_state = 'archived';
        $violations = $node_fr->validate();
        $this->assertCount(0, $violations);
    }
    
    /**
     * Tests that content without prior moderation information can be moderated.
     */
    public function testExistingContentWithNoModeration() {
        $this->setCurrentUser($this->adminUser);
        $node_type = NodeType::create([
            'type' => 'example',
        ]);
        $node_type->save();
        
        /** @var \Drupal\node\NodeInterface $node */
        $node = Node::create([
            'type' => 'example',
            'title' => 'Test title',
        ]);
        $node->save();
        $nid = $node->id();
        // Enable moderation for our node type.
        $workflow = $this->createEditorialWorkflow();
        $workflow->getTypePlugin()
            ->addEntityTypeAndBundle('node', 'example');
        $workflow->save();
        $node = Node::load($nid);
        // Having no previous state should not break validation.
        $violations = $node->validate();
        $this->assertCount(0, $violations);
        // Having no previous state should not break saving the node.
        $node->setTitle('New');
        $node->save();
    }
    
    /**
     * Tests that content without prior moderation information can be translated.
     */
    public function testExistingMultilingualContentWithNoModeration() {
        $this->setCurrentUser($this->adminUser);
        // Enable French.
        ConfigurableLanguage::createFromLangcode('fr')->save();
        $node_type = NodeType::create([
            'type' => 'example',
        ]);
        $node_type->save();
        
        /** @var \Drupal\node\NodeInterface $node */
        $node = Node::create([
            'type' => 'example',
            'title' => 'Test title',
            'langcode' => 'en',
        ]);
        $node->save();
        $nid = $node->id();
        $node = Node::load($nid);
        // Creating a translation shouldn't break, even though there's no previous
        // moderated revision for the new language.
        $node_fr = $node->addTranslation('fr');
        $node_fr->setTitle('Francais');
        $node_fr->save();
        // Enable moderation for our node type.
        $workflow = $this->createEditorialWorkflow();
        $workflow->getTypePlugin()
            ->addEntityTypeAndBundle('node', 'example');
        $workflow->save();
        // Reload the French version of the node.
        $node = Node::load($nid);
        $node_fr = $node->getTranslation('fr');
        
        /** @var \Drupal\node\NodeInterface $node_fr */
        $node_fr->setTitle('Nouveau');
        $node_fr->save();
    }
    
    /**
     * @dataProvider transitionAccessValidationTestCases
     */
    public function testTransitionAccessValidation($permissions, $target_state, $messages) {
        $node_type = NodeType::create([
            'type' => 'example',
        ]);
        $node_type->save();
        $workflow = $this->createEditorialWorkflow();
        $workflow->getTypePlugin()
            ->addState('foo', 'Foo');
        $workflow->getTypePlugin()
            ->addTransition('draft_to_foo', 'Draft to foo', [
            'draft',
        ], 'foo');
        $workflow->getTypePlugin()
            ->addTransition('foo_to_foo', 'Foo to foo', [
            'foo',
        ], 'foo');
        $workflow->getTypePlugin()
            ->addEntityTypeAndBundle('node', 'example');
        $workflow->save();
        $this->setCurrentUser($this->createUser($permissions));
        $node = Node::create([
            'type' => 'example',
            'title' => 'Test content',
            'moderation_state' => $target_state,
        ]);
        $this->assertTrue($node->isNew());
        $violations = $node->validate();
        $this->assertSameSize($messages, $violations);
        foreach ($messages as $i => $message) {
            $this->assertEquals($message, $violations->get($i)
                ->getMessage());
        }
    }
    
    /**
     * Test cases for ::testTransitionAccessValidation.
     */
    public function transitionAccessValidationTestCases() {
        return [
            'Invalid transition, no permissions validated' => [
                [],
                'archived',
                [
                    'Invalid state transition from <em class="placeholder">Draft</em> to <em class="placeholder">Archived</em>',
                ],
            ],
            'Valid transition, missing permission' => [
                [],
                'published',
                [
                    'You do not have access to transition from <em class="placeholder">Draft</em> to <em class="placeholder">Published</em>',
                ],
            ],
            'Valid transition, granted published permission' => [
                [
                    'use editorial transition publish',
                ],
                'published',
                [],
            ],
            'Valid transition, granted draft permission' => [
                [
                    'use editorial transition create_new_draft',
                ],
                'draft',
                [],
            ],
            'Valid transition, incorrect permission granted' => [
                [
                    'use editorial transition create_new_draft',
                ],
                'published',
                [
                    'You do not have access to transition from <em class="placeholder">Draft</em> to <em class="placeholder">Published</em>',
                ],
            ],
            // Test with an additional state and set of transitions, since the
            // "published" transition can start from either "draft" or "published", it
            // does not capture bugs that fail to correctly distinguish the initial
            // workflow state from the set state of a new entity.
'Valid transition, granted foo permission' => [
                [
                    'use editorial transition draft_to_foo',
                ],
                'foo',
                [],
            ],
            'Valid transition, incorrect  foo permission granted' => [
                [
                    'use editorial transition foo_to_foo',
                ],
                'foo',
                [
                    'You do not have access to transition from <em class="placeholder">Draft</em> to <em class="placeholder">Foo</em>',
                ],
            ],
        ];
    }

}

Classes

Title Deprecated Summary
EntityStateChangeValidationTest @coversDefaultClass \Drupal\content_moderation\Plugin\Validation\Constraint\ModerationStateConstraintValidator @group content_moderation

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.