function ResourceTestBase::testRevisions

Same name 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()
  3. 11.x 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);
        }
    }
}

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.