Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / workflows / tests / src / Functional / WorkflowUiTest.php
1 <?php
2
3 namespace Drupal\Tests\workflows\Functional;
4
5 use Drupal\Core\Url;
6 use Drupal\Tests\BrowserTestBase;
7 use Drupal\workflows\Entity\Workflow;
8
9 /**
10  * Tests workflow creation UI.
11  *
12  * @group workflows
13  */
14 class WorkflowUiTest extends BrowserTestBase {
15
16   /**
17    * Modules to enable.
18    *
19    * @var array
20    */
21   public static $modules = ['workflows', 'workflow_type_test', 'block'];
22
23   /**
24    * {@inheritdoc}
25    */
26   protected function setUp() {
27     parent::setUp();
28     // We're testing local actions.
29     $this->drupalPlaceBlock('local_actions_block');
30   }
31
32   /**
33    * Tests route access/permissions.
34    */
35   public function testAccess() {
36     // Create a minimal workflow for testing.
37     $workflow = Workflow::create(['id' => 'test', 'type' => 'workflow_type_test']);
38     $workflow
39       ->getTypePlugin()
40       ->addState('draft', 'Draft')
41       ->addState('published', 'Published')
42       ->addTransition('publish', 'Publish', ['draft', 'published'], 'published');
43     $workflow->save();
44
45     $paths = [
46       'admin/config/workflow/workflows',
47       'admin/config/workflow/workflows/add',
48       'admin/config/workflow/workflows/manage/test',
49       'admin/config/workflow/workflows/manage/test/delete',
50       'admin/config/workflow/workflows/manage/test/add_state',
51       'admin/config/workflow/workflows/manage/test/state/published',
52       'admin/config/workflow/workflows/manage/test/state/published/delete',
53       'admin/config/workflow/workflows/manage/test/add_transition',
54       'admin/config/workflow/workflows/manage/test/transition/publish',
55       'admin/config/workflow/workflows/manage/test/transition/publish/delete',
56     ];
57
58     foreach ($paths as $path) {
59       $this->drupalGet($path);
60       // No access.
61       $this->assertSession()->statusCodeEquals(403);
62     }
63     $this->drupalLogin($this->createUser(['administer workflows']));
64     foreach ($paths as $path) {
65       $this->drupalGet($path);
66       // User has access.
67       $this->assertSession()->statusCodeEquals(200);
68     }
69
70     // Ensure that default states can not be deleted.
71     \Drupal::state()->set('workflow_type_test.required_states', ['published']);
72     $this->drupalGet('admin/config/workflow/workflows/manage/test/state/published/delete');
73     $this->assertSession()->statusCodeEquals(403);
74     \Drupal::state()->set('workflow_type_test.required_states', []);
75
76     // Delete one of the states and ensure the other test cannot be deleted.
77     $this->drupalGet('admin/config/workflow/workflows/manage/test/state/published/delete');
78     $this->submitForm([], 'Delete');
79     $this->drupalGet('admin/config/workflow/workflows/manage/test/state/draft/delete');
80     $this->assertSession()->statusCodeEquals(403);
81   }
82
83   /**
84    * Test the machine name validation of the state add form.
85    */
86   public function testStateMachineNameValidation() {
87     Workflow::create([
88       'id' => 'test_workflow',
89       'type' => 'workflow_type_test',
90     ])->save();
91
92     $this->drupalLogin($this->createUser(['administer workflows']));
93     $this->drupalPostForm('admin/config/workflow/workflows/manage/test_workflow/add_state', [
94       'label' => 'Test State',
95       'id' => 'Invalid ID',
96     ], 'Save');
97     $this->assertSession()->statusCodeEquals(200);
98     $this->assertSession()->pageTextContains('The machine-readable name must contain only lowercase letters, numbers, and underscores.');
99   }
100
101   /**
102    * Tests the creation of a workflow through the UI.
103    */
104   public function testWorkflowCreation() {
105     $workflow_storage = $this->container->get('entity_type.manager')->getStorage('workflow');
106     /** @var \Drupal\workflows\WorkflowInterface $workflow */
107     $this->drupalLogin($this->createUser(['access administration pages', 'administer workflows']));
108     $this->drupalGet('admin/config/workflow');
109     $this->assertSession()->linkByHrefExists('admin/config/workflow/workflows');
110     $this->clickLink('Workflows');
111     $this->assertSession()->pageTextContains('Workflows');
112     $this->assertSession()->pageTextContains('There is no Workflow yet.');
113     $this->clickLink('Add workflow');
114     $this->submitForm(['label' => 'Test', 'id' => 'test', 'workflow_type' => 'workflow_type_test'], 'Save');
115     $this->assertSession()->pageTextContains('Created the Test Workflow.');
116     $this->assertSession()->addressEquals('admin/config/workflow/workflows/manage/test/add_state');
117     $this->drupalGet('/admin/config/workflow/workflows/manage/test');
118     $this->assertSession()->pageTextContains('This workflow has no states and will be disabled until there is at least one, add a new state.');
119     $this->assertSession()->pageTextContains('There are no states yet.');
120     $this->clickLink('Add a new state');
121     $this->submitForm(['label' => 'Published', 'id' => 'published'], 'Save');
122     $this->assertSession()->pageTextContains('Created Published state.');
123     $workflow = $workflow_storage->loadUnchanged('test');
124     $this->assertFalse($workflow->getTypePlugin()->getState('published')->canTransitionTo('published'), 'No default transition from published to published exists.');
125
126     $this->clickLink('Add a new state');
127     // Don't create a draft to draft transition by default.
128     $this->submitForm(['label' => 'Draft', 'id' => 'draft'], 'Save');
129     $this->assertSession()->pageTextContains('Created Draft state.');
130     $workflow = $workflow_storage->loadUnchanged('test');
131     $this->assertFalse($workflow->getTypePlugin()->getState('draft')->canTransitionTo('draft'), 'Can not transition from draft to draft');
132
133     $this->clickLink('Add a new transition');
134     $this->submitForm(['id' => 'publish', 'label' => 'Publish', 'from[draft]' => 'draft', 'to' => 'published'], 'Save');
135     $this->assertSession()->pageTextContains('Created Publish transition.');
136     $workflow = $workflow_storage->loadUnchanged('test');
137     $this->assertTrue($workflow->getTypePlugin()->getState('draft')->canTransitionTo('published'), 'Can transition from draft to published');
138
139     $this->clickLink('Add a new transition');
140     $this->assertCount(2, $this->cssSelect('input[name="to"][type="radio"]'));
141     $this->assertCount(0, $this->cssSelect('input[name="to"][checked="checked"][type="radio"]'));
142     $this->submitForm(['id' => 'create_new_draft', 'label' => 'Create new draft', 'from[draft]' => 'draft', 'to' => 'draft'], 'Save');
143     $this->assertSession()->pageTextContains('Created Create new draft transition.');
144     $workflow = $workflow_storage->loadUnchanged('test');
145     $this->assertTrue($workflow->getTypePlugin()->getState('draft')->canTransitionTo('draft'), 'Can transition from draft to draft');
146
147     // The fist state to edit on the page should be published.
148     $this->clickLink('Edit');
149     $this->assertSession()->fieldValueEquals('label', 'Published');
150     // Change the label.
151     $this->submitForm(['label' => 'Live'], 'Save');
152     $this->assertSession()->pageTextContains('Saved Live state.');
153
154     // Allow published to draft.
155     $this->clickLink('Edit', 3);
156     $this->submitForm(['from[published]' => 'published'], 'Save');
157     $this->assertSession()->pageTextContains('Saved Create new draft transition.');
158     $workflow = $workflow_storage->loadUnchanged('test');
159     $this->assertTrue($workflow->getTypePlugin()->getState('published')->canTransitionTo('draft'), 'Can transition from published to draft');
160
161     // Try creating a duplicate transition.
162     $this->clickLink('Add a new transition');
163     $this->submitForm(['id' => 'create_new_draft', 'label' => 'Create new draft', 'from[published]' => 'published', 'to' => 'draft'], 'Save');
164     $this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
165     // Try creating a transition which duplicates the states of another.
166     $this->submitForm(['id' => 'create_new_draft2', 'label' => 'Create new draft again', 'from[published]' => 'published', 'to' => 'draft'], 'Save');
167     $this->assertSession()->pageTextContains('The transition from Live to Draft already exists.');
168
169     // Create a new transition.
170     $this->submitForm(['id' => 'save_and_publish', 'label' => 'Save and publish', 'from[published]' => 'published', 'to' => 'published'], 'Save');
171     $this->assertSession()->pageTextContains('Created Save and publish transition.');
172     // Edit the new transition and try to add an existing transition.
173     $this->clickLink('Edit', 4);
174     $this->submitForm(['from[draft]' => 'draft'], 'Save');
175     $this->assertSession()->pageTextContains('The transition from Draft to Live already exists.');
176
177     // Delete the transition.
178     $workflow = $workflow_storage->loadUnchanged('test');
179     $this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('published', 'published'), 'Can transition from published to published');
180     $this->clickLink('Delete');
181     $this->assertSession()->pageTextContains('Are you sure you want to delete Save and publish from Test?');
182     $this->submitForm([], 'Delete');
183     $workflow = $workflow_storage->loadUnchanged('test');
184     $this->assertFalse($workflow->getTypePlugin()->hasTransitionFromStateToState('published', 'published'), 'Cannot transition from published to published');
185
186     // Try creating a duplicate state.
187     $this->drupalGet('admin/config/workflow/workflows/manage/test');
188     $this->clickLink('Add a new state');
189     $this->submitForm(['label' => 'Draft', 'id' => 'draft'], 'Save');
190     $this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
191
192     // Ensure that weight changes the state ordering.
193     $workflow = $workflow_storage->loadUnchanged('test');
194     $this->assertEquals('published', $workflow->getTypePlugin()->getInitialState()->id());
195     $this->drupalGet('admin/config/workflow/workflows/manage/test');
196     $this->submitForm(['states[draft][weight]' => '-1'], 'Save');
197     $workflow = $workflow_storage->loadUnchanged('test');
198     $this->assertEquals('draft', $workflow->getTypePlugin()->getInitialState()->id());
199
200     // Verify that we are still on the workflow edit page.
201     $this->assertSession()->addressEquals('admin/config/workflow/workflows/manage/test');
202
203     // Ensure that weight changes the transition ordering.
204     $this->assertEquals(['publish', 'create_new_draft'], array_keys($workflow->getTypePlugin()->getTransitions()));
205     $this->drupalGet('admin/config/workflow/workflows/manage/test');
206     $this->submitForm(['transitions[create_new_draft][weight]' => '-1'], 'Save');
207     $workflow = $workflow_storage->loadUnchanged('test');
208     $this->assertEquals(['create_new_draft', 'publish'], array_keys($workflow->getTypePlugin()->getTransitions()));
209
210     // Verify that we are still on the workflow edit page.
211     $this->assertSession()->addressEquals('admin/config/workflow/workflows/manage/test');
212
213     // Ensure that a delete link for the published state exists before deleting
214     // the draft state.
215     $published_delete_link = Url::fromRoute('entity.workflow.delete_state_form', [
216       'workflow' => $workflow->id(),
217       'workflow_state' => 'published',
218     ])->toString();
219     $draft_delete_link = Url::fromRoute('entity.workflow.delete_state_form', [
220       'workflow' => $workflow->id(),
221       'workflow_state' => 'draft',
222     ])->toString();
223     $this->assertSession()->elementContains('css', 'tr[data-drupal-selector="edit-states-published"]', 'Delete');
224     $this->assertSession()->linkByHrefExists($published_delete_link);
225     $this->assertSession()->linkByHrefExists($draft_delete_link);
226
227     // Make the published state a default state and ensure it is no longer
228     // linked.
229     \Drupal::state()->set('workflow_type_test.required_states', ['published']);
230     $this->getSession()->reload();
231     $this->assertSession()->linkByHrefNotExists($published_delete_link);
232     $this->assertSession()->linkByHrefExists($draft_delete_link);
233     $this->assertSession()->elementNotContains('css', 'tr[data-drupal-selector="edit-states-published"]', 'Delete');
234     \Drupal::state()->set('workflow_type_test.required_states', []);
235     $this->getSession()->reload();
236     $this->assertSession()->elementContains('css', 'tr[data-drupal-selector="edit-states-published"]', 'Delete');
237     $this->assertSession()->linkByHrefExists($published_delete_link);
238     $this->assertSession()->linkByHrefExists($draft_delete_link);
239
240     // Delete the Draft state.
241     $this->clickLink('Delete');
242     $this->assertSession()->pageTextContains('Are you sure you want to delete Draft from Test?');
243     $this->submitForm([], 'Delete');
244     $this->assertSession()->pageTextContains('State Draft deleted.');
245     $workflow = $workflow_storage->loadUnchanged('test');
246     $this->assertFalse($workflow->getTypePlugin()->hasState('draft'), 'Draft state deleted');
247     $this->assertTrue($workflow->getTypePlugin()->hasState('published'), 'Workflow still has published state');
248
249     // The last state cannot be deleted so the only delete link on the page will
250     // be for the workflow.
251     $this->assertSession()->linkByHrefNotExists($published_delete_link);
252     $this->clickLink('Delete');
253     $this->assertSession()->pageTextContains('Are you sure you want to delete Test?');
254     $this->submitForm([], 'Delete');
255     $this->assertSession()->pageTextContains('Workflow Test deleted.');
256     $this->assertSession()->pageTextContains('There is no Workflow yet.');
257     $this->assertNull($workflow_storage->loadUnchanged('test'), 'The test workflow has been deleted');
258
259     // Ensure that workflow types with default configuration are initialized
260     // correctly.
261     $this->drupalGet('admin/config/workflow/workflows');
262     $this->clickLink('Add workflow');
263     $this->submitForm(['label' => 'Test 2', 'id' => 'test2', 'workflow_type' => 'workflow_type_required_state_test'], 'Save');
264     $this->assertSession()->addressEquals('admin/config/workflow/workflows/manage/test2');
265     $workflow = $workflow_storage->loadUnchanged('test2');
266     $this->assertTrue($workflow->getTypePlugin()->hasState('fresh'), 'The workflow has the "fresh" state');
267     $this->assertTrue($workflow->getTypePlugin()->hasState('rotten'), 'The workflow has the "rotten" state');
268     $this->assertTrue($workflow->getTypePlugin()->hasTransition('rot'), 'The workflow has the "rot" transition');
269     $this->assertSession()->pageTextContains('Fresh');
270     $this->assertSession()->pageTextContains('Rotten');
271   }
272
273   /**
274    * Test the workflow configuration form.
275    */
276   public function testWorkflowConfigurationForm() {
277     $workflow = Workflow::create(['id' => 'test', 'type' => 'workflow_type_complex_test', 'label' => 'Test']);
278     $workflow
279       ->getTypePlugin()
280       ->addState('published', 'Published')
281       ->addTransition('publish', 'Publish', ['published'], 'published');
282     $workflow->save();
283
284     $this->drupalLogin($this->createUser(['administer workflows']));
285
286     // Add additional information to the workflow via the configuration form.
287     $this->drupalGet('admin/config/workflow/workflows/manage/test');
288     $this->assertSession()->pageTextContains('Example global workflow setting');
289     $this->submitForm(['type_settings[example_setting]' => 'Extra global settings'], 'Save');
290
291     $workflow_storage = $this->container->get('entity_type.manager')->getStorage('workflow');
292     $workflow = $workflow_storage->loadUnchanged('test');
293     $this->assertEquals('Extra global settings', $workflow->getTypePlugin()->getConfiguration()['example_setting']);
294   }
295
296   /**
297    * Test a workflow, state, and transition can have a numeric ID and label.
298    */
299   public function testNumericIds() {
300     $this->drupalLogin($this->createUser(['administer workflows']));
301     $this->drupalGet('admin/config/workflow/workflows');
302     $this->clickLink('Add workflow');
303     $this->submitForm(['label' => 123, 'id' => 123, 'workflow_type' => 'workflow_type_complex_test'], 'Save');
304
305     $this->assertSession()->addressEquals('admin/config/workflow/workflows/manage/123/add_state');
306
307     $this->submitForm(['label' => 456, 'id' => 456], 'Save');
308     $this->assertSession()->pageTextContains('Created 456 state.');
309
310     $this->clickLink('Add a new state');
311     $this->submitForm(['label' => 789, 'id' => 789], 'Save');
312     $this->assertSession()->pageTextContains('Created 789 state.');
313
314     $this->clickLink('Add a new transition');
315     $this->submitForm(['id' => 101112, 'label' => 101112, 'from[456]' => 456, 'to' => 789], 'Save');
316     $this->assertSession()->pageTextContains('Created 101112 transition.');
317
318     $workflow = $this->container->get('entity_type.manager')->getStorage('workflow')->loadUnchanged(123);
319     $this->assertEquals(123, $workflow->id());
320     $this->assertEquals(456, $workflow->getTypePlugin()->getState(456)->id());
321     $this->assertEquals(101112, $workflow->getTypePlugin()->getTransition(101112)->id());
322     $this->assertEquals(789, $workflow->getTypePlugin()->getTransition(101112)->to()->id());
323   }
324
325   /**
326    * Test the sorting of states and transitions by weight and label.
327    */
328   public function testSorting() {
329     $workflow = Workflow::create(['id' => 'test', 'type' => 'workflow_type_complex_test', 'label' => 'Test']);
330     $workflow
331       ->getTypePlugin()
332       ->setConfiguration([
333         'states' => [
334           'twoa' => [
335             'label' => 'twoa',
336             'weight' => 2,
337           ],
338           'three' => [
339             'label' => 'three',
340             'weight' => 3,
341           ],
342           'twob' => [
343             'label' => 'twob',
344             'weight' => 2,
345           ],
346           'one' => [
347             'label' => 'one',
348             'weight' => 1,
349           ],
350         ],
351         'transitions' => [
352           'three' => [
353             'label' => 'three',
354             'from' => ['three'],
355             'to' => 'three',
356             'weight' => 3,
357           ],
358           'twoa' => [
359             'label' => 'twoa',
360             'from' => ['twoa'],
361             'to' => 'twoa',
362             'weight' => 2,
363           ],
364           'one' => [
365             'label' => 'one',
366             'from' => ['one'],
367             'to' => 'one',
368             'weight' => 1,
369           ],
370           'twob' => [
371             'label' => 'twob',
372             'from' => ['twob'],
373             'to' => 'twob',
374             'weight' => 2,
375           ],
376         ],
377       ]);
378     $workflow->save();
379
380     $this->drupalLogin($this->createUser(['administer workflows']));
381     $this->drupalGet('admin/config/workflow/workflows/manage/test');
382     $expected_states = ['one', 'twoa', 'twob', 'three'];
383     $elements = $this->xpath('//details[@id="edit-states-container"]//table/tbody/tr');
384     foreach ($elements as $key => $element) {
385       $this->assertEquals($expected_states[$key], $element->find('xpath', 'td')->getText());
386     }
387     $expected_transitions = ['one', 'twoa', 'twob', 'three'];
388     $elements = $this->xpath('//details[@id="edit-transitions-container"]//table/tbody/tr');
389     foreach ($elements as $key => $element) {
390       $this->assertEquals($expected_transitions[$key], $element->find('xpath', 'td')->getText());
391     }
392   }
393
394 }