You are here

public function ResourceTestBase::testRevisions in Drupal 8

Same name and namespace in other branches
  1. 9 core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php \Drupal\Tests\jsonapi\Functional\ResourceTestBase::testRevisions()
  2. 10 core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php \Drupal\Tests\jsonapi\Functional\ResourceTestBase::testRevisions()

Tests individual and collection revisions.

1 method overrides ResourceTestBase::testRevisions()
MessageTest::testRevisions in core/modules/jsonapi/tests/src/Functional/MessageTest.php
Tests individual and collection revisions.

File

core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php, line 2681

Class

ResourceTestBase
Subclass this for every JSON:API resource type.

Namespace

Drupal\Tests\jsonapi\Functional

Code

public function testRevisions() {
  if (!$this->entity
    ->getEntityType()
    ->isRevisionable() || !$this->entity instanceof FieldableEntityInterface) {
    return;
  }
  assert($this->entity instanceof RevisionableInterface);

  // JSON:API will only support node and media revisions until Drupal core has
  // a generic revision access API.
  if (!static::$resourceTypeIsVersionable) {
    $this
      ->setUpRevisionAuthorization('GET');
    $url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), [
      'entity' => $this->entity
        ->uuid(),
    ])
      ->setAbsolute();
    $url
      ->setOption('query', [
      'resourceVersion' => 'id:' . $this->entity
        ->getRevisionId(),
    ]);
    $request_options = [];
    $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
    $request_options = NestedArray::mergeDeep($request_options, $this
      ->getAuthenticationRequestOptions());
    $response = $this
      ->request('GET', $url, $request_options);
    $detail = 'JSON:API does not yet support resource versioning for this resource type.';
    $detail .= ' For context, see https://www.drupal.org/project/drupal/issues/2992833#comment-12818258.';
    $detail .= ' To contribute, see https://www.drupal.org/project/drupal/issues/2350939 and https://www.drupal.org/project/drupal/issues/2809177.';
    $expected_cache_contexts = [
      'url.path',
      'url.query_args:resourceVersion',
      'url.site',
    ];
    $this
      ->assertResourceErrorResponse(501, $detail, $url, $response, FALSE, [
      'http_response',
    ], $expected_cache_contexts);
    return;
  }

  // Add a field to modify in order to test revisions.
  FieldStorageConfig::create([
    'entity_type' => static::$entityTypeId,
    'field_name' => 'field_revisionable_number',
    'type' => 'integer',
  ])
    ->setCardinality(1)
    ->save();
  FieldConfig::create([
    'entity_type' => static::$entityTypeId,
    'field_name' => 'field_revisionable_number',
    'bundle' => $this->entity
      ->bundle(),
  ])
    ->setLabel('Revisionable text field')
    ->setTranslatable(FALSE)
    ->save();

  // Reload entity so that it has the new field.
  $entity = $this
    ->entityLoadUnchanged($this->entity
    ->id());

  // Set up test data.

  /* @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
  $entity
    ->set('field_revisionable_number', 42);
  $entity
    ->save();
  $original_revision_id = (int) $entity
    ->getRevisionId();
  $entity
    ->set('field_revisionable_number', 99);
  $entity
    ->setNewRevision();
  $entity
    ->save();
  $latest_revision_id = (int) $entity
    ->getRevisionId();

  // @todo Remove line below in favor of commented line in https://www.drupal.org/project/drupal/issues/2878463.
  $url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), [
    'entity' => $this->entity
      ->uuid(),
  ])
    ->setAbsolute();

  // $url = $this->entity->toUrl('jsonapi');
  $collection_url = Url::fromRoute(sprintf('jsonapi.%s.collection', static::$resourceTypeName))
    ->setAbsolute();
  $relationship_url = Url::fromRoute(sprintf('jsonapi.%s.%s.relationship.get', static::$resourceTypeName, 'field_jsonapi_test_entity_ref'), [
    'entity' => $this->entity
      ->uuid(),
  ])
    ->setAbsolute();
  $related_url = Url::fromRoute(sprintf('jsonapi.%s.%s.related', static::$resourceTypeName, 'field_jsonapi_test_entity_ref'), [
    'entity' => $this->entity
      ->uuid(),
  ])
    ->setAbsolute();
  $original_revision_id_url = clone $url;
  $original_revision_id_url
    ->setOption('query', [
    'resourceVersion' => "id:{$original_revision_id}",
  ]);
  $original_revision_id_relationship_url = clone $relationship_url;
  $original_revision_id_relationship_url
    ->setOption('query', [
    'resourceVersion' => "id:{$original_revision_id}",
  ]);
  $original_revision_id_related_url = clone $related_url;
  $original_revision_id_related_url
    ->setOption('query', [
    'resourceVersion' => "id:{$original_revision_id}",
  ]);
  $latest_revision_id_url = clone $url;
  $latest_revision_id_url
    ->setOption('query', [
    'resourceVersion' => "id:{$latest_revision_id}",
  ]);
  $latest_revision_id_relationship_url = clone $relationship_url;
  $latest_revision_id_relationship_url
    ->setOption('query', [
    'resourceVersion' => "id:{$latest_revision_id}",
  ]);
  $latest_revision_id_related_url = clone $related_url;
  $latest_revision_id_related_url
    ->setOption('query', [
    'resourceVersion' => "id:{$latest_revision_id}",
  ]);
  $rel_latest_version_url = clone $url;
  $rel_latest_version_url
    ->setOption('query', [
    'resourceVersion' => 'rel:latest-version',
  ]);
  $rel_latest_version_relationship_url = clone $relationship_url;
  $rel_latest_version_relationship_url
    ->setOption('query', [
    'resourceVersion' => 'rel:latest-version',
  ]);
  $rel_latest_version_related_url = clone $related_url;
  $rel_latest_version_related_url
    ->setOption('query', [
    'resourceVersion' => 'rel:latest-version',
  ]);
  $rel_latest_version_collection_url = clone $collection_url;
  $rel_latest_version_collection_url
    ->setOption('query', [
    'resourceVersion' => 'rel:latest-version',
  ]);
  $rel_working_copy_url = clone $url;
  $rel_working_copy_url
    ->setOption('query', [
    'resourceVersion' => 'rel:working-copy',
  ]);
  $rel_working_copy_relationship_url = clone $relationship_url;
  $rel_working_copy_relationship_url
    ->setOption('query', [
    'resourceVersion' => 'rel:working-copy',
  ]);
  $rel_working_copy_related_url = clone $related_url;
  $rel_working_copy_related_url
    ->setOption('query', [
    'resourceVersion' => 'rel:working-copy',
  ]);
  $rel_working_copy_collection_url = clone $collection_url;
  $rel_working_copy_collection_url
    ->setOption('query', [
    'resourceVersion' => 'rel:working-copy',
  ]);
  $rel_invalid_collection_url = clone $collection_url;
  $rel_invalid_collection_url
    ->setOption('query', [
    'resourceVersion' => 'rel:invalid',
  ]);
  $revision_id_key = 'drupal_internal__' . $this->entity
    ->getEntityType()
    ->getKey('revision');
  $published_key = $this->entity
    ->getEntityType()
    ->getKey('published');
  $revision_translation_affected_key = $this->entity
    ->getEntityType()
    ->getKey('revision_translation_affected');
  $amend_relationship_urls = function (array &$document, $revision_id) {
    if (!empty($document['data']['relationships'])) {
      foreach ($document['data']['relationships'] as &$relationship) {
        $pattern = '/resourceVersion=id%3A\\d/';
        $replacement = 'resourceVersion=' . urlencode("id:{$revision_id}");
        $relationship['links']['self']['href'] = preg_replace($pattern, $replacement, $relationship['links']['self']['href']);
        $relationship['links']['related']['href'] = preg_replace($pattern, $replacement, $relationship['links']['related']['href']);
      }
    }
  };
  $request_options = [];
  $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
  $request_options = NestedArray::mergeDeep($request_options, $this
    ->getAuthenticationRequestOptions());

  // Ensure 403 forbidden on typical GET.
  $actual_response = $this
    ->request('GET', $url, $request_options);
  $expected_cacheability = $this
    ->getExpectedUnauthorizedAccessCacheability();
  $result = $entity
    ->access('view', $this->account, TRUE);
  $detail = 'The current user is not allowed to GET the selected resource.';
  if ($result instanceof AccessResultReasonInterface && ($reason = $result
    ->getReason()) && !empty($reason)) {
    $detail .= ' ' . $reason;
  }
  $this
    ->assertResourceErrorResponse(403, $detail, $url, $actual_response, '/data', $expected_cacheability
    ->getCacheTags(), $expected_cacheability
    ->getCacheContexts(), FALSE, 'MISS');

  // Ensure that targeting a revision does not bypass access.
  $actual_response = $this
    ->request('GET', $original_revision_id_url, $request_options);
  $expected_cacheability = $this
    ->getExpectedUnauthorizedAccessCacheability();
  $detail = 'The current user is not allowed to GET the selected resource. The user does not have access to the requested version.';
  if ($result instanceof AccessResultReasonInterface && ($reason = $result
    ->getReason()) && !empty($reason)) {
    $detail .= ' ' . $reason;
  }
  $this
    ->assertResourceErrorResponse(403, $detail, $url, $actual_response, '/data', $expected_cacheability
    ->getCacheTags(), $expected_cacheability
    ->getCacheContexts(), FALSE, 'MISS');
  $this
    ->setUpRevisionAuthorization('GET');

  // Ensure that the URL without a `resourceVersion` query parameter returns
  // the default revision. This is always the latest revision when
  // content_moderation is not installed.
  $actual_response = $this
    ->request('GET', $url, $request_options);
  $expected_document = $this
    ->getExpectedDocument();

  // The resource object should always links to the specific revision it
  // represents.
  $expected_document['data']['links']['self']['href'] = $latest_revision_id_url
    ->setAbsolute()
    ->toString();
  $amend_relationship_urls($expected_document, $latest_revision_id);

  // Resource objects always link to their specific revision by revision ID.
  $expected_document['data']['attributes'][$revision_id_key] = $latest_revision_id;
  $expected_document['data']['attributes']['field_revisionable_number'] = 99;
  $expected_cache_tags = $this
    ->getExpectedCacheTags();
  $expected_cache_contexts = $this
    ->getExpectedCacheContexts();
  $this
    ->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');

  // Fetch the same revision using its revision ID.
  $actual_response = $this
    ->request('GET', $latest_revision_id_url, $request_options);

  // The top-level document object's `self` link should always link to the
  // request URL.
  $expected_document['links']['self']['href'] = $latest_revision_id_url
    ->setAbsolute()
    ->toString();
  $this
    ->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');

  // Ensure dynamic cache HIT on second request when using a version
  // negotiator.
  $actual_response = $this
    ->request('GET', $latest_revision_id_url, $request_options);
  $this
    ->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'HIT');

  // Fetch the same revision using the `latest-version` link relation type
  // negotiator. Without content_moderation, this is always the most recent
  // revision.
  $actual_response = $this
    ->request('GET', $rel_latest_version_url, $request_options);
  $expected_document['links']['self']['href'] = $rel_latest_version_url
    ->setAbsolute()
    ->toString();
  $this
    ->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');

  // Fetch the same revision using the `working-copy` link relation type
  // negotiator. Without content_moderation, this is always the most recent
  // revision.
  $actual_response = $this
    ->request('GET', $rel_working_copy_url, $request_options);
  $expected_document['links']['self']['href'] = $rel_working_copy_url
    ->setAbsolute()
    ->toString();
  $this
    ->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');

  // Fetch the prior revision.
  $actual_response = $this
    ->request('GET', $original_revision_id_url, $request_options);
  $expected_document['data']['attributes'][$revision_id_key] = $original_revision_id;
  $expected_document['data']['attributes']['field_revisionable_number'] = 42;
  $expected_document['links']['self']['href'] = $original_revision_id_url
    ->setAbsolute()
    ->toString();

  // The resource object should always links to the specific revision it
  // represents.
  $expected_document['data']['links']['self']['href'] = $original_revision_id_url
    ->setAbsolute()
    ->toString();
  $amend_relationship_urls($expected_document, $original_revision_id);

  // When the resource object is not the latest version or the working copy,
  // a link should be provided that links to those versions. Therefore, the
  // presence or absence of these links communicates the state of the resource
  // object.
  $expected_document['data']['links']['latest-version']['href'] = $rel_latest_version_url
    ->setAbsolute()
    ->toString();
  $expected_document['data']['links']['working-copy']['href'] = $rel_working_copy_url
    ->setAbsolute()
    ->toString();
  $this
    ->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');

  // Install content_moderation module.
  $this
    ->assertTrue($this->container
    ->get('module_installer')
    ->install([
    'content_moderation',
  ], TRUE), 'Installed modules.');

  // Set up an editorial workflow.
  $workflow = $this
    ->createEditorialWorkflow();
  $workflow
    ->getTypePlugin()
    ->addEntityTypeAndBundle(static::$entityTypeId, $this->entity
    ->bundle());
  $workflow
    ->save();

  // Ensure the test entity has content_moderation fields attached to it.

  /* @var \Drupal\Core\Entity\FieldableEntityInterface|\Drupal\Core\Entity\TranslatableRevisionableInterface $entity */
  $entity = $this->entityStorage
    ->load($entity
    ->id());

  // Set the published moderation state on the test entity.
  $entity
    ->set('moderation_state', 'published');
  $entity
    ->setNewRevision();
  $entity
    ->save();
  $default_revision_id = (int) $entity
    ->getRevisionId();

  // Fetch the published revision by using the `rel` version negotiator and
  // the `latest-version` version argument. With content_moderation, this is
  // now the most recent revision where the moderation state was the 'default'
  // one.
  $actual_response = $this
    ->request('GET', $rel_latest_version_url, $request_options);
  $expected_document['data']['attributes'][$revision_id_key] = $default_revision_id;
  $expected_document['data']['attributes']['moderation_state'] = 'published';
  $expected_document['data']['attributes'][$published_key] = TRUE;
  $expected_document['data']['attributes']['field_revisionable_number'] = 99;
  $expected_document['links']['self']['href'] = $rel_latest_version_url
    ->toString();
  $expected_document['data']['attributes'][$revision_translation_affected_key] = $entity
    ->isRevisionTranslationAffected();

  // The resource object now must link to the new revision.
  $default_revision_id_url = clone $url;
  $default_revision_id_url = $default_revision_id_url
    ->setOption('query', [
    'resourceVersion' => "id:{$default_revision_id}",
  ]);
  $expected_document['data']['links']['self']['href'] = $default_revision_id_url
    ->setAbsolute()
    ->toString();
  $amend_relationship_urls($expected_document, $default_revision_id);

  // Since the requested version is the latest version and working copy, there
  // should be no links.
  unset($expected_document['data']['links']['latest-version']);
  unset($expected_document['data']['links']['working-copy']);
  $this
    ->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');

  // Fetch the collection URL using the `latest-version` version argument.
  $actual_response = $this
    ->request('GET', $rel_latest_version_collection_url, $request_options);
  $expected_response = $this
    ->getExpectedCollectionResponse([
    $entity,
  ], $rel_latest_version_collection_url
    ->toString(), $request_options);
  $expected_collection_document = $expected_response
    ->getResponseData();
  $expected_cacheability = $expected_response
    ->getCacheableMetadata();
  $this
    ->assertResourceResponse(200, $expected_collection_document, $actual_response, $expected_cacheability
    ->getCacheTags(), $expected_cacheability
    ->getCacheContexts(), FALSE, 'MISS');

  // Fetch the published revision by using the `working-copy` version
  // argument. With content_moderation, this is always the most recent
  // revision regardless of moderation state.
  $actual_response = $this
    ->request('GET', $rel_working_copy_url, $request_options);
  $expected_document['links']['self']['href'] = $rel_working_copy_url
    ->toString();
  $this
    ->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');

  // Fetch the collection URL using the `working-copy` version argument.
  $actual_response = $this
    ->request('GET', $rel_working_copy_collection_url, $request_options);
  $expected_collection_document['links']['self']['href'] = $rel_working_copy_collection_url
    ->toString();
  $this
    ->assertResourceResponse(200, $expected_collection_document, $actual_response, $expected_cacheability
    ->getCacheTags(), $expected_cacheability
    ->getCacheContexts(), FALSE, 'MISS');

  // @todo: remove the next assertion when Drupal core supports entity query access control on revisions.
  $rel_working_copy_collection_url_filtered = clone $rel_working_copy_collection_url;
  $rel_working_copy_collection_url_filtered
    ->setOption('query', [
    'filter[foo]' => 'bar',
  ] + $rel_working_copy_collection_url
    ->getOption('query'));
  $actual_response = $this
    ->request('GET', $rel_working_copy_collection_url_filtered, $request_options);
  $filtered_collection_expected_cache_contexts = [
    'url.path',
    'url.query_args:filter',
    'url.query_args:resourceVersion',
    'url.site',
  ];
  $this
    ->assertResourceErrorResponse(501, 'JSON:API does not support filtering on revisions other than the latest version because a secure Drupal core API does not yet exist to do so.', $rel_working_copy_collection_url_filtered, $actual_response, FALSE, [
    'http_response',
  ], $filtered_collection_expected_cache_contexts);

  // Fetch the collection URL using an invalid version identifier.
  $actual_response = $this
    ->request('GET', $rel_invalid_collection_url, $request_options);
  $invalid_version_expected_cache_contexts = [
    'url.path',
    'url.query_args:resourceVersion',
    'url.site',
  ];
  $this
    ->assertResourceErrorResponse(400, 'Collection resources only support the following resource version identifiers: rel:latest-version, rel:working-copy', $rel_invalid_collection_url, $actual_response, FALSE, [
    '4xx-response',
    'http_response',
  ], $invalid_version_expected_cache_contexts);

  // Move the entity to its draft moderation state.
  $entity
    ->set('field_revisionable_number', 42);

  // Change a relationship field so revisions can be tested on related and
  // relationship routes.
  $new_user = $this
    ->createUser();
  $new_user
    ->save();
  $entity
    ->set('field_jsonapi_test_entity_ref', [
    'target_id' => $new_user
      ->id(),
  ]);
  $entity
    ->set('moderation_state', 'draft');
  $entity
    ->setNewRevision();
  $entity
    ->save();
  $forward_revision_id = (int) $entity
    ->getRevisionId();

  // The `latest-version` link should *still* reference the same revision
  // since a draft is not a default revision.
  $actual_response = $this
    ->request('GET', $rel_latest_version_url, $request_options);
  $expected_document['links']['self']['href'] = $rel_latest_version_url
    ->toString();

  // Since the latest version is no longer also the working copy, a
  // `working-copy` link is required to indicate that there is a forward
  // revision available.
  $expected_document['data']['links']['working-copy']['href'] = $rel_working_copy_url
    ->setAbsolute()
    ->toString();
  $this
    ->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');

  // And the same should be true for collections.
  $actual_response = $this
    ->request('GET', $rel_latest_version_collection_url, $request_options);
  $expected_collection_document['data'][0] = $expected_document['data'];
  $expected_collection_document['links']['self']['href'] = $rel_latest_version_collection_url
    ->toString();
  $this
    ->assertResourceResponse(200, $expected_collection_document, $actual_response, $expected_cacheability
    ->getCacheTags(), $expected_cacheability
    ->getCacheContexts(), FALSE, 'MISS');

  // Ensure that the `latest-version` response is same as the default link,
  // aside from the document's `self` link.
  $actual_response = $this
    ->request('GET', $url, $request_options);
  $expected_document['links']['self']['href'] = $url
    ->toString();
  $this
    ->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');

  // And the same should be true for collections.
  $actual_response = $this
    ->request('GET', $collection_url, $request_options);
  $expected_collection_document['links']['self']['href'] = $collection_url
    ->toString();
  $this
    ->assertResourceResponse(200, $expected_collection_document, $actual_response, $expected_cacheability
    ->getCacheTags(), $expected_cacheability
    ->getCacheContexts(), FALSE, 'MISS');

  // Now, the `working-copy` link should reference the draft revision. This
  // is significant because without content_moderation, the two responses
  // would still been the same.
  //
  // Access is checked before any special permissions are granted. This
  // asserts a 403 forbidden if the user is not allowed to see unpublished
  // content.
  $result = $entity
    ->access('view', $this->account, TRUE);
  if (!$result
    ->isAllowed()) {
    $actual_response = $this
      ->request('GET', $rel_working_copy_url, $request_options);
    $expected_cacheability = $this
      ->getExpectedUnauthorizedAccessCacheability();
    $expected_cache_tags = Cache::mergeTags($expected_cacheability
      ->getCacheTags(), $entity
      ->getCacheTags());
    $expected_cache_contexts = $expected_cacheability
      ->getCacheContexts();
    $detail = 'The current user is not allowed to GET the selected resource. The user does not have access to the requested version.';
    $message = $result instanceof AccessResultReasonInterface ? trim($detail . ' ' . $result
      ->getReason()) : $detail;
    $this
      ->assertResourceErrorResponse(403, $message, $url, $actual_response, '/data', $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');

    // On the collection URL, we should expect to see the draft omitted from
    // the collection.
    $actual_response = $this
      ->request('GET', $rel_working_copy_collection_url, $request_options);
    $expected_response = static::getExpectedCollectionResponse([
      $entity,
    ], $rel_working_copy_collection_url
      ->toString(), $request_options);
    $expected_collection_document = $expected_response
      ->getResponseData();
    $expected_collection_document['data'] = [];
    $expected_cacheability = $expected_response
      ->getCacheableMetadata();
    $access_denied_response = static::getAccessDeniedResponse($entity, $result, $url, NULL, $detail)
      ->getResponseData();
    static::addOmittedObject($expected_collection_document, static::errorsToOmittedObject($access_denied_response['errors']));
    $this
      ->assertResourceResponse(200, $expected_collection_document, $actual_response, $expected_cacheability
      ->getCacheTags(), $expected_cacheability
      ->getCacheContexts(), FALSE, 'MISS');
  }

  // Since additional permissions are required to see 'draft' entities,
  // grant those permissions.
  $this
    ->grantPermissionsToTestedRole($this
    ->getEditorialPermissions());

  // Now, the `working-copy` link should be latest revision and be accessible.
  $actual_response = $this
    ->request('GET', $rel_working_copy_url, $request_options);
  $expected_document['data']['attributes'][$revision_id_key] = $forward_revision_id;
  $expected_document['data']['attributes']['moderation_state'] = 'draft';
  $expected_document['data']['attributes'][$published_key] = FALSE;
  $expected_document['data']['attributes']['field_revisionable_number'] = 42;
  $expected_document['links']['self']['href'] = $rel_working_copy_url
    ->setAbsolute()
    ->toString();
  $expected_document['data']['attributes'][$revision_translation_affected_key] = $entity
    ->isRevisionTranslationAffected();

  // The resource object now must link to the forward revision.
  $forward_revision_id_url = clone $url;
  $forward_revision_id_url = $forward_revision_id_url
    ->setOption('query', [
    'resourceVersion' => "id:{$forward_revision_id}",
  ]);
  $expected_document['data']['links']['self']['href'] = $forward_revision_id_url
    ->setAbsolute()
    ->toString();
  $amend_relationship_urls($expected_document, $forward_revision_id);

  // Since the working copy is not the default revision. A `latest-version`
  // link is required to indicate that the requested version is not the
  // default revision.
  unset($expected_document['data']['links']['working-copy']);
  $expected_document['data']['links']['latest-version']['href'] = $rel_latest_version_url
    ->setAbsolute()
    ->toString();
  $expected_cache_tags = $this
    ->getExpectedCacheTags();
  $expected_cache_contexts = $this
    ->getExpectedCacheContexts();
  $this
    ->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');

  // And the collection response should also have the latest revision.
  $actual_response = $this
    ->request('GET', $rel_working_copy_collection_url, $request_options);
  $expected_response = static::getExpectedCollectionResponse([
    $entity,
  ], $rel_working_copy_collection_url
    ->toString(), $request_options);
  $expected_collection_document = $expected_response
    ->getResponseData();
  $expected_collection_document['data'] = [
    $expected_document['data'],
  ];
  $expected_cacheability = $expected_response
    ->getCacheableMetadata();
  $this
    ->assertResourceResponse(200, $expected_collection_document, $actual_response, $expected_cacheability
    ->getCacheTags(), $expected_cacheability
    ->getCacheContexts(), FALSE, 'MISS');

  // Test relationship responses.
  // Fetch the prior revision's relationship URL.
  $test_relationship_urls = [
    [
      NULL,
      $relationship_url,
      $related_url,
    ],
    [
      $original_revision_id,
      $original_revision_id_relationship_url,
      $original_revision_id_related_url,
    ],
    [
      $latest_revision_id,
      $latest_revision_id_relationship_url,
      $latest_revision_id_related_url,
    ],
    [
      $default_revision_id,
      $rel_latest_version_relationship_url,
      $rel_latest_version_related_url,
    ],
    [
      $forward_revision_id,
      $rel_working_copy_relationship_url,
      $rel_working_copy_related_url,
    ],
  ];
  foreach ($test_relationship_urls as $revision_case) {
    list($revision_id, $relationship_url, $related_url) = $revision_case;

    // Load the revision that will be requested.
    $this->entityStorage
      ->resetCache([
      $entity
        ->id(),
    ]);
    $revision = is_null($revision_id) ? $this->entityStorage
      ->load($entity
      ->id()) : $this->entityStorage
      ->loadRevision($revision_id);

    // Request the relationship resource without access to the relationship
    // field.
    $actual_response = $this
      ->request('GET', $relationship_url, $request_options);
    $expected_response = $this
      ->getExpectedGetRelationshipResponse('field_jsonapi_test_entity_ref', $revision);
    $expected_document = $expected_response
      ->getResponseData();
    $expected_cacheability = $expected_response
      ->getCacheableMetadata();
    $expected_document['errors'][0]['links']['via']['href'] = $relationship_url
      ->toString();
    $this
      ->assertResourceResponse(403, $expected_document, $actual_response, $expected_cacheability
      ->getCacheTags(), $expected_cacheability
      ->getCacheContexts());

    // Request the related route.
    $actual_response = $this
      ->request('GET', $related_url, $request_options);

    // @todo: refactor self::getExpectedRelatedResponses() into a function which returns a single response.
    $expected_response = $this
      ->getExpectedRelatedResponses([
      'field_jsonapi_test_entity_ref',
    ], $request_options, $revision)['field_jsonapi_test_entity_ref'];
    $expected_document = $expected_response
      ->getResponseData();
    $expected_cacheability = $expected_response
      ->getCacheableMetadata();
    $expected_document['errors'][0]['links']['via']['href'] = $related_url
      ->toString();
    $this
      ->assertResourceResponse(403, $expected_document, $actual_response, $expected_cacheability
      ->getCacheTags(), $expected_cacheability
      ->getCacheContexts());
  }
  $this
    ->grantPermissionsToTestedRole([
    'field_jsonapi_test_entity_ref view access',
  ]);
  foreach ($test_relationship_urls as $revision_case) {
    list($revision_id, $relationship_url, $related_url) = $revision_case;

    // Load the revision that will be requested.
    $this->entityStorage
      ->resetCache([
      $entity
        ->id(),
    ]);
    $revision = is_null($revision_id) ? $this->entityStorage
      ->load($entity
      ->id()) : $this->entityStorage
      ->loadRevision($revision_id);

    // Request the relationship resource after granting access to the
    // relationship field.
    $actual_response = $this
      ->request('GET', $relationship_url, $request_options);
    $expected_response = $this
      ->getExpectedGetRelationshipResponse('field_jsonapi_test_entity_ref', $revision);
    $expected_document = $expected_response
      ->getResponseData();
    $expected_document['links']['self']['href'] = $relationship_url
      ->setAbsolute()
      ->toString();
    $expected_cacheability = $expected_response
      ->getCacheableMetadata();
    $this
      ->assertResourceResponse(200, $expected_document, $actual_response, $expected_cacheability
      ->getCacheTags(), $expected_cacheability
      ->getCacheContexts(), FALSE, 'MISS');

    // Request the related route.
    $actual_response = $this
      ->request('GET', $related_url, $request_options);
    $expected_response = $this
      ->getExpectedRelatedResponse('field_jsonapi_test_entity_ref', $request_options, $revision);
    $expected_document = $expected_response
      ->getResponseData();
    $expected_cacheability = $expected_response
      ->getCacheableMetadata();
    $expected_document['links']['self']['href'] = $related_url
      ->toString();

    // MISS or UNCACHEABLE depends on data. It must not be HIT.
    $dynamic_cache = !empty(array_intersect([
      'user',
      'session',
    ], $expected_cacheability
      ->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS';
    $this
      ->assertResourceResponse(200, $expected_document, $actual_response, $expected_cacheability
      ->getCacheTags(), $expected_cacheability
      ->getCacheContexts(), FALSE, $dynamic_cache);
  }
  $this
    ->config('jsonapi.settings')
    ->set('read_only', FALSE)
    ->save(TRUE);

  // Ensures that PATCH and DELETE on individual resources with a
  // `resourceVersion` query parameter is not supported.
  $individual_urls = [
    $original_revision_id_url,
    $latest_revision_id_url,
    $rel_latest_version_url,
    $rel_working_copy_url,
  ];
  $request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
  foreach ($individual_urls as $url) {
    foreach ([
      'PATCH',
      'DELETE',
    ] as $method) {
      $actual_response = $this
        ->request($method, $url, $request_options);
      $this
        ->assertResourceErrorResponse(400, sprintf('%s requests with a `%s` query parameter are not supported.', $method, 'resourceVersion'), $url, $actual_response);
    }
  }

  // Ensures that PATCH, POST and DELETE on relationship resources with a
  // `resourceVersion` query parameter is not supported.
  $relationship_urls = [
    $original_revision_id_relationship_url,
    $latest_revision_id_relationship_url,
    $rel_latest_version_relationship_url,
    $rel_working_copy_relationship_url,
  ];
  foreach ($relationship_urls as $url) {
    foreach ([
      'PATCH',
      'POST',
      'DELETE',
    ] as $method) {
      $actual_response = $this
        ->request($method, $url, $request_options);
      $this
        ->assertResourceErrorResponse(400, sprintf('%s requests with a `%s` query parameter are not supported.', $method, 'resourceVersion'), $url, $actual_response);
    }
  }

  // Ensures that POST on collection resources with a `resourceVersion` query
  // parameter is not supported.
  $collection_urls = [
    $rel_latest_version_collection_url,
    $rel_working_copy_collection_url,
  ];
  foreach ($collection_urls as $url) {
    foreach ([
      'POST',
    ] as $method) {
      $actual_response = $this
        ->request($method, $url, $request_options);
      $this
        ->assertResourceErrorResponse(400, sprintf('%s requests with a `%s` query parameter are not supported.', $method, 'resourceVersion'), $url, $actual_response);
    }
  }
}