Upgraded drupal core with security updates
[yaffs-website] / web / core / modules / rest / src / Tests / Views / StyleSerializerTest.php
1 <?php
2
3 namespace Drupal\rest\Tests\Views;
4
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
7 use Drupal\Core\Field\FieldStorageDefinitionInterface;
8 use Drupal\entity_test\Entity\EntityTest;
9 use Drupal\field\Entity\FieldConfig;
10 use Drupal\field\Entity\FieldStorageConfig;
11 use Drupal\language\Entity\ConfigurableLanguage;
12 use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
13 use Drupal\views\Entity\View;
14 use Drupal\views\Plugin\views\display\DisplayPluginBase;
15 use Drupal\views\Views;
16 use Drupal\views\Tests\Plugin\PluginTestBase;
17 use Drupal\views\Tests\ViewTestData;
18 use Symfony\Component\HttpFoundation\Request;
19
20 /**
21  * Tests the serializer style plugin.
22  *
23  * @group rest
24  * @see \Drupal\rest\Plugin\views\display\RestExport
25  * @see \Drupal\rest\Plugin\views\style\Serializer
26  * @see \Drupal\rest\Plugin\views\row\DataEntityRow
27  * @see \Drupal\rest\Plugin\views\row\DataFieldRow
28  */
29 class StyleSerializerTest extends PluginTestBase {
30
31   use AssertPageCacheContextsAndTagsTrait;
32
33   /**
34    * {@inheritdoc}
35    */
36   protected $dumpHeaders = TRUE;
37
38   /**
39    * Modules to install.
40    *
41    * @var array
42    */
43   public static $modules = ['views_ui', 'entity_test', 'hal', 'rest_test_views', 'node', 'text', 'field', 'language', 'basic_auth'];
44
45   /**
46    * Views used by this test.
47    *
48    * @var array
49    */
50   public static $testViews = ['test_serializer_display_field', 'test_serializer_display_entity', 'test_serializer_display_entity_translated', 'test_serializer_node_display_field', 'test_serializer_node_exposed_filter'];
51
52   /**
53    * A user with administrative privileges to look at test entity and configure views.
54    */
55   protected $adminUser;
56
57   protected function setUp() {
58     parent::setUp();
59
60     ViewTestData::createTestViews(get_class($this), ['rest_test_views']);
61
62     $this->adminUser = $this->drupalCreateUser(['administer views', 'administer entity_test content', 'access user profiles', 'view test entity']);
63
64     // Save some entity_test entities.
65     for ($i = 1; $i <= 10; $i++) {
66       EntityTest::create(['name' => 'test_' . $i, 'user_id' => $this->adminUser->id()])->save();
67     }
68
69     $this->enableViewsTestModule();
70   }
71
72   /**
73    * Checks that the auth options restricts access to a REST views display.
74    */
75   public function testRestViewsAuthentication() {
76     // Assume the view is hidden behind a permission.
77     $this->drupalGetWithFormat('test/serialize/auth_with_perm', 'json');
78     $this->assertResponse(401);
79
80     // Not even logging in would make it possible to see the view, because then
81     // we are denied based on authentication method (cookie).
82     $this->drupalLogin($this->adminUser);
83     $this->drupalGetWithFormat('test/serialize/auth_with_perm', 'json');
84     $this->assertResponse(403);
85     $this->drupalLogout();
86
87     // But if we use the basic auth authentication strategy, we should be able
88     // to see the page.
89     $url = $this->buildUrl('test/serialize/auth_with_perm');
90     $response = \Drupal::httpClient()->get($url, [
91       'auth' => [$this->adminUser->getUsername(), $this->adminUser->pass_raw],
92     ]);
93
94     // Ensure that any changes to variables in the other thread are picked up.
95     $this->refreshVariables();
96
97     $headers = $response->getHeaders();
98     $this->verbose('GET request to: ' . $url .
99       '<hr />Code: ' . curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE) .
100       '<hr />Response headers: ' . nl2br(print_r($headers, TRUE)) .
101       '<hr />Response body: ' . (string) $response->getBody());
102     $this->assertResponse(200);
103   }
104
105   /**
106    * Checks the behavior of the Serializer callback paths and row plugins.
107    */
108   public function testSerializerResponses() {
109     // Test the serialize callback.
110     $view = Views::getView('test_serializer_display_field');
111     $view->initDisplay();
112     $this->executeView($view);
113
114     $actual_json = $this->drupalGetWithFormat('test/serialize/field', 'json');
115     $this->assertResponse(200);
116     $this->assertCacheTags($view->getCacheTags());
117     $this->assertCacheContexts(['languages:language_interface', 'theme', 'request_format']);
118     // @todo Due to https://www.drupal.org/node/2352009 we can't yet test the
119     // propagation of cache max-age.
120
121     // Test the http Content-type.
122     $headers = $this->drupalGetHeaders();
123     $this->assertEqual($headers['content-type'], 'application/json', 'The header Content-type is correct.');
124
125     $expected = [];
126     foreach ($view->result as $row) {
127       $expected_row = [];
128       foreach ($view->field as $id => $field) {
129         $expected_row[$id] = $field->render($row);
130       }
131       $expected[] = $expected_row;
132     }
133
134     $this->assertIdentical($actual_json, json_encode($expected), 'The expected JSON output was found.');
135
136
137     // Test that the rendered output and the preview output are the same.
138     $view->destroy();
139     $view->setDisplay('rest_export_1');
140     // Mock the request content type by setting it on the display handler.
141     $view->display_handler->setContentType('json');
142     $output = $view->preview();
143     $this->assertIdentical($actual_json, (string) drupal_render_root($output), 'The expected JSON preview output was found.');
144
145     // Test a 403 callback.
146     $this->drupalGet('test/serialize/denied');
147     $this->assertResponse(403);
148
149     // Test the entity rows.
150     $view = Views::getView('test_serializer_display_entity');
151     $view->initDisplay();
152     $this->executeView($view);
153
154     // Get the serializer service.
155     $serializer = $this->container->get('serializer');
156
157     $entities = [];
158     foreach ($view->result as $row) {
159       $entities[] = $row->_entity;
160     }
161
162     $expected = $serializer->serialize($entities, 'json');
163
164     $actual_json = $this->drupalGetWithFormat('test/serialize/entity', 'json');
165     $this->assertResponse(200);
166     $this->assertIdentical($actual_json, $expected, 'The expected JSON output was found.');
167     $expected_cache_tags = $view->getCacheTags();
168     $expected_cache_tags[] = 'entity_test_list';
169     /** @var \Drupal\Core\Entity\EntityInterface $entity */
170     foreach ($entities as $entity) {
171       $expected_cache_tags = Cache::mergeTags($expected_cache_tags, $entity->getCacheTags());
172     }
173     $this->assertCacheTags($expected_cache_tags);
174     $this->assertCacheContexts(['languages:language_interface', 'theme', 'entity_test_view_grants', 'request_format']);
175
176     $expected = $serializer->serialize($entities, 'hal_json');
177     $actual_json = $this->drupalGetWithFormat('test/serialize/entity', 'hal_json');
178     $this->assertIdentical($actual_json, $expected, 'The expected HAL output was found.');
179     $this->assertCacheTags($expected_cache_tags);
180
181     // Change the default format to xml.
182     $view->setDisplay('rest_export_1');
183     $view->getDisplay()->setOption('style', [
184       'type' => 'serializer',
185       'options' => [
186         'uses_fields' => FALSE,
187         'formats' => [
188           'xml' => 'xml',
189         ],
190       ],
191     ]);
192     $view->save();
193     $expected = $serializer->serialize($entities, 'xml');
194     $actual_xml = $this->drupalGet('test/serialize/entity');
195     $this->assertIdentical($actual_xml, $expected, 'The expected XML output was found.');
196     $this->assertCacheContexts(['languages:language_interface', 'theme', 'entity_test_view_grants', 'request_format']);
197
198     // Allow multiple formats.
199     $view->setDisplay('rest_export_1');
200     $view->getDisplay()->setOption('style', [
201       'type' => 'serializer',
202       'options' => [
203         'uses_fields' => FALSE,
204         'formats' => [
205           'xml' => 'xml',
206           'json' => 'json',
207         ],
208       ],
209     ]);
210     $view->save();
211     $expected = $serializer->serialize($entities, 'json');
212     $actual_json = $this->drupalGetWithFormat('test/serialize/entity', 'json');
213     $this->assertIdentical($actual_json, $expected, 'The expected JSON output was found.');
214     $expected = $serializer->serialize($entities, 'xml');
215     $actual_xml = $this->drupalGetWithFormat('test/serialize/entity', 'xml');
216     $this->assertIdentical($actual_xml, $expected, 'The expected XML output was found.');
217   }
218
219   /**
220    * Verifies site maintenance mode functionality.
221    */
222   public function testSiteMaintenance() {
223     $view = Views::getView('test_serializer_display_field');
224     $view->initDisplay();
225     $this->executeView($view);
226
227     // Set the site to maintenance mode.
228     $this->container->get('state')->set('system.maintenance_mode', TRUE);
229
230     $this->drupalGetWithFormat('test/serialize/entity', 'json');
231     // Verify that the endpoint is unavailable for anonymous users.
232     $this->assertResponse(503);
233   }
234
235   /**
236    * Sets up a request on the request stack with a specified format.
237    *
238    * @param string $format
239    *   The new request format.
240    */
241   protected function addRequestWithFormat($format) {
242     $request = \Drupal::request();
243     $request = clone $request;
244     $request->setRequestFormat($format);
245
246     \Drupal::requestStack()->push($request);
247   }
248
249   /**
250    * Tests REST export with views render caching enabled.
251    */
252   public function testRestRenderCaching() {
253     $this->drupalLogin($this->adminUser);
254     /** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */
255     $render_cache = \Drupal::service('render_cache');
256
257     // Enable render caching for the views.
258     /** @var \Drupal\views\ViewEntityInterface $storage */
259     $storage = View::load('test_serializer_display_entity');
260     $options = &$storage->getDisplay('default');
261     $options['display_options']['cache'] = [
262       'type' => 'tag',
263     ];
264     $storage->save();
265
266     $original = DisplayPluginBase::buildBasicRenderable('test_serializer_display_entity', 'rest_export_1');
267
268     // Ensure that there is no corresponding render cache item yet.
269     $original['#cache'] += ['contexts' => []];
270     $original['#cache']['contexts'] = Cache::mergeContexts($original['#cache']['contexts'], $this->container->getParameter('renderer.config')['required_cache_contexts']);
271
272     $cache_tags = [
273       'config:views.view.test_serializer_display_entity',
274       'entity_test:1',
275       'entity_test:10',
276       'entity_test:2',
277       'entity_test:3',
278       'entity_test:4',
279       'entity_test:5',
280       'entity_test:6',
281       'entity_test:7',
282       'entity_test:8',
283       'entity_test:9',
284       'entity_test_list'
285     ];
286     $cache_contexts = [
287       'entity_test_view_grants',
288       'languages:language_interface',
289       'theme',
290       'request_format',
291     ];
292
293     $this->assertFalse($render_cache->get($original));
294
295     // Request the page, once in XML and once in JSON to ensure that the caching
296     // varies by it.
297     $result1 = $this->drupalGetJSON('test/serialize/entity');
298     $this->addRequestWithFormat('json');
299     $this->assertHeader('content-type', 'application/json');
300     $this->assertCacheContexts($cache_contexts);
301     $this->assertCacheTags($cache_tags);
302     $this->assertTrue($render_cache->get($original));
303
304     $result_xml = $this->drupalGetWithFormat('test/serialize/entity', 'xml');
305     $this->addRequestWithFormat('xml');
306     $this->assertHeader('content-type', 'text/xml; charset=UTF-8');
307     $this->assertCacheContexts($cache_contexts);
308     $this->assertCacheTags($cache_tags);
309     $this->assertTrue($render_cache->get($original));
310
311     // Ensure that the XML output is different from the JSON one.
312     $this->assertNotEqual($result1, $result_xml);
313
314     // Ensure that the cached page works.
315     $result2 = $this->drupalGetJSON('test/serialize/entity');
316     $this->addRequestWithFormat('json');
317     $this->assertHeader('content-type', 'application/json');
318     $this->assertEqual($result2, $result1);
319     $this->assertCacheContexts($cache_contexts);
320     $this->assertCacheTags($cache_tags);
321     $this->assertTrue($render_cache->get($original));
322
323     // Create a new entity and ensure that the cache tags are taken over.
324     EntityTest::create(['name' => 'test_11', 'user_id' => $this->adminUser->id()])->save();
325     $result3 = $this->drupalGetJSON('test/serialize/entity');
326     $this->addRequestWithFormat('json');
327     $this->assertHeader('content-type', 'application/json');
328     $this->assertNotEqual($result3, $result2);
329
330     // Add the new entity cache tag and remove the first one, because we just
331     // show 10 items in total.
332     $cache_tags[] = 'entity_test:11';
333     unset($cache_tags[array_search('entity_test:1', $cache_tags)]);
334
335     $this->assertCacheContexts($cache_contexts);
336     $this->assertCacheTags($cache_tags);
337     $this->assertTrue($render_cache->get($original));
338   }
339
340   /**
341    * Tests the response format configuration.
342    */
343   public function testResponseFormatConfiguration() {
344     $this->drupalLogin($this->adminUser);
345
346     $style_options = 'admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/style_options';
347
348     // Select only 'xml' as an accepted format.
349     $this->drupalPostForm($style_options, ['style_options[formats][xml]' => 'xml'], t('Apply'));
350     $this->drupalPostForm(NULL, [], t('Save'));
351
352     // Should return a 406.
353     $this->drupalGetWithFormat('test/serialize/field', 'json');
354     $this->assertHeader('content-type', 'application/json');
355     $this->assertResponse(406, 'A 406 response was returned when JSON was requested.');
356     // Should return a 200.
357     $this->drupalGetWithFormat('test/serialize/field', 'xml');
358     $this->assertHeader('content-type', 'text/xml; charset=UTF-8');
359     $this->assertResponse(200, 'A 200 response was returned when XML was requested.');
360
361     // Add 'json' as an accepted format, so we have multiple.
362     $this->drupalPostForm($style_options, ['style_options[formats][json]' => 'json'], t('Apply'));
363     $this->drupalPostForm(NULL, [], t('Save'));
364
365     // Should return a 200.
366     // @todo This should be fixed when we have better content negotiation.
367     $this->drupalGet('test/serialize/field');
368     $this->assertHeader('content-type', 'application/json');
369     $this->assertResponse(200, 'A 200 response was returned when any format was requested.');
370
371     // Should return a 200. Emulates a sample Firefox header.
372     $this->drupalGet('test/serialize/field', [], ['Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8']);
373     $this->assertHeader('content-type', 'application/json');
374     $this->assertResponse(200, 'A 200 response was returned when a browser accept header was requested.');
375
376     // Should return a 200.
377     $this->drupalGetWithFormat('test/serialize/field', 'json');
378     $this->assertHeader('content-type', 'application/json');
379     $this->assertResponse(200, 'A 200 response was returned when JSON was requested.');
380     $headers = $this->drupalGetHeaders();
381     $this->assertEqual($headers['content-type'], 'application/json', 'The header Content-type is correct.');
382     // Should return a 200.
383     $this->drupalGetWithFormat('test/serialize/field', 'xml');
384     $this->assertHeader('content-type', 'text/xml; charset=UTF-8');
385     $this->assertResponse(200, 'A 200 response was returned when XML was requested');
386     $headers = $this->drupalGetHeaders();
387     $this->assertTrue(strpos($headers['content-type'], 'text/xml') !== FALSE, 'The header Content-type is correct.');
388     // Should return a 406.
389     $this->drupalGetWithFormat('test/serialize/field', 'html');
390     // We want to show the first format by default, see
391     // \Drupal\rest\Plugin\views\style\Serializer::render.
392     $this->assertHeader('content-type', 'application/json');
393     $this->assertResponse(200, 'A 200 response was returned when HTML was requested.');
394
395     // Now configure now format, so all of them should be allowed.
396     $this->drupalPostForm($style_options, ['style_options[formats][json]' => '0', 'style_options[formats][xml]' => '0'], t('Apply'));
397
398     // Should return a 200.
399     $this->drupalGetWithFormat('test/serialize/field', 'json');
400     $this->assertHeader('content-type', 'application/json');
401     $this->assertResponse(200, 'A 200 response was returned when JSON was requested.');
402     // Should return a 200.
403     $this->drupalGetWithFormat('test/serialize/field', 'xml');
404     $this->assertHeader('content-type', 'text/xml; charset=UTF-8');
405     $this->assertResponse(200, 'A 200 response was returned when XML was requested');
406     // Should return a 200.
407     $this->drupalGetWithFormat('test/serialize/field', 'html');
408     // We want to show the first format by default, see
409     // \Drupal\rest\Plugin\views\style\Serializer::render.
410     $this->assertHeader('content-type', 'application/json');
411     $this->assertResponse(200, 'A 200 response was returned when HTML was requested.');
412   }
413
414   /**
415    * Test the field ID alias functionality of the DataFieldRow plugin.
416    */
417   public function testUIFieldAlias() {
418     $this->drupalLogin($this->adminUser);
419
420     // Test the UI settings for adding field ID aliases.
421     $this->drupalGet('admin/structure/views/view/test_serializer_display_field/edit/rest_export_1');
422     $row_options = 'admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/row_options';
423     $this->assertLinkByHref($row_options);
424
425     // Test an empty string for an alias, this should not be used. This also
426     // tests that the form can be submitted with no aliases.
427     $this->drupalPostForm($row_options, ['row_options[field_options][name][alias]' => ''], t('Apply'));
428     $this->drupalPostForm(NULL, [], t('Save'));
429
430     $view = Views::getView('test_serializer_display_field');
431     $view->setDisplay('rest_export_1');
432     $this->executeView($view);
433
434     $expected = [];
435     foreach ($view->result as $row) {
436       $expected_row = [];
437       foreach ($view->field as $id => $field) {
438         $expected_row[$id] = $field->render($row);
439       }
440       $expected[] = $expected_row;
441     }
442
443     $this->assertIdentical($this->drupalGetJSON('test/serialize/field'), $this->castSafeStrings($expected));
444
445     // Test a random aliases for fields, they should be replaced.
446     $alias_map = [
447       'name' => $this->randomMachineName(),
448       // Use # to produce an invalid character for the validation.
449       'nothing' => '#' . $this->randomMachineName(),
450       'created' => 'created',
451     ];
452
453     $edit = ['row_options[field_options][name][alias]' => $alias_map['name'], 'row_options[field_options][nothing][alias]' => $alias_map['nothing']];
454     $this->drupalPostForm($row_options, $edit, t('Apply'));
455     $this->assertText(t('The machine-readable name must contain only letters, numbers, dashes and underscores.'));
456
457     // Change the map alias value to a valid one.
458     $alias_map['nothing'] = $this->randomMachineName();
459
460     $edit = ['row_options[field_options][name][alias]' => $alias_map['name'], 'row_options[field_options][nothing][alias]' => $alias_map['nothing']];
461     $this->drupalPostForm($row_options, $edit, t('Apply'));
462
463     $this->drupalPostForm(NULL, [], t('Save'));
464
465     $view = Views::getView('test_serializer_display_field');
466     $view->setDisplay('rest_export_1');
467     $this->executeView($view);
468
469     $expected = [];
470     foreach ($view->result as $row) {
471       $expected_row = [];
472       foreach ($view->field as $id => $field) {
473         $expected_row[$alias_map[$id]] = $field->render($row);
474       }
475       $expected[] = $expected_row;
476     }
477
478     $this->assertIdentical($this->drupalGetJSON('test/serialize/field'), $this->castSafeStrings($expected));
479   }
480
481   /**
482    * Tests the raw output options for row field rendering.
483    */
484   public function testFieldRawOutput() {
485     $this->drupalLogin($this->adminUser);
486
487     // Test the UI settings for adding field ID aliases.
488     $this->drupalGet('admin/structure/views/view/test_serializer_display_field/edit/rest_export_1');
489     $row_options = 'admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/row_options';
490     $this->assertLinkByHref($row_options);
491
492     // Test an empty string for an alias, this should not be used. This also
493     // tests that the form can be submitted with no aliases.
494     $values = [
495       'row_options[field_options][created][raw_output]' => '1',
496       'row_options[field_options][name][raw_output]' => '1',
497     ];
498     $this->drupalPostForm($row_options, $values, t('Apply'));
499     $this->drupalPostForm(NULL, [], t('Save'));
500
501     $view = Views::getView('test_serializer_display_field');
502     $view->setDisplay('rest_export_1');
503     $this->executeView($view);
504
505     $storage = $this->container->get('entity_type.manager')->getStorage('entity_test');
506
507     // Update the name for each to include a script tag.
508     foreach ($storage->loadMultiple() as $entity_test) {
509       $name = $entity_test->name->value;
510       $entity_test->set('name', "<script>$name</script>");
511       $entity_test->save();
512     }
513
514     // Just test the raw 'created' value against each row.
515     foreach ($this->drupalGetJSON('test/serialize/field') as $index => $values) {
516       $this->assertIdentical($values['created'], $view->result[$index]->views_test_data_created, 'Expected raw created value found.');
517       $this->assertIdentical($values['name'], $view->result[$index]->views_test_data_name, 'Expected raw name value found.');
518     }
519
520     // Test result with an excluded field.
521     $view->setDisplay('rest_export_1');
522     $view->displayHandlers->get('rest_export_1')->overrideOption('fields', [
523       'name' => [
524         'id' => 'name',
525         'table' => 'views_test_data',
526         'field' => 'name',
527         'relationship' => 'none',
528       ],
529       'created' => [
530         'id' => 'created',
531         'exclude' => TRUE,
532         'table' => 'views_test_data',
533         'field' => 'created',
534         'relationship' => 'none',
535       ],
536     ]);
537     $view->save();
538     $this->executeView($view);
539     foreach ($this->drupalGetJSON('test/serialize/field') as $index => $values) {
540       $this->assertTrue(!isset($values['created']), 'Excluded value not found.');
541     }
542     // Test that the excluded field is not shown in the row options.
543     $this->drupalGet('admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/row_options');
544     $this->assertNoText('created');
545   }
546
547   /**
548    * Tests the live preview output for json output.
549    */
550   public function testLivePreview() {
551     // We set up a request so it looks like an request in the live preview.
552     $request = new Request();
553     $request->query->add([MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']);
554     /** @var \Symfony\Component\HttpFoundation\RequestStack $request_stack */
555     $request_stack = \Drupal::service('request_stack');
556     $request_stack->push($request);
557
558     $view = Views::getView('test_serializer_display_entity');
559     $view->setDisplay('rest_export_1');
560     $this->executeView($view);
561
562     // Get the serializer service.
563     $serializer = $this->container->get('serializer');
564
565     $entities = [];
566     foreach ($view->result as $row) {
567       $entities[] = $row->_entity;
568     }
569
570     $expected = $serializer->serialize($entities, 'json');
571
572     $view->live_preview = TRUE;
573
574     $build = $view->preview();
575     $rendered_json = $build['#plain_text'];
576     $this->assertTrue(!isset($build['#markup']) && $rendered_json == $expected, 'Ensure the previewed json is escaped.');
577     $view->destroy();
578
579     $expected = $serializer->serialize($entities, 'xml');
580
581     // Change the request format to xml.
582     $view->setDisplay('rest_export_1');
583     $view->getDisplay()->setOption('style', [
584       'type' => 'serializer',
585       'options' => [
586         'uses_fields' => FALSE,
587         'formats' => [
588           'xml' => 'xml',
589         ],
590       ],
591     ]);
592
593     $this->executeView($view);
594     $build = $view->preview();
595     $rendered_xml = $build['#plain_text'];
596     $this->assertEqual($rendered_xml, $expected, 'Ensure we preview xml when we change the request format.');
597   }
598
599   /**
600    * Tests the views interface for REST export displays.
601    */
602   public function testSerializerViewsUI() {
603     $this->drupalLogin($this->adminUser);
604     // Click the "Update preview button".
605     $this->drupalPostForm('admin/structure/views/view/test_serializer_display_field/edit/rest_export_1', $edit = [], t('Update preview'));
606     $this->assertResponse(200);
607     // Check if we receive the expected result.
608     $result = $this->xpath('//div[@id="views-live-preview"]/pre');
609     $this->assertIdentical($this->drupalGet('test/serialize/field'), (string) $result[0], 'The expected JSON preview output was found.');
610   }
611
612   /**
613    * Tests the field row style using fieldapi fields.
614    */
615   public function testFieldapiField() {
616     $this->drupalCreateContentType(['type' => 'page']);
617     $node = $this->drupalCreateNode();
618
619     $result = $this->drupalGetJSON('test/serialize/node-field');
620     $this->assertEqual($result[0]['nid'], $node->id());
621     $this->assertEqual($result[0]['body'], $node->body->processed);
622
623     // Make sure that serialized fields are not exposed to XSS.
624     $node = $this->drupalCreateNode();
625     $node->body = [
626       'value' => '<script type="text/javascript">alert("node-body");</script>' . $this->randomMachineName(32),
627       'format' => filter_default_format(),
628     ];
629     $node->save();
630     $result = $this->drupalGetJSON('test/serialize/node-field');
631     $this->assertEqual($result[1]['nid'], $node->id());
632     $this->assertTrue(strpos($this->getRawContent(), "<script") === FALSE, "No script tag is present in the raw page contents.");
633
634     $this->drupalLogin($this->adminUser);
635
636     // Add an alias and make the output raw.
637     $row_options = 'admin/structure/views/nojs/display/test_serializer_node_display_field/rest_export_1/row_options';
638
639     // Test an empty string for an alias, this should not be used. This also
640     // tests that the form can be submitted with no aliases.
641     $this->drupalPostForm($row_options, ['row_options[field_options][title][raw_output]' => '1'], t('Apply'));
642     $this->drupalPostForm(NULL, [], t('Save'));
643
644     $view = Views::getView('test_serializer_node_display_field');
645     $view->setDisplay('rest_export_1');
646     $this->executeView($view);
647
648     // Test the raw 'created' value against each row.
649     foreach ($this->drupalGetJSON('test/serialize/node-field') as $index => $values) {
650       $this->assertIdentical($values['title'], $view->result[$index]->_entity->title->value, 'Expected raw title value found.');
651     }
652
653     // Test that multiple raw body fields are shown.
654     // Make the body field unlimited cardinatlity.
655     $storage_definition = $node->getFieldDefinition('body')->getFieldStorageDefinition();
656     $storage_definition->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
657     $storage_definition->save();
658
659     $this->drupalPostForm($row_options, ['row_options[field_options][body][raw_output]' => '1'], t('Apply'));
660     $this->drupalPostForm(NULL, [], t('Save'));
661
662     $node = $this->drupalCreateNode();
663
664     $body = [
665       'value' => '<script type="text/javascript">alert("node-body");</script>' . $this->randomMachineName(32),
666       'format' => filter_default_format(),
667     ];
668     // Add two body items.
669     $node->body = [$body, $body];
670     $node->save();
671
672     $view = Views::getView('test_serializer_node_display_field');
673     $view->setDisplay('rest_export_1');
674     $this->executeView($view);
675
676     $result = $this->drupalGetJSON('test/serialize/node-field');
677     $this->assertEqual(count($result[2]['body']), $node->body->count(), 'Expected count of values');
678     $this->assertEqual($result[2]['body'], array_map(function($item) { return $item['value']; }, $node->body->getValue()), 'Expected raw body values found.');
679   }
680
681   /**
682    * Tests the "Grouped rows" functionality.
683    */
684   public function testGroupRows() {
685     /** @var \Drupal\Core\Render\RendererInterface $renderer */
686     $renderer = $this->container->get('renderer');
687     $this->drupalCreateContentType(['type' => 'page']);
688     // Create a text field with cardinality set to unlimited.
689     $field_name = 'field_group_rows';
690     $field_storage = FieldStorageConfig::create([
691       'field_name' => $field_name,
692       'entity_type' => 'node',
693       'type' => 'string',
694       'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
695     ]);
696     $field_storage->save();
697     // Create an instance of the text field on the content type.
698     $field = FieldConfig::create([
699       'field_storage' => $field_storage,
700       'bundle' => 'page',
701     ]);
702     $field->save();
703     $grouped_field_values = ['a', 'b', 'c'];
704     $edit = [
705       'title' => $this->randomMachineName(),
706       $field_name => $grouped_field_values,
707     ];
708     $this->drupalCreateNode($edit);
709     $view = Views::getView('test_serializer_node_display_field');
710     $view->setDisplay('rest_export_1');
711     // Override the view's fields to include the field_group_rows field, set the
712     // group_rows setting to true.
713     $fields = [
714       $field_name => [
715         'id' => $field_name,
716         'table' => 'node__' . $field_name,
717         'field' => $field_name,
718         'type' => 'string',
719         'group_rows' => TRUE,
720       ],
721     ];
722     $view->displayHandlers->get('default')->overrideOption('fields', $fields);
723     $build = $view->preview();
724     // Get the serializer service.
725     $serializer = $this->container->get('serializer');
726     // Check if the field_group_rows field is grouped.
727     $expected = [];
728     $expected[] = [$field_name => implode(', ', $grouped_field_values)];
729     $this->assertEqual($serializer->serialize($expected, 'json'), (string) $renderer->renderRoot($build));
730     // Set the group rows setting to false.
731     $view = Views::getView('test_serializer_node_display_field');
732     $view->setDisplay('rest_export_1');
733     $fields[$field_name]['group_rows'] = FALSE;
734     $view->displayHandlers->get('default')->overrideOption('fields', $fields);
735     $build = $view->preview();
736     // Check if the field_group_rows field is ungrouped and displayed per row.
737     $expected = [];
738     foreach ($grouped_field_values as $grouped_field_value) {
739       $expected[] = [$field_name => $grouped_field_value];
740     }
741     $this->assertEqual($serializer->serialize($expected, 'json'), (string) $renderer->renderRoot($build));
742   }
743
744   /**
745    * Tests the exposed filter works.
746    *
747    * There is an exposed filter on the title field which takes a title query
748    * parameter. This is set to filter nodes by those whose title starts with
749    * the value provided.
750    */
751   public function testRestViewExposedFilter() {
752     $this->drupalCreateContentType(['type' => 'page']);
753     $node0 = $this->drupalCreateNode(['title' => 'Node 1']);
754     $node1 = $this->drupalCreateNode(['title' => 'Node 11']);
755     $node2 = $this->drupalCreateNode(['title' => 'Node 111']);
756
757     // Test that no filter brings back all three nodes.
758     $result = $this->drupalGetJSON('test/serialize/node-exposed-filter');
759
760     $expected = [
761       0 => [
762         'nid' => $node0->id(),
763         'body' => $node0->body->processed,
764       ],
765       1 => [
766         'nid' => $node1->id(),
767         'body' => $node1->body->processed,
768       ],
769       2 => [
770         'nid' => $node2->id(),
771         'body' => $node2->body->processed,
772       ],
773     ];
774
775     $this->assertEqual($result, $expected, 'Querying a view with no exposed filter returns all nodes.');
776
777     // Test that title starts with 'Node 11' query finds 2 of the 3 nodes.
778     $result = $this->drupalGetJSON('test/serialize/node-exposed-filter', ['query' => ['title' => 'Node 11']]);
779
780     $expected = [
781       0 => [
782         'nid' => $node1->id(),
783         'body' => $node1->body->processed,
784       ],
785       1 => [
786         'nid' => $node2->id(),
787         'body' => $node2->body->processed,
788       ],
789     ];
790
791     $cache_contexts = [
792       'languages:language_content',
793       'languages:language_interface',
794       'theme',
795       'request_format',
796       'user.node_grants:view',
797       'url',
798     ];
799
800     $this->assertEqual($result, $expected, 'Querying a view with a starts with exposed filter on the title returns nodes whose title starts with value provided.');
801     $this->assertCacheContexts($cache_contexts);
802   }
803
804   /**
805    * Test multilingual entity rows.
806    */
807   public function testMulEntityRows() {
808     // Create some languages.
809     ConfigurableLanguage::createFromLangcode('l1')->save();
810     ConfigurableLanguage::createFromLangcode('l2')->save();
811
812     // Create an entity with no translations.
813     $storage = \Drupal::entityTypeManager()->getStorage('entity_test_mul');
814     $storage->create(['langcode' => 'l1', 'name' => 'mul-none'])->save();
815
816     // Create some entities with translations.
817     $entity = $storage->create(['langcode' => 'l1', 'name' => 'mul-l1-orig']);
818     $entity->save();
819     $entity->addTranslation('l2', ['name' => 'mul-l1-l2'])->save();
820     $entity = $storage->create(['langcode' => 'l2', 'name' => 'mul-l2-orig']);
821     $entity->save();
822     $entity->addTranslation('l1', ['name' => 'mul-l2-l1'])->save();
823
824     // Get the names of the output.
825     $json = $this->drupalGetWithFormat('test/serialize/translated_entity', 'json');
826     $decoded = $this->container->get('serializer')->decode($json, 'hal_json');
827     $names = [];
828     foreach ($decoded as $item) {
829       $names[] = $item['name'][0]['value'];
830     }
831     sort($names);
832
833     // Check that the names are correct.
834     $expected = ['mul-l1-l2', 'mul-l1-orig', 'mul-l2-l1', 'mul-l2-orig', 'mul-none'];
835     $this->assertIdentical($names, $expected, 'The translated content was found in the JSON.');
836   }
837
838 }