comparison core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTest.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\Core\Url;
7 use Drupal\jsonapi\Query\OffsetPage;
8 use Drupal\node\Entity\Node;
9
10 /**
11 * General functional test class.
12 *
13 * @group jsonapi
14 * @group legacy
15 *
16 * @internal
17 */
18 class JsonApiFunctionalTest extends JsonApiFunctionalTestBase {
19
20 /**
21 * {@inheritdoc}
22 */
23 public static $modules = [
24 'basic_auth',
25 ];
26
27 /**
28 * Test the GET method.
29 */
30 public function testRead() {
31 $this->createDefaultContent(61, 5, TRUE, TRUE, static::IS_NOT_MULTILINGUAL, FALSE);
32 // Unpublish the last entity, so we can check access.
33 $this->nodes[60]->setUnpublished()->save();
34
35 // Different databases have different sort orders, so a sort is required so
36 // test expectations do not need to vary per database.
37 $default_sort = ['sort' => 'drupal_internal__nid'];
38
39 // 0. HEAD request allows a client to verify that JSON:API is installed.
40 $this->httpClient->request('HEAD', $this->buildUrl('/jsonapi/node/article'));
41 $this->assertSession()->statusCodeEquals(200);
42 // 1. Load all articles (1st page).
43 $collection_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
44 'query' => $default_sort,
45 ]));
46 $this->assertSession()->statusCodeEquals(200);
47 $this->assertEquals(OffsetPage::SIZE_MAX, count($collection_output['data']));
48 $this->assertSession()
49 ->responseHeaderEquals('Content-Type', 'application/vnd.api+json');
50 // 2. Load all articles (Offset 3).
51 $collection_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
52 'query' => ['page' => ['offset' => 3]] + $default_sort,
53 ]));
54 $this->assertSession()->statusCodeEquals(200);
55 $this->assertEquals(OffsetPage::SIZE_MAX, count($collection_output['data']));
56 $this->assertContains('page%5Boffset%5D=53', $collection_output['links']['next']['href']);
57 // 3. Load all articles (1st page, 2 items)
58 $collection_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
59 'query' => ['page' => ['limit' => 2]] + $default_sort,
60 ]));
61 $this->assertSession()->statusCodeEquals(200);
62 $this->assertEquals(2, count($collection_output['data']));
63 // 4. Load all articles (2nd page, 2 items).
64 $collection_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
65 'query' => [
66 'page' => [
67 'limit' => 2,
68 'offset' => 2,
69 ],
70 ] + $default_sort,
71 ]));
72 $this->assertSession()->statusCodeEquals(200);
73 $this->assertEquals(2, count($collection_output['data']));
74 $this->assertContains('page%5Boffset%5D=4', $collection_output['links']['next']['href']);
75 // 5. Single article.
76 $uuid = $this->nodes[0]->uuid();
77 $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid));
78 $this->assertSession()->statusCodeEquals(200);
79 $this->assertArrayHasKey('type', $single_output['data']);
80 $this->assertEquals($this->nodes[0]->getTitle(), $single_output['data']['attributes']['title']);
81
82 // 5.1 Single article with access denied because unauthenticated.
83 Json::decode($this->drupalGet('/jsonapi/node/article/' . $this->nodes[60]->uuid()));
84 $this->assertSession()->statusCodeEquals(401);
85
86 // 5.1 Single article with access denied while authenticated.
87 $this->drupalLogin($this->userCanViewProfiles);
88 $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $this->nodes[60]->uuid()));
89 $this->assertSession()->statusCodeEquals(403);
90 $this->assertEquals('/data', $single_output['errors'][0]['source']['pointer']);
91 $this->drupalLogout();
92
93 // 6. Single relationship item.
94 $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid . '/relationships/node_type'));
95 $this->assertSession()->statusCodeEquals(200);
96 $this->assertArrayHasKey('type', $single_output['data']);
97 $this->assertArrayNotHasKey('attributes', $single_output['data']);
98 $this->assertArrayHasKey('related', $single_output['links']);
99 // 7. Single relationship image.
100 $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid . '/relationships/field_image'));
101 $this->assertSession()->statusCodeEquals(200);
102 $this->assertArrayHasKey('type', $single_output['data']);
103 $this->assertArrayNotHasKey('attributes', $single_output['data']);
104 $this->assertArrayHasKey('related', $single_output['links']);
105 // 8. Multiple relationship item.
106 $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid . '/relationships/field_tags'));
107 $this->assertSession()->statusCodeEquals(200);
108 $this->assertArrayHasKey('type', $single_output['data'][0]);
109 $this->assertArrayNotHasKey('attributes', $single_output['data'][0]);
110 $this->assertArrayHasKey('related', $single_output['links']);
111 // 8b. Single related item, empty.
112 $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid . '/field_heroless'));
113 $this->assertSession()->statusCodeEquals(200);
114 $this->assertSame(NULL, $single_output['data']);
115 // 9. Related tags with includes.
116 $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid . '/field_tags', [
117 'query' => ['include' => 'vid'],
118 ]));
119 $this->assertSession()->statusCodeEquals(200);
120 $this->assertEquals('taxonomy_term--tags', $single_output['data'][0]['type']);
121 $this->assertArrayNotHasKey('tid', $single_output['data'][0]['attributes']);
122 $this->assertContains(
123 '/taxonomy_term/tags/',
124 $single_output['data'][0]['links']['self']['href']
125 );
126 $this->assertEquals(
127 'taxonomy_vocabulary--taxonomy_vocabulary',
128 $single_output['included'][0]['type']
129 );
130 // 10. Single article with includes.
131 $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid, [
132 'query' => ['include' => 'uid,field_tags'],
133 ]));
134 $this->assertSession()->statusCodeEquals(200);
135 $this->assertEquals('node--article', $single_output['data']['type']);
136 $first_include = reset($single_output['included']);
137 $this->assertEquals(
138 'user--user',
139 $first_include['type']
140 );
141 $last_include = end($single_output['included']);
142 $this->assertEquals(
143 'taxonomy_term--tags',
144 $last_include['type']
145 );
146
147 // 10b. Single article with nested includes.
148 $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid, [
149 'query' => ['include' => 'field_tags,field_tags.vid'],
150 ]));
151 $this->assertSession()->statusCodeEquals(200);
152 $this->assertEquals('node--article', $single_output['data']['type']);
153 $first_include = reset($single_output['included']);
154 $this->assertEquals(
155 'taxonomy_term--tags',
156 $first_include['type']
157 );
158 $last_include = end($single_output['included']);
159 $this->assertEquals(
160 'taxonomy_vocabulary--taxonomy_vocabulary',
161 $last_include['type']
162 );
163
164 // 11. Includes with relationships.
165 $this->drupalGet('/jsonapi/node/article/' . $uuid . '/relationships/uid');
166 $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid . '/relationships/uid', [
167 'query' => ['include' => 'uid'],
168 ]));
169 $this->assertSession()->statusCodeEquals(200);
170 $this->assertEquals('user--user', $single_output['data']['type']);
171 $this->assertArrayHasKey('related', $single_output['links']);
172 $this->assertArrayHasKey('included', $single_output);
173 $first_include = reset($single_output['included']);
174 $this->assertEquals(
175 'user--user',
176 $first_include['type']
177 );
178 $this->assertFalse(empty($first_include['attributes']));
179 $this->assertTrue(empty($first_include['attributes']['mail']));
180 $this->assertTrue(empty($first_include['attributes']['pass']));
181 // 12. Collection with one access denied.
182 $this->nodes[1]->set('status', FALSE);
183 $this->nodes[1]->save();
184 $single_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
185 'query' => ['page' => ['limit' => 2]] + $default_sort,
186 ]));
187 $this->assertSession()->statusCodeEquals(200);
188 $this->assertEquals(1, count($single_output['data']));
189 $this->assertEquals(1, count(array_filter(array_keys($single_output['meta']['omitted']['links']), function ($key) {
190 return $key !== 'help';
191 })));
192 $link_keys = array_keys($single_output['meta']['omitted']['links']);
193 $this->assertSame('help', reset($link_keys));
194 $this->assertRegExp('/^item:[a-zA-Z0-9]{7}$/', next($link_keys));
195 $this->nodes[1]->set('status', TRUE);
196 $this->nodes[1]->save();
197 // 13. Test filtering when using short syntax.
198 $filter = [
199 'uid.id' => ['value' => $this->user->uuid()],
200 'field_tags.id' => ['value' => $this->tags[0]->uuid()],
201 ];
202 $single_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
203 'query' => ['filter' => $filter, 'include' => 'uid,field_tags'],
204 ]));
205 $this->assertSession()->statusCodeEquals(200);
206 $this->assertGreaterThan(0, count($single_output['data']));
207 // 14. Test filtering when using long syntax.
208 $filter = [
209 'and_group' => ['group' => ['conjunction' => 'AND']],
210 'filter_user' => [
211 'condition' => [
212 'path' => 'uid.id',
213 'value' => $this->user->uuid(),
214 'memberOf' => 'and_group',
215 ],
216 ],
217 'filter_tags' => [
218 'condition' => [
219 'path' => 'field_tags.id',
220 'value' => $this->tags[0]->uuid(),
221 'memberOf' => 'and_group',
222 ],
223 ],
224 ];
225 $single_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
226 'query' => ['filter' => $filter, 'include' => 'uid,field_tags'],
227 ]));
228 $this->assertSession()->statusCodeEquals(200);
229 $this->assertGreaterThan(0, count($single_output['data']));
230 // 15. Test filtering when using invalid syntax.
231 $filter = [
232 'and_group' => ['group' => ['conjunction' => 'AND']],
233 'filter_user' => [
234 'condition' => [
235 'name-with-a-typo' => 'uid.id',
236 'value' => $this->user->uuid(),
237 'memberOf' => 'and_group',
238 ],
239 ],
240 ];
241 $this->drupalGet('/jsonapi/node/article', [
242 'query' => ['filter' => $filter] + $default_sort,
243 ]);
244 $this->assertSession()->statusCodeEquals(400);
245 // 16. Test filtering on the same field.
246 $filter = [
247 'or_group' => ['group' => ['conjunction' => 'OR']],
248 'filter_tags_1' => [
249 'condition' => [
250 'path' => 'field_tags.id',
251 'value' => $this->tags[0]->uuid(),
252 'memberOf' => 'or_group',
253 ],
254 ],
255 'filter_tags_2' => [
256 'condition' => [
257 'path' => 'field_tags.id',
258 'value' => $this->tags[1]->uuid(),
259 'memberOf' => 'or_group',
260 ],
261 ],
262 ];
263 $single_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
264 'query' => ['filter' => $filter, 'include' => 'field_tags'] + $default_sort,
265 ]));
266 $this->assertSession()->statusCodeEquals(200);
267 $this->assertGreaterThanOrEqual(2, count($single_output['included']));
268 // 17. Single user (check fields lacking 'view' access).
269 $user_url = Url::fromRoute('jsonapi.user--user.individual', [
270 'entity' => $this->user->uuid(),
271 ]);
272 $response = $this->request('GET', $user_url, [
273 'auth' => [
274 $this->userCanViewProfiles->getUsername(),
275 $this->userCanViewProfiles->pass_raw,
276 ],
277 ]);
278 $single_output = Json::decode($response->getBody()->__toString());
279 $this->assertEquals(200, $response->getStatusCode());
280 $this->assertEquals('user--user', $single_output['data']['type']);
281 $this->assertEquals($this->user->get('name')->value, $single_output['data']['attributes']['name']);
282 $this->assertTrue(empty($single_output['data']['attributes']['mail']));
283 $this->assertTrue(empty($single_output['data']['attributes']['pass']));
284 // 18. Test filtering on the column of a link.
285 $filter = [
286 'linkUri' => [
287 'condition' => [
288 'path' => 'field_link.uri',
289 'value' => 'https://',
290 'operator' => 'STARTS_WITH',
291 ],
292 ],
293 ];
294 $single_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
295 'query' => ['filter' => $filter] + $default_sort,
296 ]));
297 $this->assertSession()->statusCodeEquals(200);
298 $this->assertGreaterThanOrEqual(1, count($single_output['data']));
299 // 19. Test non-existing route without 'Accept' header.
300 $this->drupalGet('/jsonapi/node/article/broccoli');
301 $this->assertSession()->statusCodeEquals(404);
302 // Even without the 'Accept' header the 404 error is formatted as JSON:API.
303 $this->assertSession()->responseHeaderEquals('Content-Type', 'application/vnd.api+json');
304 // 20. Test non-existing route with 'Accept' header.
305 $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/broccoli', [], [
306 'Accept' => 'application/vnd.api+json',
307 ]));
308 $this->assertEquals(404, $single_output['errors'][0]['status']);
309 $this->assertSession()->statusCodeEquals(404);
310 // With the 'Accept' header we can know we want the 404 error formatted as
311 // JSON:API.
312 $this->assertSession()->responseHeaderContains('Content-Type', 'application/vnd.api+json');
313 // 22. Test sort criteria on multiple fields: both ASC.
314 $output = Json::decode($this->drupalGet('/jsonapi/node/article', [
315 'query' => [
316 'page[limit]' => 6,
317 'sort' => 'field_sort1,field_sort2',
318 ],
319 ]));
320 $output_uuids = array_map(function ($result) {
321 return $result['id'];
322 }, $output['data']);
323 $this->assertCount(6, $output_uuids);
324 $this->assertSame([
325 Node::load(5)->uuid(),
326 Node::load(4)->uuid(),
327 Node::load(3)->uuid(),
328 Node::load(2)->uuid(),
329 Node::load(1)->uuid(),
330 Node::load(10)->uuid(),
331 ], $output_uuids);
332 // 23. Test sort criteria on multiple fields: first ASC, second DESC.
333 $output = Json::decode($this->drupalGet('/jsonapi/node/article', [
334 'query' => [
335 'page[limit]' => 6,
336 'sort' => 'field_sort1,-field_sort2',
337 ],
338 ]));
339 $output_uuids = array_map(function ($result) {
340 return $result['id'];
341 }, $output['data']);
342 $this->assertCount(6, $output_uuids);
343 $this->assertSame([
344 Node::load(1)->uuid(),
345 Node::load(2)->uuid(),
346 Node::load(3)->uuid(),
347 Node::load(4)->uuid(),
348 Node::load(5)->uuid(),
349 Node::load(6)->uuid(),
350 ], $output_uuids);
351 // 24. Test sort criteria on multiple fields: first DESC, second ASC.
352 $output = Json::decode($this->drupalGet('/jsonapi/node/article', [
353 'query' => [
354 'page[limit]' => 6,
355 'sort' => '-field_sort1,field_sort2',
356 ],
357 ]));
358 $output_uuids = array_map(function ($result) {
359 return $result['id'];
360 }, $output['data']);
361 $this->assertCount(5, $output_uuids);
362 $this->assertCount(2, $output['meta']['omitted']['links']);
363 $this->assertSame([
364 Node::load(60)->uuid(),
365 Node::load(59)->uuid(),
366 Node::load(58)->uuid(),
367 Node::load(57)->uuid(),
368 Node::load(56)->uuid(),
369 ], $output_uuids);
370 // 25. Test sort criteria on multiple fields: both DESC.
371 $output = Json::decode($this->drupalGet('/jsonapi/node/article', [
372 'query' => [
373 'page[limit]' => 6,
374 'sort' => '-field_sort1,-field_sort2',
375 ],
376 ]));
377 $output_uuids = array_map(function ($result) {
378 return $result['id'];
379 }, $output['data']);
380 $this->assertCount(5, $output_uuids);
381 $this->assertCount(2, $output['meta']['omitted']['links']);
382 $this->assertSame([
383 Node::load(56)->uuid(),
384 Node::load(57)->uuid(),
385 Node::load(58)->uuid(),
386 Node::load(59)->uuid(),
387 Node::load(60)->uuid(),
388 ], $output_uuids);
389 // 25. Test collection count.
390 $this->container->get('module_installer')->install(['jsonapi_test_collection_count']);
391 $collection_output = Json::decode($this->drupalGet('/jsonapi/node/article'));
392 $this->assertSession()->statusCodeEquals(200);
393 $this->assertEquals(61, $collection_output['meta']['count']);
394 $this->container->get('module_installer')->uninstall(['jsonapi_test_collection_count']);
395
396 // Test documentation filtering examples.
397 // 1. Only get published nodes.
398 $filter = [
399 'status-filter' => [
400 'condition' => [
401 'path' => 'status',
402 'value' => 1,
403 ],
404 ],
405 ];
406 $collection_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
407 'query' => ['filter' => $filter] + $default_sort,
408 ]));
409 $this->assertSession()->statusCodeEquals(200);
410 $this->assertGreaterThanOrEqual(OffsetPage::SIZE_MAX, count($collection_output['data']));
411 // 2. Nested Filters: Get nodes created by user admin.
412 $filter = [
413 'name-filter' => [
414 'condition' => [
415 'path' => 'uid.name',
416 'value' => $this->user->getAccountName(),
417 ],
418 ],
419 ];
420 $collection_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
421 'query' => ['filter' => $filter] + $default_sort,
422 ]));
423 $this->assertSession()->statusCodeEquals(200);
424 $this->assertGreaterThanOrEqual(OffsetPage::SIZE_MAX, count($collection_output['data']));
425 // 3. Filtering with arrays: Get nodes created by users [admin, john].
426 $filter = [
427 'name-filter' => [
428 'condition' => [
429 'path' => 'uid.name',
430 'operator' => 'IN',
431 'value' => [
432 $this->user->getAccountName(),
433 $this->getRandomGenerator()->name(),
434 ],
435 ],
436 ],
437 ];
438 $collection_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
439 'query' => ['filter' => $filter] + $default_sort,
440 ]));
441 $this->assertSession()->statusCodeEquals(200);
442 $this->assertGreaterThanOrEqual(OffsetPage::SIZE_MAX, count($collection_output['data']));
443 // 4. Grouping filters: Get nodes that are published and create by admin.
444 $filter = [
445 'and-group' => [
446 'group' => [
447 'conjunction' => 'AND',
448 ],
449 ],
450 'name-filter' => [
451 'condition' => [
452 'path' => 'uid.name',
453 'value' => $this->user->getAccountName(),
454 'memberOf' => 'and-group',
455 ],
456 ],
457 'status-filter' => [
458 'condition' => [
459 'path' => 'status',
460 'value' => 1,
461 'memberOf' => 'and-group',
462 ],
463 ],
464 ];
465 $collection_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
466 'query' => ['filter' => $filter] + $default_sort,
467 ]));
468 $this->assertSession()->statusCodeEquals(200);
469 $this->assertGreaterThanOrEqual(OffsetPage::SIZE_MAX, count($collection_output['data']));
470 // 5. Grouping grouped filters: Get nodes that are promoted or sticky and
471 // created by admin.
472 $filter = [
473 'and-group' => [
474 'group' => [
475 'conjunction' => 'AND',
476 ],
477 ],
478 'or-group' => [
479 'group' => [
480 'conjunction' => 'OR',
481 'memberOf' => 'and-group',
482 ],
483 ],
484 'admin-filter' => [
485 'condition' => [
486 'path' => 'uid.name',
487 'value' => $this->user->getAccountName(),
488 'memberOf' => 'and-group',
489 ],
490 ],
491 'sticky-filter' => [
492 'condition' => [
493 'path' => 'sticky',
494 'value' => 1,
495 'memberOf' => 'or-group',
496 ],
497 ],
498 'promote-filter' => [
499 'condition' => [
500 'path' => 'promote',
501 'value' => 0,
502 'memberOf' => 'or-group',
503 ],
504 ],
505 ];
506 $collection_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
507 'query' => ['filter' => $filter] + $default_sort,
508 ]));
509 $this->assertSession()->statusCodeEquals(200);
510 $this->assertEquals(0, count($collection_output['data']));
511 }
512
513 /**
514 * Test the GET method on articles referencing the same tag twice.
515 */
516 public function testReferencingTwiceRead() {
517 $this->createDefaultContent(1, 1, FALSE, FALSE, static::IS_NOT_MULTILINGUAL, TRUE);
518
519 // 1. Load all articles (1st page).
520 $collection_output = Json::decode($this->drupalGet('/jsonapi/node/article'));
521 $this->assertSession()->statusCodeEquals(200);
522 $this->assertEquals(1, count($collection_output['data']));
523 $this->assertSession()
524 ->responseHeaderEquals('Content-Type', 'application/vnd.api+json');
525 }
526
527 /**
528 * Test POST, PATCH and DELETE.
529 */
530 public function testWrite() {
531 $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
532
533 $this->createDefaultContent(0, 3, FALSE, FALSE, static::IS_NOT_MULTILINGUAL, FALSE);
534 // 1. Successful post.
535 $collection_url = Url::fromRoute('jsonapi.node--article.collection.post');
536 $body = [
537 'data' => [
538 'type' => 'node--article',
539 'attributes' => [
540 'langcode' => 'en',
541 'title' => 'My custom title',
542 'default_langcode' => '1',
543 'body' => [
544 'value' => 'Custom value',
545 'format' => 'plain_text',
546 'summary' => 'Custom summary',
547 ],
548 ],
549 'relationships' => [
550 'field_tags' => [
551 'data' => [
552 [
553 'type' => 'taxonomy_term--tags',
554 'id' => $this->tags[0]->uuid(),
555 ],
556 [
557 'type' => 'taxonomy_term--tags',
558 'id' => $this->tags[1]->uuid(),
559 ],
560 ],
561 ],
562 ],
563 ],
564 ];
565 $response = $this->request('POST', $collection_url, [
566 'body' => Json::encode($body),
567 'auth' => [$this->user->getUsername(), $this->user->pass_raw],
568 'headers' => ['Content-Type' => 'application/vnd.api+json'],
569 ]);
570 $created_response = Json::decode($response->getBody()->__toString());
571 $this->assertEquals(201, $response->getStatusCode());
572 $this->assertArrayNotHasKey('uuid', $created_response['data']['attributes']);
573 $uuid = $created_response['data']['id'];
574 $this->assertEquals(2, count($created_response['data']['relationships']['field_tags']['data']));
575 $this->assertEquals($created_response['data']['links']['self']['href'], $response->getHeader('Location')[0]);
576
577 // 2. Authorization error.
578 $response = $this->request('POST', $collection_url, [
579 'body' => Json::encode($body),
580 'headers' => ['Content-Type' => 'application/vnd.api+json'],
581 ]);
582 $created_response = Json::decode($response->getBody()->__toString());
583 $this->assertEquals(401, $response->getStatusCode());
584 $this->assertNotEmpty($created_response['errors']);
585 $this->assertEquals('Unauthorized', $created_response['errors'][0]['title']);
586
587 // 2.1 Authorization error with a user without create permissions.
588 $response = $this->request('POST', $collection_url, [
589 'body' => Json::encode($body),
590 'auth' => [$this->userCanViewProfiles->getUsername(), $this->userCanViewProfiles->pass_raw],
591 'headers' => ['Content-Type' => 'application/vnd.api+json'],
592 ]);
593 $created_response = Json::decode($response->getBody()->__toString());
594 $this->assertEquals(403, $response->getStatusCode());
595 $this->assertNotEmpty($created_response['errors']);
596 $this->assertEquals('Forbidden', $created_response['errors'][0]['title']);
597
598 // 3. Missing Content-Type error.
599 $response = $this->request('POST', $collection_url, [
600 'body' => Json::encode($body),
601 'auth' => [$this->user->getUsername(), $this->user->pass_raw],
602 'headers' => ['Accept' => 'application/vnd.api+json'],
603 ]);
604 $created_response = Json::decode($response->getBody()->__toString());
605 $this->assertEquals(415, $response->getStatusCode());
606
607 // 4. Article with a duplicate ID.
608 $invalid_body = $body;
609 $invalid_body['data']['id'] = Node::load(1)->uuid();
610 $response = $this->request('POST', $collection_url, [
611 'body' => Json::encode($invalid_body),
612 'auth' => [$this->user->getUsername(), $this->user->pass_raw],
613 'headers' => [
614 'Accept' => 'application/vnd.api+json',
615 'Content-Type' => 'application/vnd.api+json',
616 ],
617 ]);
618 $created_response = Json::decode($response->getBody()->__toString());
619 $this->assertEquals(409, $response->getStatusCode());
620 $this->assertNotEmpty($created_response['errors']);
621 $this->assertEquals('Conflict', $created_response['errors'][0]['title']);
622 // 5. Article with wrong reference UUIDs for tags.
623 $body_invalid_tags = $body;
624 $body_invalid_tags['data']['relationships']['field_tags']['data'][0]['id'] = 'lorem';
625 $body_invalid_tags['data']['relationships']['field_tags']['data'][1]['id'] = 'ipsum';
626 $response = $this->request('POST', $collection_url, [
627 'body' => Json::encode($body_invalid_tags),
628 'auth' => [$this->user->getUsername(), $this->user->pass_raw],
629 'headers' => ['Content-Type' => 'application/vnd.api+json'],
630 ]);
631 $created_response = Json::decode($response->getBody()->__toString());
632 $this->assertEquals(404, $response->getStatusCode());
633 // 6. Decoding error.
634 $response = $this->request('POST', $collection_url, [
635 'body' => '{"bad json",,,}',
636 'auth' => [$this->user->getUsername(), $this->user->pass_raw],
637 'headers' => [
638 'Content-Type' => 'application/vnd.api+json',
639 'Accept' => 'application/vnd.api+json',
640 ],
641 ]);
642 $created_response = Json::decode($response->getBody()->__toString());
643 $this->assertEquals(400, $response->getStatusCode());
644 $this->assertNotEmpty($created_response['errors']);
645 $this->assertEquals('Bad Request', $created_response['errors'][0]['title']);
646 // 6.1 Denormalizing error.
647 $response = $this->request('POST', $collection_url, [
648 'body' => '{"data":{"type":"something"},"valid yet nonsensical json":[]}',
649 'auth' => [$this->user->getUsername(), $this->user->pass_raw],
650 'headers' => [
651 'Content-Type' => 'application/vnd.api+json',
652 'Accept' => 'application/vnd.api+json',
653 ],
654 ]);
655 $created_response = Json::decode($response->getBody()->__toString());
656 $this->assertEquals(422, $response->getStatusCode());
657 $this->assertNotEmpty($created_response['errors']);
658 $this->assertEquals('Unprocessable Entity', $created_response['errors'][0]['title']);
659 // 6.2 Relationships are not included in "data".
660 $malformed_body = $body;
661 unset($malformed_body['data']['relationships']);
662 $malformed_body['relationships'] = $body['data']['relationships'];
663 $response = $this->request('POST', $collection_url, [
664 'body' => Json::encode($malformed_body),
665 'auth' => [$this->user->getUsername(), $this->user->pass_raw],
666 'headers' => [
667 'Accept' => 'application/vnd.api+json',
668 'Content-Type' => 'application/vnd.api+json',
669 ],
670 ]);
671 $created_response = Json::decode((string) $response->getBody());
672 $this->assertSame(400, $response->getStatusCode());
673 $this->assertNotEmpty($created_response['errors']);
674 $this->assertSame("Bad Request", $created_response['errors'][0]['title']);
675 $this->assertSame("Found \"relationships\" within the document's top level. The \"relationships\" key must be within resource object.", $created_response['errors'][0]['detail']);
676 // 6.2 "type" not included in "data".
677 $missing_type = $body;
678 unset($missing_type['data']['type']);
679 $response = $this->request('POST', $collection_url, [
680 'body' => Json::encode($missing_type),
681 'auth' => [$this->user->getUsername(), $this->user->pass_raw],
682 'headers' => [
683 'Accept' => 'application/vnd.api+json',
684 'Content-Type' => 'application/vnd.api+json',
685 ],
686 ]);
687 $created_response = Json::decode((string) $response->getBody());
688 $this->assertSame(400, $response->getStatusCode());
689 $this->assertNotEmpty($created_response['errors']);
690 $this->assertSame("Bad Request", $created_response['errors'][0]['title']);
691 $this->assertSame("Resource object must include a \"type\".", $created_response['errors'][0]['detail']);
692 // 7. Successful PATCH.
693 $body = [
694 'data' => [
695 'id' => $uuid,
696 'type' => 'node--article',
697 'attributes' => ['title' => 'My updated title'],
698 ],
699 ];
700 $individual_url = Url::fromRoute('jsonapi.node--article.individual', [
701 'entity' => $uuid,
702 ]);
703 $response = $this->request('PATCH', $individual_url, [
704 'body' => Json::encode($body),
705 'auth' => [$this->user->getUsername(), $this->user->pass_raw],
706 'headers' => ['Content-Type' => 'application/vnd.api+json'],
707 ]);
708 $updated_response = Json::decode($response->getBody()->__toString());
709 $this->assertEquals(200, $response->getStatusCode());
710 $this->assertEquals('My updated title', $updated_response['data']['attributes']['title']);
711
712 // 7.1 Unsuccessful PATCH due to access restrictions.
713 $body = [
714 'data' => [
715 'id' => $uuid,
716 'type' => 'node--article',
717 'attributes' => ['title' => 'My updated title'],
718 ],
719 ];
720 $individual_url = Url::fromRoute('jsonapi.node--article.individual', [
721 'entity' => $uuid,
722 ]);
723 $response = $this->request('PATCH', $individual_url, [
724 'body' => Json::encode($body),
725 'auth' => [$this->userCanViewProfiles->getUsername(), $this->userCanViewProfiles->pass_raw],
726 'headers' => ['Content-Type' => 'application/vnd.api+json'],
727 ]);
728 $this->assertEquals(403, $response->getStatusCode());
729
730 // 8. Field access forbidden check.
731 $body = [
732 'data' => [
733 'id' => $uuid,
734 'type' => 'node--article',
735 'attributes' => [
736 'title' => 'My updated title',
737 'status' => 0,
738 ],
739 ],
740 ];
741 $response = $this->request('PATCH', $individual_url, [
742 'body' => Json::encode($body),
743 'auth' => [$this->user->getUsername(), $this->user->pass_raw],
744 'headers' => ['Content-Type' => 'application/vnd.api+json'],
745 ]);
746 $updated_response = Json::decode($response->getBody()->__toString());
747 $this->assertEquals(403, $response->getStatusCode());
748 $this->assertEquals("The current user is not allowed to PATCH the selected field (status). The 'administer nodes' permission is required.",
749 $updated_response['errors'][0]['detail']);
750
751 $node = \Drupal::entityManager()->loadEntityByUuid('node', $uuid);
752 $this->assertEquals(1, $node->get('status')->value, 'Node status was not changed.');
753 // 9. Successful POST to related endpoint.
754 $body = [
755 'data' => [
756 [
757 'id' => $this->tags[2]->uuid(),
758 'type' => 'taxonomy_term--tags',
759 ],
760 ],
761 ];
762 $relationship_url = Url::fromRoute('jsonapi.node--article.field_tags.relationship.post', [
763 'entity' => $uuid,
764 ]);
765 $response = $this->request('POST', $relationship_url, [
766 'body' => Json::encode($body),
767 'auth' => [$this->user->getUsername(), $this->user->pass_raw],
768 'headers' => ['Content-Type' => 'application/vnd.api+json'],
769 ]);
770 $updated_response = Json::decode($response->getBody()->__toString());
771 $this->assertEquals(200, $response->getStatusCode());
772 $this->assertEquals(3, count($updated_response['data']));
773 $this->assertEquals('taxonomy_term--tags', $updated_response['data'][2]['type']);
774 $this->assertEquals($this->tags[2]->uuid(), $updated_response['data'][2]['id']);
775 // 10. Successful PATCH to related endpoint.
776 $body = [
777 'data' => [
778 [
779 'id' => $this->tags[1]->uuid(),
780 'type' => 'taxonomy_term--tags',
781 ],
782 ],
783 ];
784 $response = $this->request('PATCH', $relationship_url, [
785 'body' => Json::encode($body),
786 'auth' => [$this->user->getUsername(), $this->user->pass_raw],
787 'headers' => ['Content-Type' => 'application/vnd.api+json'],
788 ]);
789 $this->assertEquals(204, $response->getStatusCode());
790 $this->assertEmpty($response->getBody()->__toString());
791 // 11. Successful DELETE to related endpoint.
792 $response = $this->request('DELETE', $relationship_url, [
793 // Send a request with no body.
794 'auth' => [$this->user->getUsername(), $this->user->pass_raw],
795 'headers' => [
796 'Content-Type' => 'application/vnd.api+json',
797 'Accept' => 'application/vnd.api+json',
798 ],
799 ]);
800 $updated_response = Json::decode($response->getBody()->__toString());
801 $this->assertEquals(
802 'You need to provide a body for DELETE operations on a relationship (field_tags).',
803 $updated_response['errors'][0]['detail']
804 );
805 $this->assertEquals(400, $response->getStatusCode());
806 $response = $this->request('DELETE', $relationship_url, [
807 // Send a request with no authentication.
808 'body' => Json::encode($body),
809 'headers' => ['Content-Type' => 'application/vnd.api+json'],
810 ]);
811 $this->assertEquals(401, $response->getStatusCode());
812 $response = $this->request('DELETE', $relationship_url, [
813 // Remove the existing relationship item.
814 'body' => Json::encode($body),
815 'auth' => [$this->user->getUsername(), $this->user->pass_raw],
816 'headers' => ['Content-Type' => 'application/vnd.api+json'],
817 ]);
818 $this->assertEquals(204, $response->getStatusCode());
819 $this->assertEmpty($response->getBody()->__toString());
820 // 12. PATCH with invalid title and body format.
821 $body = [
822 'data' => [
823 'id' => $uuid,
824 'type' => 'node--article',
825 'attributes' => [
826 'title' => '',
827 'body' => [
828 'value' => 'Custom value',
829 'format' => 'invalid_format',
830 'summary' => 'Custom summary',
831 ],
832 ],
833 ],
834 ];
835 $response = $this->request('PATCH', $individual_url, [
836 'body' => Json::encode($body),
837 'auth' => [$this->user->getUsername(), $this->user->pass_raw],
838 'headers' => [
839 'Content-Type' => 'application/vnd.api+json',
840 'Accept' => 'application/vnd.api+json',
841 ],
842 ]);
843 $updated_response = Json::decode($response->getBody()->__toString());
844 $this->assertEquals(422, $response->getStatusCode());
845 $this->assertCount(2, $updated_response['errors']);
846 for ($i = 0; $i < 2; $i++) {
847 $this->assertEquals("Unprocessable Entity", $updated_response['errors'][$i]['title']);
848 $this->assertEquals(422, $updated_response['errors'][$i]['status']);
849 }
850 $this->assertEquals("title: This value should not be null.", $updated_response['errors'][0]['detail']);
851 $this->assertEquals("body.0.format: The value you selected is not a valid choice.", $updated_response['errors'][1]['detail']);
852 $this->assertEquals("/data/attributes/title", $updated_response['errors'][0]['source']['pointer']);
853 $this->assertEquals("/data/attributes/body/format", $updated_response['errors'][1]['source']['pointer']);
854 // 13. PATCH with field that doesn't exist on Entity.
855 $body = [
856 'data' => [
857 'id' => $uuid,
858 'type' => 'node--article',
859 'attributes' => [
860 'field_that_doesnt_exist' => 'foobar',
861 ],
862 ],
863 ];
864 $response = $this->request('PATCH', $individual_url, [
865 'body' => Json::encode($body),
866 'auth' => [$this->user->getUsername(), $this->user->pass_raw],
867 'headers' => [
868 'Content-Type' => 'application/vnd.api+json',
869 'Accept' => 'application/vnd.api+json',
870 ],
871 ]);
872 $updated_response = Json::decode($response->getBody()->__toString());
873 $this->assertEquals(422, $response->getStatusCode());
874 $this->assertEquals("The attribute field_that_doesnt_exist does not exist on the node--article resource type.",
875 $updated_response['errors']['0']['detail']);
876 // 14. Successful DELETE.
877 $response = $this->request('DELETE', $individual_url, [
878 'auth' => [$this->user->getUsername(), $this->user->pass_raw],
879 ]);
880 $this->assertEquals(204, $response->getStatusCode());
881 $response = $this->request('GET', $individual_url, []);
882 $this->assertEquals(404, $response->getStatusCode());
883 }
884
885 }