3 namespace Drupal\Tests\rest\Unit\EventSubscriber;
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;
27 * @coversDefaultClass \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
30 class ResourceResponseSubscriberTest extends UnitTestCase {
33 * @covers ::onResponse
34 * @dataProvider providerTestSerialization
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']));
40 $handler_response = new ResourceResponse($data);
41 $resource_response_subscriber = $this->getFunctioningResourceResponseSubscriber($route_match);
42 $event = new FilterResponseEvent(
43 $this->prophesize(HttpKernelInterface::class)->reveal(),
45 HttpKernelInterface::MASTER_REQUEST,
48 $resource_response_subscriber->onResponse($event);
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());
54 public function providerTestSerialization() {
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
68 // [(object) ['test' => 'foobar']],
73 * @covers ::getResponseFormat
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.
79 * @dataProvider providerTestResponseFormat
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) {
83 if ($request_format !== FALSE) {
84 $parameters['_format'] = $request_format;
87 foreach ($request_headers as $key => $value) {
88 unset($request_headers[$key]);
89 $key = strtoupper(str_replace('-', '_', $key));
90 $request_headers[$key] = $value;
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)]));
98 $resource_response_subscriber = new ResourceResponseSubscriber(
99 $this->prophesize(SerializerInterface::class)->reveal(),
100 $this->prophesize(RendererInterface::class)->reveal(),
104 $this->assertSame($expected_response_format, $resource_response_subscriber->getResponseFormat($route_match, $request));
109 * @covers ::onResponse
110 * @covers ::getResponseFormat
111 * @covers ::renderResponseBody
112 * @covers ::flattenResponse
114 * @dataProvider providerTestResponseFormat
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();
120 if ($request_format !== FALSE) {
121 $parameters['_format'] = $request_format;
124 foreach ($request_headers as $key => $value) {
125 unset($request_headers[$key]);
126 $key = strtoupper(str_replace('-', '_', $key));
127 $request_headers[$key] = $value;
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)]));
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);
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(),
146 HttpKernelInterface::MASTER_REQUEST,
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());
159 * @covers ::onResponse
160 * @covers ::getResponseFormat
161 * @covers ::renderResponseBody
162 * @covers ::flattenResponse
164 * @dataProvider providerTestResponseFormat
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();
170 if ($request_format !== FALSE) {
171 $parameters['_format'] = $request_format;
174 foreach ($request_headers as $key => $value) {
175 unset($request_headers[$key]);
176 $key = strtoupper(str_replace('-', '_', $key));
177 $request_headers[$key] = $value;
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)]));
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);
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(),
196 HttpKernelInterface::MASTER_REQUEST,
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());
211 * 1. supported formats for route requirements
215 * 5. expected response format
216 * 6. expected response content type
217 * 7. expected response body
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";
223 $safe_method_test_cases = [
224 'safe methods: client requested format (JSON)' => [
225 // @todo add 'HEAD' in https://www.drupal.org/node/2752325
235 'safe methods: client requested format (XML)' => [
236 // @todo add 'HEAD' in https://www.drupal.org/node/2752325
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
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
270 $unsafe_method_bodied_test_cases = [
271 'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (JSON)' => [
275 ['Content-Type' => 'application/json'],
281 'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (XML)' => [
285 ['Content-Type' => 'text/xml'],
291 'unsafe methods with response (POST, PATCH): client requested format other than request body format (JSON): response format should use requested format (XML)' => [
295 ['Content-Type' => 'application/json'],
301 'unsafe methods with response (POST, PATCH): client requested format other than request body format (XML), but is allowed for the request body (JSON)' => [
305 ['Content-Type' => 'text/xml'],
313 $unsafe_method_bodyless_test_cases = [
314 'unsafe methods with response bodies (DELETE): client requested no format, response should have no format' => [
318 ['Content-Type' => 'application/json'],
324 'unsafe methods with response bodies (DELETE): client requested format (XML), response should have no format' => [
328 ['Content-Type' => 'application/json'],
334 'unsafe methods with response bodies (DELETE): client requested format (JSON), response should have no format' => [
338 ['Content-Type' => 'application/json'],
346 return $safe_method_test_cases + $unsafe_method_bodied_test_cases + $unsafe_method_bodyless_test_cases;
350 * @return \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
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];
361 // Instantiate the ResourceResponseSubscriber we will test.
362 $resource_response_subscriber = new ResourceResponseSubscriber(
363 new Serializer([], [new JsonEncoder(), new XmlEncoder()]),
368 return $resource_response_subscriber;