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\workflows\Entity\Workflow;
18 * Tests links between a content entity and a content_moderation_state entity.
20 * @group content_moderation
22 class ContentModerationStateTest extends KernelTestBase {
27 public static $modules = [
41 'content_translation',
47 * @var \Drupal\Core\Entity\EntityTypeManager
49 protected $entityTypeManager;
54 protected function setUp() {
57 $this->installSchema('node', 'node_access');
58 $this->installEntitySchema('node');
59 $this->installEntitySchema('user');
60 $this->installEntitySchema('entity_test_rev');
61 $this->installEntitySchema('entity_test_no_bundle');
62 $this->installEntitySchema('entity_test_mulrevpub');
63 $this->installEntitySchema('block_content');
64 $this->installEntitySchema('media');
65 $this->installEntitySchema('file');
66 $this->installEntitySchema('content_moderation_state');
67 $this->installConfig('content_moderation');
68 $this->installSchema('file', 'file_usage');
69 $this->installConfig(['field', 'system', 'image', 'file', 'media']);
71 $this->entityTypeManager = $this->container->get('entity_type.manager');
75 * Tests basic monolingual content moderation through the API.
77 * @dataProvider basicModerationTestCases
79 public function testBasicModeration($entity_type_id) {
80 $entity = $this->createEntity($entity_type_id);
81 if ($entity instanceof EntityPublishedInterface) {
82 $entity->setUnpublished();
85 $entity = $this->reloadEntity($entity);
86 $this->assertEquals('draft', $entity->moderation_state->value);
88 $entity->moderation_state->value = 'published';
91 $entity = $this->reloadEntity($entity);
92 $this->assertEquals('published', $entity->moderation_state->value);
94 // Change the state without saving the node.
95 $content_moderation_state = ContentModerationState::load(1);
96 $content_moderation_state->set('moderation_state', 'draft');
97 $content_moderation_state->setNewRevision(TRUE);
98 $content_moderation_state->save();
100 $entity = $this->reloadEntity($entity, 3);
101 $this->assertEquals('draft', $entity->moderation_state->value);
102 if ($entity instanceof EntityPublishedInterface) {
103 $this->assertFalse($entity->isPublished());
106 // Get the default revision.
107 $entity = $this->reloadEntity($entity);
108 if ($entity instanceof EntityPublishedInterface) {
109 $this->assertTrue((bool) $entity->isPublished());
111 $this->assertEquals(2, $entity->getRevisionId());
113 $entity->moderation_state->value = 'published';
116 $entity = $this->reloadEntity($entity, 4);
117 $this->assertEquals('published', $entity->moderation_state->value);
119 // Get the default revision.
120 $entity = $this->reloadEntity($entity);
121 if ($entity instanceof EntityPublishedInterface) {
122 $this->assertTrue((bool) $entity->isPublished());
124 $this->assertEquals(4, $entity->getRevisionId());
126 // Update the node to archived which will then be the default revision.
127 $entity->moderation_state->value = 'archived';
130 // Revert to the previous (published) revision.
131 $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
132 $previous_revision = $entity_storage->loadRevision(4);
133 $previous_revision->isDefaultRevision(TRUE);
134 $previous_revision->setNewRevision(TRUE);
135 $previous_revision->save();
137 // Get the default revision.
138 $entity = $this->reloadEntity($entity);
139 $this->assertEquals('published', $entity->moderation_state->value);
140 if ($entity instanceof EntityPublishedInterface) {
141 $this->assertTrue($entity->isPublished());
144 // Set an invalid moderation state.
145 $this->setExpectedException(EntityStorageException::class);
146 $entity->moderation_state->value = 'foobar';
151 * Test cases for basic moderation test.
153 public function basicModerationTestCases() {
164 'Test entity - revisions, data table, and published interface' => [
165 'entity_test_mulrevpub',
167 'Entity Test with revisions' => [
170 'Entity without bundle' => [
171 'entity_test_no_bundle',
177 * Tests removal of content moderation state entity.
179 * @dataProvider basicModerationTestCases
181 public function testContentModerationStateDataRemoval($entity_type_id) {
182 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
183 $entity = $this->createEntity($entity_type_id);
185 $entity = $this->reloadEntity($entity);
187 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
188 $this->assertFalse($content_moderation_state);
192 * Tests removal of content moderation state entity revisions.
194 * @dataProvider basicModerationTestCases
196 public function testContentModerationStateRevisionDataRemoval($entity_type_id) {
197 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
198 $entity = $this->createEntity($entity_type_id);
200 $revision = clone $entity;
201 $revision->isDefaultRevision(FALSE);
202 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($revision);
203 $this->assertTrue($content_moderation_state);
204 $entity = $this->reloadEntity($entity);
205 $entity->setNewRevision(TRUE);
207 $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
208 $entity_storage->deleteRevision($revision->getRevisionId());
209 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($revision);
210 $this->assertFalse($content_moderation_state);
211 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
212 $this->assertTrue($content_moderation_state);
216 * Tests removal of content moderation state pending entity revisions.
218 * @dataProvider basicModerationTestCases
220 public function testContentModerationStatePendingRevisionDataRemoval($entity_type_id) {
221 $entity = $this->createEntity($entity_type_id);
222 $entity->moderation_state = 'published';
224 $entity->setNewRevision(TRUE);
225 $entity->moderation_state = 'draft';
228 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
229 $this->assertTrue($content_moderation_state);
231 $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
232 $entity_storage->deleteRevision($entity->getRevisionId());
234 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
235 $this->assertFalse($content_moderation_state);
239 * Tests removal of content moderation state translations.
241 * @dataProvider basicModerationTestCases
243 public function testContentModerationStateTranslationDataRemoval($entity_type_id) {
244 // Test content moderation state translation deletion.
245 if ($this->entityTypeManager->getDefinition($entity_type_id)->isTranslatable()) {
246 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
247 $entity = $this->createEntity($entity_type_id);
249 ConfigurableLanguage::createFromLangcode($langcode)
252 $translation = $entity->addTranslation($langcode, ['title' => 'Titolo test']);
253 // Make sure we add values for all of the required fields.
254 if ($entity_type_id == 'block_content') {
255 $translation->info = $this->randomString();
257 $translation->save();
258 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
259 $this->assertTrue($content_moderation_state->hasTranslation($langcode));
260 $entity->removeTranslation($langcode);
262 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
263 $this->assertFalse($content_moderation_state->hasTranslation($langcode));
268 * Tests basic multilingual content moderation through the API.
270 public function testMultilingualModeration() {
272 ConfigurableLanguage::createFromLangcode('fr')->save();
273 $node_type = NodeType::create([
278 $workflow = Workflow::load('editorial');
279 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
282 $english_node = Node::create([
284 'title' => 'Test title',
290 $this->assertEquals('draft', $english_node->moderation_state->value);
291 $this->assertFalse($english_node->isPublished());
293 // Create a French translation.
294 $french_node = $english_node->addTranslation('fr', ['title' => 'French title']);
295 $french_node->setUnpublished();
297 $french_node->save();
298 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
299 $this->assertEquals('draft', $french_node->moderation_state->value);
300 $this->assertFalse($french_node->isPublished());
302 // Move English node to create another draft.
303 $english_node = $this->reloadEntity($english_node);
304 $english_node->moderation_state->value = 'draft';
305 // Revision 2 (en, fr).
306 $english_node->save();
307 $english_node = $this->reloadEntity($english_node);
308 $this->assertEquals('draft', $english_node->moderation_state->value);
310 // French node should still be in draft.
311 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
312 $this->assertEquals('draft', $french_node->moderation_state->value);
314 // Publish the French node.
315 $french_node->moderation_state->value = 'published';
316 // Revision 3 (en, fr).
317 $french_node->save();
318 $french_node = $this->reloadEntity($french_node)->getTranslation('fr');
319 $this->assertTrue($french_node->isPublished());
320 $this->assertEquals('published', $french_node->moderation_state->value);
321 $this->assertTrue($french_node->isPublished());
322 $english_node = $french_node->getTranslation('en');
323 $this->assertEquals('draft', $english_node->moderation_state->value);
325 // Publish the English node.
326 $english_node->moderation_state->value = 'published';
327 // Revision 4 (en, fr).
328 $english_node->save();
329 $english_node = $this->reloadEntity($english_node);
330 $this->assertTrue($english_node->isPublished());
332 // Move the French node back to draft.
333 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
334 $this->assertTrue($french_node->isPublished());
335 $french_node->moderation_state->value = 'draft';
336 // Revision 5 (en, fr).
337 $french_node->save();
338 $french_node = $this->reloadEntity($english_node, 5)->getTranslation('fr');
339 $this->assertFalse($french_node->isPublished());
340 $this->assertTrue($french_node->getTranslation('en')->isPublished());
342 // Republish the French node.
343 $french_node->moderation_state->value = 'published';
344 // Revision 6 (en, fr).
345 $french_node->save();
346 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
347 $this->assertTrue($french_node->isPublished());
349 // Change the EN state without saving the node.
350 $content_moderation_state = ContentModerationState::load(1);
351 $content_moderation_state->set('moderation_state', 'draft');
352 $content_moderation_state->setNewRevision(TRUE);
353 // Revision 7 (en, fr).
354 $content_moderation_state->save();
355 $english_node = $this->reloadEntity($french_node, $french_node->getRevisionId() + 1);
357 $this->assertEquals('draft', $english_node->moderation_state->value);
358 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
359 $this->assertEquals('published', $french_node->moderation_state->value);
361 // This should unpublish the French node.
362 $content_moderation_state = ContentModerationState::load(1);
363 $content_moderation_state = $content_moderation_state->getTranslation('fr');
364 $content_moderation_state->set('moderation_state', 'draft');
365 $content_moderation_state->setNewRevision(TRUE);
366 // Revision 8 (en, fr).
367 $content_moderation_state->save();
369 $english_node = $this->reloadEntity($english_node, $english_node->getRevisionId());
370 $this->assertEquals('draft', $english_node->moderation_state->value);
371 $french_node = $this->reloadEntity($english_node, '8')->getTranslation('fr');
372 $this->assertEquals('draft', $french_node->moderation_state->value);
373 // Switching the moderation state to an unpublished state should update the
375 $this->assertFalse($french_node->isPublished());
377 // Get the default english node.
378 $english_node = $this->reloadEntity($english_node);
379 $this->assertTrue($english_node->isPublished());
380 $this->assertEquals(6, $english_node->getRevisionId());
384 * Tests moderation when the moderation_state field has a config override.
386 public function testModerationWithFieldConfigOverride() {
388 'type' => 'test_type',
391 $workflow = Workflow::load('editorial');
392 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'test_type');
395 $fields = $this->container->get('entity_field.manager')->getFieldDefinitions('node', 'test_type');
396 $field_config = $fields['moderation_state']->getConfig('test_type');
397 $field_config->setLabel('Field Override!');
398 $field_config->save();
400 $node = Node::create([
401 'title' => 'Test node',
402 'type' => 'test_type',
405 $this->assertFalse($node->isPublished());
406 $this->assertEquals('draft', $node->moderation_state->value);
408 $node->moderation_state = 'published';
410 $this->assertTrue($node->isPublished());
411 $this->assertEquals('published', $node->moderation_state->value);
415 * Tests that entities with special languages can be moderated.
417 public function testModerationWithSpecialLanguages() {
418 $workflow = Workflow::load('editorial');
419 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
422 // Create a test entity.
423 $entity = EntityTestRev::create([
424 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
427 $this->assertEquals('draft', $entity->moderation_state->value);
429 $entity->moderation_state->value = 'published';
432 $this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
436 * Tests that a non-translatable entity type with a langcode can be moderated.
438 public function testNonTranslatableEntityTypeModeration() {
439 $workflow = Workflow::load('editorial');
440 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
443 // Check that the tested entity type is not translatable.
444 $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_rev');
445 $this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
447 // Create a test entity.
448 $entity = EntityTestRev::create();
450 $this->assertEquals('draft', $entity->moderation_state->value);
452 $entity->moderation_state->value = 'published';
455 $this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
459 * Tests that a non-translatable entity type without a langcode can be
462 public function testNonLangcodeEntityTypeModeration() {
463 // Unset the langcode entity key for 'entity_test_rev'.
464 $entity_type = clone \Drupal::entityTypeManager()->getDefinition('entity_test_rev');
465 $keys = $entity_type->getKeys();
466 unset($keys['langcode']);
467 $entity_type->set('entity_keys', $keys);
468 \Drupal::state()->set('entity_test_rev.entity_type', $entity_type);
470 // Update the entity type in order to remove the 'langcode' field.
471 \Drupal::entityDefinitionUpdateManager()->applyUpdates();
473 $workflow = Workflow::load('editorial');
474 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
477 // Check that the tested entity type is not translatable and does not have a
478 // 'langcode' entity key.
479 $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_rev');
480 $this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
481 $this->assertFalse($entity_type->getKey('langcode'), "The test entity type does not have a 'langcode' entity key.");
483 // Create a test entity.
484 $entity = EntityTestRev::create();
486 $this->assertEquals('draft', $entity->moderation_state->value);
488 $entity->moderation_state->value = 'published';
491 $this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
495 * Tests the dependencies of the workflow when using content moderation.
497 public function testWorkflowDependencies() {
498 $node_type = NodeType::create([
503 $workflow = Workflow::load('editorial');
504 // Test both a config and non-config based bundle and entity type.
505 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
506 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
507 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_no_bundle', 'entity_test_no_bundle');
510 $this->assertEquals([
512 'content_moderation',
518 ], $workflow->getDependencies());
520 $this->assertEquals([
521 'entity_test_no_bundle',
524 ], $workflow->getTypePlugin()->getEntityTypes());
526 // Delete the node type and ensure it is removed from the workflow.
527 $node_type->delete();
528 $workflow = Workflow::load('editorial');
529 $entity_types = $workflow->getTypePlugin()->getEntityTypes();
530 $this->assertFalse(in_array('node', $entity_types));
532 // Uninstall entity test and ensure it's removed from the workflow.
533 $this->container->get('config.manager')->uninstall('module', 'entity_test');
534 $workflow = Workflow::load('editorial');
535 $entity_types = $workflow->getTypePlugin()->getEntityTypes();
536 $this->assertEquals([], $entity_types);
540 * Test the content moderation workflow dependencies for non-config bundles.
542 public function testWorkflowNonConfigBundleDependencies() {
543 // Create a bundle not based on any particular configuration.
544 entity_test_create_bundle('test_bundle');
546 $workflow = Workflow::load('editorial');
547 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test', 'test_bundle');
550 // Ensure the bundle is correctly added to the workflow.
551 $this->assertEquals([
553 'content_moderation',
556 ], $workflow->getDependencies());
557 $this->assertEquals([
559 ], $workflow->getTypePlugin()->getBundlesForEntityType('entity_test'));
561 // Delete the test bundle to ensure the workflow entity responds
563 entity_test_delete_bundle('test_bundle');
565 $workflow = Workflow::load('editorial');
566 $this->assertEquals([], $workflow->getTypePlugin()->getBundlesForEntityType('entity_test'));
567 $this->assertEquals([
569 'content_moderation',
571 ], $workflow->getDependencies());
575 * Test the revision default state of the moderation state entity revisions.
577 * @param string $entity_type_id
578 * The ID of entity type to be tested.
580 * @dataProvider basicModerationTestCases
582 public function testRevisionDefaultState($entity_type_id) {
583 // Check that the revision default state of the moderated entity and the
584 // content moderation state entity always match.
585 /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
586 $storage = $this->entityTypeManager->getStorage($entity_type_id);
587 /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $cms_storage */
588 $cms_storage = $this->entityTypeManager->getStorage('content_moderation_state');
590 $entity = $this->createEntity($entity_type_id);
591 $entity->get('moderation_state')->value = 'published';
592 $storage->save($entity);
593 /** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */
594 $cms_entity = $cms_storage->loadUnchanged(1);
595 $this->assertEquals($entity->getLoadedRevisionId(), $cms_entity->get('content_entity_revision_id')->value);
597 $entity->get('moderation_state')->value = 'published';
598 $storage->save($entity);
599 /** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */
600 $cms_entity = $cms_storage->loadUnchanged(1);
601 $this->assertEquals($entity->getLoadedRevisionId(), $cms_entity->get('content_entity_revision_id')->value);
603 $entity->get('moderation_state')->value = 'draft';
604 $storage->save($entity);
605 /** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */
606 $cms_entity = $cms_storage->loadUnchanged(1);
607 $this->assertEquals($entity->getLoadedRevisionId() - 1, $cms_entity->get('content_entity_revision_id')->value);
609 $entity->get('moderation_state')->value = 'published';
610 $storage->save($entity);
611 /** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */
612 $cms_entity = $cms_storage->loadUnchanged(1);
613 $this->assertEquals($entity->getLoadedRevisionId(), $cms_entity->get('content_entity_revision_id')->value);
619 * The entity will have required fields populated and the corresponding bundle
620 * will be enabled for content moderation.
622 * @param string $entity_type_id
623 * The entity type ID.
625 * @return \Drupal\Core\Entity\ContentEntityInterface
626 * The created entity.
628 protected function createEntity($entity_type_id) {
629 $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
631 $bundle_id = $entity_type_id;
632 // Set up a bundle entity type for the specified entity type, if needed.
633 if ($bundle_entity_type_id = $entity_type->getBundleEntityType()) {
634 $bundle_entity_type = $this->entityTypeManager->getDefinition($bundle_entity_type_id);
635 $bundle_entity_storage = $this->entityTypeManager->getStorage($bundle_entity_type_id);
637 $bundle_id = 'example';
638 if (!$bundle_entity_storage->load($bundle_id)) {
639 $bundle_entity = $bundle_entity_storage->create([
640 $bundle_entity_type->getKey('id') => 'example',
642 if ($entity_type_id == 'media') {
643 $bundle_entity->set('source', 'test');
644 $bundle_entity->save();
645 $source_field = $bundle_entity->getSource()->createSourceField($bundle_entity);
646 $source_field->getFieldStorageDefinition()->save();
647 $source_field->save();
648 $bundle_entity->set('source_configuration', [
649 'source_field' => $source_field->getName(),
652 $bundle_entity->save();
656 $workflow = Workflow::load('editorial');
657 $workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle_id);
660 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
661 $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
662 $entity = $entity_storage->create([
663 $entity_type->getKey('label') => 'Test title',
664 $entity_type->getKey('bundle') => $bundle_id,
666 // Make sure we add values for all of the required fields.
667 if ($entity_type_id == 'block_content') {
668 $entity->info = $this->randomString();
674 * Reloads the entity after clearing the static cache.
676 * @param \Drupal\Core\Entity\EntityInterface $entity
677 * The entity to reload.
678 * @param int|bool $revision_id
679 * The specific revision ID to load. Defaults FALSE and just loads the
682 * @return \Drupal\Core\Entity\EntityInterface
683 * The reloaded entity.
685 protected function reloadEntity(EntityInterface $entity, $revision_id = FALSE) {
686 $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId());
687 $storage->resetCache([$entity->id()]);
689 return $storage->loadRevision($revision_id);
691 return $storage->load($entity->id());