diff --git a/tests/modules/jsonapi_test_field_access/jsonapi_test_field_access.module b/tests/modules/jsonapi_test_field_access/jsonapi_test_field_access.module index daf20ce..8e808f9 100644 --- a/tests/modules/jsonapi_test_field_access/jsonapi_test_field_access.module +++ b/tests/modules/jsonapi_test_field_access/jsonapi_test_field_access.module @@ -7,15 +7,20 @@ use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Session\AccountInterface; /** * Implements hook_entity_field_access(). */ -function jsonapi_test_field_access_entity_field_access($operation, FieldDefinitionInterface $field_definition) { - // @see \Drupal\Tests\jsonapi\Functional\ResourceTestBase::testGetRelationships(). +function jsonapi_test_field_access_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account) { + // @see \Drupal\Tests\jsonapi\Functional\ResourceTestBase::testRelationships(). if ($field_definition->getName() === 'field_jsonapi_test_entity_ref') { // Forbid access in all cases. - return AccessResult::forbidden(); + $permission = "field_jsonapi_test_entity_ref $operation access"; + $access_result = $account->hasPermission($permission) + ? AccessResult::allowed() + : AccessResult::forbidden("The '$permission' permission is required."); + return $access_result->addCacheContexts(['user.permissions']); } // No opinion. diff --git a/tests/modules/jsonapi_test_field_access/jsonapi_test_field_access.permissions.yml b/tests/modules/jsonapi_test_field_access/jsonapi_test_field_access.permissions.yml new file mode 100644 index 0000000..18780ca --- /dev/null +++ b/tests/modules/jsonapi_test_field_access/jsonapi_test_field_access.permissions.yml @@ -0,0 +1,7 @@ +field_jsonapi_test_entity_ref update access: + description: 'Gives access to to PATCH, POST or DELETE values of the relationship test field.' + title: 'Update JSON API relationship test field' + +field_jsonapi_test_entity_ref edit access: + description: 'Gives access to to PATCH, POST or DELETE values of the relationship test field.' + title: 'Update JSON API relationship test field' diff --git a/tests/src/Functional/FeedTest.php b/tests/src/Functional/FeedTest.php index 9df2a72..06b9ef8 100644 --- a/tests/src/Functional/FeedTest.php +++ b/tests/src/Functional/FeedTest.php @@ -86,6 +86,18 @@ class FeedTest extends ResourceTestBase { return $feed; } + /** + * {@inheritdoc} + */ + protected function createAnotherEntity($key) { + /* @var \Drupal\aggregator\FeedInterface $duplicate */ + $duplicate = $this->getEntityDuplicate($this->entity, $key); + $duplicate->set('field_rest_test', 'Duplicate feed entity'); + $duplicate->setUrl("http://example.com/$key.xml"); + $duplicate->save(); + return $duplicate; + } + /** * {@inheritdoc} */ diff --git a/tests/src/Functional/FileTest.php b/tests/src/Functional/FileTest.php index 930ad41..2aafca7 100644 --- a/tests/src/Functional/FileTest.php +++ b/tests/src/Functional/FileTest.php @@ -105,6 +105,17 @@ class FileTest extends ResourceTestBase { return $file; } + /** + * {@inheritdoc} + */ + protected function createAnotherEntity($key) { + /* @var \Drupal\file\FileInterface $duplicate */ + $duplicate = parent::createAnotherEntity($key); + $duplicate->setFileUri("public://$key.txt"); + $duplicate->save(); + return $duplicate; + } + /** * {@inheritdoc} */ diff --git a/tests/src/Functional/ItemTest.php b/tests/src/Functional/ItemTest.php index 0935f36..52b6f20 100644 --- a/tests/src/Functional/ItemTest.php +++ b/tests/src/Functional/ItemTest.php @@ -147,7 +147,7 @@ class ItemTest extends ResourceTestBase { /** * {@inheritdoc} */ - public function testGetRelationships() { + public function testRelationships() { $this->markTestSkipped('Remove this override in https://www.drupal.org/project/drupal/issues/2149851'); } diff --git a/tests/src/Functional/MediaTest.php b/tests/src/Functional/MediaTest.php index 45c16dd..eb8ceea 100644 --- a/tests/src/Functional/MediaTest.php +++ b/tests/src/Functional/MediaTest.php @@ -346,7 +346,7 @@ class MediaTest extends ResourceTestBase { * @todo Determine if this override should be removed in https://www.drupal.org/project/jsonapi/issues/2952522 */ protected function getExpectedGetRelationshipDocumentData($relationship_field_name, EntityInterface $entity = NULL) { - $data = parent::getExpectedGetRelationshipDocumentData($relationship_field_name); + $data = parent::getExpectedGetRelationshipDocumentData($relationship_field_name, $entity); switch ($relationship_field_name) { case 'thumbnail': $data['meta'] = [ @@ -369,4 +369,14 @@ class MediaTest extends ResourceTestBase { } } + /** + * {@inheritdoc} + * + * @todo Remove this in https://www.drupal.org/node/2824851. + */ + protected function doTestRelationshipPost(array $request_options) { + $this->grantPermissionsToTestedRole(['access content']); + parent::doTestRelationshipPost($request_options); + } + } diff --git a/tests/src/Functional/MessageTest.php b/tests/src/Functional/MessageTest.php index b92b6f7..ac0f5bc 100644 --- a/tests/src/Functional/MessageTest.php +++ b/tests/src/Functional/MessageTest.php @@ -153,7 +153,7 @@ class MessageTest extends ResourceTestBase { /** * {@inheritdoc} */ - public function testGetRelationships() { + public function testRelationships() { // Contact Message entities are not stored, so they cannot be retrieved. $this->setExpectedException(RouteNotFoundException::class, 'Route "jsonapi.contact_message--camelids.relationship" does not exist.'); diff --git a/tests/src/Functional/ResourceTestBase.php b/tests/src/Functional/ResourceTestBase.php index b9ccbe0..652a4db 100644 --- a/tests/src/Functional/ResourceTestBase.php +++ b/tests/src/Functional/ResourceTestBase.php @@ -247,7 +247,7 @@ abstract class ResourceTestBase extends BrowserTestBase { ->setTranslatable(FALSE) ->setSetting('handler', 'default') ->setSetting('handler_settings', [ - 'target_bundles' => [$account_bundle => $account_bundle], + 'target_bundles' => NULL, ]) ->save(); @@ -729,10 +729,18 @@ abstract class ResourceTestBase extends BrowserTestBase { $strip_error_identifiers($expected_document); $strip_error_identifiers($actual_document); - $this->assertSame(array_keys($expected_document), array_keys($actual_document), 'The documents must share the same top-level members'); + $expected_keys = array_keys($expected_document); + $actual_keys = array_keys($actual_document); + $missing_member_names = array_diff($expected_keys, $actual_keys); + $extra_member_names = array_diff($actual_keys, $expected_keys); + if (!empty($missing_member_names) || !empty($extra_member_names)) { + $message_format = "The document members did not match the expected values. Missing: [ %s ]. Unexpected: [ %s ]"; + $message = sprintf($message_format, implode(', ', $missing_member_names), implode(', ', $extra_member_names)); + $this->assertSame($expected_document, $actual_document, $message); + } foreach ($expected_document as $member_name => $expected_member) { $actual_member = $actual_document[$member_name]; - $this->assertSame($expected_member, $actual_member, "The $member_name member was not as expected."); + $this->assertSame($expected_member, $actual_member, "The '$member_name' member was not as expected."); } } @@ -1228,7 +1236,7 @@ abstract class ResourceTestBase extends BrowserTestBase { } /** - * Tests GETing relationships of an individual resource. + * Tests CRUD of individual resource relationship data. * * Unlike the "related" routes, relationship routes only return information * about the "relationship" itself, not the targeted resources. For JSON API @@ -1237,13 +1245,32 @@ abstract class ResourceTestBase extends BrowserTestBase { * targeted resource and the target resource IDs. These type+ID combos are * referred to as "resource identifiers." */ - public function testGetRelationships() { + public function testRelationships() { + if ($this->entity instanceof ConfigEntityInterface) { + $this->markTestSkipped('Configuration entities cannot have relationships.'); + } + $request_options = []; $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json'; $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions()); - $this->doTestGetRelationships($request_options); + + // Test GET. + $this->doTestRelationshipGet($request_options); $this->setUpAuthorization('GET'); - $this->doTestGetRelationships($request_options); + $this->doTestRelationshipGet($request_options); + + // Test POST. + $this->doTestRelationshipPost($request_options); + // Grant entity-level edit access. + $this->setUpAuthorization('PATCH'); + $this->doTestRelationshipPost($request_options); + // Field edit access is still forbidden, grant it. + $this->grantPermissionsToTestedRole([ + 'field_jsonapi_test_entity_ref view access', + 'field_jsonapi_test_entity_ref edit access', + 'field_jsonapi_test_entity_ref update access', + ]); + $this->doTestRelationshipPost($request_options); } /** @@ -1302,14 +1329,16 @@ abstract class ResourceTestBase extends BrowserTestBase { * Request options to apply. * * @see \GuzzleHttp\ClientInterface::request() - * @see ::doTestRelated + * @see ::testRelationships */ - protected function doTestGetRelationships(array $request_options) { + protected function doTestRelationshipGet(array $request_options) { $relationship_field_names = $this->getRelationshipFieldNames($this->entity); // If there are no relationship fields, we can't test relationship routes. if (empty($relationship_field_names)) { return; } + + // Test GET. $related_responses = $this->getRelationshipResponses($relationship_field_names, $request_options); foreach ($relationship_field_names as $relationship_field_name) { $expected_resource_response = $this->getExpectedGetRelationshipResponse($relationship_field_name); @@ -1322,6 +1351,96 @@ abstract class ResourceTestBase extends BrowserTestBase { } } + /** + * Performs one round of relationship POST route testing. + * + * @param array $request_options + * Request options to apply. + * + * @see \GuzzleHttp\ClientInterface::request() + * @see ::testRelationships + */ + protected function doTestRelationshipPost(array $request_options) { + /* @var \Drupal\Core\Entity\FieldableEntityInterface $resource */ + $resource = $this->createAnotherEntity('dupe'); + $resource->set('field_jsonapi_test_entity_ref', NULL); + assert(($violations = $resource->validate())->count() === 0, (string) $violations); + $resource->save(); + $target_resource = $this->createUser(); + assert(($violations = $target_resource->validate())->count() === 0, (string) $violations); + $target_resource->save(); + $target_identifier = static::toResourceIdentifier($target_resource); + $resource_identifier = static::toResourceIdentifier($resource); + $relationship_field_name = 'field_jsonapi_test_entity_ref'; + /* @var \Drupal\Core\Access\AccessResultReasonInterface $update_access */ + $update_access = static::entityAccess($resource, 'update', $this->account) + ->andIf(static::entityFieldAccess($resource, $relationship_field_name, 'update', $this->account)); + $url = Url::fromRoute(sprintf("jsonapi.{$resource_identifier['type']}.relationship"), [ + 'related' => $relationship_field_name, + $resource->getEntityTypeId() => $resource->uuid(), + ]); + if ($update_access->isAllowed()) { + // Test POST: empty body. + $response = $this->request('POST', $url, $request_options); + $this->assertResourceErrorResponse(400, 'Empty request body.', $response); + + // Test POST: empty data. + $request_options[RequestOptions::BODY] = Json::encode(['data' => []]); + $response = $this->request('POST', $url, $request_options); + // @todo Remove line below in favor of commented line in https://www.drupal.org/project/jsonapi/issues/2977653. + $this->assertResourceResponse(201, FALSE, $response); + // $this->assertResourceResponse(204, NULL, $response); + + // Test POST: data as resource identifier, not array of identifiers. + $request_options[RequestOptions::BODY] = Json::encode(['data' => $target_identifier]); + $response = $this->request('POST', $url, $request_options); + $this->assertResourceErrorResponse(400, 'Invalid body payload for the relationship.', $response); + + // Test POST: missing field. + // @todo write this test. + // Test POST: invalid target. + // @todo write this test. + // Test POST: internal resource type target. + // @todo write this test. + + // Test POST: success. + $request_options[RequestOptions::BODY] = Json::encode(['data' => [$target_identifier]]); + $response = $this->request('POST', $url, $request_options); + $resource->set($relationship_field_name, [$target_resource]); + $expected_document = $this->getExpectedGetRelationshipDocument($relationship_field_name, $resource); + // @todo Remove line below in favor of commented line in https://www.drupal.org/project/jsonapi/issues/2977653. + $this->assertResourceResponse(201, $expected_document, $response); + // $this->assertResourceResponse(204, NULL, $response); + + // @todo: Uncomment the following two assertions in https://www.drupal.org/project/jsonapi/issues/2977659. + //// Test POST: success, relationship already exists, no arity. + //$response = $this->request('POST', $url, $request_options); + //$this->assertResourceResponse(204, NULL, $response); + + //// Test POST: success, relationship already exists, with unique arity. + //$request_options[RequestOptions::BODY] = Json::encode([ + // 'data' => [ + // $target_identifier + ['meta' => ['arity' => 1]] + // ], + //]); + //$response = $this->request('POST', $url, $request_options); + //$resource->set($relationship_field_name, [$target_resource, $target_resource]); + //$expected_document = $this->getExpectedGetRelationshipDocument($relationship_field_name, $resource); + //$this->assertResourceResponse(200, $expected_document, $response); + } + else { + $request_options[RequestOptions::BODY] = Json::encode(['data' => [$target_identifier]]); + $response = $this->request('POST', $url, $request_options); + $message = 'The current user is not allowed to update this relationship.'; + $message .= ($reason = $update_access->getReason()) ? ' ' . $reason : ''; + $this->assertResourceErrorResponse(403, $message, $response, $relationship_field_name); + } + + // Remove the test entities that were created. + $resource->delete(); + $target_resource->delete(); + } + /** * Gets an expected ResourceResponse for the given relationship. * @@ -1543,8 +1662,8 @@ abstract class ResourceTestBase extends BrowserTestBase { } if (!empty($resource_document['meta']['errors'])) { foreach ($resource_document['meta']['errors'] as $error) { - // @todo remove this conditional when inaccessible relationships are able to raise errors. - if ($error['detail'] !== 'The current user is not allowed to view this relationship.') { + // @todo remove this when inaccessible relationships are able to raise errors in https://www.drupal.org/project/jsonapi/issues/2956084. + if (strpos($error['detail'], 'The current user is not allowed to view this relationship.') !== 0) { $expected_document['meta']['errors'][] = $error; } } @@ -2467,7 +2586,7 @@ abstract class ResourceTestBase extends BrowserTestBase { // should later expect the document to have 'meta' errors too. foreach ($related_document['errors'] as $error) { // @todo remove this when inaccessible relationships are able to raise errors in https://www.drupal.org/project/jsonapi/issues/2956084. - if ($error['detail'] !== 'The current user is not allowed to view this relationship.') { + if (strpos($error['detail'], 'The current user is not allowed to view this relationship.') !== 0) { unset($error['source']['pointer']); $expected_document['meta']['errors'][] = $error; } diff --git a/tests/src/Functional/ShortcutTest.php b/tests/src/Functional/ShortcutTest.php index b9ce5db..0657f7a 100644 --- a/tests/src/Functional/ShortcutTest.php +++ b/tests/src/Functional/ShortcutTest.php @@ -56,7 +56,7 @@ class ShortcutTest extends ResourceTestBase { 'title' => t('Comments'), 'weight' => -20, 'link' => [ - 'uri' => 'internal:/admin/content/comment', + 'uri' => 'internal:/user/logout', ], ]); $shortcut->save(); @@ -92,7 +92,7 @@ class ShortcutTest extends ResourceTestBase { 'id' => (int) $this->entity->id(), 'title' => 'Comments', 'link' => [ - 'uri' => 'internal:/admin/content/comment', + 'uri' => 'internal:/user/logout', 'title' => NULL, 'options' => [], ], diff --git a/tests/src/Functional/UserTest.php b/tests/src/Functional/UserTest.php index acde288..5830dbb 100644 --- a/tests/src/Functional/UserTest.php +++ b/tests/src/Functional/UserTest.php @@ -104,6 +104,7 @@ class UserTest extends ResourceTestBase { /** @var \Drupal\user\UserInterface $user */ $user = $this->getEntityDuplicate($this->entity, $key); $user->setUsername($user->label() . '_' . $key); + $user->setEmail("$key@example.com"); $user->save(); return $user; }