function EntityResourceTestBase::testGet

Same name and namespace in other branches
  1. 9 core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testGet()
  2. 8.9.x core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testGet()
  3. 11.x 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.

1 method overrides EntityResourceTestBase::testGet()
MessageResourceTestBase::testGet in core/modules/contact/tests/src/Functional/Rest/MessageResourceTestBase.php

File

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

Class

EntityResourceTestBase
Defines a base class for testing all entity resources.

Namespace

Drupal\Tests\rest\Functional\EntityResource

Code

public function testGet() : void {
  $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));
    // Mitigate https://www.drupal.org/project/drupal/issues/3451483 until
    // it gets resolved.
    $response = $response->withoutHeader('X-Drupal-Dynamic-Cache');
    $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);
    // Mitigate https://www.drupal.org/project/drupal/issues/3451483 until
    // it gets resolved.
    $response = $response->withoutHeader('X-Drupal-Dynamic-Cache');
    $this->assertResponseWhenMissingAuthentication('GET', $response);
  }
  $request_options[RequestOptions::HEADERS]['REST-test-auth'] = '1';
  // DX: 403 when attempting to use disallowed 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 disallowed 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);
  // Mitigate https://www.drupal.org/project/drupal/issues/3451483 until
  // it gets resolved.
  $response = $response->withoutHeader('X-Drupal-Dynamic-Cache');
  $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);
  // Mitigate https://www.drupal.org/project/drupal/issues/3451483 until
  // it gets resolved.
  $response = $response->withoutHeader('X-Drupal-Dynamic-Cache');
  $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', [
    'data',
  ])
    ->condition('c.cid', '%[route]=rest.%', 'LIKE')
    ->execute()
    ->fetchAll();
  if (!$is_cacheable_by_dynamic_page_cache) {
    $this->assertCount(0, $cache_items);
  }
  else {
    $this->assertLessThanOrEqual(2, 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(ResourceResponseInterface::class, $cached_response);
        $this->assertInstanceOf(CacheableResponseInterface::class, $cached_response);
      }
    }
    $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) {
      $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 (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);
  $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);
}

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