View source
<?php
namespace Drupal\Tests\rest\Functional\EntityResource;
use Drupal\Component\Assertion\Inspector;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Random;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\ContentEntityNullStorage;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\path\Plugin\Field\FieldType\PathItem;
use Drupal\rest\ResourceResponseInterface;
use Drupal\Tests\rest\Functional\ResourceTestBase;
use GuzzleHttp\RequestOptions;
use Psr\Http\Message\ResponseInterface;
abstract class EntityResourceTestBase extends ResourceTestBase {
protected static $entityTypeId = NULL;
protected static $patchProtectedFieldNames;
protected static $uniqueFieldNames = [];
protected static $labelFieldName = NULL;
protected static $firstCreatedEntityId = 2;
protected static $secondCreatedEntityId = 3;
protected $entity;
protected $anotherEntity;
protected $entityStorage;
protected static $modules = [
'rest_test',
'text',
];
protected function provisionEntityResource($single_format = FALSE) {
if ($existing = $this->resourceConfigStorage
->load(static::$resourceConfigId)) {
$existing
->delete();
}
$format = $single_format ? [
static::$format,
] : [
static::$format,
'foobar',
];
$auth = isset(static::$auth) ? [
static::$auth,
] : [];
$this
->provisionResource($format, $auth);
}
protected function setUp() : void {
parent::setUp();
static::$resourceConfigId = 'entity.' . static::$entityTypeId;
$this->entityStorage = $this->container
->get('entity_type.manager')
->getStorage(static::$entityTypeId);
$this->entity = $this
->createEntity();
if ($this->entity instanceof FieldableEntityInterface) {
FieldStorageConfig::create([
'entity_type' => static::$entityTypeId,
'field_name' => 'field_rest_test',
'type' => 'text',
])
->setCardinality(1)
->save();
FieldConfig::create([
'entity_type' => static::$entityTypeId,
'field_name' => 'field_rest_test',
'bundle' => $this->entity
->bundle(),
])
->setLabel('Test field')
->setTranslatable(FALSE)
->save();
FieldStorageConfig::create([
'entity_type' => static::$entityTypeId,
'field_name' => 'field_rest_test_multivalue',
'type' => 'string',
])
->setCardinality(3)
->save();
FieldConfig::create([
'entity_type' => static::$entityTypeId,
'field_name' => 'field_rest_test_multivalue',
'bundle' => $this->entity
->bundle(),
])
->setLabel('Test field: multi-value')
->setTranslatable(FALSE)
->save();
$reloaded_entity = $this->entityStorage
->loadUnchanged($this->entity
->id());
if ($reloaded_entity !== NULL) {
$this->entity = $reloaded_entity;
$this->entity
->set('field_rest_test', [
'value' => 'All the faith they had had had had no effect on the outcome of their life.',
]);
$this->entity
->set('field_rest_test_multivalue', [
[
'value' => 'One',
],
[
'value' => 'Two',
],
]);
$this->entity
->set('rest_test_validation', [
'value' => 'allowed value',
]);
$this->entity
->save();
}
}
}
protected abstract function createEntity();
protected function createAnotherEntity() {
$entity = $this->entity
->createDuplicate();
$label_key = $entity
->getEntityType()
->getKey('label');
if ($label_key) {
$entity
->set($label_key, $entity
->label() . '_dupe');
}
$entity
->save();
return $entity;
}
protected abstract function getExpectedNormalizedEntity();
protected abstract function getNormalizedPostEntity();
protected function getNormalizedPatchEntity() {
return $this
->getNormalizedPostEntity();
}
protected function getModifiedEntityForPostTesting() {
$normalized_entity = $this
->getNormalizedPostEntity();
foreach (static::$uniqueFieldNames as $field_name) {
$field_definition = $this->entity
->getFieldDefinition($field_name);
$field_type_class = $field_definition
->getItemDefinition()
->getClass();
$normalized_entity[$field_name] = $field_type_class::generateSampleValue($field_definition);
}
return $normalized_entity;
}
protected function getExpectedUnauthorizedAccessMessage($method) {
$permission = $this->entity
->getEntityType()
->getAdminPermission();
if ($permission !== FALSE) {
return "The '{$permission}' permission is required.";
}
$http_method_to_entity_operation = [
'GET' => 'view',
'POST' => 'create',
'PATCH' => 'update',
'DELETE' => 'delete',
];
$operation = $http_method_to_entity_operation[$method];
$message = sprintf('You are not authorized to %s this %s entity', $operation, $this->entity
->getEntityTypeId());
if ($this->entity
->bundle() !== $this->entity
->getEntityTypeId()) {
$message .= ' of bundle ' . $this->entity
->bundle();
}
return "{$message}.";
}
protected function getExpectedUnauthorizedAccessCacheability() {
return (new CacheableMetadata())
->setCacheTags(static::$auth ? [
'4xx-response',
'http_response',
] : [
'4xx-response',
'config:user.role.anonymous',
'http_response',
])
->setCacheContexts([
'user.permissions',
]);
}
protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
return new CacheableMetadata();
}
protected function getExpectedCacheTags() {
$expected_cache_tags = [
'config:rest.resource.entity.' . static::$entityTypeId,
];
if (!static::$auth) {
$expected_cache_tags[] = 'config:user.role.anonymous';
}
$expected_cache_tags[] = 'http_response';
return Cache::mergeTags($expected_cache_tags, $this->entity
->getCacheTags());
}
protected function getExpectedCacheContexts() {
return [
'url.site',
'user.permissions',
];
}
public function testGet() {
$this
->initAuthentication();
$has_canonical_url = $this->entity
->hasLinkTemplate('canonical');
$url = $this
->getEntityResourceUrl();
$request_options = [];
$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,
]);
$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();
if (static::$auth) {
$response = $this
->request('GET', $url, $request_options);
$this
->assertResponseWhenMissingAuthentication('GET', $response);
}
$request_options[RequestOptions::HEADERS]['REST-test-auth'] = '1';
$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';
$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'));
$this
->provisionEntityResource(TRUE);
$expected_403_cacheability = $this
->getExpectedUnauthorizedAccessCacheability()
->addCacheableDependency($this
->getExpectedUnauthorizedEntityAccessCacheability(static::$auth !== FALSE));
$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'));
$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);
$this
->provisionEntityResource();
$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'));
$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');
$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();
$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');
$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);
}
$expected = $this
->getExpectedNormalizedEntity();
static::recursiveKSort($expected);
$actual = $this->serializer
->decode((string) $response
->getBody(), static::$format);
static::recursiveKSort($actual);
$this
->assertEqualsCanonicalizing($expected, $actual);
if (static::$format !== 'xml') {
$unserialized = $this->serializer
->deserialize((string) $response
->getBody(), get_class($this->entity), static::$format);
$this
->assertSame($unserialized
->uuid(), $this->entity
->uuid());
}
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();
$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();
$this
->assertResourceNotAvailable($url, $request_options);
$this->resourceConfigStorage
->load(static::$resourceConfigId)
->enable()
->save();
$this
->refreshTestStateAfterRestConfigChange();
$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();
$this
->assertResourceNotAvailable($url, $request_options);
$this
->provisionEntityResource();
$url
->setOption('query', [
'_format' => 'non_existing_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;
$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,
]);
$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);
}
protected static function castToString(array $normalization) {
foreach ($normalization as $key => $value) {
if (is_bool($value)) {
$normalization[$key] = (string) (int) $value;
}
elseif (is_int($value) || is_float($value)) {
$normalization[$key] = (string) $value;
}
elseif (is_array($value)) {
$normalization[$key] = static::castToString($value);
}
}
return $normalization;
}
public function testPost() {
if ($this->entity instanceof ConfigEntityInterface) {
$this
->markTestSkipped('POSTing config entities is not yet supported.');
}
$this
->initAuthentication();
$has_canonical_url = $this->entity
->hasLinkTemplate('canonical');
$unparseable_request_body = '!{>}<';
$parseable_valid_request_body = $this->serializer
->encode($this
->getNormalizedPostEntity(), static::$format);
$parseable_invalid_request_body = $this->serializer
->encode($this
->makeNormalizationInvalid($this
->getNormalizedPostEntity(), 'label'), static::$format);
$parseable_invalid_request_body_2 = $this->serializer
->encode($this
->getNormalizedPostEntity() + [
'uuid' => [
$this
->randomMachineName(129),
],
], static::$format);
$parseable_invalid_request_body_3 = $this->serializer
->encode($this
->getNormalizedPostEntity() + [
'field_rest_test' => [
[
'value' => $this
->randomString(),
],
],
], static::$format);
$url = $this
->getEntityResourcePostUrl();
$request_options = [];
$response = $this
->request('POST', $url, $request_options);
$this
->assertSame(404, $response
->getStatusCode());
$this
->assertSame([
'text/html; charset=UTF-8',
], $response
->getHeader('Content-Type'));
$url
->setOption('query', [
'_format' => static::$format,
]);
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(404, 'No route found for "POST ' . $this
->getEntityResourcePostUrl()
->setAbsolute()
->toString() . '"', $response);
$this
->provisionEntityResource();
$url
->setOption('query', []);
$response = $this
->request('POST', $url, $request_options);
$this
->assertSame(415, $response
->getStatusCode());
$this
->assertSame([
'text/html; charset=UTF-8',
], $response
->getHeader('Content-Type'));
$this
->assertStringContainsString('A client error happened', (string) $response
->getBody());
$url
->setOption('query', [
'_format' => static::$format,
]);
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(415, 'No "Content-Type" request header specified', $response);
$request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
if (static::$auth) {
$response = $this
->request('POST', $url, $request_options);
$this
->assertResponseWhenMissingAuthentication('POST', $response);
}
$request_options = NestedArray::mergeDeep($request_options, $this
->getAuthenticationRequestOptions('POST'));
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(403, $this
->getExpectedUnauthorizedAccessMessage('POST'), $response);
$this
->setUpAuthorization('POST');
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(400, 'No entity content received.', $response);
$request_options[RequestOptions::BODY] = $unparseable_request_body;
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(400, 'Syntax error', $response);
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body;
$response = $this
->request('POST', $url, $request_options);
if ($label_field = $this->entity
->getEntityType()
->hasKey('label') ? $this->entity
->getEntityType()
->getKey('label') : static::$labelFieldName) {
$label_field_capitalized = $this->entity
->getFieldDefinition($label_field)
->getLabel();
$this
->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n{$label_field}: {$label_field_capitalized}: this field cannot hold more than 1 values.\n", $response);
}
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2;
if ($this->entity
->getEntityType()
->hasKey('uuid')) {
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n", $response);
}
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3;
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(403, "Access denied on creating field 'field_rest_test'.", $response);
$request_options[RequestOptions::BODY] = $parseable_valid_request_body;
$this
->assertNormalizationEdgeCases('POST', $url, $request_options);
$this
->assertAuthenticationEdgeCases('POST', $url, $request_options);
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'text/xml';
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(415, 'No route found that matches "Content-Type: text/xml"', $response);
$request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceResponse(201, FALSE, $response);
if ($has_canonical_url) {
$location = $this->entityStorage
->load(static::$firstCreatedEntityId)
->toUrl('canonical')
->setAbsolute(TRUE)
->toString();
$this
->assertSame([
$location,
], $response
->getHeader('Location'));
}
else {
$this
->assertSame([], $response
->getHeader('Location'));
}
$this
->assertFalse($response
->hasHeader('X-Drupal-Cache'));
if (get_class($this->entityStorage) !== ContentEntityNullStorage::class) {
$created_entity = $this->entityStorage
->loadUnchanged(static::$firstCreatedEntityId);
$created_entity_normalization = $this->serializer
->normalize($created_entity, static::$format, [
'account' => $this->account,
]);
$this
->assertSame($created_entity_normalization, $this->serializer
->decode((string) $response
->getBody(), static::$format));
$this
->assertStoredEntityMatchesSentNormalization($this
->getNormalizedPostEntity(), $created_entity);
}
if ($this->entity
->getEntityType()
->getStorageClass() !== ContentEntityNullStorage::class && $this->entity
->getEntityType()
->hasKey('uuid')) {
$normalized_entity = $this
->getModifiedEntityForPostTesting();
$normalized_entity[$created_entity
->getEntityType()
->getKey('uuid')] = [
[
'value' => $created_entity
->uuid(),
],
];
if ($label_field) {
$normalized_entity[$label_field] = [
[
'value' => $this
->randomMachineName(),
],
];
}
$request_options[RequestOptions::BODY] = $this->serializer
->encode($normalized_entity, static::$format);
$response = $this
->request('POST', $url, $request_options);
$this
->assertSame(500, $response
->getStatusCode());
$this
->assertStringContainsString('Internal Server Error', (string) $response
->getBody());
$normalized_entity = $this
->getModifiedEntityForPostTesting();
$new_uuid = \Drupal::service('uuid')
->generate();
$normalized_entity[$created_entity
->getEntityType()
->getKey('uuid')] = [
[
'value' => $new_uuid,
],
];
if ($label_field) {
$normalized_entity[$label_field] = [
[
'value' => $this
->randomMachineName(),
],
];
}
$request_options[RequestOptions::BODY] = $this->serializer
->encode($normalized_entity, static::$format);
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceResponse(201, FALSE, $response);
$entities = $this->entityStorage
->loadByProperties([
$created_entity
->getEntityType()
->getKey('uuid') => $new_uuid,
]);
$new_entity = reset($entities);
$this
->assertNotNull($new_entity);
$new_entity
->delete();
}
}
public function testPatch() {
if ($this->entity instanceof ConfigEntityInterface) {
$this
->markTestSkipped('PATCHing config entities is not yet supported.');
}
$this->anotherEntity = $this
->createAnotherEntity();
$this
->initAuthentication();
$has_canonical_url = $this->entity
->hasLinkTemplate('canonical');
$unparseable_request_body = '!{>}<';
$parseable_valid_request_body = $this->serializer
->encode($this
->getNormalizedPatchEntity(), static::$format);
$parseable_invalid_request_body = $this->serializer
->encode($this
->makeNormalizationInvalid($this
->getNormalizedPatchEntity(), 'label'), static::$format);
$parseable_invalid_request_body_2 = $this->serializer
->encode($this
->getNormalizedPatchEntity() + [
'field_rest_test' => [
[
'value' => $this
->randomString(),
],
],
], static::$format);
$parseable_invalid_request_body_3 = $this->serializer
->encode($this
->getNormalizedPatchEntity() + [
'field_rest_test' => $this->entity
->get('field_rest_test')
->getValue(),
], static::$format);
$url = $this
->getEntityResourceUrl();
$request_options = [];
$response = $this
->request('PATCH', $url, $request_options);
if ($has_canonical_url) {
$this
->assertSame(405, $response
->getStatusCode());
$this
->assertSame([
'GET, POST, HEAD',
], $response
->getHeader('Allow'));
$this
->assertSame([
'text/html; charset=UTF-8',
], $response
->getHeader('Content-Type'));
$this
->assertStringContainsString('A client error happened', (string) $response
->getBody());
}
else {
$this
->assertSame(404, $response
->getStatusCode());
$this
->assertSame([
'text/html; charset=UTF-8',
], $response
->getHeader('Content-Type'));
}
$url
->setOption('query', [
'_format' => static::$format,
]);
$response = $this
->request('PATCH', $url, $request_options);
if ($has_canonical_url) {
$this
->assertResourceErrorResponse(405, 'No route found for "PATCH ' . $this
->getEntityResourceUrl()
->setAbsolute()
->toString() . '": Method Not Allowed (Allow: GET, POST, HEAD)', $response);
}
else {
$this
->assertResourceErrorResponse(404, 'No route found for "PATCH ' . $this
->getEntityResourceUrl()
->setAbsolute()
->toString() . '"', $response);
}
$this
->provisionEntityResource();
$url
->setOption('query', []);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertSame(415, $response
->getStatusCode());
$this
->assertSame([
'text/html; charset=UTF-8',
], $response
->getHeader('Content-Type'));
$this
->assertStringContainsString('A client error happened', (string) $response
->getBody());
$url
->setOption('query', [
'_format' => static::$format,
]);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(415, 'No "Content-Type" request header specified', $response);
$request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
if (static::$auth) {
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResponseWhenMissingAuthentication('PATCH', $response);
}
$request_options = NestedArray::mergeDeep($request_options, $this
->getAuthenticationRequestOptions('PATCH'));
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(403, $this
->getExpectedUnauthorizedAccessMessage('PATCH'), $response);
$this
->setUpAuthorization('PATCH');
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(400, 'No entity content received.', $response);
$request_options[RequestOptions::BODY] = $unparseable_request_body;
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(400, 'Syntax error', $response);
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body;
$response = $this
->request('PATCH', $url, $request_options);
if ($label_field = $this->entity
->getEntityType()
->hasKey('label') ? $this->entity
->getEntityType()
->getKey('label') : static::$labelFieldName) {
$label_field_capitalized = $this->entity
->getFieldDefinition($label_field)
->getLabel();
$this
->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n{$label_field}: {$label_field_capitalized}: this field cannot hold more than 1 values.\n", $response);
}
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2;
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(403, "Access denied on updating field 'field_rest_test'.", $response);
$request_options[RequestOptions::BODY] = $this->serializer
->encode($this
->makeNormalizationInvalid($this
->getNormalizedPatchEntity(), 'id'), static::$format);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(403, "Access denied on updating field '{$this->entity->getEntityType()->getKey('id')}'. The entity ID cannot be changed.", $response);
if ($this->entity
->getEntityType()
->hasKey('uuid')) {
$request_options[RequestOptions::BODY] = $this->serializer
->encode($this
->makeNormalizationInvalid($this
->getNormalizedPatchEntity(), 'uuid'), static::$format);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(403, "Access denied on updating field '{$this->entity->getEntityType()->getKey('uuid')}'. The entity UUID cannot be changed.", $response);
}
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3;
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(403, "Access denied on updating field 'field_rest_test'.", $response);
$this
->assertPatchProtectedFieldNamesStructure();
[
$modified_entity,
$original_values,
] = static::getModifiedEntityForPatchTesting($this->entity);
foreach (static::$patchProtectedFieldNames as $patch_protected_field_name => $reason) {
$request_options[RequestOptions::BODY] = $this->serializer
->serialize($modified_entity, static::$format);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(403, "Access denied on updating field '" . $patch_protected_field_name . "'." . ($reason !== NULL ? ' ' . $reason : ''), $response);
$modified_entity
->get($patch_protected_field_name)
->setValue($original_values[$patch_protected_field_name]);
}
if ($this->entity instanceof FieldableEntityInterface) {
$override = [
'rest_test_validation' => [
[
'value' => 'ALWAYS_FAIL',
],
],
];
$valid_request_body = $override + $this
->getNormalizedPatchEntity() + $this->serializer
->normalize($modified_entity, static::$format);
$request_options[RequestOptions::BODY] = $this->serializer
->serialize($valid_request_body, static::$format);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nrest_test_validation: REST test validation failed\n", $response);
$this->entity
->set('rest_test_validation', 'ALWAYS_FAIL');
$this->entity
->save();
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nrest_test_validation: REST test validation failed\n", $response);
}
$valid_request_body = $this
->getNormalizedPatchEntity() + $this->serializer
->normalize($this->entity, static::$format);
$request_options[RequestOptions::BODY] = $this->serializer
->serialize($valid_request_body, static::$format);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceResponse(200, FALSE, $response);
$request_options[RequestOptions::BODY] = $parseable_valid_request_body;
$this
->assertNormalizationEdgeCases('PATCH', $url, $request_options);
$this
->assertAuthenticationEdgeCases('PATCH', $url, $request_options);
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'text/xml';
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(415, 'No route found that matches "Content-Type: text/xml"', $response);
$request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceResponse(200, FALSE, $response);
$this
->assertFalse($response
->hasHeader('X-Drupal-Cache'));
$updated_entity = $this->entityStorage
->loadUnchanged($this->entity
->id());
$updated_entity_normalization = $this->serializer
->normalize($updated_entity, static::$format, [
'account' => $this->account,
]);
$this
->assertSame($updated_entity_normalization, $this->serializer
->decode((string) $response
->getBody(), static::$format));
$this
->assertStoredEntityMatchesSentNormalization($this
->getNormalizedPatchEntity(), $updated_entity);
$this
->assertSame('All the faith they had had had had no effect on the outcome of their life.', $updated_entity
->get('field_rest_test')->value);
$normalization_multi_value_tests = $this
->getNormalizedPatchEntity();
$normalization_multi_value_tests['field_rest_test_multivalue'] = $this->entity
->get('field_rest_test_multivalue')
->getValue();
$normalization_remove_item = $normalization_multi_value_tests;
unset($normalization_remove_item['field_rest_test_multivalue'][0]);
$request_options[RequestOptions::BODY] = $this->serializer
->encode($normalization_remove_item, static::$format);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceResponse(200, FALSE, $response);
$this
->assertSame([
0 => [
'value' => 'Two',
],
], $this->entityStorage
->loadUnchanged($this->entity
->id())
->get('field_rest_test_multivalue')
->getValue());
$normalization_add_items = $normalization_multi_value_tests;
$normalization_add_items['field_rest_test_multivalue'][2] = [
'value' => 'Three',
];
$request_options[RequestOptions::BODY] = $this->serializer
->encode($normalization_add_items, static::$format);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceResponse(200, FALSE, $response);
$this
->assertSame([
0 => [
'value' => 'One',
],
1 => [
'value' => 'Two',
],
2 => [
'value' => 'Three',
],
], $this->entityStorage
->loadUnchanged($this->entity
->id())
->get('field_rest_test_multivalue')
->getValue());
}
public function testDelete() {
if ($this->entity instanceof ConfigEntityInterface) {
$this
->markTestSkipped('DELETEing config entities is not yet supported.');
}
$this
->initAuthentication();
$has_canonical_url = $this->entity
->hasLinkTemplate('canonical');
$url = $this
->getEntityResourceUrl();
$request_options = [];
$response = $this
->request('DELETE', $url, $request_options);
if ($has_canonical_url) {
$this
->assertSame(405, $response
->getStatusCode());
$this
->assertSame([
'GET, POST, HEAD',
], $response
->getHeader('Allow'));
$this
->assertSame([
'text/html; charset=UTF-8',
], $response
->getHeader('Content-Type'));
$this
->assertStringContainsString('A client error happened', (string) $response
->getBody());
}
else {
$this
->assertSame(404, $response
->getStatusCode());
$this
->assertSame([
'text/html; charset=UTF-8',
], $response
->getHeader('Content-Type'));
}
$url
->setOption('query', [
'_format' => static::$format,
]);
$response = $this
->request('DELETE', $url, $request_options);
if ($has_canonical_url) {
$this
->assertSame([
'GET, POST, HEAD',
], $response
->getHeader('Allow'));
$this
->assertResourceErrorResponse(405, 'No route found for "DELETE ' . $this
->getEntityResourceUrl()
->setAbsolute()
->toString() . '": Method Not Allowed (Allow: GET, POST, HEAD)', $response);
}
else {
$this
->assertResourceErrorResponse(404, 'No route found for "DELETE ' . $this
->getEntityResourceUrl()
->setAbsolute()
->toString() . '"', $response);
}
$this
->provisionEntityResource();
if (static::$auth) {
$response = $this
->request('DELETE', $url, $request_options);
$this
->assertResponseWhenMissingAuthentication('DELETE', $response);
}
$request_options = NestedArray::mergeDeep($request_options, $this
->getAuthenticationRequestOptions('PATCH'));
$response = $this
->request('DELETE', $url, $request_options);
$this
->assertResourceErrorResponse(403, $this
->getExpectedUnauthorizedAccessMessage('DELETE'), $response);
$this
->setUpAuthorization('DELETE');
$this
->assertAuthenticationEdgeCases('DELETE', $url, $request_options);
$response = $this
->request('DELETE', $url, $request_options);
$this
->assertResourceResponse(204, '', $response);
}
protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) {
$entity_type = $this->entity
->getEntityType();
if ($entity_type
->hasKey('bundle')) {
$bundle_field_name = $this->entity
->getEntityType()
->getKey('bundle');
$normalization = $this
->getNormalizedPostEntity();
if ($entity_type
->getBundleEntityType()) {
$normalization[$bundle_field_name] = 'bad_bundle_name';
$request_options[RequestOptions::BODY] = $this->serializer
->encode($normalization, static::$format);
$response = $this
->request($method, $url, $request_options);
$this
->assertResourceErrorResponse(422, '"bad_bundle_name" is not a valid bundle type for denormalization.', $response);
}
unset($normalization[$bundle_field_name]);
$request_options[RequestOptions::BODY] = $this->serializer
->encode($normalization, static::$format);
$response = $this
->request($method, $url, $request_options);
$this
->assertResourceErrorResponse(422, sprintf('Could not determine entity type bundle: "%s" field is missing.', $bundle_field_name), $response);
}
}
protected function assertPatchProtectedFieldNamesStructure() {
$is_null_or_string = function ($value) {
return is_null($value) || is_string($value);
};
$this
->assertTrue(Inspector::assertAllStrings(array_keys(static::$patchProtectedFieldNames)), 'In Drupal 8.6, the structure of $patchProtectedFieldNames changed. It used to be an array with field names as values. Now those values are the keys, and their values should be either NULL or a string: a string containing the reason for why the field cannot be PATCHed, or NULL otherwise.');
$this
->assertTrue(Inspector::assertAll($is_null_or_string, static::$patchProtectedFieldNames), 'In Drupal 8.6, the structure of $patchProtectedFieldNames changed. It used to be an array with field names as values. Now those values are the keys, and their values should be either NULL or a string: a string containing the reason for why the field cannot be PATCHed, or NULL otherwise.');
}
protected function getEntityResourceUrl() {
$has_canonical_url = $this->entity
->hasLinkTemplate('canonical');
return $has_canonical_url ? $this->entity
->toUrl('canonical') : Url::fromUri('base:entity/' . static::$entityTypeId . '/' . $this->entity
->id());
}
protected function getEntityResourcePostUrl() {
$has_create_url = $this->entity
->hasLinkTemplate('create');
return $has_create_url ? Url::fromUri('internal:' . $this->entity
->getEntityType()
->getLinkTemplate('create')) : Url::fromUri('base:entity/' . static::$entityTypeId);
}
protected static function getModifiedEntityForPatchTesting(EntityInterface $entity) {
$modified_entity = clone $entity;
$original_values = [];
foreach (array_keys(static::$patchProtectedFieldNames) as $field_name) {
$field = $modified_entity
->get($field_name);
$original_values[$field_name] = $field
->getValue();
switch ($field
->getItemDefinition()
->getClass()) {
case EntityReferenceItem::class:
$field
->setValue([
'target_id' => 99999,
]);
break;
case BooleanItem::class:
$field->value = (int) $field->value === 1 ? '0' : '1';
break;
case PathItem::class:
$field->alias = str_replace(' ', '-', strtolower((new Random())
->sentences(3)));
break;
default:
$original_field = clone $field;
while ($field
->equals($original_field)) {
$field
->generateSampleItems();
}
break;
}
}
return [
$modified_entity,
$original_values,
];
}
protected function makeNormalizationInvalid(array $normalization, $entity_key) {
$entity_type = $this->entity
->getEntityType();
switch ($entity_key) {
case 'label':
if ($label_field = $entity_type
->hasKey('label') ? $entity_type
->getKey('label') : static::$labelFieldName) {
$normalization[$label_field][1]['value'] = 'Second Title';
}
break;
case 'id':
$normalization[$entity_type
->getKey('id')][0]['value'] = $this->anotherEntity
->id();
break;
case 'uuid':
$normalization[$entity_type
->getKey('uuid')][0]['value'] = $this->anotherEntity
->uuid();
break;
}
return $normalization;
}
protected function assert406Response(ResponseInterface $response) {
if ($this->entity
->hasLinkTemplate('canonical') && ($this->account && static::$auth !== 'cookie')) {
$this
->assertSame(403, $response
->getStatusCode());
}
else {
$this
->assertSame(406, $response
->getStatusCode());
$actual_link_header = $response
->getHeader('Link');
if ($actual_link_header) {
$this
->assertIsArray($actual_link_header);
$expected_type = explode(';', static::$mimeType)[0];
$this
->assertStringContainsString('?_format=' . static::$format . '>; rel="alternate"; type="' . $expected_type . '"', $actual_link_header[0]);
$this
->assertStringContainsString('?_format=foobar>; rel="alternate"', $actual_link_header[0]);
}
}
}
protected function assertResourceNotAvailable(Url $url, array $request_options) {
$has_canonical_url = $this->entity
->hasLinkTemplate('canonical');
$response = $this
->request('GET', $url, $request_options);
if (!$has_canonical_url) {
$this
->assertSame(404, $response
->getStatusCode());
}
else {
$this
->assert406Response($response);
}
}
protected function assertStoredEntityMatchesSentNormalization(array $sent_normalization, FieldableEntityInterface $modified_entity) {
foreach ($sent_normalization as $field_name => $field_normalization) {
if ($modified_entity
->hasField($field_name)) {
$field_definition = $modified_entity
->get($field_name)
->getFieldDefinition();
$property_definitions = $field_definition
->getItemDefinition()
->getPropertyDefinitions();
$expected_stored_data = [];
if (empty($property_definitions)) {
$expected_stored_data = $field_normalization;
}
else {
$denormalization_context = [
'field_definition' => $field_definition,
];
foreach ($field_normalization as $delta => $expected_field_item_normalization) {
foreach ($property_definitions as $property_name => $property_definition) {
if (!array_key_exists($property_name, $field_normalization[$delta])) {
continue;
}
if ($property_definition
->isComputed()) {
continue;
}
$property_value = $field_normalization[$delta][$property_name];
$property_value_class = $property_definitions[$property_name]
->getClass();
$expected_stored_data[$delta][$property_name] = $this->serializer
->supportsDenormalization($property_value, $property_value_class, NULL, $denormalization_context) ? $this->serializer
->denormalize($property_value, $property_value_class, NULL, $denormalization_context) : $property_value;
}
}
$expected_stored_data = static::castToString($expected_stored_data);
}
$this
->assertEntityArraySubset($expected_stored_data, $modified_entity
->get($field_name)
->getValue());
}
}
}
protected function assertEntityArraySubset($expected, $actual) {
foreach ($expected as $key => $value) {
if (is_array($value)) {
$this
->assertEntityArraySubset($value, $actual[$key]);
}
else {
$this
->assertSame($value, $actual[$key]);
}
}
}
}