3 namespace Drupal\Tests\content_moderation\Kernel;
5 use Drupal\content_moderation\Entity\ContentModerationState;
6 use Drupal\Core\Entity\EntityInterface;
7 use Drupal\Core\Entity\EntityPublishedInterface;
8 use Drupal\Core\Entity\EntityStorageException;
9 use Drupal\Core\Language\LanguageInterface;
10 use Drupal\entity_test\Entity\EntityTestRev;
11 use Drupal\KernelTests\KernelTestBase;
12 use Drupal\language\Entity\ConfigurableLanguage;
13 use Drupal\node\Entity\Node;
14 use Drupal\node\Entity\NodeType;
15 use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
16 use Drupal\workflows\Entity\Workflow;
19 * Tests links between a content entity and a content_moderation_state entity.
21 * @group content_moderation
23 class ContentModerationStateTest extends KernelTestBase {
25 use ContentModerationTestTrait;
30 public static $modules = [
44 'content_translation',
50 * @var \Drupal\Core\Entity\EntityTypeManager
52 protected $entityTypeManager;
57 protected function setUp() {
60 $this->installSchema('node', 'node_access');
61 $this->installEntitySchema('node');
62 $this->installEntitySchema('user');
63 $this->installEntitySchema('entity_test_rev');
64 $this->installEntitySchema('entity_test_no_bundle');
65 $this->installEntitySchema('entity_test_mulrevpub');
66 $this->installEntitySchema('block_content');
67 $this->installEntitySchema('media');
68 $this->installEntitySchema('file');
69 $this->installEntitySchema('content_moderation_state');
70 $this->installConfig('content_moderation');
71 $this->installSchema('file', 'file_usage');
72 $this->installConfig(['field', 'system', 'image', 'file', 'media']);
74 $this->entityTypeManager = $this->container->get('entity_type.manager');
78 * Tests basic monolingual content moderation through the API.
80 * @dataProvider basicModerationTestCases
82 public function testBasicModeration($entity_type_id) {
83 $entity = $this->createEntity($entity_type_id);
84 if ($entity instanceof EntityPublishedInterface) {
85 $entity->setUnpublished();
88 $entity = $this->reloadEntity($entity);
89 $this->assertEquals('draft', $entity->moderation_state->value);
91 $entity->moderation_state->value = 'published';
94 $entity = $this->reloadEntity($entity);
95 $this->assertEquals('published', $entity->moderation_state->value);
97 // Change the state without saving the node.
98 $content_moderation_state = ContentModerationState::load(1);
99 $content_moderation_state->set('moderation_state', 'draft');
100 $content_moderation_state->setNewRevision(TRUE);
101 $content_moderation_state->save();
103 $entity = $this->reloadEntity($entity, 3);
104 $this->assertEquals('draft', $entity->moderation_state->value);
105 if ($entity instanceof EntityPublishedInterface) {
106 $this->assertFalse($entity->isPublished());
109 // Get the default revision.
110 $entity = $this->reloadEntity($entity);
111 if ($entity instanceof EntityPublishedInterface) {
112 $this->assertTrue((bool) $entity->isPublished());
114 $this->assertEquals(2, $entity->getRevisionId());
116 $entity->moderation_state->value = 'published';
119 $entity = $this->reloadEntity($entity, 4);
120 $this->assertEquals('published', $entity->moderation_state->value);
122 // Get the default revision.
123 $entity = $this->reloadEntity($entity);
124 if ($entity instanceof EntityPublishedInterface) {
125 $this->assertTrue((bool) $entity->isPublished());
127 $this->assertEquals(4, $entity->getRevisionId());
129 // Update the node to archived which will then be the default revision.
130 $entity->moderation_state->value = 'archived';
133 // Revert to the previous (published) revision.
134 $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
135 $previous_revision = $entity_storage->loadRevision(4);
136 $previous_revision->isDefaultRevision(TRUE);
137 $previous_revision->setNewRevision(TRUE);
138 $previous_revision->save();
140 // Get the default revision.
141 $entity = $this->reloadEntity($entity);
142 $this->assertEquals('published', $entity->moderation_state->value);
143 if ($entity instanceof EntityPublishedInterface) {
144 $this->assertTrue($entity->isPublished());
147 // Set an invalid moderation state.
148 $this->setExpectedException(EntityStorageException::class);
149 $entity->moderation_state->value = 'foobar';
154 * Test cases for basic moderation test.
156 public function basicModerationTestCases() {
167 'Test entity - revisions, data table, and published interface' => [
168 'entity_test_mulrevpub',
170 'Entity Test with revisions' => [
173 'Entity without bundle' => [
174 'entity_test_no_bundle',
180 * Tests removal of content moderation state entity.
182 * @dataProvider basicModerationTestCases
184 public function testContentModerationStateDataRemoval($entity_type_id) {
185 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
186 $entity = $this->createEntity($entity_type_id);
188 $entity = $this->reloadEntity($entity);
190 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
191 $this->assertFalse($content_moderation_state);
195 * Tests removal of content moderation state entity revisions.
197 * @dataProvider basicModerationTestCases
199 public function testContentModerationStateRevisionDataRemoval($entity_type_id) {
200 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
201 $entity = $this->createEntity($entity_type_id);
203 $revision = clone $entity;
204 $revision->isDefaultRevision(FALSE);
205 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($revision);
206 $this->assertTrue($content_moderation_state);
207 $entity = $this->reloadEntity($entity);
208 $entity->setNewRevision(TRUE);
210 $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
211 $entity_storage->deleteRevision($revision->getRevisionId());
212 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($revision);
213 $this->assertFalse($content_moderation_state);
214 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
215 $this->assertTrue($content_moderation_state);
219 * Tests removal of content moderation state pending entity revisions.
221 * @dataProvider basicModerationTestCases
223 public function testContentModerationStatePendingRevisionDataRemoval($entity_type_id) {
224 $entity = $this->createEntity($entity_type_id);
225 $entity->moderation_state = 'published';
227 $entity->setNewRevision(TRUE);
228 $entity->moderation_state = 'draft';
231 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
232 $this->assertTrue($content_moderation_state);
234 $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
235 $entity_storage->deleteRevision($entity->getRevisionId());
237 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
238 $this->assertFalse($content_moderation_state);
242 * Tests removal of content moderation state translations.
244 * @dataProvider basicModerationTestCases
246 public function testContentModerationStateTranslationDataRemoval($entity_type_id) {
247 // Test content moderation state translation deletion.
248 if ($this->entityTypeManager->getDefinition($entity_type_id)->isTranslatable()) {
249 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
250 $entity = $this->createEntity($entity_type_id);
252 ConfigurableLanguage::createFromLangcode($langcode)
255 $translation = $entity->addTranslation($langcode, ['title' => 'Titolo test']);
256 // Make sure we add values for all of the required fields.
257 if ($entity_type_id == 'block_content') {
258 $translation->info = $this->randomString();
260 $translation->save();
261 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
262 $this->assertTrue($content_moderation_state->hasTranslation($langcode));
263 $entity->removeTranslation($langcode);
265 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
266 $this->assertFalse($content_moderation_state->hasTranslation($langcode));
271 * Tests basic multilingual content moderation through the API.
273 public function testMultilingualModeration() {
275 ConfigurableLanguage::createFromLangcode('fr')->save();
276 $node_type = NodeType::create([
281 $workflow = $this->createEditorialWorkflow();
282 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
285 $english_node = Node::create([
287 'title' => 'Test title',
293 $this->assertEquals('draft', $english_node->moderation_state->value);
294 $this->assertFalse($english_node->isPublished());
296 // Create a French translation.
297 $french_node = $english_node->addTranslation('fr', ['title' => 'French title']);
298 $french_node->setUnpublished();
300 $french_node->save();
301 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
302 $this->assertEquals('draft', $french_node->moderation_state->value);
303 $this->assertFalse($french_node->isPublished());
305 // Move English node to create another draft.
306 $english_node = $this->reloadEntity($english_node);
307 $english_node->moderation_state->value = 'draft';
308 // Revision 2 (en, fr).
309 $english_node->save();
310 $english_node = $this->reloadEntity($english_node);
311 $this->assertEquals('draft', $english_node->moderation_state->value);
313 // French node should still be in draft.
314 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
315 $this->assertEquals('draft', $french_node->moderation_state->value);
317 // Publish the French node.
318 $french_node->moderation_state->value = 'published';
319 // Revision 3 (en, fr).
320 $french_node->save();
321 $french_node = $this->reloadEntity($french_node)->getTranslation('fr');
322 $this->assertTrue($french_node->isPublished());
323 $this->assertEquals('published', $french_node->moderation_state->value);
324 $this->assertTrue($french_node->isPublished());
325 $english_node = $french_node->getTranslation('en');
326 $this->assertEquals('draft', $english_node->moderation_state->value);
328 // Publish the English node.
329 $english_node->moderation_state->value = 'published';
330 // Revision 4 (en, fr).
331 $english_node->save();
332 $english_node = $this->reloadEntity($english_node);
333 $this->assertTrue($english_node->isPublished());
335 // Move the French node back to draft.
336 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
337 $this->assertTrue($french_node->isPublished());
338 $french_node->moderation_state->value = 'draft';
339 // Revision 5 (en, fr).
340 $french_node->save();
341 $french_node = $this->reloadEntity($english_node, 5)->getTranslation('fr');
342 $this->assertFalse($french_node->isPublished());
343 $this->assertTrue($french_node->getTranslation('en')->isPublished());
345 // Republish the French node.
346 $french_node->moderation_state->value = 'published';
347 // Revision 6 (en, fr).
348 $french_node->save();
349 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
350 $this->assertTrue($french_node->isPublished());
352 // Change the EN state without saving the node.
353 $content_moderation_state = ContentModerationState::load(1);
354 $content_moderation_state->set('moderation_state', 'draft');
355 $content_moderation_state->setNewRevision(TRUE);
356 // Revision 7 (en, fr).
357 $content_moderation_state->save();
358 $english_node = $this->reloadEntity($french_node, $french_node->getRevisionId() + 1);
360 $this->assertEquals('draft', $english_node->moderation_state->value);
361 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
362 $this->assertEquals('published', $french_node->moderation_state->value);
364 // This should unpublish the French node.
365 $content_moderation_state = ContentModerationState::load(1);
366 $content_moderation_state = $content_moderation_state->getTranslation('fr');
367 $content_moderation_state->set('moderation_state', 'draft');
368 $content_moderation_state->setNewRevision(TRUE);
369 // Revision 8 (en, fr).
370 $content_moderation_state->save();
372 $english_node = $this->reloadEntity($english_node, $english_node->getRevisionId());
373 $this->assertEquals('draft', $english_node->moderation_state->value);
374 $french_node = $this->reloadEntity($english_node, '8')->getTranslation('fr');
375 $this->assertEquals('draft', $french_node->moderation_state->value);
376 // Switching the moderation state to an unpublished state should update the
378 $this->assertFalse($french_node->isPublished());
380 // Get the default english node.
381 $english_node = $this->reloadEntity($english_node);
382 $this->assertTrue($english_node->isPublished());
383 $this->assertEquals(6, $english_node->getRevisionId());
387 * Tests moderation when the moderation_state field has a config override.
389 public function testModerationWithFieldConfigOverride() {
391 'type' => 'test_type',
394 $workflow = $this->createEditorialWorkflow();
395 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'test_type');
398 $fields = $this->container->get('entity_field.manager')->getFieldDefinitions('node', 'test_type');
399 $field_config = $fields['moderation_state']->getConfig('test_type');
400 $field_config->setLabel('Field Override!');
401 $field_config->save();
403 $node = Node::create([
404 'title' => 'Test node',
405 'type' => 'test_type',
408 $this->assertFalse($node->isPublished());
409 $this->assertEquals('draft', $node->moderation_state->value);
411 $node->moderation_state = 'published';
413 $this->assertTrue($node->isPublished());
414 $this->assertEquals('published', $node->moderation_state->value);
418 * Tests that entities with special languages can be moderated.
420 public function testModerationWithSpecialLanguages() {
421 $workflow = $this->createEditorialWorkflow();
422 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
425 // Create a test entity.
426 $entity = EntityTestRev::create([
427 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
430 $this->assertEquals('draft', $entity->moderation_state->value);
432 $entity->moderation_state->value = 'published';
435 $this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
439 * Tests that a non-translatable entity type with a langcode can be moderated.
441 public function testNonTranslatableEntityTypeModeration() {
442 $workflow = $this->createEditorialWorkflow();
443 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
446 // Check that the tested entity type is not translatable.
447 $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_rev');
448 $this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
450 // Create a test entity.
451 $entity = EntityTestRev::create();
453 $this->assertEquals('draft', $entity->moderation_state->value);
455 $entity->moderation_state->value = 'published';
458 $this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
462 * Tests that a non-translatable entity type without a langcode can be
465 public function testNonLangcodeEntityTypeModeration() {
466 // Unset the langcode entity key for 'entity_test_rev'.
467 $entity_type = clone \Drupal::entityTypeManager()->getDefinition('entity_test_rev');
468 $keys = $entity_type->getKeys();
469 unset($keys['langcode']);
470 $entity_type->set('entity_keys', $keys);
471 \Drupal::state()->set('entity_test_rev.entity_type', $entity_type);
473 // Update the entity type in order to remove the 'langcode' field.
474 \Drupal::entityDefinitionUpdateManager()->applyUpdates();
476 $workflow = $this->createEditorialWorkflow();
477 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
480 // Check that the tested entity type is not translatable and does not have a
481 // 'langcode' entity key.
482 $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_rev');
483 $this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
484 $this->assertFalse($entity_type->getKey('langcode'), "The test entity type does not have a 'langcode' entity key.");
486 // Create a test entity.
487 $entity = EntityTestRev::create();
489 $this->assertEquals('draft', $entity->moderation_state->value);
491 $entity->moderation_state->value = 'published';
494 $this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
498 * Tests the dependencies of the workflow when using content moderation.
500 public function testWorkflowDependencies() {
501 $node_type = NodeType::create([
506 $workflow = $this->createEditorialWorkflow();
507 // Test both a config and non-config based bundle and entity type.
508 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
509 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
510 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_no_bundle', 'entity_test_no_bundle');
513 $this->assertEquals([
515 'content_moderation',
521 ], $workflow->getDependencies());
523 $this->assertEquals([
524 'entity_test_no_bundle',
527 ], $workflow->getTypePlugin()->getEntityTypes());
529 // Delete the node type and ensure it is removed from the workflow.
530 $node_type->delete();
531 $workflow = Workflow::load('editorial');
532 $entity_types = $workflow->getTypePlugin()->getEntityTypes();
533 $this->assertFalse(in_array('node', $entity_types));
535 // Uninstall entity test and ensure it's removed from the workflow.
536 $this->container->get('config.manager')->uninstall('module', 'entity_test');
537 $workflow = Workflow::load('editorial');
538 $entity_types = $workflow->getTypePlugin()->getEntityTypes();
539 $this->assertEquals([], $entity_types);
543 * Test the content moderation workflow dependencies for non-config bundles.
545 public function testWorkflowNonConfigBundleDependencies() {
546 // Create a bundle not based on any particular configuration.
547 entity_test_create_bundle('test_bundle');
549 $workflow = $this->createEditorialWorkflow();
550 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test', 'test_bundle');
553 // Ensure the bundle is correctly added to the workflow.
554 $this->assertEquals([
556 'content_moderation',
559 ], $workflow->getDependencies());
560 $this->assertEquals([
562 ], $workflow->getTypePlugin()->getBundlesForEntityType('entity_test'));
564 // Delete the test bundle to ensure the workflow entity responds
566 entity_test_delete_bundle('test_bundle');
568 $workflow = Workflow::load('editorial');
569 $this->assertEquals([], $workflow->getTypePlugin()->getBundlesForEntityType('entity_test'));
570 $this->assertEquals([
572 'content_moderation',
574 ], $workflow->getDependencies());
578 * Test the revision default state of the moderation state entity revisions.
580 * @param string $entity_type_id
581 * The ID of entity type to be tested.
583 * @dataProvider basicModerationTestCases
585 public function testRevisionDefaultState($entity_type_id) {
586 // Check that the revision default state of the moderated entity and the
587 // content moderation state entity always match.
588 /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
589 $storage = $this->entityTypeManager->getStorage($entity_type_id);
590 /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $cms_storage */
591 $cms_storage = $this->entityTypeManager->getStorage('content_moderation_state');
593 $entity = $this->createEntity($entity_type_id);
594 $entity->get('moderation_state')->value = 'published';
595 $storage->save($entity);
596 /** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */
597 $cms_entity = $cms_storage->loadUnchanged(1);
598 $this->assertEquals($entity->getLoadedRevisionId(), $cms_entity->get('content_entity_revision_id')->value);
600 $entity->get('moderation_state')->value = 'published';
601 $storage->save($entity);
602 /** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */
603 $cms_entity = $cms_storage->loadUnchanged(1);
604 $this->assertEquals($entity->getLoadedRevisionId(), $cms_entity->get('content_entity_revision_id')->value);
606 $entity->get('moderation_state')->value = 'draft';
607 $storage->save($entity);
608 /** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */
609 $cms_entity = $cms_storage->loadUnchanged(1);
610 $this->assertEquals($entity->getLoadedRevisionId() - 1, $cms_entity->get('content_entity_revision_id')->value);
612 $entity->get('moderation_state')->value = 'published';
613 $storage->save($entity);
614 /** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */
615 $cms_entity = $cms_storage->loadUnchanged(1);
616 $this->assertEquals($entity->getLoadedRevisionId(), $cms_entity->get('content_entity_revision_id')->value);
622 * The entity will have required fields populated and the corresponding bundle
623 * will be enabled for content moderation.
625 * @param string $entity_type_id
626 * The entity type ID.
628 * @return \Drupal\Core\Entity\ContentEntityInterface
629 * The created entity.
631 protected function createEntity($entity_type_id) {
632 $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
634 $bundle_id = $entity_type_id;
635 // Set up a bundle entity type for the specified entity type, if needed.
636 if ($bundle_entity_type_id = $entity_type->getBundleEntityType()) {
637 $bundle_entity_type = $this->entityTypeManager->getDefinition($bundle_entity_type_id);
638 $bundle_entity_storage = $this->entityTypeManager->getStorage($bundle_entity_type_id);
640 $bundle_id = 'example';
641 if (!$bundle_entity_storage->load($bundle_id)) {
642 $bundle_entity = $bundle_entity_storage->create([
643 $bundle_entity_type->getKey('id') => 'example',
645 if ($entity_type_id == 'media') {
646 $bundle_entity->set('source', 'test');
647 $bundle_entity->save();
648 $source_field = $bundle_entity->getSource()->createSourceField($bundle_entity);
649 $source_field->getFieldStorageDefinition()->save();
650 $source_field->save();
651 $bundle_entity->set('source_configuration', [
652 'source_field' => $source_field->getName(),
655 $bundle_entity->save();
659 $workflow = $this->createEditorialWorkflow();
660 $workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle_id);
663 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
664 $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
665 $entity = $entity_storage->create([
666 $entity_type->getKey('label') => 'Test title',
667 $entity_type->getKey('bundle') => $bundle_id,
669 // Make sure we add values for all of the required fields.
670 if ($entity_type_id == 'block_content') {
671 $entity->info = $this->randomString();
677 * Reloads the entity after clearing the static cache.
679 * @param \Drupal\Core\Entity\EntityInterface $entity
680 * The entity to reload.
681 * @param int|bool $revision_id
682 * The specific revision ID to load. Defaults FALSE and just loads the
685 * @return \Drupal\Core\Entity\EntityInterface
686 * The reloaded entity.
688 protected function reloadEntity(EntityInterface $entity, $revision_id = FALSE) {
689 $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId());
690 $storage->resetCache([$entity->id()]);
692 return $storage->loadRevision($revision_id);
694 return $storage->load($entity->id());