comparison core/modules/jsonapi/tests/src/Functional/NodeTest.php @ 5:12f9dff5fda9 tip

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