1<?php 2 3namespace Drupal\Tests\content_moderation\Kernel; 4 5use Drupal\KernelTests\KernelTestBase; 6use Drupal\language\Entity\ConfigurableLanguage; 7use Drupal\node\Entity\Node; 8use Drupal\node\Entity\NodeType; 9use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait; 10use Drupal\Tests\user\Traits\UserCreationTrait; 11use Drupal\workflows\Entity\Workflow; 12 13/** 14 * @coversDefaultClass \Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList 15 * 16 * @group content_moderation 17 */ 18class ModerationStateFieldItemListTest extends KernelTestBase { 19 20 use ContentModerationTestTrait; 21 use UserCreationTrait; 22 23 /** 24 * {@inheritdoc} 25 */ 26 protected static $modules = [ 27 'node', 28 'content_moderation', 29 'user', 30 'system', 31 'language', 32 'workflows', 33 ]; 34 35 /** 36 * @var \Drupal\node\NodeInterface 37 */ 38 protected $testNode; 39 40 /** 41 * {@inheritdoc} 42 */ 43 protected function setUp(): void { 44 parent::setUp(); 45 46 $this->installSchema('node', 'node_access'); 47 $this->installSchema('system', 'sequences'); 48 $this->installEntitySchema('node'); 49 $this->installEntitySchema('user'); 50 $this->installEntitySchema('content_moderation_state'); 51 $this->installConfig('content_moderation'); 52 53 NodeType::create([ 54 'type' => 'unmoderated', 55 ])->save(); 56 57 $node_type = NodeType::create([ 58 'type' => 'example', 59 ]); 60 $node_type->save(); 61 $workflow = $this->createEditorialWorkflow(); 62 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example'); 63 $workflow->save(); 64 65 $this->testNode = Node::create([ 66 'type' => 'example', 67 'title' => 'Test title', 68 ]); 69 $this->testNode->save(); 70 \Drupal::entityTypeManager()->getStorage('node')->resetCache(); 71 $this->testNode = Node::load($this->testNode->id()); 72 73 ConfigurableLanguage::createFromLangcode('de')->save(); 74 } 75 76 /** 77 * Tests the field item list when accessing an index. 78 */ 79 public function testArrayIndex() { 80 $this->assertFalse($this->testNode->isPublished()); 81 $this->assertEquals('draft', $this->testNode->moderation_state[0]->value); 82 } 83 84 /** 85 * Tests the field item list when iterating. 86 */ 87 public function testArrayIteration() { 88 $states = []; 89 foreach ($this->testNode->moderation_state as $item) { 90 $states[] = $item->value; 91 } 92 $this->assertEquals(['draft'], $states); 93 } 94 95 /** 96 * @covers ::getValue 97 */ 98 public function testGetValue() { 99 $this->assertEquals([['value' => 'draft']], $this->testNode->moderation_state->getValue()); 100 } 101 102 /** 103 * @covers ::get 104 */ 105 public function testGet() { 106 $this->assertEquals('draft', $this->testNode->moderation_state->get(0)->value); 107 $this->expectException(\InvalidArgumentException::class); 108 $this->testNode->moderation_state->get(2); 109 } 110 111 /** 112 * Tests the item list when it is emptied and appended to. 113 */ 114 public function testEmptyStateAndAppend() { 115 // This test case mimics the lifecycle of an entity that is being patched in 116 // a rest resource. 117 $this->testNode->moderation_state->setValue([]); 118 $this->assertTrue($this->testNode->moderation_state->isEmpty()); 119 $this->assertEmptiedModerationFieldItemList(); 120 121 $this->testNode->moderation_state->appendItem(); 122 $this->assertEquals(1, $this->testNode->moderation_state->count()); 123 $this->assertEquals(NULL, $this->testNode->moderation_state->value); 124 $this->assertEmptiedModerationFieldItemList(); 125 } 126 127 /** 128 * Tests an empty value assigned to the field item. 129 */ 130 public function testEmptyFieldItem() { 131 $this->testNode->moderation_state->value = ''; 132 $this->assertEquals('', $this->testNode->moderation_state->value); 133 $this->assertEmptiedModerationFieldItemList(); 134 } 135 136 /** 137 * Tests an empty value assigned to the field item list. 138 */ 139 public function testEmptyFieldItemList() { 140 $this->testNode->moderation_state = ''; 141 $this->assertEquals('', $this->testNode->moderation_state->value); 142 $this->assertEmptiedModerationFieldItemList(); 143 } 144 145 /** 146 * Tests the field item when it is unset. 147 */ 148 public function testUnsetItemList() { 149 unset($this->testNode->moderation_state); 150 $this->assertEquals(NULL, $this->testNode->moderation_state->value); 151 $this->assertEmptiedModerationFieldItemList(); 152 } 153 154 /** 155 * Tests the field item when it is assigned NULL. 156 */ 157 public function testAssignNullItemList() { 158 $this->testNode->moderation_state = NULL; 159 $this->assertEquals(NULL, $this->testNode->moderation_state->value); 160 $this->assertEmptiedModerationFieldItemList(); 161 } 162 163 /** 164 * Assert the set of expectations when the moderation state field is emptied. 165 */ 166 protected function assertEmptiedModerationFieldItemList() { 167 $this->assertTrue($this->testNode->moderation_state->isEmpty()); 168 // Test the empty value causes a violation in the entity. 169 $violations = $this->testNode->validate(); 170 $this->assertCount(1, $violations); 171 $this->assertEquals('This value should not be null.', $violations->get(0)->getMessage()); 172 // Test that incorrectly saving the entity regardless will not produce a 173 // change in the moderation state. 174 $this->testNode->save(); 175 $this->assertEquals('draft', Node::load($this->testNode->id())->moderation_state->value); 176 } 177 178 /** 179 * Tests the list class with a non moderated entity. 180 */ 181 public function testNonModeratedEntity() { 182 $unmoderated_node = Node::create([ 183 'type' => 'unmoderated', 184 'title' => 'Test title', 185 ]); 186 $unmoderated_node->save(); 187 $this->assertEquals(0, $unmoderated_node->moderation_state->count()); 188 189 $unmoderated_node->moderation_state = NULL; 190 $this->assertEquals(0, $unmoderated_node->moderation_state->count()); 191 $this->assertCount(0, $unmoderated_node->validate()); 192 } 193 194 /** 195 * Tests that moderation state changes also change the related entity state. 196 * 197 * @dataProvider moderationStateChangesTestCases 198 */ 199 public function testModerationStateChanges($initial_state, $final_state, $first_published, $first_is_default, $second_published, $second_is_default) { 200 $this->testNode->moderation_state->value = $initial_state; 201 $this->assertEquals($first_published, $this->testNode->isPublished()); 202 $this->assertEquals($first_is_default, $this->testNode->isDefaultRevision()); 203 $this->testNode->save(); 204 205 $this->testNode->moderation_state->value = $final_state; 206 $this->assertEquals($second_published, $this->testNode->isPublished()); 207 $this->assertEquals($second_is_default, $this->testNode->isDefaultRevision()); 208 } 209 210 /** 211 * Data provider for ::testModerationStateChanges. 212 */ 213 public function moderationStateChangesTestCases() { 214 return [ 215 'Draft to draft' => [ 216 'draft', 217 'draft', 218 FALSE, 219 TRUE, 220 FALSE, 221 TRUE, 222 ], 223 'Draft to published' => [ 224 'draft', 225 'published', 226 FALSE, 227 TRUE, 228 TRUE, 229 TRUE, 230 ], 231 'Published to published' => [ 232 'published', 233 'published', 234 TRUE, 235 TRUE, 236 TRUE, 237 TRUE, 238 ], 239 'Published to draft' => [ 240 'published', 241 'draft', 242 TRUE, 243 TRUE, 244 FALSE, 245 FALSE, 246 ], 247 ]; 248 } 249 250 /** 251 * Tests updating the state for an entity without a workflow. 252 */ 253 public function testEntityWithNoWorkflow() { 254 $node_type = NodeType::create([ 255 'type' => 'example_no_workflow', 256 ]); 257 $node_type->save(); 258 $test_node = Node::create([ 259 'type' => 'example_no_workflow', 260 'title' => 'Test node with no workflow', 261 ]); 262 $test_node->save(); 263 264 /** @var \Drupal\content_moderation\ModerationInformationInterface $content_moderation_info */ 265 $content_moderation_info = \Drupal::service('content_moderation.moderation_information'); 266 $workflow = $content_moderation_info->getWorkflowForEntity($test_node); 267 $this->assertNull($workflow); 268 269 $this->assertTrue($test_node->isPublished()); 270 $test_node->moderation_state->setValue('draft'); 271 // The entity is still published because there is not a workflow. 272 $this->assertTrue($test_node->isPublished()); 273 } 274 275 /** 276 * Tests the moderation_state field after an entity has been serialized. 277 * 278 * @dataProvider entityUnserializeTestCases 279 */ 280 public function testEntityUnserialize($state, $default, $published) { 281 $this->testNode->moderation_state->value = $state; 282 283 $this->assertEquals($state, $this->testNode->moderation_state->value); 284 $this->assertEquals($default, $this->testNode->isDefaultRevision()); 285 $this->assertEquals($published, $this->testNode->isPublished()); 286 287 $unserialized = unserialize(serialize($this->testNode)); 288 289 $this->assertEquals($state, $unserialized->moderation_state->value); 290 $this->assertEquals($default, $unserialized->isDefaultRevision()); 291 $this->assertEquals($published, $unserialized->isPublished()); 292 } 293 294 /** 295 * Test cases for ::testEntityUnserialize. 296 */ 297 public function entityUnserializeTestCases() { 298 return [ 299 'Default draft state' => [ 300 'draft', 301 TRUE, 302 FALSE, 303 ], 304 'Non-default published state' => [ 305 'published', 306 TRUE, 307 TRUE, 308 ], 309 ]; 310 } 311 312 /** 313 * Tests saving a moderated node with an existing ID. 314 * 315 * @dataProvider moderatedEntityWithExistingIdTestCases 316 */ 317 public function testModeratedEntityWithExistingId($state) { 318 $node = Node::create([ 319 'title' => 'Test title', 320 'type' => 'example', 321 'nid' => 999, 322 'moderation_state' => $state, 323 ]); 324 $node->save(); 325 $this->assertEquals($state, $node->moderation_state->value); 326 } 327 328 /** 329 * Tests cases for ::testModeratedEntityWithExistingId. 330 */ 331 public function moderatedEntityWithExistingIdTestCases() { 332 return [ 333 'Draft non-default state' => [ 334 'draft', 335 ], 336 'Published default state' => [ 337 'published', 338 ], 339 ]; 340 } 341 342 /** 343 * Test customizing the default moderation state. 344 */ 345 public function testWorkflowCustomizedInitialState() { 346 $workflow = Workflow::load('editorial'); 347 $configuration = $workflow->getTypePlugin()->getConfiguration(); 348 349 // Test a node for a workflow that hasn't been updated to include the 350 // 'default_moderation_state' setting. We must be backwards compatible with 351 // configuration that was exported before this change was introduced. 352 $this->assertFalse(isset($configuration['default_moderation_state'])); 353 $legacy_configuration_node = Node::create([ 354 'title' => 'Test title', 355 'type' => 'example', 356 ]); 357 $this->assertEquals('draft', $legacy_configuration_node->moderation_state->value); 358 $legacy_configuration_node->save(); 359 $this->assertEquals('draft', $legacy_configuration_node->moderation_state->value); 360 361 $configuration['default_moderation_state'] = 'published'; 362 $workflow->getTypePlugin()->setConfiguration($configuration); 363 $workflow->save(); 364 365 $updated_default_node = Node::create([ 366 'title' => 'Test title', 367 'type' => 'example', 368 ]); 369 $this->assertEquals('published', $updated_default_node->moderation_state->value); 370 $legacy_configuration_node->save(); 371 $this->assertEquals('published', $updated_default_node->moderation_state->value); 372 } 373 374 /** 375 * Tests the field item list when used with existing unmoderated content. 376 */ 377 public function testWithExistingUnmoderatedContent() { 378 $node = Node::create([ 379 'title' => 'Test title', 380 'type' => 'unmoderated', 381 ]); 382 $node->save(); 383 $translation = $node->addTranslation('de', $node->toArray()); 384 $translation->title = 'Translated'; 385 $translation->save(); 386 387 $workflow = Workflow::load('editorial'); 388 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'unmoderated'); 389 $workflow->save(); 390 391 // After enabling moderation, both the original node and translation should 392 // have a published moderation state. 393 $node = Node::load($node->id()); 394 $translation = $node->getTranslation('de'); 395 $this->assertEquals('published', $node->moderation_state->value); 396 $this->assertEquals('published', $translation->moderation_state->value); 397 398 // After the node has been updated, both the original node and translation 399 // should still have a value. 400 $node->title = 'Updated title'; 401 $node->save(); 402 $translation = $node->getTranslation('de'); 403 $this->assertEquals('published', $node->moderation_state->value); 404 $this->assertEquals('published', $translation->moderation_state->value); 405 } 406 407 /** 408 * Test generating sample values for entities with a moderation state. 409 */ 410 public function testModerationStateSampleValues() { 411 $this->container->get('current_user')->setAccount( 412 $this->createUser([ 413 'use editorial transition create_new_draft', 414 'use editorial transition publish', 415 ]) 416 ); 417 $sample = $this->container->get('entity_type.manager') 418 ->getStorage('node') 419 ->createWithSampleValues('example'); 420 $this->assertCount(0, $sample->validate()); 421 $this->assertEquals('draft', $sample->moderation_state->value); 422 } 423 424 /** 425 * Tests field item list translation support with unmoderated content. 426 */ 427 public function testTranslationWithExistingUnmoderatedContent() { 428 $node = Node::create([ 429 'title' => 'Published en', 430 'langcode' => 'en', 431 'type' => 'unmoderated', 432 ]); 433 $node->setPublished(); 434 $node->save(); 435 436 $workflow = Workflow::load('editorial'); 437 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'unmoderated'); 438 $workflow->save(); 439 440 $translation = $node->addTranslation('de'); 441 $translation->moderation_state = 'draft'; 442 $translation->save(); 443 444 $node_storage = $this->container->get('entity_type.manager')->getStorage('node'); 445 $node = $node_storage->loadRevision($node_storage->getLatestRevisionId($node->id())); 446 447 $this->assertEquals('published', $node->moderation_state->value); 448 $this->assertEquals('draft', $translation->moderation_state->value); 449 $this->assertTrue($node->isPublished()); 450 $this->assertFalse($translation->isPublished()); 451 } 452 453} 454