.../EventSubscriber/ResourceResponseSubscriber.php | 3 +- .../rest/tests/src/Kernel/RequestHandlerTest.php | 247 +-------------- .../ResourceResponseSubscriberTest.php | 334 +++++++++++++++++++++ 3 files changed, 336 insertions(+), 248 deletions(-) diff --git a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php index ba1e3eb..ce77e01 100644 --- a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php +++ b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php @@ -92,7 +92,7 @@ public function onRespond(FilterResponseEvent $event) { * @return string * The response format. */ - protected function getResponseFormat(RouteMatchInterface $route_match, Request $request) { + public function getResponseFormat(RouteMatchInterface $route_match, Request $request) { $route = $route_match->getRouteObject(); $acceptable_request_formats = $route->hasRequirement('_format') ? explode('|', $route->getRequirement('_format')) : []; $acceptable_content_type_formats = $route->hasRequirement('_content_type_format') ? explode('|', $route->getRequirement('_content_type_format')) : []; @@ -188,7 +188,6 @@ protected function renderResponseBody(Request $request, ResourceResponseInterfac * The flattened response. */ protected function flattenResponse(ResourceResponseInterface $response) { - assert('!empty($response->getContent())'); $final_response = ($response instanceof CacheableResponseInterface) ? new CacheableResponse() : new Response(); $final_response->setContent($response->getContent()); $final_response->setStatusCode($response->getStatusCode()); diff --git a/core/modules/rest/tests/src/Kernel/RequestHandlerTest.php b/core/modules/rest/tests/src/Kernel/RequestHandlerTest.php index 905b449..336e483 100644 --- a/core/modules/rest/tests/src/Kernel/RequestHandlerTest.php +++ b/core/modules/rest/tests/src/Kernel/RequestHandlerTest.php @@ -48,11 +48,9 @@ public function setUp() { } /** - * Assert some basic handler method logic. - * * @covers ::handle */ - public function testBaseHandler() { + public function testHandle() { $request = new Request(); $route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => 'restplugin'], ['_format' => 'json'])); @@ -91,249 +89,6 @@ public function testBaseHandler() { $this->assertEquals($response, $handler_response); } - /** - * Test that given structured data, the request handler will serialize it. - * - * @dataProvider providerTestSerialization - * @covers ::handle - */ - public function testSerialization($data, $expected_response = FALSE) { - $request = new Request(); - $route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => 'restplugin'], ['_format' => 'json'])); - - $resource = $this->prophesize(StubRequestHandlerResourcePlugin::class); - - // Mock the configuration. - $config = $this->prophesize(RestResourceConfigInterface::class); - $config->getResourcePlugin()->willReturn($resource->reveal()); - $config->getCacheContexts()->willReturn([]); - $config->getCacheTags()->willReturn([]); - $config->getCacheMaxAge()->willReturn(12); - $this->entityStorage->load('restplugin')->willReturn($config->reveal()); - - $response = new ResourceResponse($data); - $resource->get(NULL, $request) - ->willReturn($response); - $handler_response = $this->requestHandler->handle($route_match, $request); - // Content is a serialized version of the data we provided. - $this->assertEquals($expected_response !== FALSE ? $expected_response : json_encode($data), $handler_response->getContent()); - } - - public function providerTestSerialization() { - return [ - // The default data for \Drupal\rest\ResourceResponse. - [NULL, ''], - [''], - ['string'], - ['Complex \ string $%^&@ with unicode ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΣὨ'], - [[]], - [['test']], - [['test' => 'foobar']], - [TRUE], - [FALSE], - // @todo Not supported. https://www.drupal.org/node/2427811 - // [new \stdClass()], - // [(object) ['test' => 'foobar']], - ]; - } - - /** - * @covers ::getResponseFormat - * - * Note this does *not* need to test formats being requested that are not - * accepted by the server, because the routing system would have already - * prevented those from reaching RequestHandler. - * - * @param string[] $methods - * The HTTP methods to test. - * @param string[] $supported_formats - * The supported formats for the REST route to be tested. - * @param string|false $request_format - * The value for the ?_format URL query argument, if any. - * @param string[] $request_headers - * The request headers to send, if any. - * @param string|null $request_body - * The request body to send, if any. - * @param string|null $expected_response_content_type - * The expected MIME type of the response, if any. - * @param string $expected_response_content - * The expected content of the response. - * - * @dataProvider providerTestResponseFormat - */ - public function testResponseFormat($methods, array $supported_formats, $request_format, array $request_headers, $request_body, $expected_response_content_type, $expected_response_content) { - $rest_config_name = $this->randomMachineName(); - - $parameters = []; - if ($request_format !== FALSE) { - $parameters['_format'] = $request_format; - } - - foreach ($request_headers as $key => $value) { - unset($request_headers[$key]); - $key = strtoupper(str_replace('-', '_', $key)); - $request_headers[$key] = $value; - } - - foreach ($methods as $method) { - $request = Request::create('/rest/test', $method, $parameters, [], [], $request_headers, $request_body); - $route_requirement_key_format = $request->isMethodSafe() ? '_format' : '_content_type_format'; - $route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => $rest_config_name], [$route_requirement_key_format => implode('|', $supported_formats)])); - - $resource = $this->prophesize(StubRequestHandlerResourcePlugin::class); - - // Mock the configuration. - $config = $this->prophesize(RestResourceConfigInterface::class); - $config->getFormats($method)->willReturn($supported_formats); - $config->getResourcePlugin()->willReturn($resource->reveal()); - $config->getCacheContexts()->willReturn([]); - $config->getCacheTags()->willReturn([]); - $config->getCacheMaxAge()->willReturn(12); - $this->entityStorage->load($rest_config_name)->willReturn($config->reveal()); - - // Mock the resource plugin. - $response = new ResourceResponse($method !== 'DELETE' ? ['REST' => 'Drupal'] : NULL); - $resource->getPluginDefinition()->willReturn([]); - $method_prophecy = new MethodProphecy($resource, strtolower($method), [Argument::any(), $request]); - $method_prophecy->willReturn($response); - $resource->addMethodProphecy($method_prophecy); - - // Test the request handler. - $handler_response = $this->requestHandler->handle($route_match, $request); - $this->assertSame($expected_response_content_type, $handler_response->headers->get('Content-Type')); - $this->assertEquals($expected_response_content, $handler_response->getContent()); - } - } - - /** - * @return array - * 0. methods to test - * 1. supported formats for route requirements - * 2. request format - * 3. request headers - * 4. request body - * 5. expected response content type - * 6. expected response body - */ - public function providerTestResponseFormat() { - $json_encoded = Json::encode(['REST' => 'Drupal']); - $xml_encoded = "\nDrupal\n"; - - $safe_method_test_cases = [ - 'safe methods: client requested format (JSON)' => [ - // @todo add 'HEAD' in https://www.drupal.org/node/2752325 - ['GET'], - ['xml', 'json'], - 'json', - [], - NULL, - 'application/json', - $json_encoded, - ], - 'safe methods: client requested format (XML)' => [ - // @todo add 'HEAD' in https://www.drupal.org/node/2752325 - ['GET'], - ['xml', 'json'], - 'xml', - [], - NULL, - 'text/xml', - $xml_encoded, - ], - 'safe methods: client requested no format: response should use the first configured format (JSON)' => [ - // @todo add 'HEAD' in https://www.drupal.org/node/2752325 - ['GET'], - ['json', 'xml'], - FALSE, - [], - NULL, - 'application/json', - $json_encoded, - ], - 'safe methods: client requested no format: response should use the first configured format (XML)' => [ - // @todo add 'HEAD' in https://www.drupal.org/node/2752325 - ['GET'], - ['xml', 'json'], - FALSE, - [], - NULL, - 'text/xml', - $xml_encoded, - ], - ]; - - $unsafe_method_bodied_test_cases = [ - 'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (JSON)' => [ - ['POST', 'PATCH'], - ['xml', 'json'], - FALSE, - ['Content-Type' => 'application/json'], - $json_encoded, - 'application/json', - $json_encoded, - ], - 'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (XML)' => [ - ['POST', 'PATCH'], - ['xml', 'json'], - FALSE, - ['Content-Type' => 'text/xml'], - $xml_encoded, - 'text/xml', - $xml_encoded, - ], - 'unsafe methods with response (POST, PATCH): client requested format other than request body format (JSON): response format should use requested format (XML)' => [ - ['POST', 'PATCH'], - ['xml', 'json'], - 'xml', - ['Content-Type' => 'application/json'], - $json_encoded, - 'text/xml', - $xml_encoded, - ], - 'unsafe methods with response (POST, PATCH): client requested format other than request body format (XML), but is allowed for the request body (JSON)' => [ - ['POST', 'PATCH'], - ['xml', 'json'], - 'json', - ['Content-Type' => 'text/xml'], - $xml_encoded, - 'application/json', - $json_encoded, - ], - ]; - - $unsafe_method_bodyless_test_cases = [ - 'unsafe methods with response bodies (DELETE): client requested no format, response should have no format' => [ - ['DELETE'], - ['xml', 'json'], - FALSE, - ['Content-Type' => 'application/json'], - $json_encoded, - NULL, - '', - ], - 'unsafe methods with response bodies (DELETE): client requested format (XML), response should have no format' => [ - ['DELETE'], - ['xml', 'json'], - 'xml', - ['Content-Type' => 'application/json'], - $json_encoded, - NULL, - '', - ], - 'unsafe methods with response bodies (DELETE): client requested format (JSON), response should have no format' => [ - ['DELETE'], - ['xml', 'json'], - 'json', - ['Content-Type' => 'application/json'], - $json_encoded, - NULL, - '', - ], - ]; - - return $safe_method_test_cases + $unsafe_method_bodied_test_cases + $unsafe_method_bodyless_test_cases; - } - } /** diff --git a/core/modules/rest/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php b/core/modules/rest/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php new file mode 100644 index 0000000..bb138fe --- /dev/null +++ b/core/modules/rest/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php @@ -0,0 +1,334 @@ + 'restplugin'], ['_format' => 'json'])); + + $handler_response = new ResourceResponse($data); + $resource_response_subscriber = $this->getFunctioningResourceResponseSubscriber($route_match); + $event = new FilterResponseEvent( + $this->prophesize(HttpKernelInterface::class)->reveal(), + $request, + HttpKernelInterface::MASTER_REQUEST, + $handler_response + ); + $resource_response_subscriber->onRespond($event); + + // Content is a serialized version of the data we provided. + $this->assertEquals($expected_response !== FALSE ? $expected_response : json_encode($data), $event->getResponse()->getContent()); + } + + public function providerTestSerialization() { + return [ + // The default data for \Drupal\rest\ResourceResponse. + [NULL, ''], + [''], + ['string'], + ['Complex \ string $%^&@ with unicode ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΣὨ'], + [[]], + [['test']], + [['test' => 'foobar']], + [TRUE], + [FALSE], + // @todo Not supported. https://www.drupal.org/node/2427811 + // [new \stdClass()], + // [(object) ['test' => 'foobar']], + ]; + } + + /** + * @covers ::getResponseFormat + * + * Note this does *not* need to test formats being requested that are not + * accepted by the server, because the routing system would have already + * prevented those from reaching the controller. + * + * @dataProvider providerTestResponseFormat + */ + public function testResponseFormat($methods, array $supported_formats, $request_format, array $request_headers, $request_body, $expected_response_format, $expected_response_content_type, $expected_response_content) { + $parameters = []; + if ($request_format !== FALSE) { + $parameters['_format'] = $request_format; + } + + foreach ($request_headers as $key => $value) { + unset($request_headers[$key]); + $key = strtoupper(str_replace('-', '_', $key)); + $request_headers[$key] = $value; + } + + foreach ($methods as $method) { + $request = Request::create('/rest/test', $method, $parameters, [], [], $request_headers, $request_body); + $route_requirement_key_format = $request->isMethodSafe() ? '_format' : '_content_type_format'; + $route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => $this->randomMachineName()], [$route_requirement_key_format => implode('|', $supported_formats)])); + + $resource_response_subscriber = new ResourceResponseSubscriber( + $this->prophesize(SerializerInterface::class)->reveal(), + $this->prophesize(RendererInterface::class)->reveal(), + $route_match + ); + + $this->assertSame($expected_response_format, $resource_response_subscriber->getResponseFormat($route_match, $request)); + } + } + + /** + * @covers ::onRespond + * @covers ::getResponseFormat + * @covers ::renderResponseBody + * @covers ::flattenResponse + * + * @dataProvider providerTestResponseFormat + */ + public function testOnRespond($methods, array $supported_formats, $request_format, array $request_headers, $request_body, $expected_response_format, $expected_response_content_type, $expected_response_content) { + $rest_config_name = $this->randomMachineName(); + + $parameters = []; + if ($request_format !== FALSE) { + $parameters['_format'] = $request_format; + } + + foreach ($request_headers as $key => $value) { + unset($request_headers[$key]); + $key = strtoupper(str_replace('-', '_', $key)); + $request_headers[$key] = $value; + } + + foreach ($methods as $method) { + $request = Request::create('/rest/test', $method, $parameters, [], [], $request_headers, $request_body); + $route_requirement_key_format = $request->isMethodSafe() ? '_format' : '_content_type_format'; + $route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => $rest_config_name], [$route_requirement_key_format => implode('|', $supported_formats)])); + + + // The RequestHandler must return a ResourceResponseInterface object. + $handler_response = new ResourceResponse($method !== 'DELETE' ? ['REST' => 'Drupal'] : NULL); + $this->assertInstanceOf(ResourceResponseInterface::class, $handler_response); + $this->assertInstanceOf(CacheableResponseInterface::class, $handler_response); + + // The ResourceResponseSubscriber must then generate a response body and + // transform it to a plain (Cacheable)Response object. + $resource_response_subscriber = $this->getFunctioningResourceResponseSubscriber($route_match); + $event = new FilterResponseEvent( + $this->prophesize(HttpKernelInterface::class)->reveal(), + $request, + HttpKernelInterface::MASTER_REQUEST, + $handler_response + ); + $resource_response_subscriber->onRespond($event); + $final_response = $event->getResponse(); + $this->assertNotInstanceOf(ResourceResponseInterface::class, $final_response); + $this->assertInstanceOf(CacheableResponseInterface::class, $handler_response); + $this->assertSame($expected_response_content_type, $handler_response->headers->get('Content-Type')); + $this->assertEquals($expected_response_content, $handler_response->getContent()); + } + } + + /** + * @return \Drupal\rest\EventSubscriber\ResourceResponseSubscriber + */ + protected function getFunctioningResourceResponseSubscriber(RouteMatchInterface $route_match) { + // Create a dummy of the renderer service. + $renderer = $this->prophesize(RendererInterface::class); + $renderer->executeInRenderContext(Argument::type(RenderContext::class), Argument::type('callable')) + ->will(function ($args) { + $callable = $args[1]; + return $callable(); + }); + + // Instantiate the ResourceResponseSubscriber we will test. + $resource_response_subscriber = new ResourceResponseSubscriber( + new Serializer([], [new JsonEncoder(), new XmlEncoder()]), + $renderer->reveal(), + $route_match + ); + + return $resource_response_subscriber; + } + + /** + * @return array + * 0. methods to test + * 1. supported formats for route requirements + * 2. request format + * 3. request headers + * 4. request body + * 5. expected response format + * 6. expected response content type + * 7. expected response body + */ + public function providerTestResponseFormat() { + $json_encoded = Json::encode(['REST' => 'Drupal']); + $xml_encoded = "\nDrupal\n"; + + $safe_method_test_cases = [ + 'safe methods: client requested format (JSON)' => [ + // @todo add 'HEAD' in https://www.drupal.org/node/2752325 + ['GET'], + ['xml', 'json'], + 'json', + [], + NULL, + 'json', + 'application/json', + $json_encoded, + ], + 'safe methods: client requested format (XML)' => [ + // @todo add 'HEAD' in https://www.drupal.org/node/2752325 + ['GET'], + ['xml', 'json'], + 'xml', + [], + NULL, + 'xml', + 'text/xml', + $xml_encoded, + ], + 'safe methods: client requested no format: response should use the first configured format (JSON)' => [ + // @todo add 'HEAD' in https://www.drupal.org/node/2752325 + ['GET'], + ['json', 'xml'], + FALSE, + [], + NULL, + 'json', + 'application/json', + $json_encoded, + ], + 'safe methods: client requested no format: response should use the first configured format (XML)' => [ + // @todo add 'HEAD' in https://www.drupal.org/node/2752325 + ['GET'], + ['xml', 'json'], + FALSE, + [], + NULL, + 'xml', + 'text/xml', + $xml_encoded, + ], + ]; + + $unsafe_method_bodied_test_cases = [ + 'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (JSON)' => [ + ['POST', 'PATCH'], + ['xml', 'json'], + FALSE, + ['Content-Type' => 'application/json'], + $json_encoded, + 'json', + 'application/json', + $json_encoded, + ], + 'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (XML)' => [ + ['POST', 'PATCH'], + ['xml', 'json'], + FALSE, + ['Content-Type' => 'text/xml'], + $xml_encoded, + 'xml', + 'text/xml', + $xml_encoded, + ], + 'unsafe methods with response (POST, PATCH): client requested format other than request body format (JSON): response format should use requested format (XML)' => [ + ['POST', 'PATCH'], + ['xml', 'json'], + 'xml', + ['Content-Type' => 'application/json'], + $json_encoded, + 'xml', + 'text/xml', + $xml_encoded, + ], + 'unsafe methods with response (POST, PATCH): client requested format other than request body format (XML), but is allowed for the request body (JSON)' => [ + ['POST', 'PATCH'], + ['xml', 'json'], + 'json', + ['Content-Type' => 'text/xml'], + $xml_encoded, + 'json', + 'application/json', + $json_encoded, + ], + ]; + + $unsafe_method_bodyless_test_cases = [ + 'unsafe methods with response bodies (DELETE): client requested no format, response should have no format' => [ + ['DELETE'], + ['xml', 'json'], + FALSE, + ['Content-Type' => 'application/json'], + NULL, + 'xml', + NULL, + '', + ], + 'unsafe methods with response bodies (DELETE): client requested format (XML), response should have no format' => [ + ['DELETE'], + ['xml', 'json'], + 'xml', + ['Content-Type' => 'application/json'], + NULL, + 'xml', + NULL, + '', + ], + 'unsafe methods with response bodies (DELETE): client requested format (JSON), response should have no format' => [ + ['DELETE'], + ['xml', 'json'], + 'json', + ['Content-Type' => 'application/json'], + NULL, + 'json', + NULL, + '', + ], + ]; + + return $safe_method_test_cases + $unsafe_method_bodied_test_cases + $unsafe_method_bodyless_test_cases; + } + +} + +/** + * Stub class where we can prophesize methods. + */ +class StubRequestHandlerResourcePlugin extends ResourceBase { + + function get() {} + function post() {} + function patch() {} + function delete() {} + +}