You are here

public function EntityResourceTestBase::testGet in Drupal 10

Same name and namespace in other branches
  1. 8 core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testGet()
  2. 9 core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testGet()

Tests a GET request for an entity, plus edge cases to ensure good DX.

2 methods override EntityResourceTestBase::testGet()
MessageResourceTestBase::testGet in core/modules/contact/tests/src/Functional/Rest/MessageResourceTestBase.php
Tests a GET request for an entity, plus edge cases to ensure good DX.
VocabularyJsonAnonTest::testGet in core/modules/taxonomy/tests/src/Functional/Rest/VocabularyJsonAnonTest.php
Disable the GET test coverage due to bug in taxonomy module. @todo Fix in https://www.drupal.org/node/2805281: remove this override.

File

core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php, line 404

Class

EntityResourceTestBase
Even though there is the generic EntityResource, it's necessary for every entity type to have its own test, because they each have different fields, validation constraints, et cetera. It's not because the generic case works, that every case…

Namespace

Drupal\Tests\rest\Functional\EntityResource

Code

public function testGet() {
  $this
    ->initAuthentication();
  $has_canonical_url = $this->entity
    ->hasLinkTemplate('canonical');

  // 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.
  $url = $this
    ->getEntityResourceUrl();
  $request_options = [];

  // DX: 404 when resource not provisioned, 403 if canonical route. HTML
  // response because missing ?_format query string.
  $response = $this
    ->request('GET', $url, $request_options);
  $this
    ->assertSame($has_canonical_url ? 403 : 404, $response
    ->getStatusCode());
  $this
    ->assertSame([
    'text/html; charset=UTF-8',
  ], $response
    ->getHeader('Content-Type'));
  $url
    ->setOption('query', [
    '_format' => static::$format,
  ]);

  // DX: 404 when resource not provisioned, 403 if canonical route. Non-HTML
  // response because ?_format query string is present.
  $response = $this
    ->request('GET', $url, $request_options);
  if ($has_canonical_url) {
    $expected_cacheability = $this
      ->getExpectedUnauthorizedAccessCacheability()
      ->addCacheTags([
      'config:user.role.anonymous',
    ]);
    $expected_cacheability
      ->addCacheableDependency($this
      ->getExpectedUnauthorizedEntityAccessCacheability(FALSE));
    $this
      ->assertResourceErrorResponse(403, $this
      ->getExpectedUnauthorizedAccessMessage('GET'), $response, $expected_cacheability
      ->getCacheTags(), $expected_cacheability
      ->getCacheContexts(), 'MISS', FALSE);
  }
  else {
    $this
      ->assertResourceErrorResponse(404, 'No route found for "GET ' . $this
      ->getEntityResourceUrl()
      ->setAbsolute()
      ->toString() . '"', $response);
  }
  $this
    ->provisionEntityResource();

  // DX: forgetting authentication: authentication provider-specific error
  // response.
  if (static::$auth) {
    $response = $this
      ->request('GET', $url, $request_options);
    $this
      ->assertResponseWhenMissingAuthentication('GET', $response);
  }
  $request_options[RequestOptions::HEADERS]['REST-test-auth'] = '1';

  // DX: 403 when attempting to use unallowed authentication provider.
  $response = $this
    ->request('GET', $url, $request_options);
  $this
    ->assertResourceErrorResponse(403, 'The used authentication method is not allowed on this route.', $response);
  unset($request_options[RequestOptions::HEADERS]['REST-test-auth']);
  $request_options[RequestOptions::HEADERS]['REST-test-auth-global'] = '1';

  // DX: 403 when attempting to use unallowed global authentication provider.
  $response = $this
    ->request('GET', $url, $request_options);
  $this
    ->assertResourceErrorResponse(403, 'The used authentication method is not allowed on this route.', $response);
  unset($request_options[RequestOptions::HEADERS]['REST-test-auth-global']);
  $request_options = NestedArray::mergeDeep($request_options, $this
    ->getAuthenticationRequestOptions('GET'));

  // First: single format. Drupal will automatically pick the only format.
  $this
    ->provisionEntityResource(TRUE);
  $expected_403_cacheability = $this
    ->getExpectedUnauthorizedAccessCacheability()
    ->addCacheableDependency($this
    ->getExpectedUnauthorizedEntityAccessCacheability(static::$auth !== FALSE));

  // DX: 403 because unauthorized single-format route, ?_format is omittable.
  $url
    ->setOption('query', []);
  $response = $this
    ->request('GET', $url, $request_options);
  if ($has_canonical_url) {
    $this
      ->assertSame(403, $response
      ->getStatusCode());
    $this
      ->assertSame([
      'text/html; charset=UTF-8',
    ], $response
      ->getHeader('Content-Type'));
  }
  else {
    $this
      ->assertResourceErrorResponse(403, FALSE, $response, $expected_403_cacheability
      ->getCacheTags(), $expected_403_cacheability
      ->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);
  }
  $this
    ->assertSame(static::$auth ? [] : [
    'MISS',
  ], $response
    ->getHeader('X-Drupal-Cache'));

  // DX: 403 because unauthorized.
  $url
    ->setOption('query', [
    '_format' => static::$format,
  ]);
  $response = $this
    ->request('GET', $url, $request_options);
  $this
    ->assertResourceErrorResponse(403, FALSE, $response, $expected_403_cacheability
    ->getCacheTags(), $expected_403_cacheability
    ->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);

  // Then, what we'll use for the remainder of the test: multiple formats.
  $this
    ->provisionEntityResource();

  // DX: 406 because despite unauthorized, ?_format is not omittable.
  $url
    ->setOption('query', []);
  $response = $this
    ->request('GET', $url, $request_options);
  if ($has_canonical_url) {
    $this
      ->assertSame(403, $response
      ->getStatusCode());
    $this
      ->assertSame([
      'HIT',
    ], $response
      ->getHeader('X-Drupal-Dynamic-Cache'));
  }
  else {
    $this
      ->assertSame(406, $response
      ->getStatusCode());
    $this
      ->assertSame([
      'UNCACHEABLE',
    ], $response
      ->getHeader('X-Drupal-Dynamic-Cache'));
  }
  $this
    ->assertSame([
    'text/html; charset=UTF-8',
  ], $response
    ->getHeader('Content-Type'));
  $this
    ->assertSame(static::$auth ? [] : [
    'MISS',
  ], $response
    ->getHeader('X-Drupal-Cache'));

  // DX: 403 because unauthorized.
  $url
    ->setOption('query', [
    '_format' => static::$format,
  ]);
  $response = $this
    ->request('GET', $url, $request_options);
  $this
    ->assertResourceErrorResponse(403, $this
    ->getExpectedUnauthorizedAccessMessage('GET'), $response, $expected_403_cacheability
    ->getCacheTags(), $expected_403_cacheability
    ->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);
  $this
    ->assertArrayNotHasKey('Link', $response
    ->getHeaders());
  $this
    ->setUpAuthorization('GET');

  // 200 for well-formed HEAD request.
  $response = $this
    ->request('HEAD', $url, $request_options);
  $is_cacheable_by_dynamic_page_cache = empty(array_intersect([
    'user',
    'session',
  ], $this
    ->getExpectedCacheContexts()));
  $this
    ->assertResourceResponse(200, '', $response, $this
    ->getExpectedCacheTags(), $this
    ->getExpectedCacheContexts(), static::$auth ? FALSE : 'MISS', $is_cacheable_by_dynamic_page_cache ? 'MISS' : 'UNCACHEABLE');
  $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, FALSE, $response, $this
    ->getExpectedCacheTags(), $this
    ->getExpectedCacheContexts(), static::$auth ? FALSE : 'HIT', $is_cacheable_by_dynamic_page_cache ? static::$auth ? 'HIT' : 'MISS' : '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\rest\EventSubscriber\ResourceResponseSubscriber::flattenResponse()
  $cache_items = $this->container
    ->get('database')
    ->select('cache_dynamic_page_cache', 'c')
    ->fields('c', [
    'cid',
    'data',
  ])
    ->condition('c.cid', '%[route]=rest.%', 'LIKE')
    ->execute()
    ->fetchAllAssoc('cid');
  if (!$is_cacheable_by_dynamic_page_cache) {
    $this
      ->assertCount(0, $cache_items);
  }
  else {
    $this
      ->assertCount(2, $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(ResourceResponseInterface::class, $cached_response);
        $this
          ->assertInstanceOf(CacheableResponseInterface::class, $cached_response);
      }
      else {
        $found_cache_redirect = TRUE;
      }
    }
    $this
      ->assertTrue($found_cache_redirect);
    $this
      ->assertTrue($found_cached_200_response);
    $this
      ->assertTrue($other_cached_responses_are_4xx);
  }

  // Sort the serialization data first so we can do an identical comparison
  // for the keys with the array order the same (it needs to match with
  // identical comparison).
  $expected = $this
    ->getExpectedNormalizedEntity();
  static::recursiveKSort($expected);
  $actual = $this->serializer
    ->decode((string) $response
    ->getBody(), static::$format);
  static::recursiveKSort($actual);
  $this
    ->assertEqualsCanonicalizing($expected, $actual);

  // Not only assert the normalization, also assert deserialization of the
  // response results in the expected object.
  // Note: deserialization of the XML format is not supported, so only test
  // this for other formats.
  if (static::$format !== 'xml') {
    $unserialized = $this->serializer
      ->deserialize((string) $response
      ->getBody(), get_class($this->entity), static::$format);
    $this
      ->assertSame($unserialized
      ->uuid(), $this->entity
      ->uuid());
  }

  // Finally, assert that the expected 'Link' headers are present.
  if ($this->entity
    ->getEntityType()
    ->getLinkTemplates()) {
    $this
      ->assertArrayHasKey('Link', $response
      ->getHeaders());
    $link_relation_type_manager = $this->container
      ->get('plugin.manager.link_relation_type');
    $expected_link_relation_headers = array_map(function ($relation_name) use ($link_relation_type_manager) {
      $link_relation_type = $link_relation_type_manager
        ->createInstance($relation_name);
      return $link_relation_type
        ->isRegistered() ? $link_relation_type
        ->getRegisteredName() : $link_relation_type
        ->getExtensionUri();
    }, array_keys($this->entity
      ->getEntityType()
      ->getLinkTemplates()));
    $parse_rel_from_link_header = function ($value) use ($link_relation_type_manager) {
      $matches = [];
      if (preg_match('/rel="([^"]+)"/', $value, $matches) === 1) {
        return $matches[1];
      }
      return FALSE;
    };
    $this
      ->assertSame($expected_link_relation_headers, array_map($parse_rel_from_link_header, $response
      ->getHeader('Link')));
  }
  $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);
  $this->resourceConfigStorage
    ->load(static::$resourceConfigId)
    ->disable()
    ->save();
  $this
    ->refreshTestStateAfterRestConfigChange();

  // DX: upon disabling a resource, it's immediately no longer available.
  $this
    ->assertResourceNotAvailable($url, $request_options);
  $this->resourceConfigStorage
    ->load(static::$resourceConfigId)
    ->enable()
    ->save();
  $this
    ->refreshTestStateAfterRestConfigChange();

  // DX: upon re-enabling a resource, immediate 200.
  $response = $this
    ->request('GET', $url, $request_options);
  $this
    ->assertResourceResponse(200, FALSE, $response, $this
    ->getExpectedCacheTags(), $this
    ->getExpectedCacheContexts(), static::$auth ? FALSE : 'MISS', $is_cacheable_by_dynamic_page_cache ? 'MISS' : 'UNCACHEABLE');
  $this->resourceConfigStorage
    ->load(static::$resourceConfigId)
    ->delete();
  $this
    ->refreshTestStateAfterRestConfigChange();

  // DX: upon deleting a resource, it's immediately no longer available.
  $this
    ->assertResourceNotAvailable($url, $request_options);
  $this
    ->provisionEntityResource();
  $url
    ->setOption('query', [
    '_format' => 'non_existing_format',
  ]);

  // DX: 406 when requesting unsupported format.
  $response = $this
    ->request('GET', $url, $request_options);
  $this
    ->assert406Response($response);
  $this
    ->assertSame([
    'text/plain; charset=UTF-8',
  ], $response
    ->getHeader('Content-Type'));
  $request_options[RequestOptions::HEADERS]['Accept'] = static::$mimeType;

  // DX: 406 when requesting unsupported format but specifying Accept header:
  // should result in a text/plain response.
  $response = $this
    ->request('GET', $url, $request_options);
  $this
    ->assert406Response($response);
  $this
    ->assertSame([
    'text/plain; charset=UTF-8',
  ], $response
    ->getHeader('Content-Type'));
  $url = Url::fromRoute('rest.entity.' . static::$entityTypeId . '.GET');
  $url
    ->setRouteParameter(static::$entityTypeId, 987654321);
  $url
    ->setOption('query', [
    '_format' => static::$format,
  ]);

  // DX: 404 when GETting non-existing entity.
  $response = $this
    ->request('GET', $url, $request_options);
  $path = str_replace('987654321', '{' . static::$entityTypeId . '}', $url
    ->setAbsolute()
    ->setOptions([
    'base_url' => '',
    'query' => [],
  ])
    ->toString());
  $message = 'The "' . static::$entityTypeId . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . static::$entityTypeId . '.GET")';
  $this
    ->assertResourceErrorResponse(404, $message, $response);
}