3 namespace Drupal\Tests\content_moderation\Kernel;
5 use Drupal\content_moderation\Entity\ContentModerationState;
6 use Drupal\Core\Entity\EntityPublishedInterface;
7 use Drupal\Core\Entity\EntityStorageException;
8 use Drupal\entity_test\Entity\EntityTestBundle;
9 use Drupal\entity_test\Entity\EntityTestWithBundle;
10 use Drupal\Core\Entity\EntityInterface;
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 = [
35 'content_translation',
41 * @var \Drupal\Core\Entity\EntityTypeManager
43 protected $entityTypeManager;
48 protected function setUp() {
51 $this->installSchema('node', 'node_access');
52 $this->installEntitySchema('node');
53 $this->installEntitySchema('user');
54 $this->installEntitySchema('entity_test_with_bundle');
55 $this->installEntitySchema('entity_test_rev');
56 $this->installEntitySchema('entity_test_no_bundle');
57 $this->installEntitySchema('entity_test_mulrevpub');
58 $this->installEntitySchema('block_content');
59 $this->installEntitySchema('content_moderation_state');
60 $this->installConfig('content_moderation');
62 $this->entityTypeManager = $this->container->get('entity_type.manager');
66 * Tests basic monolingual content moderation through the API.
68 * @dataProvider basicModerationTestCases
70 public function testBasicModeration($entity_type_id) {
71 // Make the 'entity_test_with_bundle' entity type revisionable.
72 if ($entity_type_id == 'entity_test_with_bundle') {
73 $this->setEntityTestWithBundleKeys(['revision' => 'revision_id']);
76 $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
77 $bundle_id = $entity_type_id;
78 $bundle_entity_type_id = $this->entityTypeManager->getDefinition($entity_type_id)->getBundleEntityType();
79 if ($bundle_entity_type_id) {
80 $bundle_entity_type_definition = $this->entityTypeManager->getDefinition($bundle_entity_type_id);
81 $entity_type_storage = $this->entityTypeManager->getStorage($bundle_entity_type_id);
83 $entity_type = $entity_type_storage->create([
84 $bundle_entity_type_definition->getKey('id') => 'example',
87 $bundle_id = $entity_type->id();
90 $workflow = Workflow::load('editorial');
91 $workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle_id);
94 $entity = $entity_storage->create([
95 'title' => 'Test title',
96 $this->entityTypeManager->getDefinition($entity_type_id)->getKey('bundle') => $bundle_id,
98 if ($entity instanceof EntityPublishedInterface) {
99 $entity->setUnpublished();
102 $entity = $this->reloadEntity($entity);
103 $this->assertEquals('draft', $entity->moderation_state->value);
105 $entity->moderation_state->value = 'published';
108 $entity = $this->reloadEntity($entity);
109 $this->assertEquals('published', $entity->moderation_state->value);
111 // Change the state without saving the node.
112 $content_moderation_state = ContentModerationState::load(1);
113 $content_moderation_state->set('moderation_state', 'draft');
114 $content_moderation_state->setNewRevision(TRUE);
115 $content_moderation_state->save();
117 $entity = $this->reloadEntity($entity, 3);
118 $this->assertEquals('draft', $entity->moderation_state->value);
119 if ($entity instanceof EntityPublishedInterface) {
120 $this->assertFalse($entity->isPublished());
123 // Get the default revision.
124 $entity = $this->reloadEntity($entity);
125 if ($entity instanceof EntityPublishedInterface) {
126 $this->assertTrue((bool) $entity->isPublished());
128 $this->assertEquals(2, $entity->getRevisionId());
130 $entity->moderation_state->value = 'published';
133 $entity = $this->reloadEntity($entity, 4);
134 $this->assertEquals('published', $entity->moderation_state->value);
136 // Get the default revision.
137 $entity = $this->reloadEntity($entity);
138 if ($entity instanceof EntityPublishedInterface) {
139 $this->assertTrue((bool) $entity->isPublished());
141 $this->assertEquals(4, $entity->getRevisionId());
143 // Update the node to archived which will then be the default revision.
144 $entity->moderation_state->value = 'archived';
147 // Revert to the previous (published) revision.
148 $previous_revision = $entity_storage->loadRevision(4);
149 $previous_revision->isDefaultRevision(TRUE);
150 $previous_revision->setNewRevision(TRUE);
151 $previous_revision->save();
153 // Get the default revision.
154 $entity = $this->reloadEntity($entity);
155 $this->assertEquals('published', $entity->moderation_state->value);
156 if ($entity instanceof EntityPublishedInterface) {
157 $this->assertTrue($entity->isPublished());
160 // Set an invalid moderation state.
161 $this->setExpectedException(EntityStorageException::class);
162 $entity->moderation_state->value = 'foobar';
167 * Test cases for basic moderation test.
169 public function basicModerationTestCases() {
177 'Test Entity with Bundle' => [
178 'entity_test_with_bundle',
180 'Test entity - revisions, data table, and published interface' => [
181 'entity_test_mulrevpub',
183 'Entity Test with revisions' => [
186 'Entity without bundle' => [
187 'entity_test_no_bundle',
193 * Tests basic multilingual content moderation through the API.
195 public function testMultilingualModeration() {
197 ConfigurableLanguage::createFromLangcode('fr')->save();
198 $node_type = NodeType::create([
203 $workflow = Workflow::load('editorial');
204 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
207 $english_node = Node::create([
209 'title' => 'Test title',
215 $this->assertEquals('draft', $english_node->moderation_state->value);
216 $this->assertFalse($english_node->isPublished());
218 // Create a French translation.
219 $french_node = $english_node->addTranslation('fr', ['title' => 'French title']);
220 $french_node->setUnpublished();
222 $french_node->save();
223 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
224 $this->assertEquals('draft', $french_node->moderation_state->value);
225 $this->assertFalse($french_node->isPublished());
227 // Move English node to create another draft.
228 $english_node = $this->reloadEntity($english_node);
229 $english_node->moderation_state->value = 'draft';
230 // Revision 2 (en, fr).
231 $english_node->save();
232 $english_node = $this->reloadEntity($english_node);
233 $this->assertEquals('draft', $english_node->moderation_state->value);
235 // French node should still be in draft.
236 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
237 $this->assertEquals('draft', $french_node->moderation_state->value);
239 // Publish the French node.
240 $french_node->moderation_state->value = 'published';
241 // Revision 3 (en, fr).
242 $french_node->save();
243 $french_node = $this->reloadEntity($french_node)->getTranslation('fr');
244 $this->assertTrue($french_node->isPublished());
245 $this->assertEquals('published', $french_node->moderation_state->value);
246 $this->assertTrue($french_node->isPublished());
247 $english_node = $french_node->getTranslation('en');
248 $this->assertEquals('draft', $english_node->moderation_state->value);
250 // Publish the English node.
251 $english_node->moderation_state->value = 'published';
252 // Revision 4 (en, fr).
253 $english_node->save();
254 $english_node = $this->reloadEntity($english_node);
255 $this->assertTrue($english_node->isPublished());
257 // Move the French node back to draft.
258 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
259 $this->assertTrue($french_node->isPublished());
260 $french_node->moderation_state->value = 'draft';
261 // Revision 5 (en, fr).
262 $french_node->save();
263 $french_node = $this->reloadEntity($english_node, 5)->getTranslation('fr');
264 $this->assertFalse($french_node->isPublished());
265 $this->assertTrue($french_node->getTranslation('en')->isPublished());
267 // Republish the French node.
268 $french_node->moderation_state->value = 'published';
269 // Revision 6 (en, fr).
270 $french_node->save();
271 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
272 $this->assertTrue($french_node->isPublished());
274 // Change the EN state without saving the node.
275 $content_moderation_state = ContentModerationState::load(1);
276 $content_moderation_state->set('moderation_state', 'draft');
277 $content_moderation_state->setNewRevision(TRUE);
278 // Revision 7 (en, fr).
279 $content_moderation_state->save();
280 $english_node = $this->reloadEntity($french_node, $french_node->getRevisionId() + 1);
282 $this->assertEquals('draft', $english_node->moderation_state->value);
283 $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
284 $this->assertEquals('published', $french_node->moderation_state->value);
286 // This should unpublish the French node.
287 $content_moderation_state = ContentModerationState::load(1);
288 $content_moderation_state = $content_moderation_state->getTranslation('fr');
289 $content_moderation_state->set('moderation_state', 'draft');
290 $content_moderation_state->setNewRevision(TRUE);
291 // Revision 8 (en, fr).
292 $content_moderation_state->save();
294 $english_node = $this->reloadEntity($english_node, $english_node->getRevisionId());
295 $this->assertEquals('draft', $english_node->moderation_state->value);
296 $french_node = $this->reloadEntity($english_node, '8')->getTranslation('fr');
297 $this->assertEquals('draft', $french_node->moderation_state->value);
298 // Switching the moderation state to an unpublished state should update the
300 $this->assertFalse($french_node->isPublished());
302 // Get the default english node.
303 $english_node = $this->reloadEntity($english_node);
304 $this->assertTrue($english_node->isPublished());
305 $this->assertEquals(6, $english_node->getRevisionId());
309 * Tests that a non-translatable entity type with a langcode can be moderated.
311 public function testNonTranslatableEntityTypeModeration() {
312 // Make the 'entity_test_with_bundle' entity type revisionable.
313 $this->setEntityTestWithBundleKeys(['revision' => 'revision_id']);
315 // Create a test bundle.
316 $entity_test_bundle = EntityTestBundle::create([
319 $entity_test_bundle->save();
321 $workflow = Workflow::load('editorial');
322 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_with_bundle', 'example');
325 // Check that the tested entity type is not translatable.
326 $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_with_bundle');
327 $this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
329 // Create a test entity.
330 $entity_test_with_bundle = EntityTestWithBundle::create([
333 $entity_test_with_bundle->save();
334 $this->assertEquals('draft', $entity_test_with_bundle->moderation_state->value);
336 $entity_test_with_bundle->moderation_state->value = 'published';
337 $entity_test_with_bundle->save();
339 $this->assertEquals('published', EntityTestWithBundle::load($entity_test_with_bundle->id())->moderation_state->value);
343 * Tests that a non-translatable entity type without a langcode can be
346 public function testNonLangcodeEntityTypeModeration() {
347 // Make the 'entity_test_with_bundle' entity type revisionable and unset
348 // the langcode entity key.
349 $this->setEntityTestWithBundleKeys(['revision' => 'revision_id'], ['langcode']);
351 // Create a test bundle.
352 $entity_test_bundle = EntityTestBundle::create([
355 $entity_test_bundle->save();
357 $workflow = Workflow::load('editorial');
358 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_with_bundle', 'example');
361 // Check that the tested entity type is not translatable.
362 $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_with_bundle');
363 $this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
365 // Create a test entity.
366 $entity_test_with_bundle = EntityTestWithBundle::create([
369 $entity_test_with_bundle->save();
370 $this->assertEquals('draft', $entity_test_with_bundle->moderation_state->value);
372 $entity_test_with_bundle->moderation_state->value = 'published';
373 $entity_test_with_bundle->save();
375 $this->assertEquals('published', EntityTestWithBundle::load($entity_test_with_bundle->id())->moderation_state->value);
379 * Set the keys on the test entity type.
382 * The entity keys to override
383 * @param array $remove_keys
386 protected function setEntityTestWithBundleKeys($keys, $remove_keys = []) {
387 $entity_type = clone \Drupal::entityTypeManager()->getDefinition('entity_test_with_bundle');
388 $original_keys = $entity_type->getKeys();
389 foreach ($remove_keys as $remove_key) {
390 unset($original_keys[$remove_key]);
392 $entity_type->set('entity_keys', $keys + $original_keys);
393 \Drupal::state()->set('entity_test_with_bundle.entity_type', $entity_type);
394 \Drupal::entityDefinitionUpdateManager()->applyUpdates();
398 * Tests the dependencies of the workflow when using content moderation.
400 public function testWorkflowDependencies() {
401 $node_type = NodeType::create([
406 $workflow = Workflow::load('editorial');
407 // Test both a config and non-config based bundle and entity type.
408 $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
409 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
410 $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_no_bundle', 'entity_test_no_bundle');
413 $this->assertEquals([
415 'content_moderation',
421 ], $workflow->getDependencies());
423 $this->assertEquals([
424 'entity_test_no_bundle',
427 ], $workflow->getTypePlugin()->getEntityTypes());
429 // Delete the node type and ensure it is removed from the workflow.
430 $node_type->delete();
431 $workflow = Workflow::load('editorial');
432 $entity_types = $workflow->getTypePlugin()->getEntityTypes();
433 $this->assertFalse(in_array('node', $entity_types));
435 // Uninstall entity test and ensure it's removed from the workflow.
436 $this->container->get('config.manager')->uninstall('module', 'entity_test');
437 $workflow = Workflow::load('editorial');
438 $entity_types = $workflow->getTypePlugin()->getEntityTypes();
439 $this->assertEquals([], $entity_types);
443 * Reloads the entity after clearing the static cache.
445 * @param \Drupal\Core\Entity\EntityInterface $entity
446 * The entity to reload.
447 * @param int|bool $revision_id
448 * The specific revision ID to load. Defaults FALSE and just loads the
451 * @return \Drupal\Core\Entity\EntityInterface
452 * The reloaded entity.
454 protected function reloadEntity(EntityInterface $entity, $revision_id = FALSE) {
455 $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId());
456 $storage->resetCache([$entity->id()]);
458 return $storage->loadRevision($revision_id);
460 return $storage->load($entity->id());