Updated to Drupal 8.5. Core Media not yet in use.
[yaffs-website] / web / core / modules / rest / tests / src / Unit / EventSubscriber / ResourceResponseSubscriberTest.php
1 <?php
2
3 namespace Drupal\Tests\rest\Unit\EventSubscriber;
4
5 use Drupal\Component\Serialization\Json;
6 use Drupal\Core\Cache\CacheableResponseInterface;
7 use Drupal\Core\Render\RenderContext;
8 use Drupal\Core\Render\RendererInterface;
9 use Drupal\Core\Routing\RouteMatch;
10 use Drupal\Core\Routing\RouteMatchInterface;
11 use Drupal\rest\EventSubscriber\ResourceResponseSubscriber;
12 use Drupal\rest\ModifiedResourceResponse;
13 use Drupal\rest\ResourceResponse;
14 use Drupal\rest\ResourceResponseInterface;
15 use Drupal\serialization\Encoder\JsonEncoder;
16 use Drupal\serialization\Encoder\XmlEncoder;
17 use Drupal\Tests\UnitTestCase;
18 use Prophecy\Argument;
19 use Symfony\Component\HttpFoundation\Request;
20 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
21 use Symfony\Component\HttpKernel\HttpKernelInterface;
22 use Symfony\Component\Routing\Route;
23 use Symfony\Component\Serializer\Serializer;
24 use Symfony\Component\Serializer\SerializerInterface;
25
26 /**
27  * @coversDefaultClass \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
28  * @group rest
29  */
30 class ResourceResponseSubscriberTest extends UnitTestCase {
31
32   /**
33    * @covers ::onResponse
34    * @dataProvider providerTestSerialization
35    */
36   public function testSerialization($data, $expected_response = FALSE) {
37     $request = new Request();
38     $route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => 'restplugin'], ['_format' => 'json']));
39
40     $handler_response = new ResourceResponse($data);
41     $resource_response_subscriber = $this->getFunctioningResourceResponseSubscriber($route_match);
42     $event = new FilterResponseEvent(
43       $this->prophesize(HttpKernelInterface::class)->reveal(),
44       $request,
45       HttpKernelInterface::MASTER_REQUEST,
46       $handler_response
47     );
48     $resource_response_subscriber->onResponse($event);
49
50     // Content is a serialized version of the data we provided.
51     $this->assertEquals($expected_response !== FALSE ? $expected_response : Json::encode($data), $event->getResponse()->getContent());
52   }
53
54   public function providerTestSerialization() {
55     return [
56       // The default data for \Drupal\rest\ResourceResponse.
57       'default' => [NULL, ''],
58       'empty string' => [''],
59       'simple string' => ['string'],
60       'complex string' => ['Complex \ string $%^&@ with unicode ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΣὨ'],
61       'empty array' => [[]],
62       'numeric array' => [['test']],
63       'associative array' => [['test' => 'foobar']],
64       'boolean true' => [TRUE],
65       'boolean false' => [FALSE],
66       // @todo Not supported. https://www.drupal.org/node/2427811
67       // [new \stdClass()],
68       // [(object) ['test' => 'foobar']],
69     ];
70   }
71
72   /**
73    * @covers ::getResponseFormat
74    *
75    * Note this does *not* need to test formats being requested that are not
76    * accepted by the server, because the routing system would have already
77    * prevented those from reaching the controller.
78    *
79    * @dataProvider providerTestResponseFormat
80    */
81   public function testResponseFormat($methods, array $supported_response_formats, array $supported_request_formats, $request_format, array $request_headers, $request_body, $expected_response_format, $expected_response_content_type, $expected_response_content) {
82     foreach ($request_headers as $key => $value) {
83       unset($request_headers[$key]);
84       $key = strtoupper(str_replace('-', '_', $key));
85       $request_headers[$key] = $value;
86     }
87
88     foreach ($methods as $method) {
89       $request = Request::create('/rest/test', $method, [], [], [], $request_headers, $request_body);
90       // \Drupal\Core\StackMiddleware\NegotiationMiddleware normally takes care
91       // of this so we'll hard code it here.
92       if ($request_format) {
93         $request->setRequestFormat($request_format);
94       }
95
96       $route_requirements = $this->generateRouteRequirements($supported_response_formats, $supported_request_formats);
97
98       $route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => $this->randomMachineName()], $route_requirements));
99
100       $resource_response_subscriber = new ResourceResponseSubscriber(
101         $this->prophesize(SerializerInterface::class)->reveal(),
102         $this->prophesize(RendererInterface::class)->reveal(),
103         $route_match
104       );
105
106       $this->assertSame($expected_response_format, $resource_response_subscriber->getResponseFormat($route_match, $request));
107     }
108   }
109
110   /**
111    * @covers ::onResponse
112    * @covers ::getResponseFormat
113    * @covers ::renderResponseBody
114    * @covers ::flattenResponse
115    *
116    * @dataProvider providerTestResponseFormat
117    */
118   public function testOnResponseWithCacheableResponse($methods, array $supported_response_formats, array $supported_request_formats, $request_format, array $request_headers, $request_body, $expected_response_format, $expected_response_content_type, $expected_response_content) {
119     foreach ($request_headers as $key => $value) {
120       unset($request_headers[$key]);
121       $key = strtoupper(str_replace('-', '_', $key));
122       $request_headers[$key] = $value;
123     }
124
125     foreach ($methods as $method) {
126       $request = Request::create('/rest/test', $method, [], [], [], $request_headers, $request_body);
127       // \Drupal\Core\StackMiddleware\NegotiationMiddleware normally takes care
128       // of this so we'll hard code it here.
129       if ($request_format) {
130         $request->setRequestFormat($request_format);
131       }
132
133       $route_requirements = $this->generateRouteRequirements($supported_response_formats, $supported_request_formats);
134
135       $route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => $this->randomMachineName()], $route_requirements));
136
137       // The RequestHandler must return a ResourceResponseInterface object.
138       $handler_response = new ResourceResponse($method !== 'DELETE' ? ['REST' => 'Drupal'] : NULL);
139       $this->assertInstanceOf(ResourceResponseInterface::class, $handler_response);
140       $this->assertInstanceOf(CacheableResponseInterface::class, $handler_response);
141
142       // The ResourceResponseSubscriber must then generate a response body and
143       // transform it to a plain CacheableResponse object.
144       $resource_response_subscriber = $this->getFunctioningResourceResponseSubscriber($route_match);
145       $event = new FilterResponseEvent(
146         $this->prophesize(HttpKernelInterface::class)->reveal(),
147         $request,
148         HttpKernelInterface::MASTER_REQUEST,
149         $handler_response
150       );
151       $resource_response_subscriber->onResponse($event);
152       $final_response = $event->getResponse();
153       $this->assertNotInstanceOf(ResourceResponseInterface::class, $final_response);
154       $this->assertInstanceOf(CacheableResponseInterface::class, $final_response);
155       $this->assertSame($expected_response_content_type, $final_response->headers->get('Content-Type'));
156       $this->assertEquals($expected_response_content, $final_response->getContent());
157     }
158   }
159
160   /**
161    * @covers ::onResponse
162    * @covers ::getResponseFormat
163    * @covers ::renderResponseBody
164    * @covers ::flattenResponse
165    *
166    * @dataProvider providerTestResponseFormat
167    */
168   public function testOnResponseWithUncacheableResponse($methods, array $supported_response_formats, array $supported_request_formats, $request_format, array $request_headers, $request_body, $expected_response_format, $expected_response_content_type, $expected_response_content) {
169     foreach ($request_headers as $key => $value) {
170       unset($request_headers[$key]);
171       $key = strtoupper(str_replace('-', '_', $key));
172       $request_headers[$key] = $value;
173     }
174
175     foreach ($methods as $method) {
176       $request = Request::create('/rest/test', $method, [], [], [], $request_headers, $request_body);
177       // \Drupal\Core\StackMiddleware\NegotiationMiddleware normally takes care
178       // of this so we'll hard code it here.
179       if ($request_format) {
180         $request->setRequestFormat($request_format);
181       }
182
183       $route_requirements = $this->generateRouteRequirements($supported_response_formats, $supported_request_formats);
184
185       $route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => $this->randomMachineName()], $route_requirements));
186
187       // The RequestHandler must return a ResourceResponseInterface object.
188       $handler_response = new ModifiedResourceResponse($method !== 'DELETE' ? ['REST' => 'Drupal'] : NULL);
189       $this->assertInstanceOf(ResourceResponseInterface::class, $handler_response);
190       $this->assertNotInstanceOf(CacheableResponseInterface::class, $handler_response);
191
192       // The ResourceResponseSubscriber must then generate a response body and
193       // transform it to a plain Response object.
194       $resource_response_subscriber = $this->getFunctioningResourceResponseSubscriber($route_match);
195       $event = new FilterResponseEvent(
196         $this->prophesize(HttpKernelInterface::class)->reveal(),
197         $request,
198         HttpKernelInterface::MASTER_REQUEST,
199         $handler_response
200       );
201       $resource_response_subscriber->onResponse($event);
202       $final_response = $event->getResponse();
203       $this->assertNotInstanceOf(ResourceResponseInterface::class, $final_response);
204       $this->assertNotInstanceOf(CacheableResponseInterface::class, $final_response);
205       $this->assertSame($expected_response_content_type, $final_response->headers->get('Content-Type'));
206       $this->assertEquals($expected_response_content, $final_response->getContent());
207     }
208   }
209
210   /**
211    * @return array
212    *   0. methods to test
213    *   1. supported formats for route requirements
214    *   2. request format
215    *   3. request headers
216    *   4. request body
217    *   5. expected response format
218    *   6. expected response content type
219    *   7. expected response body
220    */
221   public function providerTestResponseFormat() {
222     $json_encoded = Json::encode(['REST' => 'Drupal']);
223     $xml_encoded = "<?xml version=\"1.0\"?>\n<response><REST>Drupal</REST></response>\n";
224
225     $safe_method_test_cases = [
226       'safe methods: client requested format (JSON)' => [
227         // @todo add 'HEAD' in https://www.drupal.org/node/2752325
228         ['GET'],
229         ['xml', 'json'],
230         [],
231         'json',
232         [],
233         NULL,
234         'json',
235         'application/json',
236         $json_encoded,
237       ],
238       'safe methods: client requested format (XML)' => [
239         // @todo add 'HEAD' in https://www.drupal.org/node/2752325
240         ['GET'],
241         ['xml', 'json'],
242         [],
243         'xml',
244         [],
245         NULL,
246         'xml',
247         'text/xml',
248         $xml_encoded,
249       ],
250       'safe methods: client requested no format: response should use the first configured format (JSON)' => [
251         // @todo add 'HEAD' in https://www.drupal.org/node/2752325
252         ['GET'],
253         ['json', 'xml'],
254         [],
255         FALSE,
256         [],
257         NULL,
258         'json',
259         'application/json',
260         $json_encoded,
261       ],
262       'safe methods: client requested no format: response should use the first configured format (XML)' => [
263         // @todo add 'HEAD' in https://www.drupal.org/node/2752325
264         ['GET'],
265         ['xml', 'json'],
266         [],
267         FALSE,
268         [],
269         NULL,
270         'xml',
271         'text/xml',
272         $xml_encoded,
273       ],
274     ];
275
276     $unsafe_method_bodied_test_cases = [
277       'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (JSON)' => [
278         ['POST', 'PATCH'],
279         ['xml', 'json'],
280         ['xml', 'json'],
281         FALSE,
282         ['Content-Type' => 'application/json'],
283         $json_encoded,
284         'json',
285         'application/json',
286         $json_encoded,
287       ],
288       'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (XML)' => [
289         ['POST', 'PATCH'],
290         ['xml', 'json'],
291         ['xml', 'json'],
292         FALSE,
293         ['Content-Type' => 'text/xml'],
294         $xml_encoded,
295         'xml',
296         'text/xml',
297         $xml_encoded,
298       ],
299       'unsafe methods with response (POST, PATCH): client requested format other than request body format (JSON): response format should use requested format (XML)' => [
300         ['POST', 'PATCH'],
301         ['xml', 'json'],
302         ['xml', 'json'],
303         'xml',
304         ['Content-Type' => 'application/json'],
305         $json_encoded,
306         'xml',
307         'text/xml',
308         $xml_encoded,
309       ],
310       'unsafe methods with response (POST, PATCH): client requested format other than request body format (XML), but is allowed for the request body (JSON)' => [
311         ['POST', 'PATCH'],
312         ['xml', 'json'],
313         ['xml', 'json'],
314         'json',
315         ['Content-Type' => 'text/xml'],
316         $xml_encoded,
317         'json',
318         'application/json',
319         $json_encoded,
320       ],
321       'unsafe methods with response (POST, PATCH): client requested format other than request body format when only XML is allowed as a content type format' => [
322         ['POST', 'PATCH'],
323         ['xml'],
324         ['json'],
325         'json',
326         ['Content-Type' => 'text/xml'],
327         $xml_encoded,
328         'json',
329         'application/json',
330         $json_encoded,
331       ],
332       'unsafe methods with response (POST, PATCH): client requested format other than request body format when only JSON is allowed as a content type format' => [
333         ['POST', 'PATCH'],
334         ['json'],
335         ['xml'],
336         'xml',
337         ['Content-Type' => 'application/json'],
338         $json_encoded,
339         'xml',
340         'text/xml',
341         $xml_encoded,
342       ],
343     ];
344
345     $unsafe_method_bodyless_test_cases = [
346       'unsafe methods without response bodies (DELETE): client requested no format, response should have no format' => [
347         ['DELETE'],
348         ['xml', 'json'],
349         ['xml', 'json'],
350         FALSE,
351         ['Content-Type' => 'application/json'],
352         NULL,
353         'xml',
354         NULL,
355         '',
356       ],
357       'unsafe methods without response bodies (DELETE): client requested format (XML), response should have no format' => [
358         ['DELETE'],
359         ['xml', 'json'],
360         ['xml', 'json'],
361         'xml',
362         ['Content-Type' => 'application/json'],
363         NULL,
364         'xml',
365         NULL,
366         '',
367       ],
368       'unsafe methods without response bodies (DELETE): client requested format (JSON), response should have no format' => [
369         ['DELETE'],
370         ['xml', 'json'],
371         ['xml', 'json'],
372         'json',
373         ['Content-Type' => 'application/json'],
374         NULL,
375         'json',
376         NULL,
377         '',
378       ],
379     ];
380
381     return $safe_method_test_cases + $unsafe_method_bodied_test_cases + $unsafe_method_bodyless_test_cases;
382   }
383
384   /**
385    * @return \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
386    */
387   protected function getFunctioningResourceResponseSubscriber(RouteMatchInterface $route_match) {
388     // Create a dummy of the renderer service.
389     $renderer = $this->prophesize(RendererInterface::class);
390     $renderer->executeInRenderContext(Argument::type(RenderContext::class), Argument::type('callable'))
391       ->will(function ($args) {
392         $callable = $args[1];
393         return $callable();
394       });
395
396     // Instantiate the ResourceResponseSubscriber we will test.
397     $resource_response_subscriber = new ResourceResponseSubscriber(
398       new Serializer([], [new JsonEncoder(), new XmlEncoder()]),
399       $renderer->reveal(),
400       $route_match
401     );
402
403     return $resource_response_subscriber;
404   }
405
406   /**
407    * Generates route requirements based on supported formats.
408    *
409    * @param array $supported_response_formats
410    *   The supported response formats to add to the route requirements.
411    * @param array $supported_request_formats
412    *   The supported request formats to add to the route requirements.
413    *
414    * @return array
415    *   An array of route requirements.
416    */
417   protected function generateRouteRequirements(array $supported_response_formats, array $supported_request_formats) {
418     $route_requirements = [
419       '_format' => implode('|', $supported_response_formats),
420     ];
421     if (!empty($supported_request_formats)) {
422       $route_requirements['_content_type_format'] = implode('|', $supported_request_formats);
423     }
424
425     return $route_requirements;
426   }
427
428 }