Version 1
[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_formats, $request_format, array $request_headers, $request_body, $expected_response_format, $expected_response_content_type, $expected_response_content) {
82     $parameters = [];
83     if ($request_format !== FALSE) {
84       $parameters['_format'] = $request_format;
85     }
86
87     foreach ($request_headers as $key => $value) {
88       unset($request_headers[$key]);
89       $key = strtoupper(str_replace('-', '_', $key));
90       $request_headers[$key] = $value;
91     }
92
93     foreach ($methods as $method) {
94       $request = Request::create('/rest/test', $method, $parameters, [], [], $request_headers, $request_body);
95       $route_requirement_key_format = $request->isMethodSafe() ? '_format' : '_content_type_format';
96       $route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => $this->randomMachineName()], [$route_requirement_key_format => implode('|', $supported_formats)]));
97
98       $resource_response_subscriber = new ResourceResponseSubscriber(
99         $this->prophesize(SerializerInterface::class)->reveal(),
100         $this->prophesize(RendererInterface::class)->reveal(),
101         $route_match
102       );
103
104       $this->assertSame($expected_response_format, $resource_response_subscriber->getResponseFormat($route_match, $request));
105     }
106   }
107
108   /**
109    * @covers ::onResponse
110    * @covers ::getResponseFormat
111    * @covers ::renderResponseBody
112    * @covers ::flattenResponse
113    *
114    * @dataProvider providerTestResponseFormat
115    */
116   public function testOnResponseWithCacheableResponse($methods, array $supported_formats, $request_format, array $request_headers, $request_body, $expected_response_format, $expected_response_content_type, $expected_response_content) {
117     $rest_config_name = $this->randomMachineName();
118
119     $parameters = [];
120     if ($request_format !== FALSE) {
121       $parameters['_format'] = $request_format;
122     }
123
124     foreach ($request_headers as $key => $value) {
125       unset($request_headers[$key]);
126       $key = strtoupper(str_replace('-', '_', $key));
127       $request_headers[$key] = $value;
128     }
129
130     foreach ($methods as $method) {
131       $request = Request::create('/rest/test', $method, $parameters, [], [], $request_headers, $request_body);
132       $route_requirement_key_format = $request->isMethodSafe() ? '_format' : '_content_type_format';
133       $route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => $rest_config_name], [$route_requirement_key_format => implode('|', $supported_formats)]));
134
135       // The RequestHandler must return a ResourceResponseInterface object.
136       $handler_response = new ResourceResponse($method !== 'DELETE' ? ['REST' => 'Drupal'] : NULL);
137       $this->assertInstanceOf(ResourceResponseInterface::class, $handler_response);
138       $this->assertInstanceOf(CacheableResponseInterface::class, $handler_response);
139
140       // The ResourceResponseSubscriber must then generate a response body and
141       // transform it to a plain CacheableResponse object.
142       $resource_response_subscriber = $this->getFunctioningResourceResponseSubscriber($route_match);
143       $event = new FilterResponseEvent(
144         $this->prophesize(HttpKernelInterface::class)->reveal(),
145         $request,
146         HttpKernelInterface::MASTER_REQUEST,
147         $handler_response
148       );
149       $resource_response_subscriber->onResponse($event);
150       $final_response = $event->getResponse();
151       $this->assertNotInstanceOf(ResourceResponseInterface::class, $final_response);
152       $this->assertInstanceOf(CacheableResponseInterface::class, $final_response);
153       $this->assertSame($expected_response_content_type, $final_response->headers->get('Content-Type'));
154       $this->assertEquals($expected_response_content, $final_response->getContent());
155     }
156   }
157
158   /**
159    * @covers ::onResponse
160    * @covers ::getResponseFormat
161    * @covers ::renderResponseBody
162    * @covers ::flattenResponse
163    *
164    * @dataProvider providerTestResponseFormat
165    */
166   public function testOnResponseWithUncacheableResponse($methods, array $supported_formats, $request_format, array $request_headers, $request_body, $expected_response_format, $expected_response_content_type, $expected_response_content) {
167     $rest_config_name = $this->randomMachineName();
168
169     $parameters = [];
170     if ($request_format !== FALSE) {
171       $parameters['_format'] = $request_format;
172     }
173
174     foreach ($request_headers as $key => $value) {
175       unset($request_headers[$key]);
176       $key = strtoupper(str_replace('-', '_', $key));
177       $request_headers[$key] = $value;
178     }
179
180     foreach ($methods as $method) {
181       $request = Request::create('/rest/test', $method, $parameters, [], [], $request_headers, $request_body);
182       $route_requirement_key_format = $request->isMethodSafe() ? '_format' : '_content_type_format';
183       $route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => $rest_config_name], [$route_requirement_key_format => implode('|', $supported_formats)]));
184
185       // The RequestHandler must return a ResourceResponseInterface object.
186       $handler_response = new ModifiedResourceResponse($method !== 'DELETE' ? ['REST' => 'Drupal'] : NULL);
187       $this->assertInstanceOf(ResourceResponseInterface::class, $handler_response);
188       $this->assertNotInstanceOf(CacheableResponseInterface::class, $handler_response);
189
190       // The ResourceResponseSubscriber must then generate a response body and
191       // transform it to a plain Response object.
192       $resource_response_subscriber = $this->getFunctioningResourceResponseSubscriber($route_match);
193       $event = new FilterResponseEvent(
194         $this->prophesize(HttpKernelInterface::class)->reveal(),
195         $request,
196         HttpKernelInterface::MASTER_REQUEST,
197         $handler_response
198       );
199       $resource_response_subscriber->onResponse($event);
200       $final_response = $event->getResponse();
201       $this->assertNotInstanceOf(ResourceResponseInterface::class, $final_response);
202       $this->assertNotInstanceOf(CacheableResponseInterface::class, $final_response);
203       $this->assertSame($expected_response_content_type, $final_response->headers->get('Content-Type'));
204       $this->assertEquals($expected_response_content, $final_response->getContent());
205     }
206   }
207
208   /**
209    * @return array
210    *   0. methods to test
211    *   1. supported formats for route requirements
212    *   2. request format
213    *   3. request headers
214    *   4. request body
215    *   5. expected response format
216    *   6. expected response content type
217    *   7. expected response body
218    */
219   public function providerTestResponseFormat() {
220     $json_encoded = Json::encode(['REST' => 'Drupal']);
221     $xml_encoded = "<?xml version=\"1.0\"?>\n<response><REST>Drupal</REST></response>\n";
222
223     $safe_method_test_cases = [
224       'safe methods: client requested format (JSON)' => [
225         // @todo add 'HEAD' in https://www.drupal.org/node/2752325
226         ['GET'],
227         ['xml', 'json'],
228         'json',
229         [],
230         NULL,
231         'json',
232         'application/json',
233         $json_encoded,
234       ],
235       'safe methods: client requested format (XML)' => [
236         // @todo add 'HEAD' in https://www.drupal.org/node/2752325
237         ['GET'],
238         ['xml', 'json'],
239         'xml',
240         [],
241         NULL,
242         'xml',
243         'text/xml',
244         $xml_encoded,
245       ],
246       'safe methods: client requested no format: response should use the first configured format (JSON)' => [
247         // @todo add 'HEAD' in https://www.drupal.org/node/2752325
248         ['GET'],
249         ['json', 'xml'],
250         FALSE,
251         [],
252         NULL,
253         'json',
254         'application/json',
255         $json_encoded,
256       ],
257       'safe methods: client requested no format: response should use the first configured format (XML)' => [
258         // @todo add 'HEAD' in https://www.drupal.org/node/2752325
259         ['GET'],
260         ['xml', 'json'],
261         FALSE,
262         [],
263         NULL,
264         'xml',
265         'text/xml',
266         $xml_encoded,
267       ],
268     ];
269
270     $unsafe_method_bodied_test_cases = [
271       'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (JSON)' => [
272         ['POST', 'PATCH'],
273         ['xml', 'json'],
274         FALSE,
275         ['Content-Type' => 'application/json'],
276         $json_encoded,
277         'json',
278         'application/json',
279         $json_encoded,
280       ],
281       'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (XML)' => [
282         ['POST', 'PATCH'],
283         ['xml', 'json'],
284         FALSE,
285         ['Content-Type' => 'text/xml'],
286         $xml_encoded,
287         'xml',
288         'text/xml',
289         $xml_encoded,
290       ],
291       'unsafe methods with response (POST, PATCH): client requested format other than request body format (JSON): response format should use requested format (XML)' => [
292         ['POST', 'PATCH'],
293         ['xml', 'json'],
294         'xml',
295         ['Content-Type' => 'application/json'],
296         $json_encoded,
297         'xml',
298         'text/xml',
299         $xml_encoded,
300       ],
301       'unsafe methods with response (POST, PATCH): client requested format other than request body format (XML), but is allowed for the request body (JSON)' => [
302         ['POST', 'PATCH'],
303         ['xml', 'json'],
304         'json',
305         ['Content-Type' => 'text/xml'],
306         $xml_encoded,
307         'json',
308         'application/json',
309         $json_encoded,
310       ],
311     ];
312
313     $unsafe_method_bodyless_test_cases = [
314       'unsafe methods with response bodies (DELETE): client requested no format, response should have no format' => [
315         ['DELETE'],
316         ['xml', 'json'],
317         FALSE,
318         ['Content-Type' => 'application/json'],
319         NULL,
320         'xml',
321         NULL,
322         '',
323       ],
324       'unsafe methods with response bodies (DELETE): client requested format (XML), response should have no format' => [
325         ['DELETE'],
326         ['xml', 'json'],
327         'xml',
328         ['Content-Type' => 'application/json'],
329         NULL,
330         'xml',
331         NULL,
332         '',
333       ],
334       'unsafe methods with response bodies (DELETE): client requested format (JSON), response should have no format' => [
335         ['DELETE'],
336         ['xml', 'json'],
337         'json',
338         ['Content-Type' => 'application/json'],
339         NULL,
340         'json',
341         NULL,
342         '',
343       ],
344     ];
345
346     return $safe_method_test_cases + $unsafe_method_bodied_test_cases + $unsafe_method_bodyless_test_cases;
347   }
348
349   /**
350    * @return \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
351    */
352   protected function getFunctioningResourceResponseSubscriber(RouteMatchInterface $route_match) {
353     // Create a dummy of the renderer service.
354     $renderer = $this->prophesize(RendererInterface::class);
355     $renderer->executeInRenderContext(Argument::type(RenderContext::class), Argument::type('callable'))
356       ->will(function ($args) {
357         $callable = $args[1];
358         return $callable();
359       });
360
361     // Instantiate the ResourceResponseSubscriber we will test.
362     $resource_response_subscriber = new ResourceResponseSubscriber(
363       new Serializer([], [new JsonEncoder(), new XmlEncoder()]),
364       $renderer->reveal(),
365       $route_match
366     );
367
368     return $resource_response_subscriber;
369   }
370
371 }