Issue #3473374 by mxr576, bbrala, kristiaanvandeneynde: Improve Dynamic Page Cache header assertions in JSON:API tests

merge-requests/10354/head
Lee Rowlands 2024-11-27 15:52:39 +10:00
parent 38c850c4c9
commit 3c87a26035
No known key found for this signature in database
GPG Key ID: 2B829A3DF9204DC4
4 changed files with 106 additions and 115 deletions

View File

@ -22618,12 +22618,6 @@ $ignoreErrors[] = [
'count' => 1,
'path' => __DIR__ . '/modules/jsonapi/tests/src/Functional/ResourceTestBase.php',
];
$ignoreErrors[] = [
// identifier: variable.undefined
'message' => '#^Variable \\$dynamic_cache might not be defined\\.$#',
'count' => 1,
'path' => __DIR__ . '/modules/jsonapi/tests/src/Functional/ResourceTestBase.php',
];
$ignoreErrors[] = [
// identifier: variable.undefined
'message' => '#^Variable \\$parseable_invalid_request_body might not be defined\\.$#',

View File

@ -678,6 +678,8 @@ abstract class ResourceTestBase extends BrowserTestBase {
/**
* Asserts that a resource response has the given status code and body.
*
* Cache max-age is not yet considered when expected header is calculated.
*
* @param int $expected_status_code
* The expected response status.
* @param array|null|false $expected_document
@ -692,16 +694,17 @@ abstract class ResourceTestBase extends BrowserTestBase {
* (optional) The expected cache contexts in the X-Drupal-Cache-Contexts
* response header, or FALSE if that header should be absent. Defaults to
* FALSE.
* @param string|false $expected_page_cache_header_value
* (optional) The expected X-Drupal-Cache response header value, or FALSE if
* that header should be absent. Possible strings: 'MISS', 'HIT'. Defaults
* to FALSE.
* @param string|false $expected_dynamic_page_cache_header_value
* (optional) The expected X-Drupal-Dynamic-Cache response header value, or
* FALSE if that header should be absent. Possible strings: 'MISS', 'HIT'.
* Defaults to FALSE.
* @param 'MISS','HIT','UNCACHEABLE (request policy)','UNCACHEABLE (response policy)'|NULL $expected_page_cache_header_value
* (optional) The expected X-Drupal-Cache response header value, or NULL
* in case of no opinion on that. For possible string values, see the
* parameter type hint. Defaults to NULL.
* @param 'HIT','MISS','UNCACHEABLE (poor cacheability)','UNCACHEABLE (no cacheability)','UNCACHEABLE (request policy)','UNCACHEABLE (response policy)'|bool $expected_dynamic_page_cache_header_value
* (optional) The expected X-Drupal-Dynamic-Cache response header value
* - for possible string values, see the parameter type hint - or TRUE when
* the value should be autogenerated from expected cache contexts, or FALSE
* if that header should be absent. Defaults to FALSE.
*/
protected function assertResourceResponse($expected_status_code, $expected_document, ResponseInterface $response, $expected_cache_tags = FALSE, $expected_cache_contexts = FALSE, $expected_page_cache_header_value = FALSE, $expected_dynamic_page_cache_header_value = FALSE) {
protected function assertResourceResponse($expected_status_code, $expected_document, ResponseInterface $response, $expected_cache_tags = FALSE, $expected_cache_contexts = FALSE, $expected_page_cache_header_value = NULL, $expected_dynamic_page_cache_header_value = FALSE) {
$this->assertSame($expected_status_code, $response->getStatusCode(), var_export(Json::decode((string) $response->getBody()), TRUE));
if ($expected_status_code === 204) {
// DELETE responses should not include a Content-Type header. But Apache
@ -745,20 +748,18 @@ abstract class ResourceTestBase extends BrowserTestBase {
}
// Expected Page Cache header value: X-Drupal-Cache header.
if ($expected_page_cache_header_value !== FALSE) {
if ($expected_page_cache_header_value !== NULL) {
$this->assertTrue($response->hasHeader('X-Drupal-Cache'));
$this->assertSame($expected_page_cache_header_value, $response->getHeader('X-Drupal-Cache')[0]);
}
elseif ($response->hasHeader('X-Drupal-Cache')) {
$this->assertMatchesRegularExpression('#^UNCACHEABLE \((no cacheability|(request|response) policy)\)$#', $response->getHeader('X-Drupal-Cache')[0]);
}
// Expected Dynamic Page Cache header value: X-Drupal-Dynamic-Cache header.
if ($expected_dynamic_page_cache_header_value !== FALSE) {
$this->assertTrue($response->hasHeader('X-Drupal-Dynamic-Cache'));
$this->assertSame($expected_dynamic_page_cache_header_value, $response->getHeader('X-Drupal-Dynamic-Cache')[0]);
if ($expected_dynamic_page_cache_header_value === FALSE) {
$this->assertFalse($response->hasHeader('X-Drupal-Dynamic-Cache'));
}
elseif ($response->hasHeader('X-Drupal-Dynamic-Cache')) {
$this->assertMatchesRegularExpression('#^UNCACHEABLE \(((no|poor) cacheability|(request|response) policy)\)$#', $response->getHeader('X-Drupal-Dynamic-Cache')[0]);
else {
$this->assertTrue($response->hasHeader('X-Drupal-Dynamic-Cache'));
$this->assertSame($expected_dynamic_page_cache_header_value === TRUE ? $this->generateDynamicPageCacheExpectedHeaderValue($expected_cache_contexts) : $expected_dynamic_page_cache_header_value, $response->getHeader('X-Drupal-Dynamic-Cache')[0]);
}
}
@ -806,6 +807,8 @@ abstract class ResourceTestBase extends BrowserTestBase {
/**
* Asserts that a resource error response has the given message.
*
* Cache max-age is not yet considered when expected header is calculated.
*
* @param int $expected_status_code
* The expected response status.
* @param string $expected_message
@ -825,16 +828,17 @@ abstract class ResourceTestBase extends BrowserTestBase {
* (optional) The expected cache contexts in the X-Drupal-Cache-Contexts
* response header, or FALSE if that header should be absent. Defaults to
* FALSE.
* @param string|false $expected_page_cache_header_value
* (optional) The expected X-Drupal-Cache response header value, or FALSE if
* that header should be absent. Possible strings: 'MISS', 'HIT'. Defaults
* to FALSE.
* @param string|false $expected_dynamic_page_cache_header_value
* (optional) The expected X-Drupal-Dynamic-Cache response header value, or
* FALSE if that header should be absent. Possible strings: 'MISS', 'HIT'.
* Defaults to FALSE.
* @param 'MISS','HIT','UNCACHEABLE (request policy)','UNCACHEABLE (response policy)'|NULL $expected_page_cache_header_value
* (optional) The expected X-Drupal-Cache response header value, or NULL
* in case of no opinion on that. For possible string values, see the
* parameter type hint. Defaults to NULL.
* @param 'HIT','MISS','UNCACHEABLE (poor cacheability)','UNCACHEABLE (no cacheability)','UNCACHEABLE (request policy)','UNCACHEABLE (response policy)'|bool $expected_dynamic_page_cache_header_value
* (optional) The expected X-Drupal-Dynamic-Cache response header value
* - for possible string values, see the parameter type hint - or TRUE when
* the value should be autogenerated from expected cache contexts, or FALSE
* if that header should be absent. Defaults to FALSE.
*/
protected function assertResourceErrorResponse($expected_status_code, $expected_message, $via_link, ResponseInterface $response, $pointer = FALSE, $expected_cache_tags = FALSE, $expected_cache_contexts = FALSE, $expected_page_cache_header_value = FALSE, $expected_dynamic_page_cache_header_value = FALSE) {
protected function assertResourceErrorResponse($expected_status_code, $expected_message, $via_link, ResponseInterface $response, $pointer = FALSE, $expected_cache_tags = FALSE, $expected_cache_contexts = FALSE, $expected_page_cache_header_value = NULL, $expected_dynamic_page_cache_header_value = FALSE) {
assert(is_null($via_link) || $via_link instanceof Url);
$expected_error = [];
if (!empty(Response::$statusTexts[$expected_status_code])) {
@ -929,13 +933,12 @@ abstract class ResourceTestBase extends BrowserTestBase {
// DX: 403 when unauthorized, or 200 if the 'view label' operation is
// supported by the entity type.
$response = $this->request('GET', $url, $request_options);
$dynamic_cache_label_only = NULL;
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 (poor cacheability)' : 'MISS';
$this->assertResourceErrorResponse(403, $message, $url, $response, '/data', $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache_header_value);
$this->assertResourceErrorResponse(403, $message, $url, $response, '/data', $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', TRUE);
$this->assertArrayNotHasKey('Link', $response->getHeaders());
}
else {
@ -943,9 +946,8 @@ abstract class ResourceTestBase extends BrowserTestBase {
$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 (poor cacheability)' : 'MISS';
$this->assertResourceResponse(200, $expected_document, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts([$label_field_name]), 'UNCACHEABLE (request policy)', $dynamic_cache_label_only);
$dynamic_cache_label_only = $this->generateDynamicPageCacheExpectedHeaderValue($this->getExpectedCacheContexts([$label_field_name]));
$this->assertResourceResponse(200, $expected_document, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts([$label_field_name]), 'UNCACHEABLE (request policy)', TRUE);
}
$this->setUpAuthorization('GET');
@ -971,20 +973,18 @@ abstract class ResourceTestBase extends BrowserTestBase {
],
],
];
$this->assertResourceResponse(400, $expected_document, $response, ['4xx-response', 'http_response'], ['url.query_args', 'url.site'], 'UNCACHEABLE (request policy)', 'MISS');
$this->assertResourceResponse(400, $expected_document, $response, ['4xx-response', 'http_response'], ['url.query_args', 'url.site'], 'UNCACHEABLE (request policy)', TRUE);
// 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 (poor cacheability)' : 'MISS';
$this->assertResourceResponse(200, NULL, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache);
$this->assertResourceResponse(200, NULL, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), 'UNCACHEABLE (request policy)', TRUE);
$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(), 'UNCACHEABLE (request policy)', $dynamic_cache === 'MISS' ? 'HIT' : 'UNCACHEABLE (poor cacheability)');
$this->assertResourceResponse(200, $this->getExpectedDocument(), $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), 'UNCACHEABLE (request policy)', $this->generateDynamicPageCacheExpectedHeaderValue($this->getExpectedCacheContexts()) === 'MISS' ? 'HIT' : '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.
@ -1011,7 +1011,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
$this->assertInstanceOf(CacheableResponseInterface::class, $cached_response);
}
}
$this->assertSame($dynamic_cache !== 'UNCACHEABLE (poor cacheability)' || isset($dynamic_cache_label_only) && $dynamic_cache_label_only !== 'UNCACHEABLE (poor cacheability)', $found_cached_200_response);
$this->assertSame($this->generateDynamicPageCacheExpectedHeaderValue($this->getExpectedCacheContexts()) !== 'UNCACHEABLE (poor cacheability)' || $dynamic_cache_label_only !== 'UNCACHEABLE (poor cacheability)', $found_cached_200_response);
$this->assertTrue($other_cached_responses_are_4xx);
// Not only assert the normalization, also assert deserialization of the
@ -1085,9 +1085,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
$expected_response = $this->getExpectedCollectionResponse($entity_collection, $collection_url->toString(), $request_options);
$expected_cacheability = $expected_response->getCacheableMetadata();
$response = $this->request('HEAD', $collection_url, $request_options);
// MISS or UNCACHEABLE depends on the collection data. It must not be HIT.
$dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS';
$this->assertResourceResponse(200, NULL, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache);
$this->assertResourceResponse(200, NULL, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $this->generateDynamicPageCacheExpectedHeaderValue($expected_cacheability->getCacheContexts(), $expected_cacheability->getCacheMaxAge()));
// Different databases have different sort orders, so a sort is required so
// test expectations do not need to vary per database.
@ -1099,21 +1097,17 @@ abstract class ResourceTestBase extends BrowserTestBase {
// self::getExpectedCollectionResponse().
$expected_response = $this->getExpectedCollectionResponse($entity_collection, $collection_url->toString(), $request_options);
$expected_cacheability = $expected_response->getCacheableMetadata();
// MISS or UNCACHEABLE depends on the collection data. It must not be HIT.
$dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS';
$expected_document = $expected_response->getResponseData();
$response = $this->request('GET', $collection_url, $request_options);
$this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache);
$this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $this->generateDynamicPageCacheExpectedHeaderValue($expected_cacheability->getCacheContexts(), $expected_cacheability->getCacheMaxAge()));
$this->setUpAuthorization('GET');
// 200 for well-formed HEAD request.
$expected_response = $this->getExpectedCollectionResponse($entity_collection, $collection_url->toString(), $request_options);
$expected_cacheability = $expected_response->getCacheableMetadata();
// MISS or UNCACHEABLE depends on the collection data. It must not be HIT.
$dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS';
$response = $this->request('HEAD', $collection_url, $request_options);
$this->assertResourceResponse(200, NULL, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache);
$this->assertResourceResponse(200, NULL, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $this->generateDynamicPageCacheExpectedHeaderValue($expected_cacheability->getCacheContexts(), $expected_cacheability->getCacheMaxAge()));
// 200 for well-formed GET request.
$expected_response = $this->getExpectedCollectionResponse($entity_collection, $collection_url->toString(), $request_options);
@ -1121,7 +1115,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
$expected_document = $expected_response->getResponseData();
$response = $this->request('GET', $collection_url, $request_options);
// Dynamic Page Cache HIT unless the HEAD request was UNCACHEABLE.
$dynamic_cache = $dynamic_cache === 'UNCACHEABLE (poor cacheability)' ? 'UNCACHEABLE (poor cacheability)' : 'HIT';
$dynamic_cache = $this->generateDynamicPageCacheExpectedHeaderValue($expected_cacheability->getCacheContexts(), $expected_cacheability->getCacheMaxAge()) === 'UNCACHEABLE (poor cacheability)' ? 'UNCACHEABLE (poor cacheability)' : 'HIT';
$this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache);
if ($this->entity instanceof FieldableEntityInterface) {
@ -1144,14 +1138,14 @@ abstract class ResourceTestBase extends BrowserTestBase {
'url.site',
'user.permissions',
];
$this->assertResourceErrorResponse(403, $expected_error_message, $unauthorized_filter_url, $response, FALSE, $expected_cache_tags, $expected_cache_contexts, 'UNCACHEABLE (request policy)', 'MISS');
$this->assertResourceErrorResponse(403, $expected_error_message, $unauthorized_filter_url, $response, FALSE, $expected_cache_tags, $expected_cache_contexts, 'UNCACHEABLE (request policy)', TRUE);
$this->grantPermissionsToTestedRole(['field_jsonapi_test_entity_ref view access']);
// 403 for filtering on an unauthorized field on a related resource type.
$response = $this->request('GET', $unauthorized_filter_url, $request_options);
$expected_error_message = "The current user is not authorized to filter by the `status` field, given in the path `field_jsonapi_test_entity_ref.entity:user.status`.";
$this->assertResourceErrorResponse(403, $expected_error_message, $unauthorized_filter_url, $response, FALSE, $expected_cache_tags, $expected_cache_contexts, 'UNCACHEABLE (request policy)', 'MISS');
$this->assertResourceErrorResponse(403, $expected_error_message, $unauthorized_filter_url, $response, FALSE, $expected_cache_tags, $expected_cache_contexts, 'UNCACHEABLE (request policy)', TRUE);
}
// Remove an entity from the collection, then filter it out.
@ -1174,9 +1168,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
$expected_cacheability = $expected_response->getCacheableMetadata();
$expected_document = $expected_response->getResponseData();
$response = $this->request('GET', $filtered_collection_url, $request_options);
// MISS or UNCACHEABLE depends on the collection data. It must not be HIT.
$dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS';
$this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache);
$this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $this->generateDynamicPageCacheExpectedHeaderValue($expected_cacheability->getCacheContexts(), $expected_cacheability->getCacheMaxAge()));
// Filtered collection with includes.
$relationship_field_names = array_reduce($filtered_entity_collection, function ($relationship_field_names, $entity) {
@ -1190,9 +1182,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
$expected_cacheability->setCacheTags(array_values(array_diff($expected_cacheability->getCacheTags(), ['4xx-response'])));
$expected_document = $expected_response->getResponseData();
$response = $this->request('GET', $filtered_collection_include_url, $request_options);
// MISS or UNCACHEABLE depends on the included data. It must not be HIT.
$dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS';
$this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache);
$this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $this->generateDynamicPageCacheExpectedHeaderValue($expected_cacheability->getCacheContexts(), $expected_cacheability->getCacheMaxAge()));
// If the response should vary by a user's authorizations, grant permissions
// for the included resources and execute another request.
@ -1210,8 +1200,8 @@ abstract class ResourceTestBase extends BrowserTestBase {
$expected_document = $expected_response->getResponseData();
$response = $this->request('GET', $filtered_collection_include_url, $request_options);
$requires_include_only_permissions = !empty($flattened_permissions);
$uncacheable = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts()));
$dynamic_cache = !$uncacheable ? $requires_include_only_permissions ? 'MISS' : 'HIT' : 'UNCACHEABLE (poor cacheability)';
$is_uncacheable = $this->generateDynamicPageCacheExpectedHeaderValue($expected_cacheability->getCacheContexts(), $expected_cacheability->getCacheMaxAge()) === 'UNCACHEABLE (poor cacheability)';
$dynamic_cache = !$is_uncacheable ? ($requires_include_only_permissions ? 'MISS' : 'HIT') : 'UNCACHEABLE (poor cacheability)';
$this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache);
}
@ -1228,9 +1218,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
$expected_cacheability->setCacheTags(array_values(array_diff($expected_cacheability->getCacheTags(), ['4xx-response'])));
$expected_document = $expected_response->getResponseData();
$response = $this->request('GET', $sorted_collection_include_url, $request_options);
// MISS or UNCACHEABLE depends on the included data. It must not be HIT.
$dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS';
$this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache);
$this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $this->generateDynamicPageCacheExpectedHeaderValue([], $expected_cacheability->getCacheMaxAge()));
}
/**
@ -1378,16 +1366,17 @@ abstract class ResourceTestBase extends BrowserTestBase {
// Dynamic Page Cache miss because cache should vary based on the
// 'include' query param.
$expected_cacheability = $expected_resource_response->getCacheableMetadata();
$expected_dynamic_page_cache_header_value = $this->generateDynamicPageCacheExpectedHeaderValue($expected_cacheability->getCacheContexts(), $expected_cacheability->getCacheMaxAge());
$this->assertResourceResponse(
$expected_resource_response->getStatusCode(),
$expected_resource_response->getResponseData(),
$actual_response,
$expected_cacheability->getCacheTags(),
$expected_cacheability->getCacheContexts(),
FALSE,
NULL,
$actual_response->getStatusCode() === 200
? ($expected_cacheability->getCacheMaxAge() === 0 ? 'UNCACHEABLE (poor cacheability)' : 'MISS')
: (!empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : FALSE)
? $expected_dynamic_page_cache_header_value
: ($expected_dynamic_page_cache_header_value === 'MISS' ? FALSE : $expected_dynamic_page_cache_header_value)
);
}
}
@ -1415,16 +1404,15 @@ abstract class ResourceTestBase extends BrowserTestBase {
$expected_document = $expected_resource_response->getResponseData();
$expected_cacheability = $expected_resource_response->getCacheableMetadata();
$actual_response = $related_responses[$relationship_field_name];
$expected_dynamic_page_cache_header_value = $this->generateDynamicPageCacheExpectedHeaderValue($expected_cacheability->getCacheContexts(), $expected_cacheability->getCacheMaxAge());
$this->assertResourceResponse(
$expected_resource_response->getStatusCode(),
$expected_document,
$actual_response,
$expected_cacheability->getCacheTags(),
$expected_cacheability->getCacheContexts(),
'UNCACHEABLE (request policy)',
empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts()))
? $expected_resource_response->isSuccessful() ? 'MISS' : FALSE
: 'UNCACHEABLE (poor cacheability)'
NULL,
$expected_dynamic_page_cache_header_value === 'MISS' && !$expected_resource_response->isSuccessful() ? FALSE : $expected_dynamic_page_cache_header_value
);
}
}
@ -2722,9 +2710,6 @@ abstract class ResourceTestBase extends BrowserTestBase {
$expected_document['links']['self']['href'] = $url->setAbsolute()->toString();
$response = $this->request('GET', $url, $request_options);
// Dynamic Page Cache MISS because cache should vary based on the 'field'
// query param. (Or uncacheable if expensive cache context.)
$dynamic_cache = !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS';
$this->assertResourceResponse(
200,
$expected_document,
@ -2732,13 +2717,13 @@ abstract class ResourceTestBase extends BrowserTestBase {
$expected_cacheability->getCacheTags(),
$expected_cacheability->getCacheContexts(),
'UNCACHEABLE (request policy)',
$dynamic_cache
TRUE,
);
}
// Test Dynamic Page Cache HIT for a query with the same field set (unless
// expensive cache context is present).
$response = $this->request('GET', $url, $request_options);
$this->assertResourceResponse(200, FALSE, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache === 'MISS' ? 'HIT' : 'UNCACHEABLE (poor cacheability)');
$this->assertResourceResponse(200, FALSE, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $this->generateDynamicPageCacheExpectedHeaderValue($expected_cacheability->getCacheContexts()) === 'MISS' ? 'HIT' : 'UNCACHEABLE (poor cacheability)');
}
/**
@ -2782,16 +2767,14 @@ abstract class ResourceTestBase extends BrowserTestBase {
// Dynamic Page Cache miss because cache should vary based on the
// 'include' query param.
$expected_cacheability = $expected_response->getCacheableMetadata();
// MISS or UNCACHEABLE depends on data. It must not be HIT.
$dynamic_cache = ($expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $this->getExpectedCacheContexts()))) ? FALSE : 'MISS';
$this->assertResourceResponse(
200,
$expected_document,
$actual_response,
$expected_cacheability->getCacheTags(),
$expected_cacheability->getCacheContexts(),
FALSE,
$dynamic_cache
NULL,
$this->generateDynamicPageCacheExpectedHeaderValue($this->getExpectedCacheContexts(), $expected_cacheability->getCacheMaxAge())
);
}
}
@ -2894,9 +2877,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
if ($result instanceof AccessResultReasonInterface && ($reason = $result->getReason()) && !empty($reason)) {
$detail .= ' ' . $reason;
}
// MISS or UNCACHEABLE depends on data. It must not be HIT.
$dynamic_cache = !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS';
$this->assertResourceErrorResponse(403, $detail, $url, $actual_response, '/data', $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache);
$this->assertResourceErrorResponse(403, $detail, $url, $actual_response, '/data', $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), NULL, TRUE);
// Ensure that targeting a revision does not bypass access.
$actual_response = $this->request('GET', $original_revision_id_url, $request_options);
@ -2905,9 +2886,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
if ($result instanceof AccessResultReasonInterface && ($reason = $result->getReason()) && !empty($reason)) {
$detail .= ' ' . $reason;
}
// MISS or UNCACHEABLE depends on data. It must not be HIT.
$dynamic_cache = !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS';
$this->assertResourceErrorResponse(403, $detail, $url, $actual_response, '/data', $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache);
$this->assertResourceErrorResponse(403, $detail, $url, $actual_response, '/data', $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), NULL, TRUE);
$this->setUpRevisionAuthorization('GET');
@ -2926,29 +2905,29 @@ abstract class ResourceTestBase extends BrowserTestBase {
$expected_document['data']['attributes']['field_revisionable_number'] = 99;
$expected_cache_tags = $this->getExpectedCacheTags();
$expected_cache_contexts = $this->getExpectedCacheContexts();
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, NULL, TRUE);
// Fetch the same revision using its revision ID.
$actual_response = $this->request('GET', $latest_revision_id_url, $request_options);
// The top-level document object's `self` link should always link to the
// request URL.
$expected_document['links']['self']['href'] = $latest_revision_id_url->setAbsolute()->toString();
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, NULL, TRUE);
// Ensure dynamic cache HIT on second request when using a version
// negotiator.
$actual_response = $this->request('GET', $latest_revision_id_url, $request_options);
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'HIT');
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, NULL, $this->generateDynamicPageCacheExpectedHeaderValue($expected_cache_contexts) === 'MISS' ? 'HIT' : 'UNCACHEABLE');
// Fetch the same revision using the `latest-version` link relation type
// negotiator. Without content_moderation, this is always the most recent
// revision.
$actual_response = $this->request('GET', $rel_latest_version_url, $request_options);
$expected_document['links']['self']['href'] = $rel_latest_version_url->setAbsolute()->toString();
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, NULL, TRUE);
// Fetch the same revision using the `working-copy` link relation type
// negotiator. Without content_moderation, this is always the most recent
// revision.
$actual_response = $this->request('GET', $rel_working_copy_url, $request_options);
$expected_document['links']['self']['href'] = $rel_working_copy_url->setAbsolute()->toString();
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, NULL, TRUE);
// Fetch the prior revision.
$actual_response = $this->request('GET', $original_revision_id_url, $request_options);
@ -2965,7 +2944,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
// object.
$expected_document['data']['links']['latest-version']['href'] = $rel_latest_version_url->setAbsolute()->toString();
$expected_document['data']['links']['working-copy']['href'] = $rel_working_copy_url->setAbsolute()->toString();
$this->assertResourceResponse(200, $expected_document, $actual_response, Cache::mergeTags($expected_cache_tags, $this->getExtraRevisionCacheTags()), $expected_cache_contexts, FALSE, 'MISS');
$this->assertResourceResponse(200, $expected_document, $actual_response, Cache::mergeTags($expected_cache_tags, $this->getExtraRevisionCacheTags()), $expected_cache_contexts, NULL, TRUE);
// Install content_moderation module.
$this->assertTrue($this->container->get('module_installer')->install(['content_moderation'], TRUE), 'Installed modules.');
@ -3013,23 +2992,23 @@ abstract class ResourceTestBase extends BrowserTestBase {
unset($expected_document['data']['links']['working-copy']);
$expected_document = $this->alterExpectedDocumentForRevision($expected_document);
$expected_cache_tags = array_unique([...$expected_cache_tags, ...$workflow->getCacheTags()]);
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, NULL, TRUE);
// Fetch the collection URL using the `latest-version` version argument.
$actual_response = $this->request('GET', $rel_latest_version_collection_url, $request_options);
$expected_response = $this->getExpectedCollectionResponse([$entity], $rel_latest_version_collection_url->toString(), $request_options);
$expected_collection_document = $expected_response->getResponseData();
$expected_cacheability = $expected_response->getCacheableMetadata();
$this->assertResourceResponse(200, $expected_collection_document, $actual_response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, 'MISS');
$this->assertResourceResponse(200, $expected_collection_document, $actual_response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), NULL, TRUE);
// Fetch the published revision by using the `working-copy` version
// argument. With content_moderation, this is always the most recent
// revision regardless of moderation state.
$actual_response = $this->request('GET', $rel_working_copy_url, $request_options);
$expected_document['links']['self']['href'] = $rel_working_copy_url->toString();
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, NULL, TRUE);
// Fetch the collection URL using the `working-copy` version argument.
$actual_response = $this->request('GET', $rel_working_copy_collection_url, $request_options);
$expected_collection_document['links']['self']['href'] = $rel_working_copy_collection_url->toString();
$this->assertResourceResponse(200, $expected_collection_document, $actual_response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, 'MISS');
$this->assertResourceResponse(200, $expected_collection_document, $actual_response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), NULL, TRUE);
// @todo Remove the next assertion when Drupal core supports entity query access control on revisions.
$rel_working_copy_collection_url_filtered = clone $rel_working_copy_collection_url;
$rel_working_copy_collection_url_filtered->setOption('query', ['filter[foo]' => 'bar'] + $rel_working_copy_collection_url->getOption('query'));
@ -3069,21 +3048,21 @@ abstract class ResourceTestBase extends BrowserTestBase {
// `working-copy` link is required to indicate that there is a forward
// revision available.
$expected_document['data']['links']['working-copy']['href'] = $rel_working_copy_url->setAbsolute()->toString();
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, NULL, TRUE);
// And the same should be true for collections.
$actual_response = $this->request('GET', $rel_latest_version_collection_url, $request_options);
$expected_collection_document['data'][0] = $expected_document['data'];
$expected_collection_document['links']['self']['href'] = $rel_latest_version_collection_url->toString();
$this->assertResourceResponse(200, $expected_collection_document, $actual_response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, 'MISS');
$this->assertResourceResponse(200, $expected_collection_document, $actual_response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), NULL, TRUE);
// Ensure that the `latest-version` response is same as the default link,
// aside from the document's `self` link.
$actual_response = $this->request('GET', $url, $request_options);
$expected_document['links']['self']['href'] = $url->toString();
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cache_contexts, NULL, TRUE);
// And the same should be true for collections.
$actual_response = $this->request('GET', $collection_url, $request_options);
$expected_collection_document['links']['self']['href'] = $collection_url->toString();
$this->assertResourceResponse(200, $expected_collection_document, $actual_response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, 'MISS');
$this->assertResourceResponse(200, $expected_collection_document, $actual_response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), NULL, TRUE);
// Now, the `working-copy` link should reference the draft revision. This
// is significant because without content_moderation, the two responses
// would still been the same.
@ -3099,7 +3078,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
$expected_cache_contexts = $expected_cacheability->getCacheContexts();
$detail = 'The current user is not allowed to GET the selected resource. The user does not have access to the requested version.';
$message = $result instanceof AccessResultReasonInterface ? trim($detail . ' ' . $result->getReason()) : $detail;
$this->assertResourceErrorResponse(403, $message, $url, $actual_response, '/data', $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');
$this->assertResourceErrorResponse(403, $message, $url, $actual_response, '/data', $expected_cache_tags, $expected_cache_contexts, NULL, TRUE);
// On the collection URL, we should expect to see the draft omitted from
// the collection.
$actual_response = $this->request('GET', $rel_working_copy_collection_url, $request_options);
@ -3109,7 +3088,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
$expected_cacheability = $expected_response->getCacheableMetadata();
$access_denied_response = static::getAccessDeniedResponse($entity, $result, $url, NULL, $detail)->getResponseData();
static::addOmittedObject($expected_collection_document, static::errorsToOmittedObject($access_denied_response['errors']));
$this->assertResourceResponse(200, $expected_collection_document, $actual_response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, 'MISS');
$this->assertResourceResponse(200, $expected_collection_document, $actual_response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), NULL, TRUE);
}
// Since additional permissions are required to see 'draft' entities,
@ -3137,14 +3116,14 @@ abstract class ResourceTestBase extends BrowserTestBase {
$expected_cache_tags = $this->getExpectedCacheTags();
$expected_cache_contexts = $this->getExpectedCacheContexts();
$expected_cache_tags = array_unique([...$expected_cache_tags, ...$workflow->getCacheTags()]);
$this->assertResourceResponse(200, $expected_document, $actual_response, Cache::mergeTags($expected_cache_tags, $this->getExtraRevisionCacheTags()), $expected_cache_contexts, FALSE, 'MISS');
$this->assertResourceResponse(200, $expected_document, $actual_response, Cache::mergeTags($expected_cache_tags, $this->getExtraRevisionCacheTags()), $expected_cache_contexts, NULL, TRUE);
// And the collection response should also have the latest revision.
$actual_response = $this->request('GET', $rel_working_copy_collection_url, $request_options);
$expected_response = static::getExpectedCollectionResponse([$entity], $rel_working_copy_collection_url->toString(), $request_options);
$expected_collection_document = $expected_response->getResponseData();
$expected_collection_document['data'] = [$expected_document['data']];
$expected_cacheability = $expected_response->getCacheableMetadata();
$this->assertResourceResponse(200, $expected_collection_document, $actual_response, Cache::mergeTags($expected_cacheability->getCacheTags(), $this->getExtraRevisionCacheTags()), $expected_cacheability->getCacheContexts(), FALSE, 'MISS');
$this->assertResourceResponse(200, $expected_collection_document, $actual_response, Cache::mergeTags($expected_cacheability->getCacheTags(), $this->getExtraRevisionCacheTags()), $expected_cacheability->getCacheContexts(), NULL, TRUE);
// Test relationship responses.
// Fetch the prior revision's relationship URL.
@ -3197,6 +3176,8 @@ abstract class ResourceTestBase extends BrowserTestBase {
$expected_document['errors'][0]['links']['via']['href'] = $relationship_url->toString();
// Only add node type check tags for non-default revisions.
$expected_cache_tags = !in_array($relationship_type, $default_revision_types, TRUE) ? Cache::mergeTags($expected_cacheability->getCacheTags(), $this->getExtraRevisionCacheTags()) : $expected_cacheability->getCacheTags();
// @todo Remove this in https://www.drupal.org/project/drupal/issues/3451483.
$actual_response = $actual_response->withoutHeader('X-Drupal-Dynamic-Cache');
$this->assertResourceResponse(403, $expected_document, $actual_response, $expected_cache_tags, $expected_cacheability->getCacheContexts());
// Request the related route.
$actual_response = $this->request('GET', $related_url, $request_options);
@ -3205,6 +3186,8 @@ abstract class ResourceTestBase extends BrowserTestBase {
$expected_document = $expected_response->getResponseData();
$expected_cacheability = $expected_response->getCacheableMetadata();
$expected_document['errors'][0]['links']['via']['href'] = $related_url->toString();
// @todo Remove this in https://www.drupal.org/project/drupal/issues/3451483.
$actual_response = $actual_response->withoutHeader('X-Drupal-Dynamic-Cache');
$this->assertResourceResponse(403, $expected_document, $actual_response, $expected_cache_tags, $expected_cacheability->getCacheContexts());
}
$this->grantPermissionsToTestedRole(['field_jsonapi_test_entity_ref view access']);
@ -3229,18 +3212,15 @@ abstract class ResourceTestBase extends BrowserTestBase {
$expected_cacheability = $expected_response->getCacheableMetadata();
// Only add node type check tags for non-default revisions.
$expected_cache_tags = !in_array($relationship_type, $default_revision_types, TRUE) ? Cache::mergeTags($expected_cacheability->getCacheTags(), $this->getExtraRevisionCacheTags()) : $expected_cacheability->getCacheTags();
$dynamic_cache = !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS';
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache);
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cacheability->getCacheContexts(), NULL, TRUE);
// Request the related route.
$actual_response = $this->request('GET', $related_url, $request_options);
$expected_response = $this->getExpectedRelatedResponse('field_jsonapi_test_entity_ref', $request_options, $revision);
$expected_document = $expected_response->getResponseData();
$expected_cacheability = $expected_response->getCacheableMetadata();
$expected_document['links']['self']['href'] = $related_url->toString();
// MISS or UNCACHEABLE depends on data. It must not be HIT.
$dynamic_cache = !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS';
$expected_cache_tags = !in_array($relationship_type, $default_revision_types, TRUE) ? Cache::mergeTags($expected_cacheability->getCacheTags(), $this->getExtraRevisionCacheTags()) : $expected_cacheability->getCacheTags();
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache);
$this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cacheability->getCacheContexts(), NULL, TRUE);
}
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
@ -3597,4 +3577,21 @@ abstract class ResourceTestBase extends BrowserTestBase {
assert([] === $user_role->getPermissions(), 'The authenticated user role has no permissions at all.');
}
/**
* Generates an X-Drupal-Dynamic-Cache header value based on cacheability.
*
* @param array $cache_context
* Cache context.
* @param int|null $cache_max_age
* (optional) Cache max age.
*
* @return 'UNCACHEABLE (poor cacheability)'|'MISS'
* The X-Drupal-Dynamic-Cache header value.
*/
protected function generateDynamicPageCacheExpectedHeaderValue(array $cache_context, ?int $cache_max_age = NULL): string {
// MISS or UNCACHEABLE (poor cacheability) depends on data.
// It must not be HIT.
return $cache_max_age === 0 || !empty(array_intersect(['user', 'session'], $cache_context)) ? 'UNCACHEABLE (poor cacheability)' : 'MISS';
}
}

View File

@ -473,7 +473,7 @@ class UserTest extends ResourceTestBase {
$response = $this->request('GET', $collection_url, $request_options);
$expected_cache_contexts = ['url.path', 'url.query_args', 'url.site'];
$this->assertResourceErrorResponse(400, "Filtering on config entities is not supported by Drupal's entity API. You tried to filter on a Role config entity.", $collection_url, $response, FALSE, ['4xx-response', 'http_response'], $expected_cache_contexts, FALSE, 'MISS');
$this->assertResourceErrorResponse(400, "Filtering on config entities is not supported by Drupal's entity API. You tried to filter on a Role config entity.", $collection_url, $response, FALSE, ['4xx-response', 'http_response'], $expected_cache_contexts, NULL, 'MISS');
}
/**

View File

@ -109,7 +109,7 @@ trait CommonCollectionFilterAccessTestPatternsTrait {
'url.site',
'user.permissions',
];
$this->assertResourceErrorResponse(403, $message, $collection_filter_url, $response, FALSE, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS');
$this->assertResourceErrorResponse(403, $message, $collection_filter_url, $response, FALSE, $expected_cache_tags, $expected_cache_contexts, NULL, 'MISS');
// And ensure the it is allowed when the proper permission is granted.
$this->grantPermissionsToTestedRole(['filter by spotlight field']);
$response = $this->request('GET', $collection_filter_url, $request_options);