EntityStateChangeValidationTest.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. <?php
  2. namespace Drupal\Tests\content_moderation\Kernel;
  3. use Drupal\KernelTests\KernelTestBase;
  4. use Drupal\language\Entity\ConfigurableLanguage;
  5. use Drupal\node\Entity\Node;
  6. use Drupal\node\Entity\NodeType;
  7. use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
  8. use Drupal\Tests\user\Traits\UserCreationTrait;
  9. /**
  10. * @coversDefaultClass \Drupal\content_moderation\Plugin\Validation\Constraint\ModerationStateConstraintValidator
  11. * @group content_moderation
  12. */
  13. class EntityStateChangeValidationTest extends KernelTestBase {
  14. use ContentModerationTestTrait;
  15. use UserCreationTrait;
  16. /**
  17. * {@inheritdoc}
  18. */
  19. public static $modules = [
  20. 'node',
  21. 'content_moderation',
  22. 'user',
  23. 'system',
  24. 'language',
  25. 'content_translation',
  26. 'workflows',
  27. ];
  28. /**
  29. * An admin user.
  30. *
  31. * @var \Drupal\Core\Session\AccountInterface
  32. */
  33. protected $adminUser;
  34. /**
  35. * {@inheritdoc}
  36. */
  37. protected function setUp() {
  38. parent::setUp();
  39. $this->installSchema('node', 'node_access');
  40. $this->installEntitySchema('node');
  41. $this->installEntitySchema('user');
  42. $this->installEntitySchema('content_moderation_state');
  43. $this->installConfig('content_moderation');
  44. $this->installSchema('system', ['sequences']);
  45. $this->adminUser = $this->createUser(array_keys($this->container->get('user.permissions')->getPermissions()));
  46. }
  47. /**
  48. * Test valid transitions.
  49. *
  50. * @covers ::validate
  51. */
  52. public function testValidTransition() {
  53. $this->setCurrentUser($this->adminUser);
  54. $node_type = NodeType::create([
  55. 'type' => 'example',
  56. ]);
  57. $node_type->save();
  58. $workflow = $this->createEditorialWorkflow();
  59. $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
  60. $workflow->save();
  61. $node = Node::create([
  62. 'type' => 'example',
  63. 'title' => 'Test title',
  64. ]);
  65. $node->moderation_state->value = 'draft';
  66. $node->save();
  67. $node->moderation_state->value = 'published';
  68. $this->assertCount(0, $node->validate());
  69. $node->save();
  70. $this->assertEquals('published', $node->moderation_state->value);
  71. }
  72. /**
  73. * Test invalid transitions.
  74. *
  75. * @covers ::validate
  76. */
  77. public function testInvalidTransition() {
  78. $this->setCurrentUser($this->adminUser);
  79. $node_type = NodeType::create([
  80. 'type' => 'example',
  81. ]);
  82. $node_type->save();
  83. $workflow = $this->createEditorialWorkflow();
  84. $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
  85. $workflow->save();
  86. $node = Node::create([
  87. 'type' => 'example',
  88. 'title' => 'Test title',
  89. ]);
  90. $node->moderation_state->value = 'draft';
  91. $node->save();
  92. $node->moderation_state->value = 'archived';
  93. $violations = $node->validate();
  94. $this->assertCount(1, $violations);
  95. $this->assertEquals('Invalid state transition from <em class="placeholder">Draft</em> to <em class="placeholder">Archived</em>', $violations->get(0)->getMessage());
  96. }
  97. /**
  98. * Test validation with an invalid state.
  99. */
  100. public function testInvalidState() {
  101. $node_type = NodeType::create([
  102. 'type' => 'example',
  103. ]);
  104. $node_type->save();
  105. $workflow = $this->createEditorialWorkflow();
  106. $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
  107. $workflow->save();
  108. $node = Node::create([
  109. 'type' => 'example',
  110. 'title' => 'Test title',
  111. ]);
  112. $node->moderation_state->value = 'invalid_state';
  113. $violations = $node->validate();
  114. $this->assertCount(1, $violations);
  115. $this->assertEquals('State <em class="placeholder">invalid_state</em> does not exist on <em class="placeholder">Editorial</em> workflow', $violations->get(0)->getMessage());
  116. }
  117. /**
  118. * Test validation with content that has no initial state or an invalid state.
  119. */
  120. public function testInvalidStateWithoutExisting() {
  121. $this->setCurrentUser($this->adminUser);
  122. // Create content without moderation enabled for the content type.
  123. $node_type = NodeType::create([
  124. 'type' => 'example',
  125. ]);
  126. $node_type->save();
  127. $node = Node::create([
  128. 'type' => 'example',
  129. 'title' => 'Test title',
  130. ]);
  131. $node->save();
  132. // Enable moderation to test validation on existing content, with no
  133. // explicit state.
  134. $workflow = $this->createEditorialWorkflow();
  135. $workflow->getTypePlugin()->addState('deleted_state', 'Deleted state');
  136. $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
  137. $workflow->save();
  138. // Validate the invalid state.
  139. $node->moderation_state->value = 'invalid_state';
  140. $violations = $node->validate();
  141. $this->assertCount(1, $violations);
  142. // Assign the node to a state we're going to delete.
  143. $node->moderation_state->value = 'deleted_state';
  144. $node->save();
  145. // Delete the state so $node->original contains an invalid state when
  146. // validating.
  147. $workflow->getTypePlugin()->deleteState('deleted_state');
  148. $workflow->save();
  149. // When there is an invalid state, the content will revert to "draft". This
  150. // will allow a draft to draft transition.
  151. $node->moderation_state->value = 'draft';
  152. $violations = $node->validate();
  153. $this->assertCount(0, $violations);
  154. // This will disallow a draft to archived transition.
  155. $node->moderation_state->value = 'archived';
  156. $violations = $node->validate();
  157. $this->assertCount(1, $violations);
  158. }
  159. /**
  160. * Test state transition validation with multiple languages.
  161. */
  162. public function testInvalidStateMultilingual() {
  163. $this->setCurrentUser($this->adminUser);
  164. ConfigurableLanguage::createFromLangcode('fr')->save();
  165. $node_type = NodeType::create([
  166. 'type' => 'example',
  167. ]);
  168. $node_type->save();
  169. $workflow = $this->createEditorialWorkflow();
  170. $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
  171. $workflow->save();
  172. $node = Node::create([
  173. 'type' => 'example',
  174. 'title' => 'English Published Node',
  175. 'langcode' => 'en',
  176. 'moderation_state' => 'published',
  177. ]);
  178. $node->save();
  179. $node_fr = $node->addTranslation('fr', $node->toArray());
  180. $node_fr->setTitle('French Published Node');
  181. $node_fr->save();
  182. $this->assertEquals('published', $node_fr->moderation_state->value);
  183. // Create a pending revision of the original node.
  184. $node->moderation_state = 'draft';
  185. $node->setNewRevision(TRUE);
  186. $node->isDefaultRevision(FALSE);
  187. $node->save();
  188. // For the pending english revision, there should be a violation from draft
  189. // to archived.
  190. $node->moderation_state = 'archived';
  191. $violations = $node->validate();
  192. $this->assertCount(1, $violations);
  193. $this->assertEquals('Invalid state transition from <em class="placeholder">Draft</em> to <em class="placeholder">Archived</em>', $violations->get(0)->getMessage());
  194. // From the default french published revision, there should be none.
  195. $node_fr = Node::load($node->id())->getTranslation('fr');
  196. $this->assertEquals('published', $node_fr->moderation_state->value);
  197. $node_fr->moderation_state = 'archived';
  198. $violations = $node_fr->validate();
  199. $this->assertCount(0, $violations);
  200. // From the latest french revision, there should also be no violation.
  201. $node_fr = Node::load($node->id())->getTranslation('fr');
  202. $this->assertEquals('published', $node_fr->moderation_state->value);
  203. $node_fr->moderation_state = 'archived';
  204. $violations = $node_fr->validate();
  205. $this->assertCount(0, $violations);
  206. }
  207. /**
  208. * Tests that content without prior moderation information can be moderated.
  209. */
  210. public function testExistingContentWithNoModeration() {
  211. $this->setCurrentUser($this->adminUser);
  212. $node_type = NodeType::create([
  213. 'type' => 'example',
  214. ]);
  215. $node_type->save();
  216. /** @var \Drupal\node\NodeInterface $node */
  217. $node = Node::create([
  218. 'type' => 'example',
  219. 'title' => 'Test title',
  220. ]);
  221. $node->save();
  222. $nid = $node->id();
  223. // Enable moderation for our node type.
  224. $workflow = $this->createEditorialWorkflow();
  225. $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
  226. $workflow->save();
  227. $node = Node::load($nid);
  228. // Having no previous state should not break validation.
  229. $violations = $node->validate();
  230. $this->assertCount(0, $violations);
  231. // Having no previous state should not break saving the node.
  232. $node->setTitle('New');
  233. $node->save();
  234. }
  235. /**
  236. * Tests that content without prior moderation information can be translated.
  237. */
  238. public function testExistingMultilingualContentWithNoModeration() {
  239. $this->setCurrentUser($this->adminUser);
  240. // Enable French.
  241. ConfigurableLanguage::createFromLangcode('fr')->save();
  242. $node_type = NodeType::create([
  243. 'type' => 'example',
  244. ]);
  245. $node_type->save();
  246. /** @var \Drupal\node\NodeInterface $node */
  247. $node = Node::create([
  248. 'type' => 'example',
  249. 'title' => 'Test title',
  250. 'langcode' => 'en',
  251. ]);
  252. $node->save();
  253. $nid = $node->id();
  254. $node = Node::load($nid);
  255. // Creating a translation shouldn't break, even though there's no previous
  256. // moderated revision for the new language.
  257. $node_fr = $node->addTranslation('fr');
  258. $node_fr->setTitle('Francais');
  259. $node_fr->save();
  260. // Enable moderation for our node type.
  261. $workflow = $this->createEditorialWorkflow();
  262. $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
  263. $workflow->save();
  264. // Reload the French version of the node.
  265. $node = Node::load($nid);
  266. $node_fr = $node->getTranslation('fr');
  267. /** @var \Drupal\node\NodeInterface $node_fr */
  268. $node_fr->setTitle('Nouveau');
  269. $node_fr->save();
  270. }
  271. /**
  272. * @dataProvider transitionAccessValidationTestCases
  273. */
  274. public function testTransitionAccessValidation($permissions, $target_state, $messages) {
  275. $node_type = NodeType::create([
  276. 'type' => 'example',
  277. ]);
  278. $node_type->save();
  279. $workflow = $this->createEditorialWorkflow();
  280. $workflow->getTypePlugin()->addState('foo', 'Foo');
  281. $workflow->getTypePlugin()->addTransition('draft_to_foo', 'Draft to foo', ['draft'], 'foo');
  282. $workflow->getTypePlugin()->addTransition('foo_to_foo', 'Foo to foo', ['foo'], 'foo');
  283. $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
  284. $workflow->save();
  285. $this->setCurrentUser($this->createUser($permissions));
  286. $node = Node::create([
  287. 'type' => 'example',
  288. 'title' => 'Test content',
  289. 'moderation_state' => $target_state,
  290. ]);
  291. $this->assertTrue($node->isNew());
  292. $violations = $node->validate();
  293. $this->assertCount(count($messages), $violations);
  294. foreach ($messages as $i => $message) {
  295. $this->assertEquals($message, $violations->get($i)->getMessage());
  296. }
  297. }
  298. /**
  299. * Test cases for ::testTransitionAccessValidation.
  300. */
  301. public function transitionAccessValidationTestCases() {
  302. return [
  303. 'Invalid transition, no permissions validated' => [
  304. [],
  305. 'archived',
  306. ['Invalid state transition from <em class="placeholder">Draft</em> to <em class="placeholder">Archived</em>'],
  307. ],
  308. 'Valid transition, missing permission' => [
  309. [],
  310. 'published',
  311. ['You do not have access to transition from <em class="placeholder">Draft</em> to <em class="placeholder">Published</em>'],
  312. ],
  313. 'Valid transition, granted published permission' => [
  314. ['use editorial transition publish'],
  315. 'published',
  316. [],
  317. ],
  318. 'Valid transition, granted draft permission' => [
  319. ['use editorial transition create_new_draft'],
  320. 'draft',
  321. [],
  322. ],
  323. 'Valid transition, incorrect permission granted' => [
  324. ['use editorial transition create_new_draft'],
  325. 'published',
  326. ['You do not have access to transition from <em class="placeholder">Draft</em> to <em class="placeholder">Published</em>'],
  327. ],
  328. // Test with an additional state and set of transitions, since the
  329. // "published" transition can start from either "draft" or "published", it
  330. // does not capture bugs that fail to correctly distinguish the initial
  331. // workflow state from the set state of a new entity.
  332. 'Valid transition, granted foo permission' => [
  333. ['use editorial transition draft_to_foo'],
  334. 'foo',
  335. [],
  336. ],
  337. 'Valid transition, incorrect foo permission granted' => [
  338. ['use editorial transition foo_to_foo'],
  339. 'foo',
  340. ['You do not have access to transition from <em class="placeholder">Draft</em> to <em class="placeholder">Foo</em>'],
  341. ],
  342. ];
  343. }
  344. }