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