function ResourceTestBase::testGetIndividual

Same name in other branches
  1. 9 core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php \Drupal\Tests\jsonapi\Functional\ResourceTestBase::testGetIndividual()
  2. 8.9.x core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php \Drupal\Tests\jsonapi\Functional\ResourceTestBase::testGetIndividual()
  3. 11.x core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php \Drupal\Tests\jsonapi\Functional\ResourceTestBase::testGetIndividual()

Tests GETting an individual resource, plus edge cases to ensure good DX.

1 call to ResourceTestBase::testGetIndividual()
NodeTest::testGetIndividual in core/modules/jsonapi/tests/src/Functional/NodeTest.php
Tests GETting an individual resource, plus edge cases to ensure good DX.
2 methods override ResourceTestBase::testGetIndividual()
MessageTest::testGetIndividual in core/modules/jsonapi/tests/src/Functional/MessageTest.php
Tests GETting an individual resource, plus edge cases to ensure good DX.
NodeTest::testGetIndividual in core/modules/jsonapi/tests/src/Functional/NodeTest.php
Tests GETting an individual resource, plus edge cases to ensure good DX.

File

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

Class

ResourceTestBase
Subclass this for every JSON:API resource type.

Namespace

Drupal\Tests\jsonapi\Functional

Code

public function testGetIndividual() : void {
    // The URL and Guzzle request options that will be used in this test. The
    // request options will be modified/expanded throughout this test:
    // - to first test all mistakes a developer might make, and assert that the
    //   error responses provide a good DX
    // - to eventually result in a well-formed request that succeeds.
    // @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(),
    ]);
    // $url = $this->entity->toUrl('jsonapi');
    $request_options = [];
    $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
    $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
    // DX: 403 when unauthorized, or 200 if the 'view label' operation is
    // supported by the entity type.
    $response = $this->request('GET', $url, $request_options);
    if (!static::$anonymousUsersCanViewLabels) {
        $expected_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability();
        $reason = $this->getExpectedUnauthorizedAccessMessage('GET');
        $message = trim("The current user is not allowed to GET the selected resource. {$reason}");
        // MISS or UNCACHEABLE depends on data. It must not be HIT.
        $dynamic_cache_header_value = !empty(array_intersect([
            'user',
            'session',
        ], $expected_403_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS';
        $this->assertResourceErrorResponse(403, $message, $url, $response, '/data', $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), FALSE, $dynamic_cache_header_value);
        $this->assertArrayNotHasKey('Link', $response->getHeaders());
    }
    else {
        $expected_document = $this->getExpectedDocument();
        $label_field_name = $this->entity
            ->getEntityType()
            ->hasKey('label') ? $this->entity
            ->getEntityType()
            ->getKey('label') : static::$labelFieldName;
        $expected_document['data']['attributes'] = array_intersect_key($expected_document['data']['attributes'], [
            $label_field_name => TRUE,
        ]);
        unset($expected_document['data']['relationships']);
        // MISS or UNCACHEABLE depends on data. It must not be HIT.
        $dynamic_cache_label_only = !empty(array_intersect([
            'user',
            'session',
        ], $this->getExpectedCacheContexts([
            $label_field_name,
        ]))) ? 'UNCACHEABLE' : 'MISS';
        $this->assertResourceResponse(200, $expected_document, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts([
            $label_field_name,
        ]), FALSE, $dynamic_cache_label_only);
    }
    $this->setUpAuthorization('GET');
    // Set body despite that being nonsensical: should be ignored.
    $request_options[RequestOptions::BODY] = Json::encode($this->getExpectedDocument());
    // 400 for GET request with reserved custom query parameter.
    $url_reserved_custom_query_parameter = clone $url;
    $url_reserved_custom_query_parameter = $url_reserved_custom_query_parameter->setOption('query', [
        'foo' => 'bar',
    ]);
    $response = $this->request('GET', $url_reserved_custom_query_parameter, $request_options);
    $expected_document = [
        'jsonapi' => static::$jsonApiMember,
        'errors' => [
            [
                'title' => 'Bad Request',
                'status' => '400',
                'detail' => "The following query parameters violate the JSON:API spec: 'foo'.",
                'links' => [
                    'info' => [
                        'href' => 'http://jsonapi.org/format/#query-parameters',
                    ],
                    'via' => [
                        'href' => $url_reserved_custom_query_parameter->toString(),
                    ],
                ],
            ],
        ],
    ];
    $this->assertResourceResponse(400, $expected_document, $response, [
        '4xx-response',
        'http_response',
    ], [
        'url.query_args',
        'url.site',
    ], FALSE, 'MISS');
    // 200 for well-formed HEAD request.
    $response = $this->request('HEAD', $url, $request_options);
    // MISS or UNCACHEABLE depends on data. It must not be HIT.
    $dynamic_cache = !empty(array_intersect([
        'user',
        'session',
    ], $this->getExpectedCacheContexts())) ? 'UNCACHEABLE' : 'MISS';
    $this->assertResourceResponse(200, NULL, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), FALSE, $dynamic_cache);
    $head_headers = $response->getHeaders();
    // 200 for well-formed GET request. Page Cache hit because of HEAD request.
    // Same for Dynamic Page Cache hit.
    $response = $this->request('GET', $url, $request_options);
    $this->assertResourceResponse(200, $this->getExpectedDocument(), $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), FALSE, $dynamic_cache === 'MISS' ? 'HIT' : 'UNCACHEABLE');
    // Assert that Dynamic Page Cache did not store a ResourceResponse object,
    // which needs serialization after every cache hit. Instead, it should
    // contain a flattened response. Otherwise performance suffers.
    // @see \Drupal\jsonapi\EventSubscriber\ResourceResponseSubscriber::flattenResponse()
    $cache_items = $this->container
        ->get('database')
        ->select('cache_dynamic_page_cache', 'cdp')
        ->fields('cdp', [
        'data',
    ])
        ->condition('cid', '%[route]=jsonapi.%', 'LIKE')
        ->execute()
        ->fetchAll();
    $this->assertLessThanOrEqual(5, count($cache_items));
    $found_cached_200_response = FALSE;
    $other_cached_responses_are_4xx = TRUE;
    foreach ($cache_items as $cache_item) {
        $cached_response = unserialize($cache_item->data);
        if (!$cached_response instanceof CacheRedirect) {
            if ($cached_response->getStatusCode() === 200) {
                $found_cached_200_response = TRUE;
            }
            elseif (!$cached_response->isClientError()) {
                $other_cached_responses_are_4xx = FALSE;
            }
            $this->assertNotInstanceOf(ResourceResponse::class, $cached_response);
            $this->assertInstanceOf(CacheableResponseInterface::class, $cached_response);
        }
    }
    $this->assertSame($dynamic_cache !== 'UNCACHEABLE' || isset($dynamic_cache_label_only) && $dynamic_cache_label_only !== 'UNCACHEABLE', $found_cached_200_response);
    $this->assertTrue($other_cached_responses_are_4xx);
    // Not only assert the normalization, also assert deserialization of the
    // response results in the expected object.
    $unserialized = $this->serializer
        ->deserialize((string) $response->getBody(), JsonApiDocumentTopLevel::class, 'api_json', [
        'target_entity' => static::$entityTypeId,
        'resource_type' => $this->container
            ->get('jsonapi.resource_type.repository')
            ->getByTypeName(static::$resourceTypeName),
    ]);
    $this->assertSame($unserialized->uuid(), $this->entity
        ->uuid());
    $get_headers = $response->getHeaders();
    // Verify that the GET and HEAD responses are the same. The only difference
    // is that there's no body. For this reason the 'Transfer-Encoding' and
    // 'Vary' headers are also added to the list of headers to ignore, as they
    // may be added to GET requests, depending on web server configuration. They
    // are usually 'Transfer-Encoding: chunked' and 'Vary: Accept-Encoding'.
    $ignored_headers = [
        'Date',
        'Content-Length',
        'X-Drupal-Cache',
        'X-Drupal-Dynamic-Cache',
        'Transfer-Encoding',
        'Vary',
    ];
    $header_cleaner = function ($headers) use ($ignored_headers) {
        foreach ($headers as $header => $value) {
            if (str_starts_with($header, 'X-Drupal-Assertion-') || in_array($header, $ignored_headers)) {
                unset($headers[$header]);
            }
        }
        return $headers;
    };
    $get_headers = $header_cleaner($get_headers);
    $head_headers = $header_cleaner($head_headers);
    $this->assertSame($get_headers, $head_headers);
    // Feature: Sparse fieldsets.
    $this->doTestSparseFieldSets($url, $request_options);
    // Feature: Included.
    $this->doTestIncluded($url, $request_options);
    // DX: 404 when GETting non-existing entity.
    $random_uuid = \Drupal::service('uuid')->generate();
    $url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), [
        'entity' => $random_uuid,
    ]);
    $response = $this->request('GET', $url, $request_options);
    $message_url = clone $url;
    $path = str_replace($random_uuid, '{entity}', $message_url->setAbsolute()
        ->setOptions([
        'base_url' => '',
        'query' => [],
    ])
        ->toString());
    $message = 'The "entity" parameter was not converted for the path "' . $path . '" (route name: "jsonapi.' . static::$resourceTypeName . '.individual")';
    $this->assertResourceErrorResponse(404, $message, $url, $response, FALSE, [
        '4xx-response',
        'http_response',
    ], [
        'url.query_args',
        'url.site',
    ], FALSE, 'UNCACHEABLE');
    // DX: when Accept request header is missing, still 404, same response.
    unset($request_options[RequestOptions::HEADERS]['Accept']);
    $response = $this->request('GET', $url, $request_options);
    $this->assertResourceErrorResponse(404, $message, $url, $response, FALSE, [
        '4xx-response',
        'http_response',
    ], [
        'url.query_args',
        'url.site',
    ], FALSE, 'UNCACHEABLE');
}

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