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