You are here

public function ResourceTestBase::testGetIndividual in Drupal 10

Same name and namespace in other branches
  1. 8 core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php \Drupal\Tests\jsonapi\Functional\ResourceTestBase::testGetIndividual()
  2. 9 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.

File

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

Class

ResourceTestBase
Subclass this for every JSON:API resource type.

Namespace

Drupal\Tests\jsonapi\Functional

Code

public function testGetIndividual() {

  // 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}");
    $this
      ->assertResourceErrorResponse(403, $message, $url, $response, '/data', $expected_403_cacheability
      ->getCacheTags(), $expected_403_cacheability
      ->getCacheContexts(), FALSE, 'MISS');
    $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', [
    'cid',
    'data',
  ])
    ->condition('cid', '%[route]=jsonapi.%', 'LIKE')
    ->execute()
    ->fetchAllAssoc('cid');
  $this
    ->assertGreaterThanOrEqual(2, count($cache_items));
  $found_cache_redirect = FALSE;
  $found_cached_200_response = FALSE;
  $other_cached_responses_are_4xx = TRUE;
  foreach ($cache_items as $cid => $cache_item) {
    $cached_data = unserialize($cache_item->data);
    if (!isset($cached_data['#cache_redirect'])) {
      $cached_response = $cached_data['#response'];
      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);
    }
    else {
      $found_cache_redirect = TRUE;
    }
  }
  $this
    ->assertTrue($found_cache_redirect);
  $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 (strpos($header, 'X-Drupal-Assertion-') === 0 || 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.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.site',
  ], FALSE, 'UNCACHEABLE');
}