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 field data.
179 * @dataProvider basicModerationTestCases
181 public function testContentModerationStateDataRemoval($entity_type_id) {
182 // Test content moderation state deletion.
183 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
184 $entity = $this->createEntity($entity_type_id);
186 $entity = $this->reloadEntity($entity);
188 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
189 $this->assertFalse($content_moderation_state);
191 // Test content moderation state revision deletion.
192 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity2 */
193 $entity2 = $this->createEntity($entity_type_id);
195 $revision = clone $entity2;
196 $revision->isDefaultRevision(FALSE);
197 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($revision);
198 $this->assertTrue($content_moderation_state);
199 $entity2 = $this->reloadEntity($entity2);
200 $entity2->setNewRevision(TRUE);
202 $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
203 $entity_storage->deleteRevision($revision->getRevisionId());
204 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($revision);
205 $this->assertFalse($content_moderation_state);
206 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity2);
207 $this->assertTrue($content_moderation_state);
209 // Test content moderation state translation deletion.
210 if ($this->entityTypeManager->getDefinition($entity_type_id)->isTranslatable()) {
211 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity3 */
212 $entity3 = $this->createEntity($entity_type_id);
214 ConfigurableLanguage::createFromLangcode($langcode)
217 $translation = $entity3->addTranslation($langcode, ['title' => 'Titolo test']);
218 // Make sure we add values for all of the required fields.
219 if ($entity_type_id == 'block_content') {
220 $translation->info = $this->randomString();
222 $translation->save();
223 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity3);
224 $this->assertTrue($content_moderation_state->hasTranslation($langcode));
225 $entity3->removeTranslation($langcode);
227 $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity3);
228 $this->assertFalse($content_moderation_state->hasTranslation($langcode));
233 * Tests basic multilingual content moderation through the API.
235 public function testMultilingualModeration() {
237 ConfigurableLanguage::createFromLangcode('fr')->save();
238 $node_type = NodeType::create([
243 $workflow = Workflow::load('editorial');
244 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
247 $english_node = Node::create([
249 'title' => 'Test title',
255 $this->assertEquals('draft', $english_node->moderation_state->value);
256 $this->assertFalse($english_node->isPublished());
258 // Create a French translation.
259 $french_node = $english_node->addTranslation('fr', ['title' => 'French title']);
260 $french_node->setUnpublished();
262 $french_node->save();
263 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
264 $this->assertEquals('draft', $french_node->moderation_state->value);
265 $this->assertFalse($french_node->isPublished());
267 // Move English node to create another draft.
268 $english_node = $this->reloadEntity($english_node);
269 $english_node->moderation_state->value = 'draft';
270 // Revision 2 (en, fr).
271 $english_node->save();
272 $english_node = $this->reloadEntity($english_node);
273 $this->assertEquals('draft', $english_node->moderation_state->value);
275 // French node should still be in draft.
276 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
277 $this->assertEquals('draft', $french_node->moderation_state->value);
279 // Publish the French node.
280 $french_node->moderation_state->value = 'published';
281 // Revision 3 (en, fr).
282 $french_node->save();
283 $french_node = $this->reloadEntity($french_node)->getTranslation('fr');
284 $this->assertTrue($french_node->isPublished());
285 $this->assertEquals('published', $french_node->moderation_state->value);
286 $this->assertTrue($french_node->isPublished());
287 $english_node = $french_node->getTranslation('en');
288 $this->assertEquals('draft', $english_node->moderation_state->value);
290 // Publish the English node.
291 $english_node->moderation_state->value = 'published';
292 // Revision 4 (en, fr).
293 $english_node->save();
294 $english_node = $this->reloadEntity($english_node);
295 $this->assertTrue($english_node->isPublished());
297 // Move the French node back to draft.
298 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
299 $this->assertTrue($french_node->isPublished());
300 $french_node->moderation_state->value = 'draft';
301 // Revision 5 (en, fr).
302 $french_node->save();
303 $french_node = $this->reloadEntity($english_node, 5)->getTranslation('fr');
304 $this->assertFalse($french_node->isPublished());
305 $this->assertTrue($french_node->getTranslation('en')->isPublished());
307 // Republish the French node.
308 $french_node->moderation_state->value = 'published';
309 // Revision 6 (en, fr).
310 $french_node->save();
311 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
312 $this->assertTrue($french_node->isPublished());
314 // Change the EN state without saving the node.
315 $content_moderation_state = ContentModerationState::load(1);
316 $content_moderation_state->set('moderation_state', 'draft');
317 $content_moderation_state->setNewRevision(TRUE);
318 // Revision 7 (en, fr).
319 $content_moderation_state->save();
320 $english_node = $this->reloadEntity($french_node, $french_node->getRevisionId() + 1);
322 $this->assertEquals('draft', $english_node->moderation_state->value);
323 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
324 $this->assertEquals('published', $french_node->moderation_state->value);
326 // This should unpublish the French node.
327 $content_moderation_state = ContentModerationState::load(1);
328 $content_moderation_state = $content_moderation_state->getTranslation('fr');
329 $content_moderation_state->set('moderation_state', 'draft');
330 $content_moderation_state->setNewRevision(TRUE);
331 // Revision 8 (en, fr).
332 $content_moderation_state->save();
334 $english_node = $this->reloadEntity($english_node, $english_node->getRevisionId());
335 $this->assertEquals('draft', $english_node->moderation_state->value);
336 $french_node = $this->reloadEntity($english_node, '8')->getTranslation('fr');
337 $this->assertEquals('draft', $french_node->moderation_state->value);
338 // Switching the moderation state to an unpublished state should update the
340 $this->assertFalse($french_node->isPublished());
342 // Get the default english node.
343 $english_node = $this->reloadEntity($english_node);
344 $this->assertTrue($english_node->isPublished());
345 $this->assertEquals(6, $english_node->getRevisionId());
349 * Tests moderation when the moderation_state field has a config override.
351 public function testModerationWithFieldConfigOverride() {
353 'type' => 'test_type',
356 $workflow = Workflow::load('editorial');
357 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'test_type');
360 $fields = $this->container->get('entity_field.manager')->getFieldDefinitions('node', 'test_type');
361 $field_config = $fields['moderation_state']->getConfig('test_type');
362 $field_config->setLabel('Field Override!');
363 $field_config->save();
365 $node = Node::create([
366 'title' => 'Test node',
367 'type' => 'test_type',
370 $this->assertFalse($node->isPublished());
371 $this->assertEquals('draft', $node->moderation_state->value);
373 $node->moderation_state = 'published';
375 $this->assertTrue($node->isPublished());
376 $this->assertEquals('published', $node->moderation_state->value);
380 * Tests that entities with special languages can be moderated.
382 public function testModerationWithSpecialLanguages() {
383 $workflow = Workflow::load('editorial');
384 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
387 // Create a test entity.
388 $entity = EntityTestRev::create([
389 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
392 $this->assertEquals('draft', $entity->moderation_state->value);
394 $entity->moderation_state->value = 'published';
397 $this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
401 * Tests that a non-translatable entity type with a langcode can be moderated.
403 public function testNonTranslatableEntityTypeModeration() {
404 $workflow = Workflow::load('editorial');
405 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
408 // Check that the tested entity type is not translatable.
409 $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_rev');
410 $this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
412 // Create a test entity.
413 $entity = EntityTestRev::create();
415 $this->assertEquals('draft', $entity->moderation_state->value);
417 $entity->moderation_state->value = 'published';
420 $this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
424 * Tests that a non-translatable entity type without a langcode can be
427 public function testNonLangcodeEntityTypeModeration() {
428 // Unset the langcode entity key for 'entity_test_rev'.
429 $entity_type = clone \Drupal::entityTypeManager()->getDefinition('entity_test_rev');
430 $keys = $entity_type->getKeys();
431 unset($keys['langcode']);
432 $entity_type->set('entity_keys', $keys);
433 \Drupal::state()->set('entity_test_rev.entity_type', $entity_type);
435 // Update the entity type in order to remove the 'langcode' field.
436 \Drupal::entityDefinitionUpdateManager()->applyUpdates();
438 $workflow = Workflow::load('editorial');
439 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
442 // Check that the tested entity type is not translatable and does not have a
443 // 'langcode' entity key.
444 $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_rev');
445 $this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
446 $this->assertFalse($entity_type->getKey('langcode'), "The test entity type does not have a 'langcode' entity key.");
448 // Create a test entity.
449 $entity = EntityTestRev::create();
451 $this->assertEquals('draft', $entity->moderation_state->value);
453 $entity->moderation_state->value = 'published';
456 $this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
460 * Tests the dependencies of the workflow when using content moderation.
462 public function testWorkflowDependencies() {
463 $node_type = NodeType::create([
468 $workflow = Workflow::load('editorial');
469 // Test both a config and non-config based bundle and entity type.
470 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
471 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
472 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_no_bundle', 'entity_test_no_bundle');
475 $this->assertEquals([
477 'content_moderation',
483 ], $workflow->getDependencies());
485 $this->assertEquals([
486 'entity_test_no_bundle',
489 ], $workflow->getTypePlugin()->getEntityTypes());
491 // Delete the node type and ensure it is removed from the workflow.
492 $node_type->delete();
493 $workflow = Workflow::load('editorial');
494 $entity_types = $workflow->getTypePlugin()->getEntityTypes();
495 $this->assertFalse(in_array('node', $entity_types));
497 // Uninstall entity test and ensure it's removed from the workflow.
498 $this->container->get('config.manager')->uninstall('module', 'entity_test');
499 $workflow = Workflow::load('editorial');
500 $entity_types = $workflow->getTypePlugin()->getEntityTypes();
501 $this->assertEquals([], $entity_types);
505 * Test the content moderation workflow dependencies for non-config bundles.
507 public function testWorkflowNonConfigBundleDependencies() {
508 // Create a bundle not based on any particular configuration.
509 entity_test_create_bundle('test_bundle');
511 $workflow = Workflow::load('editorial');
512 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test', 'test_bundle');
515 // Ensure the bundle is correctly added to the workflow.
516 $this->assertEquals([
518 'content_moderation',
521 ], $workflow->getDependencies());
522 $this->assertEquals([
524 ], $workflow->getTypePlugin()->getBundlesForEntityType('entity_test'));
526 // Delete the test bundle to ensure the workflow entity responds
528 entity_test_delete_bundle('test_bundle');
530 $workflow = Workflow::load('editorial');
531 $this->assertEquals([], $workflow->getTypePlugin()->getBundlesForEntityType('entity_test'));
532 $this->assertEquals([
534 'content_moderation',
536 ], $workflow->getDependencies());
542 * The entity will have required fields populated and the corresponding bundle
543 * will be enabled for content moderation.
545 * @param string $entity_type_id
546 * The entity type ID.
548 * @return \Drupal\Core\Entity\ContentEntityInterface
549 * The created entity.
551 protected function createEntity($entity_type_id) {
552 $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
554 $bundle_id = $entity_type_id;
555 // Set up a bundle entity type for the specified entity type, if needed.
556 if ($bundle_entity_type_id = $entity_type->getBundleEntityType()) {
557 $bundle_entity_type = $this->entityTypeManager->getDefinition($bundle_entity_type_id);
558 $bundle_entity_storage = $this->entityTypeManager->getStorage($bundle_entity_type_id);
560 $bundle_id = 'example';
561 if (!$bundle_entity_storage->load($bundle_id)) {
562 $bundle_entity = $bundle_entity_storage->create([
563 $bundle_entity_type->getKey('id') => 'example',
565 if ($entity_type_id == 'media') {
566 $bundle_entity->set('source', 'test');
567 $bundle_entity->save();
568 $source_field = $bundle_entity->getSource()->createSourceField($bundle_entity);
569 $source_field->getFieldStorageDefinition()->save();
570 $source_field->save();
571 $bundle_entity->set('source_configuration', [
572 'source_field' => $source_field->getName(),
575 $bundle_entity->save();
579 $workflow = Workflow::load('editorial');
580 $workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle_id);
583 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
584 $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
585 $entity = $entity_storage->create([
586 $entity_type->getKey('label') => 'Test title',
587 $entity_type->getKey('bundle') => $bundle_id,
589 // Make sure we add values for all of the required fields.
590 if ($entity_type_id == 'block_content') {
591 $entity->info = $this->randomString();
597 * Reloads the entity after clearing the static cache.
599 * @param \Drupal\Core\Entity\EntityInterface $entity
600 * The entity to reload.
601 * @param int|bool $revision_id
602 * The specific revision ID to load. Defaults FALSE and just loads the
605 * @return \Drupal\Core\Entity\EntityInterface
606 * The reloaded entity.
608 protected function reloadEntity(EntityInterface $entity, $revision_id = FALSE) {
609 $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId());
610 $storage->resetCache([$entity->id()]);
612 return $storage->loadRevision($revision_id);
614 return $storage->load($entity->id());