function ResourceTestBase::testGetIndividual

Same name and namespace 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.