Mercurial > hg > cmmr2012-drupal-site
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 } |