');
+ $page = $this->getSession()->getPage();
+
+ $this->assertTrue($page->hasContent('Workspace switcher'));
+ }
+
+ /**
+ * Sets a given workspace as "active" for subsequent requests.
+ *
+ * This assumes that the switcher block has already been setup by calling
+ * setupWorkspaceSwitcherBlock().
+ *
+ * @param \Drupal\workspace\WorkspaceInterface $workspace
+ * The workspace to set active.
+ */
+ protected function switchToWorkspace(WorkspaceInterface $workspace) {
+ /** @var \Drupal\workspace\WorkspaceManager $workspace_manager */
+ $workspace_manager = \Drupal::service('workspace.manager');
+ if ($workspace_manager->getActiveWorkspace()->id() !== $workspace->id()) {
+ // Switch the system under test to the specified workspace.
+ /** @var \Drupal\Tests\WebAssert $session */
+ $session = $this->assertSession();
+ $session->buttonExists('Activate');
+ $this->drupalPostForm(NULL, ['workspace_id' => $workspace->id()], t('Activate'));
+ $session->pageTextContains($workspace->label() . ' is now the active workspace.');
+
+ // Switch the test runner's context to the specified workspace.
+ \Drupal::service('workspace.manager')->setActiveWorkspace($workspace);
+
+ // If we don't do both of those, test runner utility methods will not be
+ // run in the same workspace as the system under test, and you'll be left
+ // wondering why your test runner cannot find content you just created.
+ }
+ }
+
+ /**
+ * Creates a new node type.
+ *
+ * @param string $label
+ * The human-readable label of the type to create.
+ * @param string $machine_name
+ * The machine name of the type to create.
+ */
+ protected function createNodeType($label, $machine_name) {
+ $node_type = NodeType::create([
+ 'type' => $machine_name,
+ 'label' => $label,
+ ]);
+ $node_type->save();
+ }
+
+ /**
+ * Creates a node by "clicking" buttons.
+ *
+ * @param string $label
+ * The label of the Node to create.
+ * @param string $bundle
+ * The bundle of the Node to create.
+ * @param bool $publish
+ * The publishing status to set.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface
+ * The Node that was just created.
+ *
+ * @throws \Behat\Mink\Exception\ElementNotFoundException
+ */
+ protected function createNodeThroughUi($label, $bundle, $publish = TRUE) {
+ $this->drupalGet('/node/add/' . $bundle);
+
+ /** @var \Behat\Mink\Session $session */
+ $session = $this->getSession();
+ $this->assertSession()->statusCodeEquals(200);
+
+ /** @var \Behat\Mink\Element\DocumentElement $page */
+ $page = $session->getPage();
+ $page->fillField('Title', $label);
+ if ($publish) {
+ $page->findButton(t('Save'))->click();
+ }
+ else {
+ $page->uncheckField('Published');
+ $page->findButton(t('Save'))->click();
+ }
+
+ $session->getPage()->hasContent("{$label} has been created");
+
+ return $this->getOneEntityByLabel('node', $label);
+ }
+
+ /**
+ * Determine if the content list has an entity's label.
+ *
+ * This assertion can be used to validate a particular entity exists in the
+ * current workspace.
+ */
+ protected function isLabelInContentOverview($label) {
+ $this->drupalGet('/admin/content');
+ $session = $this->getSession();
+ $this->assertSession()->statusCodeEquals(200);
+ $page = $session->getPage();
+ return $page->hasContent($label);
+ }
+
+}
diff --git a/core/modules/workspace/tests/src/Functional/WorkspaceUninstallTest.php b/core/modules/workspace/tests/src/Functional/WorkspaceUninstallTest.php
new file mode 100644
index 0000000..a0ed21b
--- /dev/null
+++ b/core/modules/workspace/tests/src/Functional/WorkspaceUninstallTest.php
@@ -0,0 +1,41 @@
+drupalLogin($this->rootUser);
+ $this->drupalGet('/admin/modules/uninstall');
+ $session = $this->assertSession();
+ $session->linkExists('Remove workspaces');
+ $this->clickLink('Remove workspaces');
+ $session->pageTextContains('Are you sure you want to delete all workspaces?');
+ $this->drupalPostForm('/admin/modules/uninstall/entity/workspace', [], 'Delete all workspaces');
+ $this->drupalPostForm('admin/modules/uninstall', ['uninstall[workspace]' => TRUE], 'Uninstall');
+ $this->drupalPostForm(NULL, [], 'Uninstall');
+ $session->pageTextContains('The selected modules have been uninstalled.');
+ $session->pageTextNotContains('Workspace');
+ }
+
+}
diff --git a/core/modules/workspace/tests/src/Functional/WorkspaceViewTest.php b/core/modules/workspace/tests/src/Functional/WorkspaceViewTest.php
new file mode 100644
index 0000000..27ca922
--- /dev/null
+++ b/core/modules/workspace/tests/src/Functional/WorkspaceViewTest.php
@@ -0,0 +1,98 @@
+drupalCreateUser($permissions);
+
+ // Login as a limited-access user and create a workspace.
+ $this->drupalLogin($editor1);
+ $this->createWorkspaceThroughUi('Bears', 'bears');
+
+ $bears = Workspace::load('bears');
+
+ // Now login as a different user and create a workspace.
+ $editor2 = $this->drupalCreateUser($permissions);
+
+ $this->drupalLogin($editor2);
+ $this->createWorkspaceThroughUi('Packers', 'packers');
+
+ $packers = Workspace::load('packers');
+
+ // Load the activate form for the Bears workspace. It should fail because
+ // the workspace belongs to someone else.
+ $this->drupalGet("admin/config/workflow/workspace/{$bears->id()}/activate");
+ $this->assertSession()->statusCodeEquals(403);
+
+ // But editor 2 should be able to activate the Packers workspace.
+ $this->drupalGet("admin/config/workflow/workspace/{$packers->id()}/activate");
+ $this->assertSession()->statusCodeEquals(200);
+ }
+
+ /**
+ * Verifies that a user can view any workspace.
+ */
+ public function testViewAnyWorkspace() {
+ $permissions = [
+ 'access administration pages',
+ 'administer site configuration',
+ 'create workspace',
+ 'edit own workspace',
+ 'view any workspace',
+ ];
+
+ $editor1 = $this->drupalCreateUser($permissions);
+
+ // Login as a limited-access user and create a workspace.
+ $this->drupalLogin($editor1);
+
+ $this->createWorkspaceThroughUi('Bears', 'bears');
+
+ $bears = Workspace::load('bears');
+
+ // Now login as a different user and create a workspace.
+ $editor2 = $this->drupalCreateUser($permissions);
+
+ $this->drupalLogin($editor2);
+ $this->createWorkspaceThroughUi('Packers', 'packers');
+
+ $packers = Workspace::load('packers');
+
+ // Load the activate form for the Bears workspace. This user should be
+ // able to see both workspaces because of the "view any" permission.
+ $this->drupalGet("admin/config/workflow/workspace/{$bears->id()}/activate");
+
+ $this->assertSession()->statusCodeEquals(200);
+
+ // But editor 2 should be able to activate the Packers workspace.
+ $this->drupalGet("admin/config/workflow/workspace/{$packers->id()}/activate");
+ $this->assertSession()->statusCodeEquals(200);
+ }
+
+}
diff --git a/core/modules/workspace/tests/src/Kernel/WorkspaceAccessTest.php b/core/modules/workspace/tests/src/Kernel/WorkspaceAccessTest.php
new file mode 100644
index 0000000..76242ef
--- /dev/null
+++ b/core/modules/workspace/tests/src/Kernel/WorkspaceAccessTest.php
@@ -0,0 +1,78 @@
+installSchema('system', ['sequences']);
+
+ $this->installEntitySchema('workspace');
+ $this->installEntitySchema('user');
+
+ // User 1.
+ $this->createUser();
+ }
+
+ /**
+ * Test cases for testWorkspaceAccess().
+ *
+ * @return array
+ * An array of operations and permissions to test with.
+ */
+ public function operationCases() {
+ return [
+ ['create', 'create workspace'],
+ ['view', 'view any workspace'],
+ ['view', 'view own workspace'],
+ ['update', 'edit any workspace'],
+ ['update', 'edit own workspace'],
+ ];
+ }
+
+ /**
+ * Verifies all workspace roles have the correct access for the operation.
+ *
+ * @param string $operation
+ * The operation to test with.
+ * @param string $permission
+ * The permission to test with.
+ *
+ * @dataProvider operationCases
+ */
+ public function testWorkspaceAccess($operation, $permission) {
+ $user = $this->createUser();
+ $this->setCurrentUser($user);
+ $workspace = Workspace::create(['id' => 'oak']);
+ $workspace->save();
+ $role = $this->createRole([$permission]);
+ $user->addRole($role);
+ $this->assertTrue($workspace->access($operation, $user));
+ }
+
+}
diff --git a/core/modules/workspace/tests/src/Kernel/WorkspaceIntegrationTest.php b/core/modules/workspace/tests/src/Kernel/WorkspaceIntegrationTest.php
new file mode 100644
index 0000000..4babd2c
--- /dev/null
+++ b/core/modules/workspace/tests/src/Kernel/WorkspaceIntegrationTest.php
@@ -0,0 +1,633 @@
+entityTypeManager = \Drupal::entityTypeManager();
+
+ $this->installConfig(['filter', 'node', 'system']);
+
+ $this->installSchema('system', ['key_value_expire', 'sequences']);
+ $this->installSchema('node', ['node_access']);
+
+ $this->installEntitySchema('entity_test_mulrev');
+ $this->installEntitySchema('node');
+ $this->installEntitySchema('user');
+
+ $this->createContentType(['type' => 'page']);
+
+ $this->setCurrentUser($this->createUser(['administer nodes']));
+
+ // Create two nodes, a published and an unpublished one, so we can test the
+ // behavior of the module with default/existing content.
+ $this->createdTimestamp = \Drupal::time()->getRequestTime();
+ $this->createNode(['title' => 'live - 1 - r1 - published', 'created' => $this->createdTimestamp++, 'status' => TRUE]);
+ $this->createNode(['title' => 'live - 2 - r2 - unpublished', 'created' => $this->createdTimestamp++, 'status' => FALSE]);
+ }
+
+ /**
+ * Enables the Workspace module and creates two workspaces.
+ */
+ protected function initializeWorkspaceModule() {
+ // Enable the Workspace module here instead of the static::$modules array so
+ // we can test it with default content.
+ $this->enableModules(['workspace']);
+ $this->container = \Drupal::getContainer();
+ $this->entityTypeManager = \Drupal::entityTypeManager();
+
+ $this->installEntitySchema('workspace');
+ $this->installEntitySchema('workspace_association');
+ $this->installEntitySchema('replication_log');
+
+ // Create two workspaces by default, 'live' and 'stage'.
+ $this->workspaces['live'] = Workspace::create(['id' => 'live']);
+ $this->workspaces['live']->save();
+ $this->workspaces['stage'] = Workspace::create(['id' => 'stage', 'upstream' => 'local_workspace:live']);
+ $this->workspaces['stage']->save();
+
+ $permissions = [
+ 'administer nodes',
+ 'create workspace',
+ 'edit any workspace',
+ 'view any workspace',
+ ];
+ $this->setCurrentUser($this->createUser($permissions));
+ }
+
+ /**
+ * Tests various scenarios for creating and deploying content in workspaces.
+ */
+ public function testWorkspaces() {
+ $this->initializeWorkspaceModule();
+
+ // Notes about the structure of the test scenarios:
+ // - 'default_revision' indicates the entity revision that should be
+ // returned by entity_load(), non-revision entity queries and non-revision
+ // views *in a given workspace*, it does not indicate what is actually
+ // stored in the base entity tables.
+ $test_scenarios = [];
+
+ // A multi-dimensional array keyed by the workspace ID, then by the entity
+ // and finally by the revision ID.
+ //
+ // In the initial state we have only the two revisions that were created
+ // before the Workspace module was installed.
+ $revision_state = [
+ 'live' => [
+ 1 => [
+ 1 => [
+ 'title' => 'live - 1 - r1 - published',
+ 'status' => TRUE,
+ 'default_revision' => TRUE,
+ ],
+ ],
+ 2 => [
+ 2 => [
+ 'title' => 'live - 2 - r2 - unpublished',
+ 'status' => FALSE,
+ 'default_revision' => TRUE,
+ ],
+ ],
+ ],
+ 'stage' => [
+ 1 => [
+ 1 => [
+ 'title' => 'live - 1 - r1 - published',
+ 'status' => TRUE,
+ 'default_revision' => TRUE,
+ ],
+ ],
+ 2 => [
+ 2 => [
+ 'title' => 'live - 2 - r2 - unpublished',
+ 'status' => FALSE,
+ 'default_revision' => TRUE,
+ ],
+ ],
+ ],
+ ];
+ $test_scenarios['initial_state'] = $revision_state;
+
+ // Unpublish node 1 in 'stage'. The new revision is also added to 'live' but
+ // it is not the default revision.
+ $revision_state = array_replace_recursive($revision_state, [
+ 'live' => [
+ 1 => [
+ 3 => [
+ 'title' => 'stage - 1 - r3 - unpublished',
+ 'status' => FALSE,
+ 'default_revision' => FALSE,
+ ],
+ ],
+ ],
+ 'stage' => [
+ 1 => [
+ 1 => ['default_revision' => FALSE],
+ 3 => [
+ 'title' => 'stage - 1 - r3 - unpublished',
+ 'status' => FALSE,
+ 'default_revision' => TRUE,
+ ],
+ ],
+ ],
+ ]);
+ $test_scenarios['unpublish_node_1_in_stage'] = $revision_state;
+
+ // Publish node 2 in 'stage'. The new revision is also added to 'live' but
+ // it is not the default revision.
+ $revision_state = array_replace_recursive($revision_state, [
+ 'live' => [
+ 2 => [
+ 4 => [
+ 'title' => 'stage - 2 - r4 - published',
+ 'status' => TRUE,
+ 'default_revision' => FALSE,
+ ],
+ ],
+ ],
+ 'stage' => [
+ 2 => [
+ 2 => ['default_revision' => FALSE],
+ 4 => [
+ 'title' => 'stage - 2 - r4 - published',
+ 'status' => TRUE,
+ 'default_revision' => TRUE,
+ ],
+ ],
+ ],
+ ]);
+ $test_scenarios['publish_node_2_in_stage'] = $revision_state;
+
+ // Adding a new unpublished node on 'stage' should create a single
+ // unpublished revision on both 'stage' and 'live'.
+ $revision_state = array_replace_recursive($revision_state, [
+ 'live' => [
+ 3 => [
+ 5 => [
+ 'title' => 'stage - 3 - r5 - unpublished',
+ 'status' => FALSE,
+ 'default_revision' => TRUE,
+ ],
+ ],
+ ],
+ 'stage' => [
+ 3 => [
+ 5 => [
+ 'title' => 'stage - 3 - r5 - unpublished',
+ 'status' => FALSE,
+ 'default_revision' => TRUE,
+ ],
+ ],
+ ],
+ ]);
+ $test_scenarios['add_unpublished_node_in_stage'] = $revision_state;
+
+ // Adding a new published node on 'stage' should create two revisions, an
+ // unpublished revision on 'live' and a published one on 'stage'.
+ $revision_state = array_replace_recursive($revision_state, [
+ 'live' => [
+ 4 => [
+ 6 => [
+ 'title' => 'stage - 4 - r6 - published',
+ 'status' => FALSE,
+ 'default_revision' => TRUE,
+ ],
+ 7 => [
+ 'title' => 'stage - 4 - r6 - published',
+ 'status' => TRUE,
+ 'default_revision' => FALSE,
+ ],
+ ],
+ ],
+ 'stage' => [
+ 4 => [
+ 6 => [
+ 'title' => 'stage - 4 - r6 - published',
+ 'status' => FALSE,
+ 'default_revision' => FALSE,
+ ],
+ 7 => [
+ 'title' => 'stage - 4 - r6 - published',
+ 'status' => TRUE,
+ 'default_revision' => TRUE,
+ ],
+ ],
+ ],
+ ]);
+ $test_scenarios['add_published_node_in_stage'] = $revision_state;
+
+ // Deploying 'stage' to 'live' should simply make the latest revisions in
+ // 'stage' the default ones in 'live'.
+ $revision_state = array_replace_recursive($revision_state, [
+ 'live' => [
+ 1 => [
+ 1 => ['default_revision' => FALSE],
+ 3 => ['default_revision' => TRUE],
+ ],
+ 2 => [
+ 2 => ['default_revision' => FALSE],
+ 4 => ['default_revision' => TRUE],
+ ],
+ // Node 3 has a single revision for both 'stage' and 'live' and it is
+ // already the default revision in both of them.
+ 4 => [
+ 6 => ['default_revision' => FALSE],
+ 7 => ['default_revision' => TRUE],
+ ],
+ ],
+ ]);
+ $test_scenarios['deploy_stage_to_live'] = $revision_state;
+
+ // Check the initial state after the module was installed.
+ $this->assertWorkspaceStatus($test_scenarios['initial_state'], 'node');
+
+ // Unpublish node 1 in 'stage'.
+ $this->switchToWorkspace('stage');
+ $node = $this->entityTypeManager->getStorage('node')->load(1);
+ $node->setTitle('stage - 1 - r3 - unpublished');
+ $node->setUnpublished();
+ $node->save();
+ $this->assertWorkspaceStatus($test_scenarios['unpublish_node_1_in_stage'], 'node');
+
+ // Publish node 2 in 'stage'.
+ $this->switchToWorkspace('stage');
+ $node = $this->entityTypeManager->getStorage('node')->load(2);
+ $node->setTitle('stage - 2 - r4 - published');
+ $node->setPublished();
+ $node->save();
+ $this->assertWorkspaceStatus($test_scenarios['publish_node_2_in_stage'], 'node');
+
+ // Add a new unpublished node on 'stage'.
+ $this->switchToWorkspace('stage');
+ $this->createNode(['title' => 'stage - 3 - r5 - unpublished', 'created' => $this->createdTimestamp++, 'status' => FALSE]);
+ $this->assertWorkspaceStatus($test_scenarios['add_unpublished_node_in_stage'], 'node');
+
+ // Add a new published node on 'stage'.
+ $this->switchToWorkspace('stage');
+ $this->createNode(['title' => 'stage - 4 - r6 - published', 'created' => $this->createdTimestamp++, 'status' => TRUE]);
+ $this->assertWorkspaceStatus($test_scenarios['add_published_node_in_stage'], 'node');
+
+ // Deploy 'stage' to 'live'.
+ $repository_handler = $this->workspaces['stage']->getRepositoryHandlerPlugin();
+ $repository_handler->replicate($this->workspaces['stage']->getLocalRepositoryHandlerPlugin(), $repository_handler);
+ $this->assertWorkspaceStatus($test_scenarios['deploy_stage_to_live'], 'node');
+ }
+
+ /**
+ * Tests the Entity Query relationship API with workspaces.
+ */
+ public function testEntityQueryRelationship() {
+ $this->initializeWorkspaceModule();
+
+ // Add an entity reference field that targets 'entity_test_mulrev' entities.
+ $this->createEntityReferenceField('node', 'page', 'field_test_entity', 'Test entity reference', 'entity_test_mulrev');
+
+ // Add an entity reference field that targets 'node' entities so we can test
+ // references to the same base tables.
+ $this->createEntityReferenceField('node', 'page', 'field_test_node', 'Test node reference', 'node');
+
+ $this->switchToWorkspace('live');
+ $node_1 = $this->createNode([
+ 'title' => 'live node 1'
+ ]);
+ $entity_test = EntityTestMulRev::create([
+ 'name' => 'live entity_test_mulrev',
+ 'non_rev_field' => 'live non-revisionable value',
+ ]);
+ $entity_test->save();
+
+ $node_2 = $this->createNode([
+ 'title' => 'live node 2',
+ 'field_test_entity' => $entity_test->id(),
+ 'field_test_node' => $node_1->id(),
+ ]);
+
+ // Switch to the 'stage' workspace and change some values for the referenced
+ // entities.
+ $this->switchToWorkspace('stage');
+ $node_1->title->value = 'stage node 1';
+ $node_1->save();
+
+ $node_2->title->value = 'stage node 2';
+ $node_2->save();
+
+ $entity_test->name->value = 'stage entity_test_mulrev';
+ $entity_test->non_rev_field->value = 'stage non-revisionable value';
+ $entity_test->save();
+
+ // Make sure that we're requesting the default revision.
+ $query = $this->entityTypeManager->getStorage('node')->getQuery();
+ $query->currentRevision();
+
+ $query
+ // Check a condition on the revision data table.
+ ->condition('title', 'stage node 2')
+ // Check a condition on the revision table.
+ ->condition('revision_uid', $node_2->getRevisionUserId())
+ // Check a condition on the data table.
+ ->condition('type', $node_2->bundle())
+ // Check a condition on the base table.
+ ->condition('uuid', $node_2->uuid());
+
+ // Add conditions for a reference to the same entity type.
+ $query
+ // Check a condition on the revision data table.
+ ->condition('field_test_node.entity.title', 'stage node 1')
+ // Check a condition on the revision table.
+ ->condition('field_test_node.entity.revision_uid', $node_1->getRevisionUserId())
+ // Check a condition on the data table.
+ ->condition('field_test_node.entity.type', $node_1->bundle())
+ // Check a condition on the base table.
+ ->condition('field_test_node.entity.uuid', $node_1->uuid());
+
+ // Add conditions for a reference to a different entity type.
+ $query
+ // Check a condition on the revision data table.
+ ->condition('field_test_entity.entity.name', 'stage entity_test_mulrev')
+ // Check a condition on the data table.
+ ->condition('field_test_entity.entity.non_rev_field', 'stage non-revisionable value')
+ // Check a condition on the base table.
+ ->condition('field_test_entity.entity.uuid', $entity_test->uuid());
+
+ $result = $query->execute();
+ $this->assertSame([$node_2->getRevisionId() => $node_2->id()], $result);
+ }
+
+ /**
+ * Checks entity load, entity queries and views results for a test scenario.
+ *
+ * @param array $expected
+ * An array of expected values, as defined in ::testWorkspaces().
+ * @param string $entity_type_id
+ * The ID of the entity type that is being tested.
+ */
+ protected function assertWorkspaceStatus(array $expected, $entity_type_id) {
+ $expected = $this->flattenExpectedValues($expected, $entity_type_id);
+
+ $entity_keys = $this->entityTypeManager->getDefinition($entity_type_id)->getKeys();
+ foreach ($expected as $workspace_id => $expected_values) {
+ $this->switchToWorkspace($workspace_id);
+
+ // Check that default revisions are swapped with the workspace revision.
+ $this->assertEntityLoad($expected_values, $entity_type_id);
+
+ // Check that non-default revisions are not changed.
+ $this->assertEntityRevisionLoad($expected_values, $entity_type_id);
+
+ // Check that entity queries return the correct results.
+ $this->assertEntityQuery($expected_values, $entity_type_id);
+
+ // Check that the 'Frontpage' view only shows published content that is
+ // also considered as the default revision in the given workspace.
+ $expected_frontpage = array_filter($expected_values, function ($expected_value) {
+ return $expected_value['status'] === TRUE && $expected_value['default_revision'] === TRUE;
+ });
+ // The 'Frontpage' view will output nodes in reverse creation order.
+ usort($expected_frontpage, function ($a, $b) {
+ return $b['nid'] - $a['nid'];
+ });
+ $view = Views::getView('frontpage');
+ $view->execute();
+ $this->assertIdenticalResultset($view, $expected_frontpage, ['nid' => 'nid']);
+
+ $rendered_view = $view->render('page_1');
+ $output = \Drupal::service('renderer')->renderRoot($rendered_view);
+ $this->setRawContent($output);
+ foreach ($expected_values as $expected_entity_values) {
+ if ($expected_entity_values[$entity_keys['published']] === TRUE && $expected_entity_values['default_revision'] === TRUE) {
+ $this->assertRaw($expected_entity_values[$entity_keys['label']]);
+ }
+ // Node 4 will always appear in the 'stage' workspace because it has
+ // both an unpublished revision as well as a published one.
+ elseif ($workspace_id != 'stage' && $expected_entity_values[$entity_keys['id']] != 4) {
+ $this->assertNoRaw($expected_entity_values[$entity_keys['label']]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Asserts that default revisions are properly swapped in a workspace.
+ *
+ * @param array $expected_values
+ * An array of expected values, as defined in ::testWorkspaces().
+ * @param string $entity_type_id
+ * The ID of the entity type to check.
+ */
+ protected function assertEntityLoad(array $expected_values, $entity_type_id) {
+ // Filter the expected values so we can check only the default revisions.
+ $expected_default_revisions = array_filter($expected_values, function ($expected_value) {
+ return $expected_value['default_revision'] === TRUE;
+ });
+
+ $entity_keys = $this->entityTypeManager->getDefinition($entity_type_id)->getKeys();
+ $id_key = $entity_keys['id'];
+ $revision_key = $entity_keys['revision'];
+ $label_key = $entity_keys['label'];
+ $published_key = $entity_keys['published'];
+
+ // Check \Drupal\Core\Entity\EntityStorageInterface::loadMultiple().
+ /** @var \Drupal\Core\Entity\ContentEntityInterface[]|\Drupal\Core\Entity\EntityPublishedInterface[] $entities */
+ $entities = $this->entityTypeManager->getStorage($entity_type_id)->loadMultiple(array_column($expected_default_revisions, $id_key));
+ foreach ($expected_default_revisions as $expected_default_revision) {
+ $entity_id = $expected_default_revision[$id_key];
+ $this->assertEquals($expected_default_revision[$revision_key], $entities[$entity_id]->getRevisionId());
+ $this->assertEquals($expected_default_revision[$label_key], $entities[$entity_id]->label());
+ $this->assertEquals($expected_default_revision[$published_key], $entities[$entity_id]->isPublished());
+ }
+
+ // Check \Drupal\Core\Entity\EntityStorageInterface::loadUnchanged().
+ foreach ($expected_default_revisions as $expected_default_revision) {
+ /** @var \Drupal\Core\Entity\ContentEntityInterface[]|\Drupal\Core\Entity\EntityPublishedInterface[] $entities */
+ $entity = $this->entityTypeManager->getStorage($entity_type_id)->loadUnchanged($expected_default_revision[$id_key]);
+ $this->assertEquals($expected_default_revision[$revision_key], $entity->getRevisionId());
+ $this->assertEquals($expected_default_revision[$label_key], $entity->label());
+ $this->assertEquals($expected_default_revision[$published_key], $entity->isPublished());
+ }
+ }
+
+ /**
+ * Asserts that non-default revisions are not changed.
+ *
+ * @param array $expected_values
+ * An array of expected values, as defined in ::testWorkspaces().
+ * @param string $entity_type_id
+ * The ID of the entity type to check.
+ */
+ protected function assertEntityRevisionLoad(array $expected_values, $entity_type_id) {
+ $entity_keys = $this->entityTypeManager->getDefinition($entity_type_id)->getKeys();
+ $id_key = $entity_keys['id'];
+ $revision_key = $entity_keys['revision'];
+ $label_key = $entity_keys['label'];
+ $published_key = $entity_keys['published'];
+
+ /** @var \Drupal\Core\Entity\ContentEntityInterface[]|\Drupal\Core\Entity\EntityPublishedInterface[] $entities */
+ $entities = $this->entityTypeManager->getStorage($entity_type_id)->loadMultipleRevisions(array_column($expected_values, $revision_key));
+ foreach ($expected_values as $expected_revision) {
+ $revision_id = $expected_revision[$revision_key];
+ $this->assertEquals($expected_revision[$id_key], $entities[$revision_id]->id());
+ $this->assertEquals($expected_revision[$revision_key], $entities[$revision_id]->getRevisionId());
+ $this->assertEquals($expected_revision[$label_key], $entities[$revision_id]->label());
+ $this->assertEquals($expected_revision[$published_key], $entities[$revision_id]->isPublished());
+ }
+ }
+
+ /**
+ * Asserts that entity queries are giving the correct results in a workspace.
+ *
+ * @param array $expected_values
+ * An array of expected values, as defined in ::testWorkspaces().
+ * @param string $entity_type_id
+ * The ID of the entity type to check.
+ */
+ protected function assertEntityQuery(array $expected_values, $entity_type_id) {
+ $storage = $this->entityTypeManager->getStorage($entity_type_id);
+ $entity_keys = $this->entityTypeManager->getDefinition($entity_type_id)->getKeys();
+ $id_key = $entity_keys['id'];
+ $revision_key = $entity_keys['revision'];
+ $label_key = $entity_keys['label'];
+ $published_key = $entity_keys['published'];
+
+ // Filter the expected values so we can check only the default revisions.
+ $expected_default_revisions = array_filter($expected_values, function ($expected_value) {
+ return $expected_value['default_revision'] === TRUE;
+ });
+
+ // Check entity query counts.
+ $result = $storage->getQuery()->count()->execute();
+ $this->assertEquals(count($expected_default_revisions), $result);
+
+ $result = $storage->getAggregateQuery()->count()->execute();
+ $this->assertEquals(count($expected_default_revisions), $result);
+
+ // Check entity queries with no conditions.
+ $result = $storage->getQuery()->execute();
+ $expected_result = array_combine(array_column($expected_default_revisions, $revision_key), array_column($expected_default_revisions, $id_key));
+ $this->assertEquals($expected_result, $result);
+
+ // Check querying each revision individually.
+ foreach ($expected_values as $expected_value) {
+ $query = $storage->getQuery();
+ $query
+ ->condition($entity_keys['id'], $expected_value[$id_key])
+ ->condition($entity_keys['label'], $expected_value[$label_key])
+ ->condition($entity_keys['published'], $expected_value[$published_key]);
+
+ // If the entity is not expected to be the default revision, we need to
+ // query all revisions if we want to find it.
+ if (!$expected_value['default_revision']) {
+ $query->allRevisions();
+ }
+
+ $result = $query->execute();
+ $this->assertEquals([$expected_value[$revision_key] => $expected_value[$id_key]], $result);
+ }
+ }
+
+ /**
+ * Sets a given workspace as active.
+ *
+ * @param string $workspace_id
+ * The ID of the workspace to switch to.
+ */
+ protected function switchToWorkspace($workspace_id) {
+ /** @var \Drupal\workspace\WorkspaceManager $workspace_manager */
+ $workspace_manager = \Drupal::service('workspace.manager');
+ if ($workspace_manager->getActiveWorkspace()->id() !== $workspace_id) {
+ // Switch the test runner's context to the specified workspace.
+ $workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
+ \Drupal::service('workspace.manager')->setActiveWorkspace($workspace);
+ }
+ }
+
+ /**
+ * Flattens the expectations array defined by testWorkspaces().
+ *
+ * @param array $expected
+ * An array as defined by testWorkspaces().
+ * @param string $entity_type_id
+ * The ID of the entity type that is being tested.
+ *
+ * @return array
+ * An array where all the entity IDs and revision IDs are merged inside each
+ * expected values array.
+ */
+ protected function flattenExpectedValues(array $expected, $entity_type_id) {
+ $flattened = [];
+
+ $entity_keys = $this->entityTypeManager->getDefinition($entity_type_id)->getKeys();
+ foreach ($expected as $workspace_id => $workspace_values) {
+ foreach ($workspace_values as $entity_id => $entity_revisions) {
+ foreach ($entity_revisions as $revision_id => $revision_values) {
+ $flattened[$workspace_id][] = [$entity_keys['id'] => $entity_id, $entity_keys['revision'] => $revision_id] + $revision_values;
+ }
+ }
+ }
+
+ return $flattened;
+ }
+
+}
diff --git a/core/modules/workspace/tests/src/Kernel/WorkspaceInternalResourceTest.php b/core/modules/workspace/tests/src/Kernel/WorkspaceInternalResourceTest.php
new file mode 100644
index 0000000..632140a
--- /dev/null
+++ b/core/modules/workspace/tests/src/Kernel/WorkspaceInternalResourceTest.php
@@ -0,0 +1,62 @@
+setExpectedException(PluginNotFoundException::class, 'The "entity:workspace_association" plugin does not exist.');
+ RestResourceConfig::create([
+ 'id' => 'entity.workspace_association',
+ 'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
+ 'configuration' => [
+ 'methods' => ['GET'],
+ 'formats' => ['json'],
+ 'authentication' => ['cookie'],
+ ],
+ ])
+ ->enable()
+ ->save();
+ }
+
+ /**
+ * Tests enabling replication logs for REST throws an exception.
+ *
+ * @see workspace_rest_resource_alter()
+ */
+ public function testCreateReplicationLogResource() {
+ $this->setExpectedException(PluginNotFoundException::class, 'The "entity:replication_log" plugin does not exist.');
+ RestResourceConfig::create([
+ 'id' => 'entity.replication_log',
+ 'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
+ 'configuration' => [
+ 'methods' => ['GET'],
+ 'formats' => ['json'],
+ 'authentication' => ['cookie'],
+ ],
+ ])
+ ->enable()
+ ->save();
+ }
+
+}
diff --git a/core/modules/workspace/workspace.info.yml b/core/modules/workspace/workspace.info.yml
new file mode 100644
index 0000000..9599e07
--- /dev/null
+++ b/core/modules/workspace/workspace.info.yml
@@ -0,0 +1,9 @@
+name: Workspace
+type: module
+description: 'Provides the ability to have multiple workspaces on a single site to facilitate things like full-site preview and content staging.'
+version: VERSION
+core: 8.x
+package: Core (Experimental)
+configure: entity.workspace.collection
+dependencies:
+ - user
diff --git a/core/modules/workspace/workspace.install b/core/modules/workspace/workspace.install
new file mode 100644
index 0000000..0b3549e
--- /dev/null
+++ b/core/modules/workspace/workspace.install
@@ -0,0 +1,47 @@
+getStorage('user_role')->getQuery()
+ ->condition('is_admin', TRUE)
+ ->execute();
+ if (!empty($admin_roles)) {
+ $query = \Drupal::entityTypeManager()->getStorage('user')->getQuery()
+ ->condition('roles', $admin_roles, 'IN')
+ ->condition('status', 1)
+ ->sort('uid', 'ASC')
+ ->range(0, 1);
+ $result = $query->execute();
+ }
+
+ // Default to user ID 1 if we could not find any other administrator users.
+ $owner_id = !empty($result) ? reset($result) : 1;
+
+ // Create two workspaces by default, 'live' and 'stage'.
+ Workspace::create([
+ 'id' => 'live',
+ 'label' => 'Live',
+ 'upstream' => RepositoryHandlerInterface::EMPTY_VALUE,
+ 'uid' => $owner_id,
+ ])->save();
+
+ Workspace::create([
+ 'id' => 'stage',
+ 'label' => 'Stage',
+ 'upstream' => 'local_workspace:live',
+ 'uid' => $owner_id,
+ ])->save();
+}
diff --git a/core/modules/workspace/workspace.libraries.yml b/core/modules/workspace/workspace.libraries.yml
new file mode 100644
index 0000000..c7aad42
--- /dev/null
+++ b/core/modules/workspace/workspace.libraries.yml
@@ -0,0 +1,5 @@
+drupal.workspace.toolbar:
+ version: VERSION
+ css:
+ theme:
+ css/workspace.toolbar.css: {}
diff --git a/core/modules/workspace/workspace.link_relation_types.yml b/core/modules/workspace/workspace.link_relation_types.yml
new file mode 100644
index 0000000..d591603
--- /dev/null
+++ b/core/modules/workspace/workspace.link_relation_types.yml
@@ -0,0 +1,8 @@
+# Workspace extension relation types.
+# See https://tools.ietf.org/html/rfc5988#section-4.2.
+activate-form:
+ uri: https://drupal.org/link-relations/activate-form
+ description: A form where a workspace can be activated.
+deploy-form:
+ uri: https://drupal.org/link-relations/deploy-form
+ description: A form where a workspace can be deployed.
diff --git a/core/modules/workspace/workspace.links.action.yml b/core/modules/workspace/workspace.links.action.yml
new file mode 100644
index 0000000..9f22598
--- /dev/null
+++ b/core/modules/workspace/workspace.links.action.yml
@@ -0,0 +1,5 @@
+entity.workspace.add_form:
+ route_name: entity.workspace.add_form
+ title: 'Add workspace'
+ appears_on:
+ - entity.workspace.collection
diff --git a/core/modules/workspace/workspace.links.menu.yml b/core/modules/workspace/workspace.links.menu.yml
new file mode 100644
index 0000000..c7faefb
--- /dev/null
+++ b/core/modules/workspace/workspace.links.menu.yml
@@ -0,0 +1,5 @@
+entity.workspace.collection:
+ title: 'Workspaces'
+ parent: system.admin_config_workflow
+ description: 'Create and manage workspaces.'
+ route_name: entity.workspace.collection
diff --git a/core/modules/workspace/workspace.module b/core/modules/workspace/workspace.module
new file mode 100644
index 0000000..708aad4
--- /dev/null
+++ b/core/modules/workspace/workspace.module
@@ -0,0 +1,234 @@
+' . t('About') . '';
+ $output .= '' . t('The Workspace module allows workspaces to be defined and switched between. Content is then assigned to the active workspace when created. For more information, see the online documentation for the Workspace module.', [':workspace' => 'https://www.drupal.org/node/2824024']) . '
';
+ return $output;
+ }
+}
+
+/**
+ * Implements hook_entity_load().
+ */
+function workspace_entity_load(array &$entities, $entity_type_id) {
+ return \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityOperations::class)
+ ->entityLoad($entities, $entity_type_id);
+}
+
+/**
+ * Implements hook_entity_presave().
+ */
+function workspace_entity_presave(EntityInterface $entity) {
+ return \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityOperations::class)
+ ->entityPresave($entity);
+}
+
+/**
+ * Implements hook_entity_insert().
+ */
+function workspace_entity_insert(EntityInterface $entity) {
+ return \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityOperations::class)
+ ->entityInsert($entity);
+}
+
+/**
+ * Implements hook_entity_update().
+ */
+function workspace_entity_update(EntityInterface $entity) {
+ return \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityOperations::class)
+ ->entityUpdate($entity);
+}
+
+/**
+ * Implements hook_entity_access().
+ *
+ * @see \Drupal\workspace\EntityAccess
+ */
+function workspace_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
+ return \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityAccess::class)
+ ->entityOperationAccess($entity, $operation, $account);
+}
+
+/**
+ * Implements hook_entity_create_access().
+ *
+ * @see \Drupal\workspace\EntityAccess
+ */
+function workspace_entity_create_access(AccountInterface $account, array $context, $entity_bundle) {
+ return \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityAccess::class)
+ ->entityCreateAccess($account, $context, $entity_bundle);
+}
+
+/**
+ * Implements hook_views_query_alter().
+ */
+function workspace_views_query_alter(ViewExecutable $view, QueryPluginBase $query) {
+ return \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(ViewsQueryAlter::class)
+ ->alterQuery($view, $query);
+}
+
+/**
+ * Implements hook_rest_resource_alter().
+ */
+function workspace_rest_resource_alter(&$definitions) {
+ // WorkspaceAssociation and ReplicationLog are internal entity types,
+ // therefore they should not be exposed via REST.
+ unset($definitions['entity:workspace_association']);
+ unset($definitions['entity:replication_log']);
+}
+
+/**
+ * Implements hook_toolbar().
+ */
+function workspace_toolbar() {
+ $items = [];
+ $items['workspace'] = [
+ '#cache' => [
+ 'contexts' => [
+ 'user.permissions',
+ ],
+ ],
+ ];
+
+ $current_user = \Drupal::currentUser();
+ if (!$current_user->hasPermission('administer workspaces')
+ || !$current_user->hasPermission('view own workspace')
+ || !$current_user->hasPermission('view any workspace')) {
+ return $items;
+ }
+
+ /** @var \Drupal\workspace\WorkspaceInterface $active_workspace */
+ $active_workspace = \Drupal::service('workspace.manager')->getActiveWorkspace();
+
+ $configure_link = NULL;
+ if ($current_user->hasPermission('administer workspaces')) {
+ $configure_link = [
+ '#type' => 'link',
+ '#title' => t('Manage workspaces'),
+ '#url' => $active_workspace->toUrl('collection'),
+ '#options' => ['attributes' => ['class' => ['manage-workspaces']]],
+ ];
+ }
+
+ $items['workspace'] = [
+ '#type' => 'toolbar_item',
+ 'tab' => [
+ '#type' => 'link',
+ '#title' => $active_workspace->label(),
+ '#url' => $active_workspace->toUrl('collection'),
+ '#attributes' => [
+ 'title' => t('Switch workspace'),
+ 'class' => ['toolbar-icon', 'toolbar-icon-workspace'],
+ ],
+ ],
+ 'tray' => [
+ '#heading' => t('Workspaces'),
+ 'workspaces' => workspace_renderable_links(),
+ 'configure' => $configure_link,
+ ],
+ '#wrapper_attributes' => [
+ 'class' => ['workspace-toolbar-tab'],
+ ],
+ '#attached' => [
+ 'library' => ['workspace/drupal.workspace.toolbar'],
+ ],
+ '#weight' => 500,
+ ];
+
+ // Add a special class to the wrapper if we are in the default workspace so we
+ // can highlight it with a different color.
+ if ($active_workspace->isDefaultWorkspace()) {
+ $items['workspace']['#wrapper_attributes']['class'][] = 'is-live';
+ }
+
+ return $items;
+}
+
+/**
+ * Returns an array of workspace activation form links, suitable for rendering.
+ *
+ * @return array
+ * A render array containing links to the workspace activation form.
+ */
+function workspace_renderable_links() {
+ $entity_type_manager = \Drupal::entityTypeManager();
+ /** @var \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository */
+ $entity_repository = \Drupal::service('entity.repository');
+ /** @var \Drupal\workspace\WorkspaceInterface $active_workspace */
+ $active_workspace = \Drupal::service('workspace.manager')->getActiveWorkspace();
+
+ $links = $cache_tags = [];
+ foreach ($entity_type_manager->getStorage('workspace')->loadMultiple() as $workspace) {
+ $workspace = $entity_repository->getTranslationFromContext($workspace);
+
+ // Add the 'is-active' class for the currently active workspace.
+ $options = [];
+ if ($workspace->id() === $active_workspace->id()) {
+ $options['attributes']['class'][] = 'is-active';
+ }
+
+ // Get the URL of the workspace activation form and display it in a modal.
+ $url = Url::fromRoute('entity.workspace.activate_form', ['workspace' => $workspace->id()], $options);
+ if ($url->access()) {
+ $links[$workspace->id()] = [
+ 'type' => 'link',
+ 'title' => $workspace->label(),
+ 'url' => $url,
+ 'attributes' => [
+ 'class' => ['use-ajax'],
+ 'data-dialog-type' => 'modal',
+ 'data-dialog-options' => Json::encode([
+ 'width' => 500,
+ ]),
+ ],
+ ];
+ $cache_tags = Cache::mergeTags($cache_tags, $workspace->getCacheTags());
+ }
+ }
+
+ if (!empty($links)) {
+ $links = [
+ '#theme' => 'links__toolbar_workspaces',
+ '#links' => $links,
+ '#attributes' => [
+ 'class' => ['toolbar-menu'],
+ ],
+ '#cache' => [
+ 'tags' => $cache_tags,
+ ],
+ ];
+ }
+
+ return $links;
+}
diff --git a/core/modules/workspace/workspace.permissions.yml b/core/modules/workspace/workspace.permissions.yml
new file mode 100644
index 0000000..36d21aa
--- /dev/null
+++ b/core/modules/workspace/workspace.permissions.yml
@@ -0,0 +1,28 @@
+administer workspaces:
+ title: Administer workspaces
+
+create workspace:
+ title: Create a new workspace
+
+view own workspace:
+ title: View own workspace
+
+view any workspace:
+ title: View any workspace
+
+edit own workspace:
+ title: Edit own workspace
+
+edit any workspace:
+ title: Edit any workspace
+
+bypass entity access own workspace:
+ title: Bypass content entity access in own workspace
+ description: Allow all Edit/Update/Delete permissions for all content entities in a workspace owned by the user.
+ restrict access: TRUE
+
+update any workspace from upstream:
+ title: Update any workspace from upstream
+
+permission_callbacks:
+ - Drupal\workspace\EntityAccess::workspacePermissions
diff --git a/core/modules/workspace/workspace.routing.yml b/core/modules/workspace/workspace.routing.yml
new file mode 100644
index 0000000..2226501
--- /dev/null
+++ b/core/modules/workspace/workspace.routing.yml
@@ -0,0 +1,27 @@
+entity.workspace.collection:
+ path: '/admin/config/workflow/workspace'
+ defaults:
+ _title: 'Workspaces'
+ _entity_list: 'workspace'
+ requirements:
+ _permission: 'administer workspaces+edit any workspace'
+
+entity.workspace.activate_form:
+ path: '/admin/config/workflow/workspace/{workspace}/activate'
+ defaults:
+ _entity_form: 'workspace.activate'
+ _title: 'Activate Workspace'
+ options:
+ _admin_route: TRUE
+ requirements:
+ _entity_access: 'workspace.view'
+
+entity.workspace.deploy_form:
+ path: '/admin/config/workflow/workspace/{workspace}/deploy'
+ defaults:
+ _entity_form: 'workspace.deploy'
+ _title: 'Deploy Workspace'
+ options:
+ _admin_route: TRUE
+ requirements:
+ _permission: 'administer workspaces'
diff --git a/core/modules/workspace/workspace.services.yml b/core/modules/workspace/workspace.services.yml
new file mode 100644
index 0000000..f68e0eb
--- /dev/null
+++ b/core/modules/workspace/workspace.services.yml
@@ -0,0 +1,27 @@
+services:
+ workspace.manager:
+ class: Drupal\workspace\WorkspaceManager
+ arguments: ['@request_stack', '@entity_type.manager', '@current_user', '@logger.channel.workspace', '@class_resolver']
+ tags:
+ - { name: service_id_collector, tag: workspace_negotiator }
+ plugin.manager.workspace.repository_handler:
+ class: Drupal\workspace\RepositoryHandlerManager
+ parent: default_plugin_manager
+ workspace.negotiator.default:
+ class: Drupal\workspace\Negotiator\DefaultWorkspaceNegotiator
+ arguments: ['@entity_type.manager']
+ tags:
+ - { name: workspace_negotiator, priority: 0 }
+ workspace.negotiator.session:
+ class: Drupal\workspace\Negotiator\SessionWorkspaceNegotiator
+ arguments: ['@current_user', '@user.private_tempstore', '@entity_type.manager']
+ tags:
+ - { name: workspace_negotiator, priority: 100 }
+ cache_context.workspace:
+ class: Drupal\workspace\WorkspaceCacheContext
+ arguments: ['@workspace.manager']
+ tags:
+ - { name: cache.context }
+ logger.channel.workspace:
+ parent: logger.channel_base
+ arguments: ['workspace']
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityAccessControlHandlerTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityAccessControlHandlerTest.php
index b3af27f..8545efe 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityAccessControlHandlerTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityAccessControlHandlerTest.php
@@ -8,6 +8,7 @@
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\entity_test\Entity\EntityTest;
+use Drupal\entity_test\Entity\EntityTestStringId;
use Drupal\entity_test\Entity\EntityTestDefaultAccess;
use Drupal\entity_test\Entity\EntityTestNoUuid;
use Drupal\entity_test\Entity\EntityTestLabel;
@@ -18,6 +19,7 @@
/**
* Tests the entity access control handler.
*
+ * @coversDefaultClass \Drupal\Core\Entity\EntityAccessControlHandler
* @group Entity
*/
class EntityAccessControlHandlerTest extends EntityLanguageTestBase {
@@ -30,6 +32,7 @@ public function setUp() {
$this->installEntitySchema('entity_test_no_uuid');
$this->installEntitySchema('entity_test_rev');
+ $this->installEntitySchema('entity_test_string_id');
}
/**
@@ -293,4 +296,73 @@ public function testHooks() {
$this->assertEqual($state->get('entity_test_entity_test_access'), TRUE);
}
+ /**
+ * Tests the default access handling for the ID and UUID fields.
+ *
+ * @covers ::fieldAccess
+ * @dataProvider providerTestFieldAccess
+ */
+ public function testFieldAccess($entity_class, array $entity_create_values, $expected_id_create_access) {
+ // Set up a non-admin user that is allowed to create and update test
+ // entities.
+ \Drupal::currentUser()->setAccount($this->createUser(['uid' => 2], ['administer entity_test content']));
+
+ // Create the entity to test field access with.
+ $entity = $entity_class::create($entity_create_values);
+
+ // On newly-created entities, field access must allow setting the UUID
+ // field.
+ $this->assertTrue($entity->get('uuid')->access('edit'));
+ $this->assertTrue($entity->get('uuid')->access('edit', NULL, TRUE)->isAllowed());
+ // On newly-created entities, field access will not allow setting the ID
+ // field if the ID is of type serial. It will allow access if it is of type
+ // string.
+ $this->assertEquals($expected_id_create_access, $entity->get('id')->access('edit'));
+ $this->assertEquals($expected_id_create_access, $entity->get('id')->access('edit', NULL, TRUE)->isAllowed());
+
+ // Save the entity and check that we can not update the ID or UUID fields
+ // anymore.
+ $entity->save();
+
+ // If the ID has been set as part of the create ensure it has been set
+ // correctly.
+ if (isset($entity_create_values['id'])) {
+ $this->assertSame($entity_create_values['id'], $entity->id());
+ }
+ // The UUID is hard-coded by the data provider.
+ $this->assertSame('60e3a179-79ed-4653-ad52-5e614c8e8fbe', $entity->uuid());
+ $this->assertFalse($entity->get('uuid')->access('edit'));
+ $access_result = $entity->get('uuid')->access('edit', NULL, TRUE);
+ $this->assertTrue($access_result->isForbidden());
+ $this->assertEquals('The entity UUID cannot be changed', $access_result->getReason());
+
+ // Ensure the ID is still not allowed to be edited.
+ $this->assertFalse($entity->get('id')->access('edit'));
+ $access_result = $entity->get('id')->access('edit', NULL, TRUE);
+ $this->assertTrue($access_result->isForbidden());
+ $this->assertEquals('The entity ID cannot be changed', $access_result->getReason());
+ }
+
+ public function providerTestFieldAccess() {
+ return [
+ 'serial ID entity' => [
+ EntityTest::class,
+ [
+ 'name' => 'A test entity',
+ 'uuid' => '60e3a179-79ed-4653-ad52-5e614c8e8fbe',
+ ],
+ FALSE
+ ],
+ 'string ID entity' => [
+ EntityTestStringId::class,
+ [
+ 'id' => 'a_test_entity',
+ 'name' => 'A test entity',
+ 'uuid' => '60e3a179-79ed-4653-ad52-5e614c8e8fbe',
+ ],
+ TRUE
+ ],
+ ];
+ }
+
}