annotate core/modules/jsonapi/tests/src/Functional/NodeTest.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@18 1 <?php
Chris@18 2
Chris@18 3 namespace Drupal\Tests\jsonapi\Functional;
Chris@18 4
Chris@18 5 use Drupal\Component\Serialization\Json;
Chris@18 6 use Drupal\Component\Utility\NestedArray;
Chris@18 7 use Drupal\Core\Cache\Cache;
Chris@18 8 use Drupal\Core\Url;
Chris@18 9 use Drupal\jsonapi\Normalizer\HttpExceptionNormalizer;
Chris@18 10 use Drupal\node\Entity\Node;
Chris@18 11 use Drupal\node\Entity\NodeType;
Chris@18 12 use Drupal\Tests\jsonapi\Traits\CommonCollectionFilterAccessTestPatternsTrait;
Chris@18 13 use Drupal\user\Entity\User;
Chris@18 14 use GuzzleHttp\RequestOptions;
Chris@18 15
Chris@18 16 /**
Chris@18 17 * JSON:API integration test for the "Node" content entity type.
Chris@18 18 *
Chris@18 19 * @group jsonapi
Chris@18 20 */
Chris@18 21 class NodeTest extends ResourceTestBase {
Chris@18 22
Chris@18 23 use CommonCollectionFilterAccessTestPatternsTrait;
Chris@18 24
Chris@18 25 /**
Chris@18 26 * {@inheritdoc}
Chris@18 27 */
Chris@18 28 public static $modules = ['node', 'path'];
Chris@18 29
Chris@18 30 /**
Chris@18 31 * {@inheritdoc}
Chris@18 32 */
Chris@18 33 protected static $entityTypeId = 'node';
Chris@18 34
Chris@18 35 /**
Chris@18 36 * {@inheritdoc}
Chris@18 37 */
Chris@18 38 protected static $resourceTypeName = 'node--camelids';
Chris@18 39
Chris@18 40 /**
Chris@18 41 * {@inheritdoc}
Chris@18 42 */
Chris@18 43 protected static $resourceTypeIsVersionable = TRUE;
Chris@18 44
Chris@18 45 /**
Chris@18 46 * {@inheritdoc}
Chris@18 47 */
Chris@18 48 protected static $newRevisionsShouldBeAutomatic = TRUE;
Chris@18 49
Chris@18 50 /**
Chris@18 51 * {@inheritdoc}
Chris@18 52 *
Chris@18 53 * @var \Drupal\node\NodeInterface
Chris@18 54 */
Chris@18 55 protected $entity;
Chris@18 56
Chris@18 57 /**
Chris@18 58 * {@inheritdoc}
Chris@18 59 */
Chris@18 60 protected static $patchProtectedFieldNames = [
Chris@18 61 'revision_timestamp' => NULL,
Chris@18 62 'created' => "The 'administer nodes' permission is required.",
Chris@18 63 'changed' => NULL,
Chris@18 64 'promote' => "The 'administer nodes' permission is required.",
Chris@18 65 'sticky' => "The 'administer nodes' permission is required.",
Chris@18 66 'path' => "The following permissions are required: 'create url aliases' OR 'administer url aliases'.",
Chris@18 67 'revision_uid' => NULL,
Chris@18 68 ];
Chris@18 69
Chris@18 70 /**
Chris@18 71 * {@inheritdoc}
Chris@18 72 */
Chris@18 73 protected function setUpAuthorization($method) {
Chris@18 74 switch ($method) {
Chris@18 75 case 'GET':
Chris@18 76 $this->grantPermissionsToTestedRole(['access content']);
Chris@18 77 break;
Chris@18 78
Chris@18 79 case 'POST':
Chris@18 80 $this->grantPermissionsToTestedRole(['access content', 'create camelids content']);
Chris@18 81 break;
Chris@18 82
Chris@18 83 case 'PATCH':
Chris@18 84 // Do not grant the 'create url aliases' permission to test the case
Chris@18 85 // when the path field is protected/not accessible, see
Chris@18 86 // \Drupal\Tests\rest\Functional\EntityResource\Term\TermResourceTestBase
Chris@18 87 // for a positive test.
Chris@18 88 $this->grantPermissionsToTestedRole(['access content', 'edit any camelids content']);
Chris@18 89 break;
Chris@18 90
Chris@18 91 case 'DELETE':
Chris@18 92 $this->grantPermissionsToTestedRole(['access content', 'delete any camelids content']);
Chris@18 93 break;
Chris@18 94 }
Chris@18 95 }
Chris@18 96
Chris@18 97 /**
Chris@18 98 * {@inheritdoc}
Chris@18 99 */
Chris@18 100 protected function setUpRevisionAuthorization($method) {
Chris@18 101 parent::setUpRevisionAuthorization($method);
Chris@18 102 $this->grantPermissionsToTestedRole(['view all revisions']);
Chris@18 103 }
Chris@18 104
Chris@18 105 /**
Chris@18 106 * {@inheritdoc}
Chris@18 107 */
Chris@18 108 protected function createEntity() {
Chris@18 109 if (!NodeType::load('camelids')) {
Chris@18 110 // Create a "Camelids" node type.
Chris@18 111 NodeType::create([
Chris@18 112 'name' => 'Camelids',
Chris@18 113 'type' => 'camelids',
Chris@18 114 ])->save();
Chris@18 115 }
Chris@18 116
Chris@18 117 // Create a "Llama" node.
Chris@18 118 $node = Node::create(['type' => 'camelids']);
Chris@18 119 $node->setTitle('Llama')
Chris@18 120 ->setOwnerId($this->account->id())
Chris@18 121 ->setPublished()
Chris@18 122 ->setCreatedTime(123456789)
Chris@18 123 ->setChangedTime(123456789)
Chris@18 124 ->setRevisionCreationTime(123456789)
Chris@18 125 ->set('path', '/llama')
Chris@18 126 ->save();
Chris@18 127
Chris@18 128 return $node;
Chris@18 129 }
Chris@18 130
Chris@18 131 /**
Chris@18 132 * {@inheritdoc}
Chris@18 133 */
Chris@18 134 protected function getExpectedDocument() {
Chris@18 135 $author = User::load($this->entity->getOwnerId());
Chris@18 136 $base_url = Url::fromUri('base:/jsonapi/node/camelids/' . $this->entity->uuid())->setAbsolute();
Chris@18 137 $self_url = clone $base_url;
Chris@18 138 $version_identifier = 'id:' . $this->entity->getRevisionId();
Chris@18 139 $self_url = $self_url->setOption('query', ['resourceVersion' => $version_identifier]);
Chris@18 140 $version_query_string = '?resourceVersion=' . urlencode($version_identifier);
Chris@18 141 return [
Chris@18 142 'jsonapi' => [
Chris@18 143 'meta' => [
Chris@18 144 'links' => [
Chris@18 145 'self' => ['href' => 'http://jsonapi.org/format/1.0/'],
Chris@18 146 ],
Chris@18 147 ],
Chris@18 148 'version' => '1.0',
Chris@18 149 ],
Chris@18 150 'links' => [
Chris@18 151 'self' => ['href' => $base_url->toString()],
Chris@18 152 ],
Chris@18 153 'data' => [
Chris@18 154 'id' => $this->entity->uuid(),
Chris@18 155 'type' => 'node--camelids',
Chris@18 156 'links' => [
Chris@18 157 'self' => ['href' => $self_url->toString()],
Chris@18 158 ],
Chris@18 159 'attributes' => [
Chris@18 160 'created' => '1973-11-29T21:33:09+00:00',
Chris@18 161 'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
Chris@18 162 'default_langcode' => TRUE,
Chris@18 163 'langcode' => 'en',
Chris@18 164 'path' => [
Chris@18 165 'alias' => '/llama',
Chris@18 166 'pid' => 1,
Chris@18 167 'langcode' => 'en',
Chris@18 168 ],
Chris@18 169 'promote' => TRUE,
Chris@18 170 'revision_log' => NULL,
Chris@18 171 'revision_timestamp' => '1973-11-29T21:33:09+00:00',
Chris@18 172 // @todo Attempt to remove this in https://www.drupal.org/project/drupal/issues/2933518.
Chris@18 173 'revision_translation_affected' => TRUE,
Chris@18 174 'status' => TRUE,
Chris@18 175 'sticky' => FALSE,
Chris@18 176 'title' => 'Llama',
Chris@18 177 'drupal_internal__nid' => 1,
Chris@18 178 'drupal_internal__vid' => 1,
Chris@18 179 ],
Chris@18 180 'relationships' => [
Chris@18 181 'node_type' => [
Chris@18 182 'data' => [
Chris@18 183 'id' => NodeType::load('camelids')->uuid(),
Chris@18 184 'type' => 'node_type--node_type',
Chris@18 185 ],
Chris@18 186 'links' => [
Chris@18 187 'related' => [
Chris@18 188 'href' => $base_url->toString() . '/node_type' . $version_query_string,
Chris@18 189 ],
Chris@18 190 'self' => [
Chris@18 191 'href' => $base_url->toString() . '/relationships/node_type' . $version_query_string,
Chris@18 192 ],
Chris@18 193 ],
Chris@18 194 ],
Chris@18 195 'uid' => [
Chris@18 196 'data' => [
Chris@18 197 'id' => $author->uuid(),
Chris@18 198 'type' => 'user--user',
Chris@18 199 ],
Chris@18 200 'links' => [
Chris@18 201 'related' => [
Chris@18 202 'href' => $base_url->toString() . '/uid' . $version_query_string,
Chris@18 203 ],
Chris@18 204 'self' => [
Chris@18 205 'href' => $base_url->toString() . '/relationships/uid' . $version_query_string,
Chris@18 206 ],
Chris@18 207 ],
Chris@18 208 ],
Chris@18 209 'revision_uid' => [
Chris@18 210 'data' => [
Chris@18 211 'id' => $author->uuid(),
Chris@18 212 'type' => 'user--user',
Chris@18 213 ],
Chris@18 214 'links' => [
Chris@18 215 'related' => [
Chris@18 216 'href' => $base_url->toString() . '/revision_uid' . $version_query_string,
Chris@18 217 ],
Chris@18 218 'self' => [
Chris@18 219 'href' => $base_url->toString() . '/relationships/revision_uid' . $version_query_string,
Chris@18 220 ],
Chris@18 221 ],
Chris@18 222 ],
Chris@18 223 ],
Chris@18 224 ],
Chris@18 225 ];
Chris@18 226 }
Chris@18 227
Chris@18 228 /**
Chris@18 229 * {@inheritdoc}
Chris@18 230 */
Chris@18 231 protected function getPostDocument() {
Chris@18 232 return [
Chris@18 233 'data' => [
Chris@18 234 'type' => 'node--camelids',
Chris@18 235 'attributes' => [
Chris@18 236 'title' => 'Dramallama',
Chris@18 237 ],
Chris@18 238 ],
Chris@18 239 ];
Chris@18 240 }
Chris@18 241
Chris@18 242 /**
Chris@18 243 * {@inheritdoc}
Chris@18 244 */
Chris@18 245 protected function getExpectedUnauthorizedAccessMessage($method) {
Chris@18 246 switch ($method) {
Chris@18 247 case 'GET':
Chris@18 248 case 'POST':
Chris@18 249 case 'PATCH':
Chris@18 250 case 'DELETE':
Chris@18 251 return "The 'access content' permission is required.";
Chris@18 252 }
Chris@18 253 }
Chris@18 254
Chris@18 255 /**
Chris@18 256 * Tests PATCHing a node's path with and without 'create url aliases'.
Chris@18 257 *
Chris@18 258 * For a positive test, see the similar test coverage for Term.
Chris@18 259 *
Chris@18 260 * @see \Drupal\Tests\jsonapi\Functional\TermTest::testPatchPath()
Chris@18 261 * @see \Drupal\Tests\rest\Functional\EntityResource\Term\TermResourceTestBase::testPatchPath()
Chris@18 262 */
Chris@18 263 public function testPatchPath() {
Chris@18 264 $this->setUpAuthorization('GET');
Chris@18 265 $this->setUpAuthorization('PATCH');
Chris@18 266 $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
Chris@18 267
Chris@18 268 // @todo Remove line below in favor of commented line in https://www.drupal.org/project/jsonapi/issues/2878463.
Chris@18 269 $url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), ['entity' => $this->entity->uuid()]);
Chris@18 270 /* $url = $this->entity->toUrl('jsonapi'); */
Chris@18 271
Chris@18 272 // GET node's current normalization.
Chris@18 273 $response = $this->request('GET', $url, $this->getAuthenticationRequestOptions());
Chris@18 274 $normalization = Json::decode((string) $response->getBody());
Chris@18 275
Chris@18 276 // Change node's path alias.
Chris@18 277 $normalization['data']['attributes']['path']['alias'] .= 's-rule-the-world';
Chris@18 278
Chris@18 279 // Create node PATCH request.
Chris@18 280 $request_options = $this->getAuthenticationRequestOptions();
Chris@18 281 $request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
Chris@18 282 $request_options[RequestOptions::BODY] = Json::encode($normalization);
Chris@18 283
Chris@18 284 // PATCH request: 403 when creating URL aliases unauthorized.
Chris@18 285 $response = $this->request('PATCH', $url, $request_options);
Chris@18 286 $this->assertResourceErrorResponse(403, "The current user is not allowed to PATCH the selected field (path). The following permissions are required: 'create url aliases' OR 'administer url aliases'.", $url, $response, '/data/attributes/path');
Chris@18 287
Chris@18 288 // Grant permission to create URL aliases.
Chris@18 289 $this->grantPermissionsToTestedRole(['create url aliases']);
Chris@18 290
Chris@18 291 // Repeat PATCH request: 200.
Chris@18 292 $response = $this->request('PATCH', $url, $request_options);
Chris@18 293 $this->assertResourceResponse(200, FALSE, $response);
Chris@18 294 $updated_normalization = Json::decode((string) $response->getBody());
Chris@18 295 $this->assertSame($normalization['data']['attributes']['path']['alias'], $updated_normalization['data']['attributes']['path']['alias']);
Chris@18 296 }
Chris@18 297
Chris@18 298 /**
Chris@18 299 * {@inheritdoc}
Chris@18 300 */
Chris@18 301 public function testGetIndividual() {
Chris@18 302 parent::testGetIndividual();
Chris@18 303
Chris@18 304 // Unpublish node.
Chris@18 305 $this->entity->setUnpublished()->save();
Chris@18 306
Chris@18 307 // @todo Remove line below in favor of commented line in https://www.drupal.org/project/jsonapi/issues/2878463.
Chris@18 308 $url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), ['entity' => $this->entity->uuid()]);
Chris@18 309 /* $url = $this->entity->toUrl('jsonapi'); */
Chris@18 310 $request_options = $this->getAuthenticationRequestOptions();
Chris@18 311
Chris@18 312 // 403 when accessing own unpublished node.
Chris@18 313 $response = $this->request('GET', $url, $request_options);
Chris@18 314 // @todo Remove $expected + assertResourceResponse() in favor of the commented line below once https://www.drupal.org/project/jsonapi/issues/2943176 lands.
Chris@18 315 $expected_document = [
Chris@18 316 'jsonapi' => static::$jsonApiMember,
Chris@18 317 'errors' => [
Chris@18 318 [
Chris@18 319 'title' => 'Forbidden',
Chris@18 320 'status' => '403',
Chris@18 321 'detail' => 'The current user is not allowed to GET the selected resource.',
Chris@18 322 'links' => [
Chris@18 323 'info' => ['href' => HttpExceptionNormalizer::getInfoUrl(403)],
Chris@18 324 'via' => ['href' => $url->setAbsolute()->toString()],
Chris@18 325 ],
Chris@18 326 'source' => [
Chris@18 327 'pointer' => '/data',
Chris@18 328 ],
Chris@18 329 ],
Chris@18 330 ],
Chris@18 331 ];
Chris@18 332 $this->assertResourceResponse(
Chris@18 333 403,
Chris@18 334 $expected_document,
Chris@18 335 $response,
Chris@18 336 ['4xx-response', 'http_response', 'node:1'],
Chris@18 337 ['url.query_args:resourceVersion', 'url.site', 'user.permissions'],
Chris@18 338 FALSE,
Chris@18 339 'MISS'
Chris@18 340 );
Chris@18 341 /* $this->assertResourceErrorResponse(403, 'The current user is not allowed to GET the selected resource.', $response, '/data'); */
Chris@18 342
Chris@18 343 // 200 after granting permission.
Chris@18 344 $this->grantPermissionsToTestedRole(['view own unpublished content']);
Chris@18 345 $response = $this->request('GET', $url, $request_options);
Chris@18 346 // The response varies by 'user', causing the 'user.permissions' cache
Chris@18 347 // context to be optimized away.
Chris@18 348 $expected_cache_contexts = Cache::mergeContexts($this->getExpectedCacheContexts(), ['user']);
Chris@18 349 $expected_cache_contexts = array_diff($expected_cache_contexts, ['user.permissions']);
Chris@18 350 $this->assertResourceResponse(200, FALSE, $response, $this->getExpectedCacheTags(), $expected_cache_contexts, FALSE, 'UNCACHEABLE');
Chris@18 351 }
Chris@18 352
Chris@18 353 /**
Chris@18 354 * {@inheritdoc}
Chris@18 355 */
Chris@18 356 protected static function getIncludePermissions() {
Chris@18 357 return [
Chris@18 358 'uid.node_type' => ['administer users'],
Chris@18 359 'uid.roles' => ['administer permissions'],
Chris@18 360 ];
Chris@18 361 }
Chris@18 362
Chris@18 363 /**
Chris@18 364 * Creating relationships to missing resources should be 404 per JSON:API 1.1.
Chris@18 365 *
Chris@18 366 * @see https://github.com/json-api/json-api/issues/1033
Chris@18 367 */
Chris@18 368 public function testPostNonExistingAuthor() {
Chris@18 369 $this->setUpAuthorization('POST');
Chris@18 370 $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
Chris@18 371 $this->grantPermissionsToTestedRole(['administer nodes']);
Chris@18 372
Chris@18 373 $random_uuid = \Drupal::service('uuid')->generate();
Chris@18 374 $doc = $this->getPostDocument();
Chris@18 375 $doc['data']['relationships']['uid']['data'] = [
Chris@18 376 'type' => 'user--user',
Chris@18 377 'id' => $random_uuid,
Chris@18 378 ];
Chris@18 379
Chris@18 380 // Create node POST request.
Chris@18 381 $url = Url::fromRoute(sprintf('jsonapi.%s.collection.post', static::$resourceTypeName));
Chris@18 382 $request_options = $this->getAuthenticationRequestOptions();
Chris@18 383 $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
Chris@18 384 $request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
Chris@18 385 $request_options[RequestOptions::BODY] = Json::encode($doc);
Chris@18 386
Chris@18 387 // POST request: 404 when adding relationships to non-existing resources.
Chris@18 388 $response = $this->request('POST', $url, $request_options);
Chris@18 389 $expected_document = [
Chris@18 390 'errors' => [
Chris@18 391 0 => [
Chris@18 392 'status' => '404',
Chris@18 393 'title' => 'Not Found',
Chris@18 394 'detail' => "The resource identified by `user--user:$random_uuid` (given as a relationship item) could not be found.",
Chris@18 395 'links' => [
Chris@18 396 'info' => ['href' => HttpExceptionNormalizer::getInfoUrl(404)],
Chris@18 397 'via' => ['href' => $url->setAbsolute()->toString()],
Chris@18 398 ],
Chris@18 399 ],
Chris@18 400 ],
Chris@18 401 'jsonapi' => static::$jsonApiMember,
Chris@18 402 ];
Chris@18 403 $this->assertResourceResponse(404, $expected_document, $response);
Chris@18 404 }
Chris@18 405
Chris@18 406 /**
Chris@18 407 * {@inheritdoc}
Chris@18 408 */
Chris@18 409 public function testCollectionFilterAccess() {
Chris@18 410 $label_field_name = 'title';
Chris@18 411 $this->doTestCollectionFilterAccessForPublishableEntities($label_field_name, 'access content', 'bypass node access');
Chris@18 412
Chris@18 413 $collection_url = Url::fromRoute('jsonapi.entity_test--bar.collection');
Chris@18 414 $collection_filter_url = $collection_url->setOption('query', ["filter[spotlight.$label_field_name]" => $this->entity->label()]);
Chris@18 415 $request_options = [];
Chris@18 416 $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
Chris@18 417 $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
Chris@18 418
Chris@18 419 $this->revokePermissionsFromTestedRole(['bypass node access']);
Chris@18 420
Chris@18 421 // 0 results because the node is unpublished.
Chris@18 422 $response = $this->request('GET', $collection_filter_url, $request_options);
Chris@18 423 $doc = Json::decode((string) $response->getBody());
Chris@18 424 $this->assertCount(0, $doc['data']);
Chris@18 425
Chris@18 426 $this->grantPermissionsToTestedRole(['view own unpublished content']);
Chris@18 427
Chris@18 428 // 1 result because the current user is the owner of the unpublished node.
Chris@18 429 $response = $this->request('GET', $collection_filter_url, $request_options);
Chris@18 430 $doc = Json::decode((string) $response->getBody());
Chris@18 431 $this->assertCount(1, $doc['data']);
Chris@18 432
Chris@18 433 $this->entity->setOwnerId(0)->save();
Chris@18 434
Chris@18 435 // 0 results because the current user is no longer the owner.
Chris@18 436 $response = $this->request('GET', $collection_filter_url, $request_options);
Chris@18 437 $doc = Json::decode((string) $response->getBody());
Chris@18 438 $this->assertCount(0, $doc['data']);
Chris@18 439
Chris@18 440 // Assert bubbling of cacheability from query alter hook.
Chris@18 441 $this->assertTrue($this->container->get('module_installer')->install(['node_access_test'], TRUE), 'Installed modules.');
Chris@18 442 node_access_rebuild();
Chris@18 443 $this->rebuildAll();
Chris@18 444 $response = $this->request('GET', $collection_filter_url, $request_options);
Chris@18 445 $this->assertTrue(in_array('user.node_grants:view', explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]), TRUE));
Chris@18 446 }
Chris@18 447
Chris@18 448 }