diff options
-rw-r--r-- | composer.json | 3 | ||||
-rw-r--r-- | src/Controller.php | 324 | ||||
-rw-r--r-- | src/Controller/RouteAction.php | 117 | ||||
-rw-r--r-- | tests/Controller/RouteActionTest.php | 110 | ||||
-rw-r--r-- | tests/ControllerTest.php | 571 | ||||
-rw-r--r-- | tests/support/TestController.php | 50 |
6 files changed, 1152 insertions, 23 deletions
diff --git a/composer.json b/composer.json index 9aaffb9..fab78e9 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,8 @@ }, "require": { "php": ">=5.6.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0", + "jasny/php-functions": "^2.0" }, "require-dev": { "jasny/php-code-quality": "^2.0" diff --git a/src/Controller.php b/src/Controller.php index c480a07..057653e 100644 --- a/src/Controller.php +++ b/src/Controller.php @@ -23,6 +23,26 @@ abstract class Controller protected $response = null; /** + * Common input and output formats with associated MIME + * @var array + */ + protected $contentFormats = [ + 'text/html' => 'html', + 'application/json' => 'json', + 'application/xml' => 'xml', + 'text/xml' => 'xml', + 'text/plain' => 'text', + 'application/javascript' => 'js', + 'text/css' => 'css', + 'image/png' => 'png', + 'image/gif' => 'gif', + 'image/jpeg' => 'jpeg', + 'image/x-icon' => 'ico', + 'application/x-www-form-urlencoded' => 'post', + 'multipart/form-data' => 'post' + ]; + + /** * Run the controller * * @return ResponseInterface @@ -65,6 +85,205 @@ abstract class Controller } /** + * Set the headers with HTTP status code and content type. + * @link http://en.wikipedia.org/wiki/List_of_HTTP_status_codes + * + * Examples: + * <code> + * $this->responseWith(200, 'json'); + * $this->responseWith(200, 'application/json'); + * $this->responseWith(204); + * $this->responseWith("204 Created"); + * $this->responseWith('json'); + * </code> + * + * @param int $code HTTP status code (may be omitted) + * @param string|array $format Mime or content format + * @return ResponseInterface $response + */ + public function responseWith($code, $format = null) + { + $response = $this->getResponse(); + + // Shift arguments if $code is omitted + if (!is_int($code) && !preg_match('/^\d{3}\b/', $code)) { + list($code, $format) = array_merge([null], func_get_args()); + } + + if ($code) { + $response = $response->withStatus((int)$code); + } + + if ($format) { + $contentType = $this->getContentType($format); + $response = $response->withHeader('Content-Type', $contentType); + } + + return $response; + } + + /** + * Response with success 200 code + * + * @return ResponseInterface $response + */ + public function ok() + { + return $this->responseWith(200); + } + + /** + * Response with created 201 code, and optionaly redirect to created location + * + * @param string $location Url of created resource + * @return ResponseInterface $response + */ + public function created($location = '') + { + $response = $this->responseWith(201); + + if ($location) { + $response = $response->withHeader('Location', $location); + } + + return $response; + } + + /** + * Response with 204 'No Content' + * + * @return ResponseInterface $response + */ + public function noContent() + { + return $this->responseWith(204); + } + + /** + * Redirect to url + * + * @param string $url + * @param int $code 301 (Moved Permanently), 303 (See Other) or 307 (Temporary Redirect) + * @return ResponseInterface $response + */ + public function redirect($url, $code = 303) + { + $response = $this->responseWith($code, 'html'); + $response = $response->withHeader('Location', $url); + $response->getBody()->write('You are being redirected to <a href="' . $url . '">' . $url . '</a>'); + + return $response; + } + + /** + * Redirect to previous page, or to home page + * + * @return ResponseInterface $response + */ + public function back() + { + return $this->redirect($this->getLocalReferer() ?: '/'); + } + + /** + * Route to 401 + * Note: While the 401 route is used, we don't respond with a 401 http status code. + * + * @return ResponseInterface $response + */ + public function requireLogin() + { + return $this->redirect('/401'); + } + + /** + * Alias of requireLogin + * + * @return ResponseInterface $response + */ + public function requireAuth() + { + return $this->requireLogin(); + } + + /** + * Set response to error 'Bad Request' state + * + * @param string $message + * @param int $code HTTP status code + * @return ResponseInterface $response + */ + public function badRequest($message, $code = 400) + { + return $this->error($message, $code); + } + + /** + * Set response to error 'Forbidden' state + * + * @param string $message + * @param int $code HTTP status code + * @return ResponseInterface $response + */ + public function forbidden($message, $code = 403) + { + return $this->error($message, $code); + } + + /** + * Set response to error 'Not Found' state + * + * @param string $message + * @param int $code HTTP status code + * @return ResponseInterface $response + */ + public function notFound($message, $code = 404) + { + return $this->error($message, $code); + } + + /** + * Set response to error 'Conflict' state + * + * @param string $message + * @param int $code HTTP status code + * @return ResponseInterface $response + */ + public function conflict($message, $code = 409) + { + return $this->error($message, $code); + } + + /** + * Set response to error 'Too Many Requests' state + * + * @param string $message + * @param int $code HTTP status code + * @return ResponseInterface $response + */ + public function tooManyRequests($message, $code = 429) + { + return $this->error($message, $code); + } + + /** + * Set response to error state + * + * @param string $message + * @param int $code HTTP status code + * @return ResponseInterface $response + */ + public function error($message, $code = 400) + { + $response = $this->getResponse(); + + $errorResponse = $response->withStatus($code); + $errorResponse->getBody()->write($message); + + return $errorResponse; + } + + /** * Check if response is 2xx succesful, or empty * * @return boolean @@ -173,15 +392,116 @@ abstract class Controller } /** + * Returns the HTTP referer if it is on the current host + * + * @return string + */ + public function getLocalReferer() + { + $request = $this->getRequest(); + $referer = $request->getHeaderLine('HTTP_REFERER'); + $host = $request->getHeaderLine('HTTP_HOST'); + + return $referer && parse_url($referer, PHP_URL_HOST) === $host ? $referer : ''; + } + + /** + * Output result + * + * @param mixed $data + * @param string $format + * @return ResponseInterface $response + */ + public function output($data, $format) + { + $response = $this->getResponse(); + $contentType = $this->getContentType($format); + $response = $response->withHeader('Content-Type', $contentType); + $content = is_scalar($data) ? $data : $this->encodeData($data, $format); + + $response->getBody()->write($content); + + return $response; + } + + /** + * Encode data to send to client + * + * @param mixed $data + * @param string $format + * @return string + */ + public function encodeData($data, $format) + { + switch ($format) { + case 'json': return $this->encodeDataAsJson($data); + case 'xml': return $this->encodeDataAsXml($data); + case 'html': + throw new \InvalidArgumentException("To encode HTML please use a view"); + default: + throw new \InvalidArgumentException("Can not encode data for format '$format'"); + } + } + + /** + * Encode data as xml + * + * @param \SimpleXMLElement $data + * @return string + */ + protected function encodeDataAsXml(\SimpleXMLElement $data) + { + return $data->asXML(); + } + + /** + * Encode data as json + * + * @param mixed + * @return string + */ + protected function encodeDataAsJson($data) + { + $data = json_encode($data); + + return $this->isJsonp() ? + $this->getRequest()->getQueryParams()['callback'] . '(' . $data . ')' : + $data; + } + + /** + * Check if we should respond with jsonp + * + * @return boolean + */ + protected function isJsonp() + { + $request = $this->getRequest(); + + return $request && !empty($request->getQueryParams()['callback']); + } + + /** * Get status code of response * * @return int */ protected function getResponseStatusCode() { - $response = $this->getResponse(); + $response = $this->getResponse(); + + return $response ? $response->getStatusCode() : 0; + } - return $response ? $response->getStatusCode() : 0; + /** + * Get valid content type by simple word description + * + * @param string $format + * @return string + */ + protected function getContentType($format) + { + return array_search($format, $this->contentFormats) ?: $format; } /** diff --git a/src/Controller/RouteAction.php b/src/Controller/RouteAction.php new file mode 100644 index 0000000..dabd501 --- /dev/null +++ b/src/Controller/RouteAction.php @@ -0,0 +1,117 @@ +<?php + +namespace Jasny\Controller; + +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; + +/** + * Execute controller on given route + */ +trait RouteAction +{ + /** + * Get request, set for controller + * + * @return ServerRequestInterface + */ + abstract public function getRequest(); + + /** + * Get response. set for controller + * + * @return ResponseInterface + */ + abstract public function getResponse(); + + /** + * Run the controller + * + * @return ResponseInterface + */ + public function run() { + $request = $this->getRequest(); + $route = $request->getAttribute('route'); + $method = $this->getActionMethod(isset($route->action) ? $route->action : 'default'); + + if (!method_exists($this, $method)) { + return $this->setResponseError(404, 'Not Found'); + } + + try { + $args = isset($route->args) ? + $route->args : + $this->getFunctionArgs($route, new \ReflectionMethod($this, $method)); + } catch (\RuntimeException $e) { + return $this->setResponseError(400, 'Bad Request'); + } + + $response = call_user_func_array([$this, $method], $args); + + return $response ?: $this->getResponse(); + } + + /** + * Get the method name of the action + * + * @param string $action + * @return string + */ + protected function getActionMethod($action) + { + return \Jasny\camelcase($action) . 'Action'; + } + + /** + * Get the arguments for a function from a route using reflection + * + * @param object $route + * @param \ReflectionFunctionAbstract $refl + * @return array + */ + protected function getFunctionArgs($route, \ReflectionFunctionAbstract $refl) + { + $args = []; + $params = $refl->getParameters(); + + foreach ($params as $param) { + $key = $param->name; + + if (property_exists($route, $key)) { + $value = $route->{$key}; + } else { + if (!$param->isOptional()) { + $fn = $refl instanceof \ReflectionMethod + ? $refl->class . ':' . $refl->name + : $refl->name; + + throw new \RuntimeException("Missing argument '$key' for $fn()"); + } + + $value = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null; + } + + $args[$key] = $value; + } + + return $args; + } + + /** + * Set response to error state + * + * @param int $code + * @param string $message + * @return ResponseInterface + */ + protected function setResponseError($code, $message) + { + $response = $this->getResponse(); + + $errorResponse = $response->withStatus($code); + $errorResponse->getBody()->write($message); + + return $errorResponse; + } +} + diff --git a/tests/Controller/RouteActionTest.php b/tests/Controller/RouteActionTest.php new file mode 100644 index 0000000..edc4bd7 --- /dev/null +++ b/tests/Controller/RouteActionTest.php @@ -0,0 +1,110 @@ +<?php + +require_once dirname(__DIR__) . '/support/TestController.php'; + +use Jasny\Controller\RouteAction; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; + +/** + * @covers Jasny\Controller\RouteAction + */ +class RouteActionTest extends PHPUnit_Framework_TestCase +{ + /** + * Test running controller action + * + * @dataProvider runPositiveProvider + * @param object $route + */ + public function testRunPositive($route) + { + $controller = new TestController(); + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $request->method('getAttribute')->with($this->equalTo('route'))->will($this->returnValue($route)); + + $result = $controller($request, $response); + $args = !empty($route->args) ? $route->args : [$route->param1, isset($route->param2) ? $route->param2 : 'defaultValue']; + + $this->assertEquals(get_class($response), get_class($result), "Controller should return 'ResponseInterface' instance"); + $this->assertEquals($args[0], $result->param1, "First route parameter was not passed correctly"); + $this->assertEquals($args[1], $result->param2, "Second route parameter was not passed correctly"); + + if (isset($route->action)) { + $this->assertTrue($result->actionCalled, "Controller action was not called"); + $this->assertFalse(isset($result->defaultActionCalled), "Controller default action was called"); + } else { + $this->assertTrue($result->defaultActionCalled, "Controller default action was not called"); + $this->assertFalse(isset($result->actionCalled), "Controller non-default action was called"); + } + } + + /** + * Provide data for testing run method + */ + public function runPositiveProvider() + { + return [ + [(object)['controller' => 'TestController', 'param1' => 'value1']], + [(object)['controller' => 'TestController', 'param1' => 'value1', 'param2' => 'value2']], + [(object)['controller' => 'TestController', 'args' => ['value1', 'value2']]], + [(object)['controller' => 'TestController', 'action' => 'test-run', 'param1' => 'value1']], + [(object)['controller' => 'TestController', 'action' => 'test-run', 'param1' => 'value1', 'param2' => 'value2']], + [(object)['controller' => 'TestController', 'action' => 'test-run', 'args' => ['value1', 'value2']]] + ]; + } + + /** + * Test running controller action + * + * @dataProvider runNegativeProvider + * @param object $route + * @param int $errorCode + * @param string $errorMessage + */ + public function testRunNegative($route, $errorCode, $errorMessage) + { + $controller = new TestController(); + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $request->method('getAttribute')->with($this->equalTo('route'))->will($this->returnValue($route)); + + $this->expectResponseError($response, $errorCode, $errorMessage); + + $result = $controller($request, $response); + + $this->assertEquals(get_class($response), get_class($result), "Controller should return 'ResponseInterface' instance"); + } + + /** + * Provide data for testing run method + */ + public function runNegativeProvider() + { + return [ + [(object)['controller' => 'TestController', 'action' => 'nonExistMethod'], 404, 'Not Found'], + [(object)['controller' => 'TestController', 'action' => 'test-run'], 400, 'Bad Request'], + [(object)['controller' => 'TestController', 'action' => 'test-run', 'param2' => 'value2'], 400, 'Bad Request'] + ]; + } + + /** + * Expect that response will be set to error state + * + * @param ResponseInterface $response + * @param int $code + * @param string $message + */ + public function expectResponseError($response, $code, $message) + { + $stream = $this->createMock(StreamInterface::class); + $stream->expects($this->once())->method('write')->with($this->equalTo($message)); + + $response->expects($this->once())->method('getBody')->will($this->returnValue($stream)); + $response->expects($this->once())->method('withStatus')->with($this->equalTo($code))->will($this->returnSelf()); + } +} diff --git a/tests/ControllerTest.php b/tests/ControllerTest.php index 72c2f79..e0ebdc2 100644 --- a/tests/ControllerTest.php +++ b/tests/ControllerTest.php @@ -3,6 +3,7 @@ use Jasny\Controller; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; /** * @covers Jasny\Controller @@ -43,7 +44,7 @@ class ControllerTest extends PHPUnit_Framework_TestCase * Test functions that check response status code * * @dataProvider responseStatusProvider - * @param int $statusCode + * @param int status code */ public function testResponseStatus($code) { @@ -59,7 +60,8 @@ class ControllerTest extends PHPUnit_Framework_TestCase $this->assertEquals($value, $controller->$func(), "Method '$func' returns incorrect value"); } - $this->assertEquals($data['isClientError'] || $data['isServerError'], $controller->isError(), "Method 'isError' returns incorrect value"); + $this->assertEquals($data['isClientError'] || $data['isServerError'], $controller->isError() + , "Method 'isError' returns incorrect value"); } /** @@ -110,48 +112,544 @@ class ControllerTest extends PHPUnit_Framework_TestCase ['GET'], ['POST'], ['PUT'], ['DELETE'], ['HEAD'] ]; } + + /** + * Test encodeData method, positive tests + * + * @dataProvider encodeDataPositiveProvider + * @param mixed $data + * @param string $format + * @param string $callback Callback name for testing jsonp request + */ + public function testEncodeDataPositive($data, $format, $callback = null) + { + $controller = $this->getController(['getRequest']); + list($request) = $this->getRequests(); + + if ($callback) { + $request->method('getQueryParams')->will($this->returnValue(['callback' => $callback])); + } + + $controller->method('getRequest')->will($this->returnValue($request)); + + $result = $controller->encodeData($data, $format); + $expect = null; + + if ($format === 'json') { + $expect = json_encode($data); + + if ($callback) $expect = "$callback($expect)"; + } else { + $expect = $data->asXML(); + } + + $this->assertNotEmpty($result, "Result should not be empty"); + $this->assertEquals($expect, $result, "Data was not encoded correctly"); + } /** - * Get map of status codes to states + * Provide data for testing encodeData method + * + * @return array + */ + public function encodeDataPositiveProvider() + { + $xml = simplexml_load_string( + "<?xml version='1.0'?> + <document> + <tag1>Test tag</tag1> + <tag2>Test</tag2> + </document>" + ); + + return [ + ['test_string', 'json'], + [['testKey' => 'testValue'], 'json'], + [['testKey' => 'testValue'], 'json', 'test_callback'], + ['', 'json'], + ['', 'json', 'test_callback'], + [$xml, 'xml'] + ]; + } + + /** + * Test encodeData method, negative tests + * + * @dataProvider encodeDataNegativeProvider + * @param mixed $data + * @param string $format + */ + public function testEncodeDataNegative($data, $format) + { + $controller = $this->getController(['getRequest']); + list($request) = $this->getRequests(); + + $controller->method('getRequest')->will($this->returnValue($request)); + $this->expectException(\InvalidArgumentException::class); + + $result = $controller->encodeData($data, $format); + } + + /** + * Provide data for testing encodeData method + * + * @return array + */ + public function encodeDataNegativeProvider() + { + return [ + ['test_string', 'html'], + ['test_string', 'jpg'] + ]; + } + + /** + * Test output + * + * @dataProvider outputProvider + * @param mixed $data + * @param string $format + * @param string $contentType + * @param string $callback Callback name for testing jsonp request + */ + public function testOutput($data, $format, $contentType, $callback = '') + { + $controller = $this->getController(['getRequest', 'getResponse']); + list($request, $response) = $this->getRequests(); + + if (is_scalar($data)) { + $content = $data; + } elseif ($format === 'json') { + $content = json_encode($data); + + if ($callback) $content = "$callback($content)"; + } elseif ($format === 'xml') { + $content = $data->asXML(); + } + + $this->expectOutput($response, $content, $contentType); + + if ($callback) { + $request->method('getQueryParams')->will($this->returnValue(['callback' => $callback])); + } + + $controller->method('getRequest')->will($this->returnValue($request)); + $controller->method('getResponse')->will($this->returnValue($response)); + + $result = $controller->output($data, $format); + + $this->assertEquals($result, $response, "Output should return response instance"); + } + + /** + * Provide data for testing output + * + * @return array + */ + public function outputProvider() + { + $xml = simplexml_load_string( + "<?xml version='1.0'?> + <document> + <tag1>Test tag</tag1> + <tag2>Test</tag2> + </document>" + ); + + return [ + ['test_string', 'text', 'text/plain'], + ['javascript:test_call();', 'js', 'application/javascript'], + ['test {}', 'css', 'text/css'], + ['test_string', 'json', 'application/json'], + [['testKey' => 'testValue'], 'json', 'application/json'], + [['testKey' => 'testValue'], 'json', 'application/json', 'test_callback'], + ['', 'json', 'application/json'], + ['', 'json', 'application/json', 'test_callback'], + [$xml, 'xml', 'application/xml'] + ]; + } + + /** + * Test functions that deal with error messages + * + * @dataProvider errorMessagesProvider + * @param string $function + * @param int $code + * @param boolean $default Is code default for this function + */ + public function testErrorMessages($function, $code, $default) + { + $message = 'Test message'; + $controller = $this->getController(['getResponse']); + list(, $response) = $this->getRequests(); + + $this->expectErrorMessage($response, $message, $code); + $controller->method('getResponse')->will($this->returnValue($response)); + + $result = $default ? + $controller->{$function}($message) : + $controller->{$function}($message, $code); + + $this->assertEquals($result, $response, "Response object should be returned"); + } + + /** + * Provide data for testing error messages functions + * + * @return array + */ + public function errorMessagesProvider() + { + return [ + ['error', 400, true], + ['error', 403, false], + ['tooManyRequests', 429, true], + ['tooManyRequests', 400, false], + ['conflict', 409, true], + ['conflict', 403, false], + ['notFound', 404, true], + ['notFound', 400, false], + ['forbidden', 403, true], + ['forbidden', 409, false], + ['badRequest', 400, true], + ['badRequest', 403, false] + ]; + } + + /** + * Test responseWith function + * + * @dataProvider responseWithProvider + * @param int|string $code + * @param string $format + * @param int $setCode Actual code that will be set in response + * @param string $contentType + */ + public function testResponseWith($code, $format, $setCode, $contentType) + { + $controller = $this->getController(['getResponse']); + list(, $response) = $this->getRequests(); + + $this->expectResponseWith($response, $setCode, $contentType); + $controller->method('getResponse')->will($this->returnValue($response)); + + $result = $controller->responseWith($code, $format); + + $this->assertEquals($result, $response, "Response object should be returned"); + } + + /** + * Test function responseWith * + * @return array + */ + public function responseWithProvider() + { + return [ + [200, 'json', 200, 'application/json'], + [200, 'application/json', 200, 'application/json'], + [204, null, 204, null], + ['204 Created', null, 204, null], + ['json', null, null, 'application/json'] + ]; + } + + /** + * Test functions that are simple wrappers around responseWith function + * + * @dataProvider responseWithWrappersProvider + * @param string $functino * @param int $code - * @return [] */ - public function getStatusCodesMap($code) + public function testResponseWithWrappers($function, $code) + { + $controller = $this->getController(['getResponse']); + list(, $response) = $this->getRequests(); + + $this->expectResponseWith($response, $code); + $controller->method('getResponse')->will($this->returnValue($response)); + + $result = $controller->{$function}(); + + $this->assertEquals($result, $response, "Response object should be returned"); + } + + /** + * Provide data for testing responseWith wrappers + * + * @return array + */ + public function responseWithWrappersProvider() { return [ - 'isSuccessful' => !$code || ($code >= 200 && $code < 300), - 'isRedirection' => $code >= 300 && $code < 400, - 'isClientError' => $code >= 400 && $code < 500, - 'isServerError' => $code >= 500 + ['ok', 200], + ['noContent', 204] ]; } /** - * Get map of request methods + * Test 'created' function + * + * @dataProvider createdProvider + * @param string $location + */ + public function testCreated($location) + { + $controller = $this->getController(['getResponse']); + list(, $response) = $this->getRequests(); + + $response->expects($this->once())->method('withStatus')->with($this->equalTo(201))->will($this->returnSelf()); + if ($location) { + $response->expects($this->once())->method('withHeader')->with($this->equalTo('Location'), $this->equalTo($location))->will($this->returnSelf()); + } + + $controller->method('getResponse')->will($this->returnValue($response)); + + $result = $controller->created($location); + + $this->assertEquals($result, $response, "Response object should be returned"); + } + + /** + * Provide data for testing 'created' function * - * @param string $method * @return array */ - public function getMethodsMap($method) + public function createdProvider() { return [ - 'isGetRequest' => $method === 'GET', - 'isPostRequest' => $method === 'POST', - 'isPutRequest' => $method === 'PUT', - 'isDeleteRequest' => $method === 'DELETE', - 'isHeadRequest' => $method === 'HEAD' + [''], ['/some-path/test'] ]; } /** - * Get controller instance + * Test 'redirect' function * + * @dataProvider redirectProvider + * @param string $url + * @param int $code + * @param boolean $default + */ + public function testRedirect($url, $code, $default) + { + $controller = $this->getController(['getResponse']); + list(, $response) = $this->getRequests(); + + $this->expectRedirect($response, $url, $code); + $controller->method('getResponse')->will($this->returnValue($response)); + + $result = $default ? + $controller->redirect($url) : + $controller->redirect($url, $code); + + $this->assertEquals($result, $response, "Response object should be returned"); + } + + /** + * Provide data for testing 'redirect' function + * + * @return array + */ + public function redirectProvider() + { + return [ + ['/test-url', 303, true], + ['/test-url', 301, false] + ]; + } + + /** + * Test 'requireLogin' function + * + * @dataProvider requireLoginProvider + * @param string $function + */ + public function testRequireLogin($function) + { + $controller = $this->getController(['getResponse']); + list(, $response) = $this->getRequests(); + + $this->expectRedirect($response, '/401', 303); + $controller->method('getResponse')->will($this->returnValue($response)); + + $result = $controller->{$function}(); + + $this->assertEquals($result, $response, "Response object should be returned"); + } + + /** + * Provide data for testing 'requireLogon' function + * + * @return array + */ + public function requireLoginProvider() + { + return [ + ['requireLogin'], ['requireAuth'] + ]; + } + + /** + * Test 'getLocalReferer' funtion + * + * @dataProvider localRefererProvider + * @param string $referer + * @param string $host + * @param boolean $local + */ + public function testLocalReferer($referer, $host, $local) + { + $controller = $this->getController(['getRequest']); + list($request) = $this->getRequests(); + + $this->expectLocalReferer($request, $referer, $host); + $controller->method('getRequest')->will($this->returnValue($request)); + + $result = $controller->getLocalReferer(); + + $local ? + $this->assertEquals($referer, $result, "Local referer should be returned") : + $this->assertEquals('', $result, "Local referer should not be returned"); + } + + /** + * Test 'back' function + * + * @dataProvider localRefererProvider + * @param string $referer + * @param string $host + * @param boolean $local + */ + public function testBack($referer, $host, $local) + { + $controller = $this->getController(['getRequest', 'getResponse']); + list($request, $response) = $this->getRequests(); + + $this->expectLocalReferer($request, $referer, $host); + $this->expectRedirect($response, $local ? $referer : '/', 303); + + $controller->method('getRequest')->will($this->returnValue($request)); + $controller->method('getResponse')->will($this->returnValue($response)); + + $result = $controller->back(); + + $this->assertEquals($result, $response, "Response object should be returned"); + } + + /** + * Provide data fot testing 'getLocalReferer' function + * + * @return array + */ + public function localRefererProvider() + { + return [ + ['http://not-local-host.com/path', 'local-host.com', false], + ['http://local-host.com/path', 'local-host.com', true] + ]; + } + + /** + * Expect for 'getLocalReferer' function to work correctly + * + * @param ServerRequestInterface $request + * @param string $referer + * @param string $host + */ + public function expectLocalReferer($request, $referer, $host) + { + $request->expects($this->exactly(2))->method('getHeaderLine')->withConsecutive( + [$this->equalTo('HTTP_REFERER')], + [$this->equalTo('HTTP_HOST')] + )->will($this->returnCallback(function($header) use ($referer, $host) { + return $header === 'HTTP_REFERER' ? $referer : $host; + })); + } + + /** + * Expect for redirect + * + * @param ResponseInterface $response + * @param string $url + * @param int $code + */ + public function expectRedirect($response, $url, $code) + { + $stream = $this->createMock(StreamInterface::class); + $stream->expects($this->once())->method('write')->with($this->equalTo('You are being redirected to <a href="' . $url . '">' . $url . '</a>')); + + $response->expects($this->once())->method('getBody')->will($this->returnValue($stream)); + $response->expects($this->once())->method('withStatus')->with($this->equalTo($code))->will($this->returnSelf()); + $response->expects($this->exactly(2))->method('withHeader')->withConsecutive( + [$this->equalTo('Content-Type'), $this->equalTo('text/html')], + [$this->equalTo('Location'), $this->equalTo($url)] + )->will($this->returnSelf()); + } + + /** + * Expect correct work of responseWith function + * + * @param ResponseInterface $response + * @param int $code + * @param string $contentType + */ + public function expectResponseWith($response, $code, $contentType = null) + { + $code ? + $response->expects($this->once())->method('withStatus')->with($this->equalTo($code))->will($this->returnSelf()) : + $response->expects($this->never())->method('withStatus')->with($this->equalTo($code)); + + $contentType ? + $response->expects($this->once())->method('withHeader')->with($this->equalTo('Content-Type'), $this->equalTo($contentType))->will($this->returnSelf()) : + $response->expects($this->never())->method('withHeader')->with($this->equalTo('Content-Type'), $this->equalTo($contentType)); + } + + /** + * Expect for correct work of error message functions + * + * @param ResponseInterface $response + * @param string $message + * @param int $code + */ + public function expectErrorMessage($response, $message, $code) + { + $stream = $this->createMock(StreamInterface::class); + $stream->expects($this->once())->method('write')->with($this->equalTo($message)); + + $response->expects($this->once())->method('withStatus')->with($this->equalTo($code))->will($this->returnSelf()); + $response->expects($this->once())->method('getBody')->will($this->returnValue($stream)); + } + + /** + * Expects that output will be set to content + * + * @param ResponseInterface $response + * @param string $content + * @param string $contentType + */ + public function expectOutput($response, $content, $contentType) + { + $stream = $this->createMock(StreamInterface::class); + $stream->expects($this->once())->method('write')->with($this->equalTo($content)); + + $response->expects($this->once())->method('withHeader')->with($this->equalTo('Content-Type'), $this->equalTo($contentType))->will($this->returnSelf()); + $response->expects($this->once())->method('getBody')->will($this->returnValue($stream)); + } + + /** + * Get mock for controller + * + * @param array $methods Methods to mock * @return Controller */ - public function getController() + public function getController($methods = []) { - return $this->getMockBuilder(Controller::class)->disableOriginalConstructor()->getMockForAbstractClass(); + $builder = $this->getMockBuilder(Controller::class)->disableOriginalConstructor(); + if ($methods) { + $builder->setMethods($methods); + } + + return $builder->getMockForAbstractClass(); } /** @@ -166,4 +664,37 @@ class ControllerTest extends PHPUnit_Framework_TestCase $this->createMock(ResponseInterface::class) ]; } + + /** + * Get map of status codes to states + * + * @param int $code + * @return [] + */ + public function getStatusCodesMap($code) + { + return [ + 'isSuccessful' => !$code || ($code >= 200 && $code < 300), + 'isRedirection' => $code >= 300 && $code < 400, + 'isClientError' => $code >= 400 && $code < 500, + 'isServerError' => $code >= 500 + ]; + } + + /** + * Get map of request methods + * + * @param string $method + * @return array + */ + public function getMethodsMap($method) + { + return [ + 'isGetRequest' => $method === 'GET', + 'isPostRequest' => $method === 'POST', + 'isPutRequest' => $method === 'PUT', + 'isDeleteRequest' => $method === 'DELETE', + 'isHeadRequest' => $method === 'HEAD' + ]; + } } diff --git a/tests/support/TestController.php b/tests/support/TestController.php new file mode 100644 index 0000000..e0bdb7b --- /dev/null +++ b/tests/support/TestController.php @@ -0,0 +1,50 @@ +<?php + +use Jasny\Controller; +use Jasny\Controller\RouteAction; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; + +/** + * Class for testing 'RouteAction' trait + */ +class TestController extends Controller +{ + use RouteAction; + + /** + * Test action for executing router + * + * @param mixed $param1 + * @param mixed $param2 + * @return ResponseInterface + */ + public function testRunAction($param1, $param2 = 'defaultValue') + { + $response = $this->getResponse(); + + $response->actionCalled = true; + $response->param1 = $param1; + $response->param2 = $param2; + + return $response; + } + + /** + * Test action for executing router + * + * @param mixed $param1 + * @param mixed $param2 + * @return ResponseInterface + */ + public function defaultAction($param1, $param2 = 'defaultValue') + { + $response = $this->getResponse(); + + $response->defaultActionCalled = true; + $response->param1 = $param1; + $response->param2 = $param2; + + return $response; + } +} |