summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--composer.json3
-rw-r--r--src/Controller.php324
-rw-r--r--src/Controller/RouteAction.php117
-rw-r--r--tests/Controller/RouteActionTest.php110
-rw-r--r--tests/ControllerTest.php571
-rw-r--r--tests/support/TestController.php50
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;
+ }
+}