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_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;
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);
96 $route_requirements = $this->generateRouteRequirements($supported_response_formats, $supported_request_formats);
98 $route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => $this->randomMachineName()], $route_requirements));
100 $resource_response_subscriber = new ResourceResponseSubscriber(
101 $this->prophesize(SerializerInterface::class)->reveal(),
102 $this->prophesize(RendererInterface::class)->reveal(),
106 $this->assertSame($expected_response_format, $resource_response_subscriber->getResponseFormat($route_match, $request));
111 * @covers ::onResponse
112 * @covers ::getResponseFormat
113 * @covers ::renderResponseBody
114 * @covers ::flattenResponse
116 * @dataProvider providerTestResponseFormat
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;
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);
133 $route_requirements = $this->generateRouteRequirements($supported_response_formats, $supported_request_formats);
135 $route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => $this->randomMachineName()], $route_requirements));
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);
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(),
148 HttpKernelInterface::MASTER_REQUEST,
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());
161 * @covers ::onResponse
162 * @covers ::getResponseFormat
163 * @covers ::renderResponseBody
164 * @covers ::flattenResponse
166 * @dataProvider providerTestResponseFormat
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;
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);
183 $route_requirements = $this->generateRouteRequirements($supported_response_formats, $supported_request_formats);
185 $route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => $this->randomMachineName()], $route_requirements));
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);
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(),
198 HttpKernelInterface::MASTER_REQUEST,
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());
213 * 1. supported formats for route requirements
217 * 5. expected response format
218 * 6. expected response content type
219 * 7. expected response body
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";
225 $safe_method_test_cases = [
226 'safe methods: client requested format (JSON)' => [
227 // @todo add 'HEAD' in https://www.drupal.org/node/2752325
238 'safe methods: client requested format (XML)' => [
239 // @todo add 'HEAD' in https://www.drupal.org/node/2752325
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
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
276 $unsafe_method_bodied_test_cases = [
277 'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (JSON)' => [
282 ['Content-Type' => 'application/json'],
288 'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (XML)' => [
293 ['Content-Type' => 'text/xml'],
299 'unsafe methods with response (POST, PATCH): client requested format other than request body format (JSON): response format should use requested format (XML)' => [
304 ['Content-Type' => 'application/json'],
310 'unsafe methods with response (POST, PATCH): client requested format other than request body format (XML), but is allowed for the request body (JSON)' => [
315 ['Content-Type' => 'text/xml'],
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' => [
326 ['Content-Type' => 'text/xml'],
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' => [
337 ['Content-Type' => 'application/json'],
345 $unsafe_method_bodyless_test_cases = [
346 'unsafe methods without response bodies (DELETE): client requested no format, response should have no format' => [
351 ['Content-Type' => 'application/json'],
357 'unsafe methods without response bodies (DELETE): client requested format (XML), response should have no format' => [
362 ['Content-Type' => 'application/json'],
368 'unsafe methods without response bodies (DELETE): client requested format (JSON), response should have no format' => [
373 ['Content-Type' => 'application/json'],
381 return $safe_method_test_cases + $unsafe_method_bodied_test_cases + $unsafe_method_bodyless_test_cases;
385 * @return \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
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];
396 // Instantiate the ResourceResponseSubscriber we will test.
397 $resource_response_subscriber = new ResourceResponseSubscriber(
398 new Serializer([], [new JsonEncoder(), new XmlEncoder()]),
403 return $resource_response_subscriber;
407 * Generates route requirements based on supported formats.
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.
415 * An array of route requirements.
417 protected function generateRouteRequirements(array $supported_response_formats, array $supported_request_formats) {
418 $route_requirements = [
419 '_format' => implode('|', $supported_response_formats),
421 if (!empty($supported_request_formats)) {
422 $route_requirements['_content_type_format'] = implode('|', $supported_request_formats);
425 return $route_requirements;