function EntityResourceTestBase::testGet
Same name in other branches
- 9 core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testGet()
- 8.9.x core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testGet()
- 10 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 - Tests a GET request for an entity, plus edge cases to ensure good DX.
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\EntityResourceCode
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 ? [
'UNCACHEABLE (request policy)',
] : [
'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());
$dynamic_cache = str_starts_with($response->getHeader('X-Drupal-Cache-Max-Age')[0], '0') || !empty(array_intersect([
'user',
'session',
], explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]))) ? 'UNCACHEABLE (poor cacheability)' : 'MISS';
$this->assertSame([
$dynamic_cache,
], $response->getHeader('X-Drupal-Dynamic-Cache'));
}
else {
$this->assertSame(406, $response->getStatusCode());
$this->assertSame([
'UNCACHEABLE (poor cacheability)',
], $response->getHeader('X-Drupal-Dynamic-Cache'));
}
$this->assertSame([
'text/html; charset=UTF-8',
], $response->getHeader('Content-Type'));
$this->assertSame(static::$auth ? [
'UNCACHEABLE (request policy)',
] : [
'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 (poor cacheability)');
$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 (poor cacheability)');
// 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 (poor cacheability)');
$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.