3 namespace Drupal\Tests\content_moderation\Kernel;
5 use Drupal\KernelTests\KernelTestBase;
6 use Drupal\language\Entity\ConfigurableLanguage;
7 use Drupal\node\Entity\Node;
8 use Drupal\node\Entity\NodeType;
9 use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
10 use Drupal\Tests\user\Traits\UserCreationTrait;
13 * @coversDefaultClass \Drupal\content_moderation\Plugin\Validation\Constraint\ModerationStateConstraintValidator
14 * @group content_moderation
16 class EntityStateChangeValidationTest extends KernelTestBase {
18 use ContentModerationTestTrait;
19 use UserCreationTrait;
24 public static $modules = [
30 'content_translation',
37 * @var \Drupal\Core\Session\AccountInterface
44 protected function setUp() {
47 $this->installSchema('node', 'node_access');
48 $this->installEntitySchema('node');
49 $this->installEntitySchema('user');
50 $this->installEntitySchema('content_moderation_state');
51 $this->installConfig('content_moderation');
52 $this->installSchema('system', ['sequences']);
54 $this->adminUser = $this->createUser(array_keys($this->container->get('user.permissions')->getPermissions()));
58 * Test valid transitions.
62 public function testValidTransition() {
63 $this->setCurrentUser($this->adminUser);
65 $node_type = NodeType::create([
69 $workflow = $this->createEditorialWorkflow();
70 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
73 $node = Node::create([
75 'title' => 'Test title',
77 $node->moderation_state->value = 'draft';
80 $node->moderation_state->value = 'published';
81 $this->assertCount(0, $node->validate());
84 $this->assertEquals('published', $node->moderation_state->value);
88 * Test invalid transitions.
92 public function testInvalidTransition() {
93 $this->setCurrentUser($this->adminUser);
95 $node_type = NodeType::create([
99 $workflow = $this->createEditorialWorkflow();
100 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
103 $node = Node::create([
105 'title' => 'Test title',
107 $node->moderation_state->value = 'draft';
110 $node->moderation_state->value = 'archived';
111 $violations = $node->validate();
112 $this->assertCount(1, $violations);
114 $this->assertEquals('Invalid state transition from <em class="placeholder">Draft</em> to <em class="placeholder">Archived</em>', $violations->get(0)->getMessage());
118 * Test validation with an invalid state.
120 public function testInvalidState() {
121 $node_type = NodeType::create([
125 $workflow = $this->createEditorialWorkflow();
126 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
129 $node = Node::create([
131 'title' => 'Test title',
133 $node->moderation_state->value = 'invalid_state';
134 $violations = $node->validate();
136 $this->assertCount(1, $violations);
137 $this->assertEquals('State <em class="placeholder">invalid_state</em> does not exist on <em class="placeholder">Editorial</em> workflow', $violations->get(0)->getMessage());
141 * Test validation with content that has no initial state or an invalid state.
143 public function testInvalidStateWithoutExisting() {
144 $this->setCurrentUser($this->adminUser);
145 // Create content without moderation enabled for the content type.
146 $node_type = NodeType::create([
150 $node = Node::create([
152 'title' => 'Test title',
156 // Enable moderation to test validation on existing content, with no
158 $workflow = $this->createEditorialWorkflow();
159 $workflow->getTypePlugin()->addState('deleted_state', 'Deleted state');
160 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
163 // Validate the invalid state.
164 $node->moderation_state->value = 'invalid_state';
165 $violations = $node->validate();
166 $this->assertCount(1, $violations);
168 // Assign the node to a state we're going to delete.
169 $node->moderation_state->value = 'deleted_state';
172 // Delete the state so $node->original contains an invalid state when
174 $workflow->getTypePlugin()->deleteState('deleted_state');
177 // When there is an invalid state, the content will revert to "draft". This
178 // will allow a draft to draft transition.
179 $node->moderation_state->value = 'draft';
180 $violations = $node->validate();
181 $this->assertCount(0, $violations);
182 // This will disallow a draft to archived transition.
183 $node->moderation_state->value = 'archived';
184 $violations = $node->validate();
185 $this->assertCount(1, $violations);
189 * Test state transition validation with multiple languages.
191 public function testInvalidStateMultilingual() {
192 $this->setCurrentUser($this->adminUser);
194 ConfigurableLanguage::createFromLangcode('fr')->save();
195 $node_type = NodeType::create([
200 $workflow = $this->createEditorialWorkflow();
201 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
204 $node = Node::create([
206 'title' => 'English Published Node',
208 'moderation_state' => 'published',
212 $node_fr = $node->addTranslation('fr', $node->toArray());
213 $node_fr->setTitle('French Published Node');
215 $this->assertEquals('published', $node_fr->moderation_state->value);
217 // Create a pending revision of the original node.
218 $node->moderation_state = 'draft';
219 $node->setNewRevision(TRUE);
220 $node->isDefaultRevision(FALSE);
223 // For the pending english revision, there should be a violation from draft
225 $node->moderation_state = 'archived';
226 $violations = $node->validate();
227 $this->assertCount(1, $violations);
228 $this->assertEquals('Invalid state transition from <em class="placeholder">Draft</em> to <em class="placeholder">Archived</em>', $violations->get(0)->getMessage());
230 // From the default french published revision, there should be none.
231 $node_fr = Node::load($node->id())->getTranslation('fr');
232 $this->assertEquals('published', $node_fr->moderation_state->value);
233 $node_fr->moderation_state = 'archived';
234 $violations = $node_fr->validate();
235 $this->assertCount(0, $violations);
237 // From the latest french revision, there should also be no violation.
238 $node_fr = Node::load($node->id())->getTranslation('fr');
239 $this->assertEquals('published', $node_fr->moderation_state->value);
240 $node_fr->moderation_state = 'archived';
241 $violations = $node_fr->validate();
242 $this->assertCount(0, $violations);
246 * Tests that content without prior moderation information can be moderated.
248 public function testExistingContentWithNoModeration() {
249 $this->setCurrentUser($this->adminUser);
251 $node_type = NodeType::create([
255 /** @var \Drupal\node\NodeInterface $node */
256 $node = Node::create([
258 'title' => 'Test title',
264 // Enable moderation for our node type.
265 $workflow = $this->createEditorialWorkflow();
266 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
269 $node = Node::load($nid);
271 // Having no previous state should not break validation.
272 $violations = $node->validate();
274 $this->assertCount(0, $violations);
276 // Having no previous state should not break saving the node.
277 $node->setTitle('New');
282 * Tests that content without prior moderation information can be translated.
284 public function testExistingMultilingualContentWithNoModeration() {
285 $this->setCurrentUser($this->adminUser);
288 ConfigurableLanguage::createFromLangcode('fr')->save();
290 $node_type = NodeType::create([
294 /** @var \Drupal\node\NodeInterface $node */
295 $node = Node::create([
297 'title' => 'Test title',
304 $node = Node::load($nid);
306 // Creating a translation shouldn't break, even though there's no previous
307 // moderated revision for the new language.
308 $node_fr = $node->addTranslation('fr');
309 $node_fr->setTitle('Francais');
312 // Enable moderation for our node type.
313 $workflow = $this->createEditorialWorkflow();
314 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
317 // Reload the French version of the node.
318 $node = Node::load($nid);
319 $node_fr = $node->getTranslation('fr');
321 /** @var \Drupal\node\NodeInterface $node_fr */
322 $node_fr->setTitle('Nouveau');
327 * @dataProvider transitionAccessValidationTestCases
329 public function testTransitionAccessValidation($permissions, $target_state, $messages) {
330 $node_type = NodeType::create([
334 $workflow = $this->createEditorialWorkflow();
335 $workflow->getTypePlugin()->addState('foo', 'Foo');
336 $workflow->getTypePlugin()->addTransition('draft_to_foo', 'Draft to foo', ['draft'], 'foo');
337 $workflow->getTypePlugin()->addTransition('foo_to_foo', 'Foo to foo', ['foo'], 'foo');
338 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
341 $this->setCurrentUser($this->createUser($permissions));
343 $node = Node::create([
345 'title' => 'Test content',
346 'moderation_state' => $target_state,
348 $this->assertTrue($node->isNew());
349 $violations = $node->validate();
350 $this->assertCount(count($messages), $violations);
351 foreach ($messages as $i => $message) {
352 $this->assertEquals($message, $violations->get($i)->getMessage());
357 * Test cases for ::testTransitionAccessValidation.
359 public function transitionAccessValidationTestCases() {
361 'Invalid transition, no permissions validated' => [
364 ['Invalid state transition from <em class="placeholder">Draft</em> to <em class="placeholder">Archived</em>'],
366 'Valid transition, missing permission' => [
369 ['You do not have access to transition from <em class="placeholder">Draft</em> to <em class="placeholder">Published</em>'],
371 'Valid transition, granted published permission' => [
372 ['use editorial transition publish'],
376 'Valid transition, granted draft permission' => [
377 ['use editorial transition create_new_draft'],
381 'Valid transition, incorrect permission granted' => [
382 ['use editorial transition create_new_draft'],
384 ['You do not have access to transition from <em class="placeholder">Draft</em> to <em class="placeholder">Published</em>'],
386 // Test with an additional state and set of transitions, since the
387 // "published" transition can start from either "draft" or "published", it
388 // does not capture bugs that fail to correctly distinguish the initial
389 // workflow state from the set state of a new entity.
390 'Valid transition, granted foo permission' => [
391 ['use editorial transition draft_to_foo'],
395 'Valid transition, incorrect foo permission granted' => [
396 ['use editorial transition foo_to_foo'],
398 ['You do not have access to transition from <em class="placeholder">Draft</em> to <em class="placeholder">Foo</em>'],