Updated to Drupal 8.6.4, which is PHP 7.3 friendly. Also updated HTMLaw library....
[yaffs-website] / web / core / modules / workspaces / tests / src / Kernel / WorkspaceIntegrationTest.php
1 <?php
2
3 namespace Drupal\Tests\workspaces\Kernel;
4
5 use Drupal\Core\Entity\EntityStorageException;
6 use Drupal\Core\Form\FormState;
7 use Drupal\entity_test\Entity\EntityTestMulRev;
8 use Drupal\entity_test\Entity\EntityTestMulRevPub;
9 use Drupal\KernelTests\KernelTestBase;
10 use Drupal\system\Form\SiteInformationForm;
11 use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
12 use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
13 use Drupal\Tests\node\Traits\NodeCreationTrait;
14 use Drupal\Tests\user\Traits\UserCreationTrait;
15 use Drupal\views\Tests\ViewResultAssertionTrait;
16 use Drupal\views\Views;
17 use Drupal\workspaces\Entity\Workspace;
18
19 /**
20  * Tests a complete deployment scenario across different workspaces.
21  *
22  * @group workspaces
23  */
24 class WorkspaceIntegrationTest extends KernelTestBase {
25
26   use ContentTypeCreationTrait;
27   use EntityReferenceTestTrait;
28   use NodeCreationTrait;
29   use UserCreationTrait;
30   use ViewResultAssertionTrait;
31
32   /**
33    * The entity type manager.
34    *
35    * @var \Drupal\Core\Entity\EntityTypeManagerInterface
36    */
37   protected $entityTypeManager;
38
39   /**
40    * An array of test workspaces, keyed by workspace ID.
41    *
42    * @var \Drupal\workspaces\WorkspaceInterface[]
43    */
44   protected $workspaces = [];
45
46   /**
47    * Creation timestamp that should be incremented for each new entity.
48    *
49    * @var int
50    */
51   protected $createdTimestamp;
52
53   /**
54    * {@inheritdoc}
55    */
56   protected static $modules = [
57     'entity_test',
58     'field',
59     'filter',
60     'node',
61     'text',
62     'user',
63     'system',
64     'views',
65   ];
66
67   /**
68    * {@inheritdoc}
69    */
70   protected function setUp() {
71     parent::setUp();
72
73     $this->entityTypeManager = \Drupal::entityTypeManager();
74
75     $this->installConfig(['filter', 'node', 'system']);
76
77     $this->installSchema('system', ['key_value_expire', 'sequences']);
78     $this->installSchema('node', ['node_access']);
79
80     $this->installEntitySchema('entity_test_mulrev');
81     $this->installEntitySchema('entity_test_mulrevpub');
82     $this->installEntitySchema('node');
83     $this->installEntitySchema('user');
84
85     $this->createContentType(['type' => 'page']);
86
87     $this->setCurrentUser($this->createUser(['administer nodes']));
88
89     // Create two nodes, a published and an unpublished one, so we can test the
90     // behavior of the module with default/existing content.
91     $this->createdTimestamp = \Drupal::time()->getRequestTime();
92     $this->createNode(['title' => 'live - 1 - r1 - published', 'created' => $this->createdTimestamp++, 'status' => TRUE]);
93     $this->createNode(['title' => 'live - 2 - r2 - unpublished', 'created' => $this->createdTimestamp++, 'status' => FALSE]);
94   }
95
96   /**
97    * Enables the Workspaces module and creates two workspaces.
98    */
99   protected function initializeWorkspacesModule() {
100     // Enable the Workspaces module here instead of the static::$modules array
101     // so we can test it with default content.
102     $this->enableModules(['workspaces']);
103     $this->container = \Drupal::getContainer();
104     $this->entityTypeManager = \Drupal::entityTypeManager();
105
106     $this->installEntitySchema('workspace');
107     $this->installEntitySchema('workspace_association');
108
109     // Create two workspaces by default, 'live' and 'stage'.
110     $this->workspaces['live'] = Workspace::create(['id' => 'live']);
111     $this->workspaces['live']->save();
112     $this->workspaces['stage'] = Workspace::create(['id' => 'stage']);
113     $this->workspaces['stage']->save();
114
115     $permissions = [
116       'administer nodes',
117       'create workspace',
118       'edit any workspace',
119       'view any workspace',
120     ];
121     $this->setCurrentUser($this->createUser($permissions));
122   }
123
124   /**
125    * Tests various scenarios for creating and deploying content in workspaces.
126    */
127   public function testWorkspaces() {
128     $this->initializeWorkspacesModule();
129
130     // Notes about the structure of the test scenarios:
131     // - a multi-dimensional array keyed by the workspace ID, then by the entity
132     //   ID and finally by the revision ID.
133     // - 'default_revision' indicates the entity revision that should be
134     //   returned by entity_load(), non-revision entity queries and non-revision
135     //   views *in a given workspace*, it does not indicate what is actually
136     //   stored in the base and data entity tables.
137     $test_scenarios = [];
138
139     // The $expected_workspace_association array holds the revision IDs which
140     // should be tracked by the Workspace Association entity type in each test
141     // scenario, keyed by workspace ID.
142     $expected_workspace_association = [];
143
144     // In the initial state we have only the two revisions that were created
145     // before the Workspaces module was installed.
146     $revision_state = [
147       'live' => [
148         1 => [
149           1 => [
150             'title' => 'live - 1 - r1 - published',
151             'status' => TRUE,
152             'default_revision' => TRUE,
153           ],
154         ],
155         2 => [
156           2 => [
157             'title' => 'live - 2 - r2 - unpublished',
158             'status' => FALSE,
159             'default_revision' => TRUE,
160           ],
161         ],
162       ],
163       'stage' => [
164         1 => [
165           1 => [
166             'title' => 'live - 1 - r1 - published',
167             'status' => TRUE,
168             'default_revision' => TRUE,
169           ],
170         ],
171         2 => [
172           2 => [
173             'title' => 'live - 2 - r2 - unpublished',
174             'status' => FALSE,
175             'default_revision' => TRUE,
176           ],
177         ],
178       ],
179     ];
180     $test_scenarios['initial_state'] = $revision_state;
181     $expected_workspace_association['initial_state'] = ['stage' => []];
182
183     // Unpublish node 1 in 'stage'. The new revision is also added to 'live' but
184     // it is not the default revision.
185     $revision_state = array_replace_recursive($revision_state, [
186       'live' => [
187         1 => [
188           3 => [
189             'title' => 'stage - 1 - r3 - unpublished',
190             'status' => FALSE,
191             'default_revision' => FALSE,
192           ],
193         ],
194       ],
195       'stage' => [
196         1 => [
197           1 => ['default_revision' => FALSE],
198           3 => [
199             'title' => 'stage - 1 - r3 - unpublished',
200             'status' => FALSE,
201             'default_revision' => TRUE,
202           ],
203         ],
204       ],
205     ]);
206     $test_scenarios['unpublish_node_1_in_stage'] = $revision_state;
207     $expected_workspace_association['unpublish_node_1_in_stage'] = ['stage' => [3]];
208
209     // Publish node 2 in 'stage'. The new revision is also added to 'live' but
210     // it is not the default revision.
211     $revision_state = array_replace_recursive($revision_state, [
212       'live' => [
213         2 => [
214           4 => [
215             'title' => 'stage - 2 - r4 - published',
216             'status' => TRUE,
217             'default_revision' => FALSE,
218           ],
219         ],
220       ],
221       'stage' => [
222         2 => [
223           2 => ['default_revision' => FALSE],
224           4 => [
225             'title' => 'stage - 2 - r4 - published',
226             'status' => TRUE,
227             'default_revision' => TRUE,
228           ],
229         ],
230       ],
231     ]);
232     $test_scenarios['publish_node_2_in_stage'] = $revision_state;
233     $expected_workspace_association['publish_node_2_in_stage'] = ['stage' => [3, 4]];
234
235     // Adding a new unpublished node on 'stage' should create a single
236     // unpublished revision on both 'stage' and 'live'.
237     $revision_state = array_replace_recursive($revision_state, [
238       'live' => [
239         3 => [
240           5 => [
241             'title' => 'stage - 3 - r5 - unpublished',
242             'status' => FALSE,
243             'default_revision' => TRUE,
244           ],
245         ],
246       ],
247       'stage' => [
248         3 => [
249           5 => [
250             'title' => 'stage - 3 - r5 - unpublished',
251             'status' => FALSE,
252             'default_revision' => TRUE,
253           ],
254         ],
255       ],
256     ]);
257     $test_scenarios['add_unpublished_node_in_stage'] = $revision_state;
258     $expected_workspace_association['add_unpublished_node_in_stage'] = ['stage' => [3, 4, 5]];
259
260     // Adding a new published node on 'stage' should create two revisions, an
261     // unpublished revision on 'live' and a published one on 'stage'.
262     $revision_state = array_replace_recursive($revision_state, [
263       'live' => [
264         4 => [
265           6 => [
266             'title' => 'stage - 4 - r6 - published',
267             'status' => FALSE,
268             'default_revision' => TRUE,
269           ],
270           7 => [
271             'title' => 'stage - 4 - r6 - published',
272             'status' => TRUE,
273             'default_revision' => FALSE,
274           ],
275         ],
276       ],
277       'stage' => [
278         4 => [
279           6 => [
280             'title' => 'stage - 4 - r6 - published',
281             'status' => FALSE,
282             'default_revision' => FALSE,
283           ],
284           7 => [
285             'title' => 'stage - 4 - r6 - published',
286             'status' => TRUE,
287             'default_revision' => TRUE,
288           ],
289         ],
290       ],
291     ]);
292     $test_scenarios['add_published_node_in_stage'] = $revision_state;
293     $expected_workspace_association['add_published_node_in_stage'] = ['stage' => [3, 4, 5, 6, 7]];
294
295     // Deploying 'stage' to 'live' should simply make the latest revisions in
296     // 'stage' the default ones in 'live'.
297     $revision_state = array_replace_recursive($revision_state, [
298       'live' => [
299         1 => [
300           1 => ['default_revision' => FALSE],
301           3 => ['default_revision' => TRUE],
302         ],
303         2 => [
304           2 => ['default_revision' => FALSE],
305           4 => ['default_revision' => TRUE],
306         ],
307         // Node 3 has a single revision for both 'stage' and 'live' and it is
308         // already the default revision in both of them.
309         4 => [
310           6 => ['default_revision' => FALSE],
311           7 => ['default_revision' => TRUE],
312         ],
313       ],
314     ]);
315     $test_scenarios['push_stage_to_live'] = $revision_state;
316     $expected_workspace_association['push_stage_to_live'] = ['stage' => []];
317
318     // Check the initial state after the module was installed.
319     $this->assertWorkspaceStatus($test_scenarios['initial_state'], 'node');
320     $this->assertWorkspaceAssociation($expected_workspace_association['initial_state'], 'node');
321
322     // Unpublish node 1 in 'stage'.
323     $this->switchToWorkspace('stage');
324     $node = $this->entityTypeManager->getStorage('node')->load(1);
325     $node->setTitle('stage - 1 - r3 - unpublished');
326     $node->setUnpublished();
327     $node->save();
328     $this->assertWorkspaceStatus($test_scenarios['unpublish_node_1_in_stage'], 'node');
329     $this->assertWorkspaceAssociation($expected_workspace_association['unpublish_node_1_in_stage'], 'node');
330
331     // Publish node 2 in 'stage'.
332     $this->switchToWorkspace('stage');
333     $node = $this->entityTypeManager->getStorage('node')->load(2);
334     $node->setTitle('stage - 2 - r4 - published');
335     $node->setPublished();
336     $node->save();
337     $this->assertWorkspaceStatus($test_scenarios['publish_node_2_in_stage'], 'node');
338     $this->assertWorkspaceAssociation($expected_workspace_association['publish_node_2_in_stage'], 'node');
339
340     // Add a new unpublished node on 'stage'.
341     $this->switchToWorkspace('stage');
342     $this->createNode(['title' => 'stage - 3 - r5 - unpublished', 'created' => $this->createdTimestamp++, 'status' => FALSE]);
343     $this->assertWorkspaceStatus($test_scenarios['add_unpublished_node_in_stage'], 'node');
344     $this->assertWorkspaceAssociation($expected_workspace_association['add_unpublished_node_in_stage'], 'node');
345
346     // Add a new published node on 'stage'.
347     $this->switchToWorkspace('stage');
348     $this->createNode(['title' => 'stage - 4 - r6 - published', 'created' => $this->createdTimestamp++, 'status' => TRUE]);
349     $this->assertWorkspaceStatus($test_scenarios['add_published_node_in_stage'], 'node');
350     $this->assertWorkspaceAssociation($expected_workspace_association['add_published_node_in_stage'], 'node');
351
352     // Deploy 'stage' to 'live'.
353     /** @var \Drupal\workspaces\WorkspacePublisher $workspace_publisher */
354     $workspace_publisher = \Drupal::service('workspaces.operation_factory')->getPublisher($this->workspaces['stage']);
355
356     // Check which revisions need to be pushed.
357     $expected = [
358       'node' => [
359         3 => 1,
360         4 => 2,
361         5 => 3,
362         7 => 4,
363       ],
364     ];
365     $this->assertEquals($expected, $workspace_publisher->getDifferringRevisionIdsOnSource());
366
367     $this->workspaces['stage']->publish();
368     $this->assertWorkspaceStatus($test_scenarios['push_stage_to_live'], 'node');
369     $this->assertWorkspaceAssociation($expected_workspace_association['push_stage_to_live'], 'node');
370
371     // Check that there are no more revisions to push.
372     $this->assertEmpty($workspace_publisher->getDifferringRevisionIdsOnSource());
373   }
374
375   /**
376    * Tests the Entity Query relationship API with workspaces.
377    */
378   public function testEntityQueryRelationship() {
379     $this->initializeWorkspacesModule();
380
381     // Add an entity reference field that targets 'entity_test_mulrevpub'
382     // entities.
383     $this->createEntityReferenceField('node', 'page', 'field_test_entity', 'Test entity reference', 'entity_test_mulrevpub');
384
385     // Add an entity reference field that targets 'node' entities so we can test
386     // references to the same base tables.
387     $this->createEntityReferenceField('node', 'page', 'field_test_node', 'Test node reference', 'node');
388
389     $this->switchToWorkspace('live');
390     $node_1 = $this->createNode([
391       'title' => 'live node 1',
392     ]);
393     $entity_test = EntityTestMulRevPub::create([
394       'name' => 'live entity_test_mulrevpub',
395       'non_rev_field' => 'live non-revisionable value',
396     ]);
397     $entity_test->save();
398
399     $node_2 = $this->createNode([
400       'title' => 'live node 2',
401       'field_test_entity' => $entity_test->id(),
402       'field_test_node' => $node_1->id(),
403     ]);
404
405     // Switch to the 'stage' workspace and change some values for the referenced
406     // entities.
407     $this->switchToWorkspace('stage');
408     $node_1->title->value = 'stage node 1';
409     $node_1->save();
410
411     $node_2->title->value = 'stage node 2';
412     $node_2->save();
413
414     $entity_test->name->value = 'stage entity_test_mulrevpub';
415     $entity_test->non_rev_field->value = 'stage non-revisionable value';
416     $entity_test->save();
417
418     // Make sure that we're requesting the default revision.
419     $query = $this->entityTypeManager->getStorage('node')->getQuery();
420     $query->currentRevision();
421
422     $query
423       // Check a condition on the revision data table.
424       ->condition('title', 'stage node 2')
425       // Check a condition on the revision table.
426       ->condition('revision_uid', $node_2->getRevisionUserId())
427       // Check a condition on the data table.
428       ->condition('type', $node_2->bundle())
429       // Check a condition on the base table.
430       ->condition('uuid', $node_2->uuid());
431
432     // Add conditions for a reference to the same entity type.
433     $query
434       // Check a condition on the revision data table.
435       ->condition('field_test_node.entity.title', 'stage node 1')
436       // Check a condition on the revision table.
437       ->condition('field_test_node.entity.revision_uid', $node_1->getRevisionUserId())
438       // Check a condition on the data table.
439       ->condition('field_test_node.entity.type', $node_1->bundle())
440       // Check a condition on the base table.
441       ->condition('field_test_node.entity.uuid', $node_1->uuid());
442
443     // Add conditions for a reference to a different entity type.
444     // @todo Re-enable the two conditions below when we find a way to not join
445     //   the workspace_association table for every duplicate entity base table
446     //   join.
447     // @see https://www.drupal.org/project/drupal/issues/2983639
448     $query
449       // Check a condition on the revision data table.
450       // ->condition('field_test_entity.entity.name', 'stage entity_test_mulrevpub')
451       // Check a condition on the data table.
452       // ->condition('field_test_entity.entity.non_rev_field', 'stage non-revisionable value')
453       // Check a condition on the base table.
454       ->condition('field_test_entity.entity.uuid', $entity_test->uuid());
455
456     $result = $query->execute();
457     $this->assertSame([$node_2->getRevisionId() => $node_2->id()], $result);
458   }
459
460   /**
461    * Tests CRUD operations for unsupported entity types.
462    */
463   public function testDisallowedEntityCRUDInNonDefaultWorkspace() {
464     $this->initializeWorkspacesModule();
465
466     // Create an unsupported entity type in the default workspace.
467     $this->switchToWorkspace('live');
468     $entity_test = EntityTestMulRev::create([
469       'name' => 'live entity_test_mulrev',
470     ]);
471     $entity_test->save();
472
473     // Switch to a non-default workspace and check that any entity type CRUD are
474     // not allowed.
475     $this->switchToWorkspace('stage');
476
477     // Check updating an existing entity.
478     $entity_test->name->value = 'stage entity_test_mulrev';
479     $entity_test->setNewRevision(TRUE);
480     $this->setExpectedException(EntityStorageException::class, 'This entity can only be saved in the default workspace.');
481     $entity_test->save();
482
483     // Check saving a new entity.
484     $new_entity_test = EntityTestMulRev::create([
485       'name' => 'stage entity_test_mulrev',
486     ]);
487     $this->setExpectedException(EntityStorageException::class, 'This entity can only be saved in the default workspace.');
488     $new_entity_test->save();
489
490     // Check deleting an existing entity.
491     $this->setExpectedException(EntityStorageException::class, 'This entity can only be deleted in the default workspace.');
492     $entity_test->delete();
493   }
494
495   /**
496    * Checks entity load, entity queries and views results for a test scenario.
497    *
498    * @param array $expected
499    *   An array of expected values, as defined in ::testWorkspaces().
500    * @param string $entity_type_id
501    *   The ID of the entity type that is being tested.
502    */
503   protected function assertWorkspaceStatus(array $expected, $entity_type_id) {
504     $expected = $this->flattenExpectedValues($expected, $entity_type_id);
505
506     $entity_keys = $this->entityTypeManager->getDefinition($entity_type_id)->getKeys();
507     foreach ($expected as $workspace_id => $expected_values) {
508       $this->switchToWorkspace($workspace_id);
509
510       // Check that default revisions are swapped with the workspace revision.
511       $this->assertEntityLoad($expected_values, $entity_type_id);
512
513       // Check that non-default revisions are not changed.
514       $this->assertEntityRevisionLoad($expected_values, $entity_type_id);
515
516       // Check that entity queries return the correct results.
517       $this->assertEntityQuery($expected_values, $entity_type_id);
518
519       // Check that the 'Frontpage' view only shows published content that is
520       // also considered as the default revision in the given workspace.
521       $expected_frontpage = array_filter($expected_values, function ($expected_value) {
522         return $expected_value['status'] === TRUE && $expected_value['default_revision'] === TRUE;
523       });
524       // The 'Frontpage' view will output nodes in reverse creation order.
525       usort($expected_frontpage, function ($a, $b) {
526         return $b['nid'] - $a['nid'];
527       });
528       $view = Views::getView('frontpage');
529       $view->execute();
530       $this->assertIdenticalResultset($view, $expected_frontpage, ['nid' => 'nid']);
531
532       $rendered_view = $view->render('page_1');
533       $output = \Drupal::service('renderer')->renderRoot($rendered_view);
534       $this->setRawContent($output);
535       foreach ($expected_values as $expected_entity_values) {
536         if ($expected_entity_values[$entity_keys['published']] === TRUE && $expected_entity_values['default_revision'] === TRUE) {
537           $this->assertRaw($expected_entity_values[$entity_keys['label']]);
538         }
539         // Node 4 will always appear in the 'stage' workspace because it has
540         // both an unpublished revision as well as a published one.
541         elseif ($workspace_id != 'stage' && $expected_entity_values[$entity_keys['id']] != 4) {
542           $this->assertNoRaw($expected_entity_values[$entity_keys['label']]);
543         }
544       }
545     }
546   }
547
548   /**
549    * Asserts that default revisions are properly swapped in a workspace.
550    *
551    * @param array $expected_values
552    *   An array of expected values, as defined in ::testWorkspaces().
553    * @param string $entity_type_id
554    *   The ID of the entity type to check.
555    */
556   protected function assertEntityLoad(array $expected_values, $entity_type_id) {
557     // Filter the expected values so we can check only the default revisions.
558     $expected_default_revisions = array_filter($expected_values, function ($expected_value) {
559       return $expected_value['default_revision'] === TRUE;
560     });
561
562     $entity_keys = $this->entityTypeManager->getDefinition($entity_type_id)->getKeys();
563     $id_key = $entity_keys['id'];
564     $revision_key = $entity_keys['revision'];
565     $label_key = $entity_keys['label'];
566     $published_key = $entity_keys['published'];
567
568     // Check \Drupal\Core\Entity\EntityStorageInterface::loadMultiple().
569     /** @var \Drupal\Core\Entity\EntityInterface[]|\Drupal\Core\Entity\RevisionableInterface[]|\Drupal\Core\Entity\EntityPublishedInterface[] $entities */
570     $entities = $this->entityTypeManager->getStorage($entity_type_id)->loadMultiple(array_column($expected_default_revisions, $id_key));
571     foreach ($expected_default_revisions as $expected_default_revision) {
572       $entity_id = $expected_default_revision[$id_key];
573       $this->assertEquals($expected_default_revision[$revision_key], $entities[$entity_id]->getRevisionId());
574       $this->assertEquals($expected_default_revision[$label_key], $entities[$entity_id]->label());
575       $this->assertEquals($expected_default_revision[$published_key], $entities[$entity_id]->isPublished());
576     }
577
578     // Check \Drupal\Core\Entity\EntityStorageInterface::loadUnchanged().
579     foreach ($expected_default_revisions as $expected_default_revision) {
580       /** @var \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface|\Drupal\Core\Entity\EntityPublishedInterface $entity */
581       $entity = $this->entityTypeManager->getStorage($entity_type_id)->loadUnchanged($expected_default_revision[$id_key]);
582       $this->assertEquals($expected_default_revision[$revision_key], $entity->getRevisionId());
583       $this->assertEquals($expected_default_revision[$label_key], $entity->label());
584       $this->assertEquals($expected_default_revision[$published_key], $entity->isPublished());
585     }
586   }
587
588   /**
589    * Asserts that non-default revisions are not changed.
590    *
591    * @param array $expected_values
592    *   An array of expected values, as defined in ::testWorkspaces().
593    * @param string $entity_type_id
594    *   The ID of the entity type to check.
595    */
596   protected function assertEntityRevisionLoad(array $expected_values, $entity_type_id) {
597     $entity_keys = $this->entityTypeManager->getDefinition($entity_type_id)->getKeys();
598     $id_key = $entity_keys['id'];
599     $revision_key = $entity_keys['revision'];
600     $label_key = $entity_keys['label'];
601     $published_key = $entity_keys['published'];
602
603     /** @var \Drupal\Core\Entity\EntityInterface[]|\Drupal\Core\Entity\RevisionableInterface[]|\Drupal\Core\Entity\EntityPublishedInterface[] $entities */
604     $entities = $this->entityTypeManager->getStorage($entity_type_id)->loadMultipleRevisions(array_column($expected_values, $revision_key));
605     foreach ($expected_values as $expected_revision) {
606       $revision_id = $expected_revision[$revision_key];
607       $this->assertEquals($expected_revision[$id_key], $entities[$revision_id]->id());
608       $this->assertEquals($expected_revision[$revision_key], $entities[$revision_id]->getRevisionId());
609       $this->assertEquals($expected_revision[$label_key], $entities[$revision_id]->label());
610       $this->assertEquals($expected_revision[$published_key], $entities[$revision_id]->isPublished());
611     }
612   }
613
614   /**
615    * Asserts that entity queries are giving the correct results in a workspace.
616    *
617    * @param array $expected_values
618    *   An array of expected values, as defined in ::testWorkspaces().
619    * @param string $entity_type_id
620    *   The ID of the entity type to check.
621    */
622   protected function assertEntityQuery(array $expected_values, $entity_type_id) {
623     $storage = $this->entityTypeManager->getStorage($entity_type_id);
624     $entity_keys = $this->entityTypeManager->getDefinition($entity_type_id)->getKeys();
625     $id_key = $entity_keys['id'];
626     $revision_key = $entity_keys['revision'];
627     $label_key = $entity_keys['label'];
628     $published_key = $entity_keys['published'];
629
630     // Filter the expected values so we can check only the default revisions.
631     $expected_default_revisions = array_filter($expected_values, function ($expected_value) {
632       return $expected_value['default_revision'] === TRUE;
633     });
634
635     // Check entity query counts.
636     $result = $storage->getQuery()->count()->execute();
637     $this->assertEquals(count($expected_default_revisions), $result);
638
639     $result = $storage->getAggregateQuery()->count()->execute();
640     $this->assertEquals(count($expected_default_revisions), $result);
641
642     // Check entity queries with no conditions.
643     $result = $storage->getQuery()->execute();
644     $expected_result = array_combine(array_column($expected_default_revisions, $revision_key), array_column($expected_default_revisions, $id_key));
645     $this->assertEquals($expected_result, $result);
646
647     // Check querying each revision individually.
648     foreach ($expected_values as $expected_value) {
649       $query = $storage->getQuery();
650       $query
651         ->condition($entity_keys['id'], $expected_value[$id_key])
652         ->condition($entity_keys['label'], $expected_value[$label_key])
653         ->condition($entity_keys['published'], (int) $expected_value[$published_key]);
654
655       // If the entity is not expected to be the default revision, we need to
656       // query all revisions if we want to find it.
657       if (!$expected_value['default_revision']) {
658         $query->allRevisions();
659       }
660
661       $result = $query->execute();
662       $this->assertEquals([$expected_value[$revision_key] => $expected_value[$id_key]], $result);
663     }
664   }
665
666   /**
667    * Checks the workspace_association entries for a test scenario.
668    *
669    * @param array $expected
670    *   An array of expected values, as defined in ::testWorkspaces().
671    * @param string $entity_type_id
672    *   The ID of the entity type that is being tested.
673    */
674   protected function assertWorkspaceAssociation(array $expected, $entity_type_id) {
675     /** @var \Drupal\workspaces\WorkspaceAssociationStorageInterface $workspace_association_storage */
676     $workspace_association_storage = $this->entityTypeManager->getStorage('workspace_association');
677     foreach ($expected as $workspace_id => $expected_tracked_revision_ids) {
678       $tracked_entities = $workspace_association_storage->getTrackedEntities($workspace_id, TRUE);
679       $tracked_revision_ids = isset($tracked_entities[$entity_type_id]) ? $tracked_entities[$entity_type_id] : [];
680       $this->assertEquals($expected_tracked_revision_ids, array_keys($tracked_revision_ids));
681     }
682   }
683
684   /**
685    * Sets a given workspace as active.
686    *
687    * @param string $workspace_id
688    *   The ID of the workspace to switch to.
689    */
690   protected function switchToWorkspace($workspace_id) {
691     // Switch the test runner's context to the specified workspace.
692     $workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
693     \Drupal::service('workspaces.manager')->setActiveWorkspace($workspace);
694   }
695
696   /**
697    * Flattens the expectations array defined by testWorkspaces().
698    *
699    * @param array $expected
700    *   An array as defined by testWorkspaces().
701    * @param string $entity_type_id
702    *   The ID of the entity type that is being tested.
703    *
704    * @return array
705    *   An array where all the entity IDs and revision IDs are merged inside each
706    *   expected values array.
707    */
708   protected function flattenExpectedValues(array $expected, $entity_type_id) {
709     $flattened = [];
710
711     $entity_keys = $this->entityTypeManager->getDefinition($entity_type_id)->getKeys();
712     foreach ($expected as $workspace_id => $workspace_values) {
713       foreach ($workspace_values as $entity_id => $entity_revisions) {
714         foreach ($entity_revisions as $revision_id => $revision_values) {
715           $flattened[$workspace_id][] = [$entity_keys['id'] => $entity_id, $entity_keys['revision'] => $revision_id] + $revision_values;
716         }
717       }
718     }
719
720     return $flattened;
721   }
722
723   /**
724    * Tests that entity forms can be stored in the form cache.
725    */
726   public function testFormCacheForEntityForms() {
727     $this->initializeWorkspacesModule();
728     $this->switchToWorkspace('stage');
729
730     $form_builder = $this->container->get('form_builder');
731
732     $form = $this->entityTypeManager->getFormObject('entity_test_mulrevpub', 'default');
733     $form->setEntity(EntityTestMulRevPub::create([]));
734
735     $form_state = new FormState();
736     $built_form = $form_builder->buildForm($form, $form_state);
737     $form_builder->setCache($built_form['#build_id'], $built_form, $form_state);
738   }
739
740   /**
741    * Tests that non-entity forms can be stored in the form cache.
742    */
743   public function testFormCacheForRegularForms() {
744     $this->initializeWorkspacesModule();
745     $this->switchToWorkspace('stage');
746
747     $form_builder = $this->container->get('form_builder');
748
749     $form_state = new FormState();
750     $built_form = $form_builder->getForm(SiteInformationForm::class, $form_state);
751     $form_builder->setCache($built_form['#build_id'], $built_form, $form_state);
752   }
753
754 }