From b0975ec5dba9aad0b9076b1621ae917c8779fa33 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Tue, 8 May 2018 00:02:34 -0500 Subject: [PATCH 1/9] Initial work on new feature testing features. --- system/CodeIgniter.php | 48 ++- system/Config/Services.php | 1 + system/HTTP/IncomingRequest.php | 2 +- system/HTTP/Request.php | 105 +++++-- system/HTTP/Response.php | 329 +++++++++++++++------ system/Router/Router.php | 2 + system/Test/CIDatabaseTestCase.php | 2 + system/Test/CIUnitTestCase.php | 80 ++++- system/Test/FeatureResponse.php | 292 ++++++++++++++++++ system/Test/FeatureTestCase.php | 251 ++++++++++++++++ system/bootstrap.php | 44 ++- tests/_support/_bootstrap.php | 95 +----- tests/feature/HomeTest.php | 14 + tests/system/Autoloader/AutoloaderTest.php | 2 +- tests/system/Events/EventsTest.php | 2 +- tests/system/HTTP/IncomingRequestTest.php | 236 --------------- tests/system/HTTP/RequestTest.php | 238 ++++++++++++++- tests/system/HTTP/ResponseTest.php | 31 ++ tests/system/Test/FeatureResponseTest.php | 209 +++++++++++++ tests/system/Test/FeatureTestCaseTest.php | 104 +++++++ 20 files changed, 1601 insertions(+), 486 deletions(-) create mode 100644 system/Test/FeatureResponse.php create mode 100644 system/Test/FeatureTestCase.php create mode 100644 tests/feature/HomeTest.php create mode 100644 tests/system/Test/FeatureResponseTest.php create mode 100644 tests/system/Test/FeatureTestCaseTest.php diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 01b638e242..86cb2ea2e8 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -36,6 +36,7 @@ * @filesource */ use CodeIgniter\HTTP\RedirectResponse; +use CodeIgniter\HTTP\Request; use Config\Services; use Config\Cache; use CodeIgniter\HTTP\URI; @@ -180,8 +181,12 @@ class CodeIgniter * makes all of the pieces work together. * * @param \CodeIgniter\Router\RouteCollectionInterface $routes + * @param bool $returnResponse + * + * @throws \CodeIgniter\HTTP\RedirectException + * @throws \Exception */ - public function run(RouteCollectionInterface $routes = null) + public function run(RouteCollectionInterface $routes = null, bool $returnResponse = false) { $this->startBenchmark(); @@ -201,7 +206,7 @@ class CodeIgniter try { - $this->handleRequest($routes, $cacheConfig); + return $this->handleRequest($routes, $cacheConfig, $returnResponse); } catch (Router\RedirectException $e) { $logger = Services::logger(); @@ -225,8 +230,11 @@ class CodeIgniter * * @param \CodeIgniter\Router\RouteCollectionInterface $routes * @param $cacheConfig + * @param bool $returnResponse + * + * @throws \CodeIgniter\Filters\Exceptions\FilterException */ - protected function handleRequest(RouteCollectionInterface $routes = null, $cacheConfig) + protected function handleRequest(RouteCollectionInterface $routes = null, $cacheConfig, bool $returnResponse = false) { $this->tryToRouteIt($routes); @@ -257,6 +265,11 @@ class CodeIgniter // Handle any redirects if ($returned instanceof RedirectResponse) { + if ($returnResponse) + { + return $returned; + } + $this->callExit(EXIT_SUCCESS); } @@ -279,12 +292,17 @@ class CodeIgniter unset($uri); - $this->sendResponse(); + if (! $returnResponse) + { + $this->sendResponse(); + } //-------------------------------------------------------------------- // Is there a post-system event? //-------------------------------------------------------------------- Events::trigger('post_system'); + + return $this->response; } //-------------------------------------------------------------------- @@ -358,6 +376,23 @@ class CodeIgniter //-------------------------------------------------------------------- + /** + * Sets a Request object to be used for this request. + * Used when running certain tests. + * + * @param \CodeIgniter\HTTP\Request $request + * + * @return \CodeIgniter\CodeIgniter + */ + public function setRequest(Request $request) + { + $this->request = $request; + + return $this; + } + + //-------------------------------------------------------------------- + /** * Get our Request object, (either IncomingRequest or CLIRequest) * and set the server protocol based on the information provided @@ -365,6 +400,11 @@ class CodeIgniter */ protected function getRequestObject() { + if ($this->request instanceof Request) + { + return; + } + if (is_cli() && ! (ENVIRONMENT == 'testing')) { $this->request = Services::clirequest($this->config); diff --git a/system/Config/Services.php b/system/Config/Services.php index bb841aab59..34cbe76489 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -37,6 +37,7 @@ */ use CodeIgniter\Database\ConnectionInterface; use CodeIgniter\Database\MigrationRunner; +use CodeIgniter\HTTP\URI; use CodeIgniter\View\RendererInterface; /** diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 8e2a887f93..b07294d75c 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -616,7 +616,7 @@ class IncomingRequest extends Request break; case 'PATH_INFO': default: - $path = $_SERVER[$protocol] ?? $this->parseRequestURI(); + $path = $this->fetchGlobal('server', $protocol) ?? $this->parseRequestURI(); break; } diff --git a/system/HTTP/Request.php b/system/HTTP/Request.php index 23da4365dc..54710d276d 100644 --- a/system/HTTP/Request.php +++ b/system/HTTP/Request.php @@ -64,6 +64,13 @@ class Request extends Message implements RequestInterface */ protected $method; + /** + * Stores values we've retrieved from + * PHP globals. + * @var array + */ + protected $globals = []; + //-------------------------------------------------------------------- /** @@ -302,6 +309,23 @@ class Request extends Message implements RequestInterface //-------------------------------------------------------------------- + /** + * Allows manually setting the value of PHP global, like $_GET, $_POST, etc. + * + * @param string $method + * @param $value + * + * @return $this + */ + public function setGlobal(string $method, $value) + { + $this->globals[$method] = $value; + + return $this; + } + + //-------------------------------------------------------------------- + /** * Fetches one or more items from a global, like cookies, get, post, etc. * Can optionally filter the input when you retrieve it by passing in @@ -312,45 +336,37 @@ class Request extends Message implements RequestInterface * * http://php.net/manual/en/filter.filters.sanitize.php * - * @param int $type Input filter constant + * @param int $method Input filter constant * @param string|array $index * @param int $filter Filter constant * @param null $flags * * @return mixed */ - protected function fetchGlobal($type, $index = null, $filter = null, $flags = null ) + public function fetchGlobal($method, $index = null, $filter = null, $flags = null ) { + $method = strtolower($method); + + if (! isset($this->globals[$method])) + { + $this->populateGlobals($method); + } + // Null filters cause null values to return. if (is_null($filter)) { $filter = FILTER_DEFAULT; } - $loopThrough = []; - switch ($type) - { - case INPUT_GET : $loopThrough = $_GET; - break; - case INPUT_POST : $loopThrough = $_POST; - break; - case INPUT_COOKIE : $loopThrough = $_COOKIE; - break; - case INPUT_SERVER : $loopThrough = $_SERVER; - break; - case INPUT_ENV : $loopThrough = $_ENV; - break; - case INPUT_REQUEST : $loopThrough = $_REQUEST; - break; - } - - // If $index is null, it means that the whole input type array is requested + // Return all values when $index is null if (is_null($index)) { $values = []; - foreach ($loopThrough as $key => $value) + foreach ($this->globals[$method] as $key => $value) { - $values[$key] = is_array($value) ? $this->fetchGlobal($type, $key, $filter, $flags) : filter_var($value, $filter, $flags); + $values[$key] = is_array($value) + ? $this->fetchGlobal($method, $key, $filter, $flags) + : filter_var($value, $filter, $flags); } return $values; @@ -363,7 +379,7 @@ class Request extends Message implements RequestInterface foreach ($index as $key) { - $output[$key] = $this->fetchGlobal($type, $key, $filter, $flags); + $output[$key] = $this->fetchGlobal($method, $key, $filter, $flags); } return $output; @@ -372,7 +388,7 @@ class Request extends Message implements RequestInterface // Does the index contain array notation? if (($count = preg_match_all('/(?:^[^\[]+)|\[[^]]*\]/', $index, $matches)) > 1) { - $value = $loopThrough; + $value = $this->globals[$method]; for ($i = 0; $i < $count; $i++) { $key = trim($matches[0][$i], '[]'); @@ -393,14 +409,12 @@ class Request extends Message implements RequestInterface } } - // Due to issues with FastCGI and testing, - // we need to do these all manually instead - // of the simpler filter_input(); if (empty($value)) { - $value = $loopThrough[$index] ?? null; + $value = $this->globals[$method][$index] ?? null; } + // Cannot filter these types of data automatically... if (is_array($value) || is_object($value) || is_null($value)) { return $value; @@ -410,4 +424,39 @@ class Request extends Message implements RequestInterface } //-------------------------------------------------------------------- + + /** + * Saves a copy of the current state of one of several PHP globals + * so we can retrieve them later. + * + * @param string $method + */ + protected function populateGlobals(string $method) + { + if (! isset($this->globals[$method])) + { + $this->globals[$method] = []; + } + + // Don't populate ENV as it might contain + // sensitive data that we don't want to get logged. + switch($method) + { + case 'get': + $this->globals['get'] = $_GET; + break; + case 'post': + $this->globals['post'] = $_POST; + break; + case 'request': + $this->globals['request'] = $_REQUEST; + break; + case 'cookie': + $this->globals['cookie'] = $_COOKIE; + break; + case 'server': + $this->globals['server'] = $_SERVER; + break; + } + } } diff --git a/system/HTTP/Response.php b/system/HTTP/Response.php index 6557ed216e..0f1a7b6bb2 100644 --- a/system/HTTP/Response.php +++ b/system/HTTP/Response.php @@ -71,75 +71,75 @@ class Response extends Message implements ResponseInterface */ protected static $statusCodes = [ // 1xx: Informational - 100 => 'Continue', - 101 => 'Switching Protocols', - 102 => 'Processing', // http://www.iana.org/go/rfc2518 - 103 => 'Early Hints', // http://www.ietf.org/rfc/rfc8297.txt + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', // http://www.iana.org/go/rfc2518 + 103 => 'Early Hints', // http://www.ietf.org/rfc/rfc8297.txt // 2xx: Success - 200 => 'OK', - 201 => 'Created', - 202 => 'Accepted', - 203 => 'Non-Authoritative Information', // 1.1 - 204 => 'No Content', - 205 => 'Reset Content', - 206 => 'Partial Content', - 207 => 'Multi-Status', // http://www.iana.org/go/rfc4918 - 208 => 'Already Reported', // http://www.iana.org/go/rfc5842 - 226 => 'IM Used', // 1.1; http://www.ietf.org/rfc/rfc3229.txt + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', // 1.1 + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', // http://www.iana.org/go/rfc4918 + 208 => 'Already Reported', // http://www.iana.org/go/rfc5842 + 226 => 'IM Used', // 1.1; http://www.ietf.org/rfc/rfc3229.txt // 3xx: Redirection - 300 => 'Multiple Choices', - 301 => 'Moved Permanently', - 302 => 'Found', // Formerly 'Moved Temporarily' - 303 => 'See Other', // 1.1 - 304 => 'Not Modified', - 305 => 'Use Proxy', // 1.1 - 306 => 'Switch Proxy', // No longer used - 307 => 'Temporary Redirect', // 1.1 - 308 => 'Permanent Redirect', // 1.1; Experimental; http://www.ietf.org/rfc/rfc7238.txt + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', // Formerly 'Moved Temporarily' + 303 => 'See Other', // 1.1 + 304 => 'Not Modified', + 305 => 'Use Proxy', // 1.1 + 306 => 'Switch Proxy', // No longer used + 307 => 'Temporary Redirect', // 1.1 + 308 => 'Permanent Redirect', // 1.1; Experimental; http://www.ietf.org/rfc/rfc7238.txt // 4xx: Client error - 400 => 'Bad Request', - 401 => 'Unauthorized', - 402 => 'Payment Required', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 406 => 'Not Acceptable', - 407 => 'Proxy Authentication Required', - 408 => 'Request Timeout', - 409 => 'Conflict', - 410 => 'Gone', - 411 => 'Length Required', - 412 => 'Precondition Failed', - 413 => 'Request Entity Too Large', - 414 => 'Request-URI Too Long', - 415 => 'Unsupported Media Type', - 416 => 'Requested Range Not Satisfiable', - 417 => 'Expectation Failed', - 418 => "I'm a teapot", // April's Fools joke; http://www.ietf.org/rfc/rfc2324.txt + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 418 => "I'm a teapot", // April's Fools joke; http://www.ietf.org/rfc/rfc2324.txt // 419 (Authentication Timeout) is a non-standard status code with unknown origin - 421 => 'Misdirected Request', // http://www.iana.org/go/rfc7540 Section 9.1.2 - 422 => 'Unprocessable Entity', // http://www.iana.org/go/rfc4918 - 423 => 'Locked', // http://www.iana.org/go/rfc4918 - 424 => 'Failed Dependency', // http://www.iana.org/go/rfc4918 - 426 => 'Upgrade Required', - 428 => 'Precondition Required', // 1.1; http://www.ietf.org/rfc/rfc6585.txt - 429 => 'Too Many Requests', // 1.1; http://www.ietf.org/rfc/rfc6585.txt - 431 => 'Request Header Fields Too Large', // 1.1; http://www.ietf.org/rfc/rfc6585.txt - 451 => 'Unavailable For Legal Reasons', // http://tools.ietf.org/html/rfc7725 - 499 => 'Client Closed Request', // http://lxr.nginx.org/source/src/http/ngx_http_request.h#0133 + 421 => 'Misdirected Request', // http://www.iana.org/go/rfc7540 Section 9.1.2 + 422 => 'Unprocessable Entity', // http://www.iana.org/go/rfc4918 + 423 => 'Locked', // http://www.iana.org/go/rfc4918 + 424 => 'Failed Dependency', // http://www.iana.org/go/rfc4918 + 426 => 'Upgrade Required', + 428 => 'Precondition Required', // 1.1; http://www.ietf.org/rfc/rfc6585.txt + 429 => 'Too Many Requests', // 1.1; http://www.ietf.org/rfc/rfc6585.txt + 431 => 'Request Header Fields Too Large', // 1.1; http://www.ietf.org/rfc/rfc6585.txt + 451 => 'Unavailable For Legal Reasons', // http://tools.ietf.org/html/rfc7725 + 499 => 'Client Closed Request', // http://lxr.nginx.org/source/src/http/ngx_http_request.h#0133 // 5xx: Server error - 500 => 'Internal Server Error', - 501 => 'Not Implemented', - 502 => 'Bad Gateway', - 503 => 'Service Unavailable', - 504 => 'Gateway Timeout', - 505 => 'HTTP Version Not Supported', - 506 => 'Variant Also Negotiates', // 1.1; http://www.ietf.org/rfc/rfc2295.txt - 507 => 'Insufficient Storage', // http://www.iana.org/go/rfc4918 - 508 => 'Loop Detected', // http://www.iana.org/go/rfc5842 - 510 => 'Not Extended', // http://www.ietf.org/rfc/rfc2774.txt - 511 => 'Network Authentication Required', // http://www.ietf.org/rfc/rfc6585.txt - 599 => 'Network Connect Timeout Error', // https://httpstatuses.com/599 + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', // 1.1; http://www.ietf.org/rfc/rfc2295.txt + 507 => 'Insufficient Storage', // http://www.iana.org/go/rfc4918 + 508 => 'Loop Detected', // http://www.iana.org/go/rfc5842 + 510 => 'Not Extended', // http://www.ietf.org/rfc/rfc2774.txt + 511 => 'Network Authentication Required', // http://www.ietf.org/rfc/rfc6585.txt + 599 => 'Network Connect Timeout Error', // https://httpstatuses.com/599 ]; /** @@ -206,6 +206,20 @@ class Response extends Message implements ResponseInterface */ protected $cookieHTTPOnly = false; + /** + * Stores all cookies that were set in the response. + * + * @var array + */ + protected $cookies = []; + + /** + * If true, will not write output. Useful during testing. + * + * @var bool + */ + protected $pretend = false; + //-------------------------------------------------------------------- /** @@ -222,14 +236,14 @@ class Response extends Message implements ResponseInterface // Are we enforcing a Content Security Policy? if ($config->CSPEnabled === true) { - $this->CSP = new ContentSecurityPolicy(new \Config\ContentSecurityPolicy()); + $this->CSP = new ContentSecurityPolicy(new \Config\ContentSecurityPolicy()); $this->CSPEnabled = true; } - $this->cookiePrefix = $config->cookiePrefix; - $this->cookieDomain = $config->cookieDomain; - $this->cookiePath = $config->cookiePath; - $this->cookieSecure = $config->cookieSecure; + $this->cookiePrefix = $config->cookiePrefix; + $this->cookieDomain = $config->cookieDomain; + $this->cookiePath = $config->cookiePath; + $this->cookieSecure = $config->cookieSecure; $this->cookieHTTPOnly = $config->cookieHTTPOnly; // Default to an HTML Content-Type. Devs can override if needed. @@ -238,6 +252,20 @@ class Response extends Message implements ResponseInterface //-------------------------------------------------------------------- + /** + * Turns "pretend" mode on or off to aid in testing. + * + * @param bool $pretend + * + * @return $this + */ + public function pretend(bool $pretend = true) + { + $this->pretend = $pretend; + + return $this; + } + /** * Gets the response status code. * @@ -284,14 +312,14 @@ class Response extends Message implements ResponseInterface } // Unknown and no message? - if ( ! array_key_exists($code, static::$statusCodes) && empty($reason)) + if (! array_key_exists($code, static::$statusCodes) && empty($reason)) { throw HTTPException::forUnkownStatusCode($code); } $this->statusCode = $code; - if ( ! empty($reason)) + if (! empty($reason)) { $this->reason = $reason; } @@ -339,7 +367,7 @@ class Response extends Message implements ResponseInterface { $date->setTimezone(new \DateTimeZone('UTC')); - $this->setHeader('Date', $date->format('D, d M Y H:i:s') . ' GMT'); + $this->setHeader('Date', $date->format('D, d M Y H:i:s').' GMT'); return $this; } @@ -360,7 +388,7 @@ class Response extends Message implements ResponseInterface // add charset attribute if not already there and provided as parm if ((strpos($mime, 'charset=') < 1) && ! empty($charset)) { - $mime .= '; charset=' . $charset; + $mime .= '; charset='.$charset; } $this->removeHeader('Content-Type'); // replace existing content type @@ -468,7 +496,7 @@ class Response extends Message implements ResponseInterface if ($date instanceof \DateTime) { $date->setTimezone(new \DateTimeZone('UTC')); - $this->setHeader('Last-Modified', $date->format('D, d M Y H:i:s') . ' GMT'); + $this->setHeader('Last-Modified', $date->format('D, d M Y H:i:s').' GMT'); } elseif (is_string($date)) { @@ -499,6 +527,7 @@ class Response extends Message implements ResponseInterface $this->sendHeaders(); $this->sendBody(); + $this->sendCookies(); return $this; } @@ -526,12 +555,13 @@ class Response extends Message implements ResponseInterface } // HTTP Status - header(sprintf('HTTP/%s %s %s', $this->protocolVersion, $this->statusCode, $this->reason), true, $this->statusCode); + header(sprintf('HTTP/%s %s %s', $this->protocolVersion, $this->statusCode, $this->reason), true, + $this->statusCode); // Send all of our headers foreach ($this->getHeaders() as $name => $values) { - header($name . ': ' . $this->getHeaderLine($name), false, $this->statusCode); + header($name.': '.$this->getHeaderLine($name), false, $this->statusCode); } return $this; @@ -553,6 +583,16 @@ class Response extends Message implements ResponseInterface //-------------------------------------------------------------------- + /** + * Grabs the current body. + * + * @return mixed|string + */ + public function getBody() + { + return $this->body; + } + /** * Perform a redirect to a new URL, in two flavors: header or location. * @@ -566,7 +606,8 @@ class Response extends Message implements ResponseInterface public function redirect(string $uri, string $method = 'auto', int $code = null) { // IIS environment likely? Use 'refresh' for better compatibility - if ($method === 'auto' && isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false) + if ($method === 'auto' && isset($_SERVER['SERVER_SOFTWARE']) + && strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false) { $method = 'refresh'; } @@ -574,8 +615,9 @@ class Response extends Message implements ResponseInterface { if (isset($_SERVER['SERVER_PROTOCOL'], $_SERVER['REQUEST_METHOD']) && $this->getProtocolVersion() >= 1.1) { - $code = ($_SERVER['REQUEST_METHOD'] !== 'GET') ? 303 // reference: http://en.wikipedia.org/wiki/Post/Redirect/Get - : 307; + $code = ($_SERVER['REQUEST_METHOD'] !== 'GET') ? 303 + // reference: http://en.wikipedia.org/wiki/Post/Redirect/Get + : 307; } else { @@ -586,7 +628,7 @@ class Response extends Message implements ResponseInterface switch ($method) { case 'refresh': - $this->setHeader('Refresh', '0;url=' . $uri); + $this->setHeader('Refresh', '0;url='.$uri); break; default: $this->setHeader('Location', $uri); @@ -618,9 +660,15 @@ class Response extends Message implements ResponseInterface * @param bool|false $httponly Whether only make the cookie accessible via HTTP (no javascript) */ public function setCookie( - $name, $value = '', $expire = '', $domain = '', $path = '/', $prefix = '', $secure = false, $httponly = false - ) - { + $name, + $value = '', + $expire = '', + $domain = '', + $path = '/', + $prefix = '', + $secure = false, + $httponly = false + ) { if (is_array($name)) { // always leave 'name' in last place, as the loop will break otherwise, due to $$item @@ -658,20 +706,108 @@ class Response extends Message implements ResponseInterface $httponly = $this->cookieHTTPOnly; } - if ( ! is_numeric($expire)) + if (! is_numeric($expire)) { - $expire = time() - 86500; + $expire = time()-86500; } else { - $expire = ($expire > 0) ? time() + $expire : 0; + $expire = ($expire > 0) ? time()+$expire : 0; } - setcookie($prefix . $name, $value, $expire, $path, $domain, $secure, $httponly); + $this->cookies[] = [ + 'name' => $prefix.$name, + 'value' => $value, + 'expires' => $expire, + 'path' => $path, + 'domain' => $domain, + 'secure' => $secure, + 'httponly' => $httponly, + ]; + + return $this; } //-------------------------------------------------------------------- + /** + * Checks to see if the Response has a specified cookie or not. + * + * @param string $name + * @param null $value + * @param string $prefix + * + * @return bool + */ + public function hasCookie(string $name, $value = null, string $prefix = '') + { + if ($prefix === '' && $this->cookiePrefix !== '') + { + $prefix = $this->cookiePrefix; + } + + $name = $prefix.$name; + + foreach ($this->cookies as $cookie) + { + if ($cookie['name'] != $prefix.$name) + { + continue; + } + + if ($value === null) + { + return true; + } + + return $cookie['value'] == $value; + } + + return false; + } + + /** + * Returns the cookie + * + * @param string $name + * @param string $prefix + * + * @return mixed + */ + public function getCookie(string $name, string $prefix = '') + { + if ($prefix === '' && $this->cookiePrefix !== '') + { + $prefix = $this->cookiePrefix; + } + + $name = $prefix.$name; + + foreach ($this->cookies as $cookie) + { + if ($cookie['name'] == $name) + { + return $cookie; + } + } + } + + /** + * Actually sets the cookies. + */ + protected function sendCookies() + { + if ($this->pretend) + { + return; + } + + foreach ($this->cookies as $params) + { + setcookie(...$params); + } + } + /** * Force a download. * @@ -690,7 +826,7 @@ class Response extends Message implements ResponseInterface } elseif ($data === null) { - if ( ! @is_file($filename) || ($filesize = @filesize($filename)) === false) + if (! @is_file($filename) || ($filesize = @filesize($filename)) === false) { return; } @@ -707,7 +843,7 @@ class Response extends Message implements ResponseInterface // Set the default MIME type to send $mime = 'application/octet-stream'; - $x = explode('.', $filename); + $x = explode('.', $filename); $extension = end($x); if ($setMime === true) @@ -729,10 +865,11 @@ class Response extends Message implements ResponseInterface * * Reference: http://digiblog.de/2011/04/19/android-and-the-download-file-headers/ */ - if (count($x) !== 1 && isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/Android\s(1|2\.[01])/', $_SERVER['HTTP_USER_AGENT'])) + if (count($x) !== 1 && isset($_SERVER['HTTP_USER_AGENT']) + && preg_match('/Android\s(1|2\.[01])/', $_SERVER['HTTP_USER_AGENT'])) { - $x[count($x) - 1] = strtoupper($extension); - $filename = implode('.', $x); + $x[count($x)-1] = strtoupper($extension); + $filename = implode('.', $x); } if ($data === null && ($fp = @fopen($filepath, 'rb')) === false) @@ -747,11 +884,11 @@ class Response extends Message implements ResponseInterface } // Generate the server headers - header('Content-Type: ' . $mime); - header('Content-Disposition: attachment; filename="' . $filename . '"'); + header('Content-Type: '.$mime); + header('Content-Disposition: attachment; filename="'.$filename.'"'); header('Expires: 0'); header('Content-Transfer-Encoding: binary'); - header('Content-Length: ' . $filesize); + header('Content-Length: '.$filesize); header('Cache-Control: private, no-transform, no-store, must-revalidate'); // If we have raw data - just dump it @@ -761,7 +898,7 @@ class Response extends Message implements ResponseInterface } // Flush 1MB chunks of data - while ( ! feof($fp) && ($data = fread($fp, 1048576)) !== false) + while (! feof($fp) && ($data = fread($fp, 1048576)) !== false) { echo $data; } diff --git a/system/Router/Router.php b/system/Router/Router.php index c7a89b76cb..590f06b4a8 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -361,6 +361,8 @@ class Router implements RouterInterface { $routes = $this->collection->getRoutes($this->collection->getHTTPVerb()); + $uri = ltrim($uri, '/ '); + // Don't waste any time if (empty($routes)) { diff --git a/system/Test/CIDatabaseTestCase.php b/system/Test/CIDatabaseTestCase.php index d102aef608..a6f67973b0 100644 --- a/system/Test/CIDatabaseTestCase.php +++ b/system/Test/CIDatabaseTestCase.php @@ -152,6 +152,8 @@ class CIDatabaseTestCase extends CIUnitTestCase */ public function setUp() { + parent::setUp(); + $this->loadDependencies(); if ($this->refresh === true) diff --git a/system/Test/CIUnitTestCase.php b/system/Test/CIUnitTestCase.php index f8bef456fc..b33b92f8cd 100644 --- a/system/Test/CIUnitTestCase.php +++ b/system/Test/CIUnitTestCase.php @@ -36,6 +36,7 @@ * @filesource */ use CodeIgniter\Events\Events; +use Config\Paths; use PHPUnit\Framework\TestCase; use CodeIgniter\Log\TestLogger; @@ -44,15 +45,40 @@ use CodeIgniter\Log\TestLogger; */ class CIUnitTestCase extends TestCase { - use ReflectionHelper; + /** + * @var \CodeIgniter\CodeIgniter + */ + protected $app; + + /** + * Path to Config folder, relative + * to the system folder. + * @var string + */ + protected $configPath = '../application/Config'; + + public function setUp() + { + parent::setUp(); + + if (! $this->app) + { + $this->app = $this->createApplication(); + } + + helper('url'); + } + /** * Custom function to hook into CodeIgniter's Logging mechanism * to check if certain messages were logged during code execution. * * @param string $level * @param null $expectedMessage + * + * @throws \Exception */ public function assertLogged(string $level, $expectedMessage = null) { @@ -61,8 +87,6 @@ class CIUnitTestCase extends TestCase $this->assertTrue($result); } - //-------------------------------------------------------------------- - /** * Hooks into CodeIgniter's Events system to check if a specific * event was triggered or not. @@ -70,6 +94,7 @@ class CIUnitTestCase extends TestCase * @param string $eventName * * @return bool + * @throws \Exception */ public function assertEventTriggered(string $eventName): bool { @@ -87,5 +112,52 @@ class CIUnitTestCase extends TestCase $this->assertTrue($found); } - //-------------------------------------------------------------------- + /** + * Loads up an instance of CodeIgniter + * and gets the environment setup. + * + * @return mixed + */ + protected function createApplication() + { + $systemPath = realpath(__DIR__.'/../'); + + require_once $systemPath.'/'.$this->configPath.'/Paths.php'; + $paths = $this->adjustPaths(new \Config\Paths()); + + $app = require $systemPath.'/bootstrap.php'; + return $app; + } + + /** + * Attempts to adjust our system paths to account + * for relative location of our tests folder. + * Not foolproof, but works well for default locations. + * + * @param \Config\Paths $paths + * + * @return \Config\Paths + */ + protected function adjustPaths(Paths $paths) + { + $tests = [ + 'systemDirectory', 'applicationDirectory', 'writableDirectory', 'testsDirectory' + ]; + + foreach ($tests as $test) + { + if (is_dir($paths->$test) || strpos($paths->$test, '../') !== 0) + { + continue; + } + + $check = substr($paths->$test, 3); + if (is_dir($check)) + { + $paths->$test = $check; + } + } + + return $paths; + } } diff --git a/system/Test/FeatureResponse.php b/system/Test/FeatureResponse.php new file mode 100644 index 0000000000..46dec88179 --- /dev/null +++ b/system/Test/FeatureResponse.php @@ -0,0 +1,292 @@ +response = $response; + + $this->domParser = (new DOMParser())->withString($this->response->getBody()); + } + + //-------------------------------------------------------------------- + // Simple Response Checks + //-------------------------------------------------------------------- + + /** + * Boils down the possible responses into a bolean valid/not-valid + * response type. + * + * @return bool + */ + public function isOK(): bool + { + // Only 200 and 300 range status codes + // are considered valid. + if ($this->response->getStatusCode() >= 400 || $this->response->getStatusCode() < 200) + { + return false; + } + + // Empty bodies are not considered valid. + if (empty($this->response->getBody())) + { + return false; + } + + return true; + } + + /** + * Returns whether or not the Response was a redirect response + * + * @return bool + */ + public function isRedirect(): bool + { + return $this->response instanceof RedirectResponse; + } + + /** + * Assert that the given response was a redirect. + * + * @throws \Exception + */ + public function assertRedirect() + { + $this->assertTrue($this->isRedirect(), "Response is not a RedirectResponse."); + } + + /** + * Asserts that the status is a specific value. + * + * @param int $code + * + * @throws \Exception + */ + public function assertStatus(int $code) + { + $this->assertEquals($code, (int)$this->response->getStatusCode()); + } + + /** + * Asserts that the Response is considered OK. + * + * @throws \Exception + */ + public function assertOK() + { + $this->assertTrue($this->isOK(), "{$this->response->getStatusCode()} is not a successful status code, or the Response has an empty body."); + } + + //-------------------------------------------------------------------- + // Session Assertions + //-------------------------------------------------------------------- + + /** + * Asserts that an SESSION key has been set and, optionally, test it's value. + * + * @param string $key + * @param null $value + * + * @throws \Exception + */ + public function assertSessionHas(string $key, $value = null) + { + $this->assertTrue(array_key_exists($key, $_SESSION), "'{$key}' is not in the current \$_SESSION"); + + if ($value !== null) + { + $this->assertEquals($value, $_SESSION[$key], "The value of '{$key}' ({$value}) does not match expected value."); + } + } + + /** + * Asserts the session is missing $key. + * + * @param string $key + * + * @throws \Exception + */ + public function assertSessionMissing(string $key) + { + $this->assertFalse(array_key_exists($key, $_SESSION), "'{$key}' should not be present in \$_SESSION."); + } + + //-------------------------------------------------------------------- + // Header Assertions + //-------------------------------------------------------------------- + + /** + * Asserts that the Response contains a specific header. + * + * @param string $key + * @param null $value + * + * @throws \Exception + */ + public function assertHeader(string $key, $value = null) + { + $this->assertTrue($this->response->hasHeader($key), "'{$key}' is not a valid Response header."); + + if ($value !== null) + { + $this->assertEquals($value, $this->response->getHeaderLine($key), "The value of '{$key}' header ({$this->response->getHeaderLine($key)}) does not match expected value."); + } + } + + /** + * Asserts the Response headers does not contain the specified header. + * + * @param string $key + * + * @throws \Exception + */ + public function assertHeaderMissing(string $key) + { + $this->assertFalse($this->response->hasHeader($key), "'{$key}' should not be in the Response headers."); + } + + //-------------------------------------------------------------------- + // Cookie Assertions + //-------------------------------------------------------------------- + + /** + * Asserts that the response has the specified cookie. + * + * @param string $key + * @param null $value + * @param string|null $prefix + * + * @throws \Exception + */ + public function assertCookie(string $key, $value = null, string $prefix = '') + { + $this->assertTrue($this->response->hasCookie($key, $value, $prefix), "No cookie found named '{$key}'."); + } + + /** + * Assert the Response does not have the specified cookie set. + * + * @param string $key + * @param null $value + * @param string $prefix + * + * @throws \Exception + */ + public function assertCookieMissing(string $key) + { + $this->assertFalse($this->response->hasCookie($key), "Cookie named '{$key}' should not be set."); + } + + /** + * Asserts that a cookie exists and has an expired time. + * + * @param string $key + * @param string $prefix + * + * @throws \Exception + */ + public function assertCookieExpired(string $key, string $prefix = '') + { + $this->assertTrue($this->response->hasCookie($key, null, $prefix)); + $this->assertGreaterThan(time(), $this->response->getCookie($key, $prefix)['expires']); + } + + //-------------------------------------------------------------------- + // DomParser Assertions + //-------------------------------------------------------------------- + + /** + * Assert that the desired text can be found in the result body. + * + * @param string|null $search + * @param string|null $element + * + * @throws \Exception + */ + public function assertSee(string $search = null, string $element = null) + { + $this->assertTrue($this->domParser->see($search, $element), "Do not see '{$search}' in response."); + } + + /** + * Asserts that we do not see the specified text. + * + * @param string|null $search + * @param string|null $element + * + * @throws \Exception + */ + public function assertDontSee(string $search = null, string $element = null) + { + $this->assertTrue($this->domParser->dontSee($search, $element), "I should not see '{$search}' in response."); + } + + /** + * Assert that we see an element selected via a CSS selector. + * + * @param string $search + * + * @throws \Exception + */ + public function assertSeeElement(string $search) + { + $this->assertTrue($this->domParser->seeElement($search), "Do not see element with selector '{$search} in response.'"); + } + + /** + * Assert that we do not see an element selected via a CSS selector. + * + * @param string $search + * + * @throws \Exception + */ + public function assertDontSeeElement(string $search) + { + $this->assertTrue($this->domParser->dontSeeElement($search), "I should not see an element with selector '{$search}' in response.'"); + } + + /** + * Assert that we see a link with the matching text and/or class. + * + * @param string $text + * @param string|null $details + * + * @throws \Exception + */ + public function assertSeeLink(string $text, string $details=null) + { + $this->assertTrue($this->domParser->seeLink($text, $details), "Do no see anchor tag with the text {$text} in response."); + } + + /** + * Assert that we see an input with name/value. + * + * @param string $field + * @param string|null $value + * + * @throws \Exception + */ + public function assertSeeInField(string $field, string $value=null) + { + $this->assertTrue($this->domParser->seeInField($field, $value), "Do no see input named {$field} with value {$value} in response."); + } + + +} diff --git a/system/Test/FeatureTestCase.php b/system/Test/FeatureTestCase.php new file mode 100644 index 0000000000..714375b405 --- /dev/null +++ b/system/Test/FeatureTestCase.php @@ -0,0 +1,251 @@ +{$route[0]}($route[1], $route[2]); + } + } + + $this->routes = $collection; + + return $this; + } + + /** + * Sets any values that should exist during this session. + * + * @param array $values + * + * @return $this + */ + public function withSession(array $values) + { + $this->session = $values; + + return $this; + } + + /** + * Don't run any events while running this test. + * + * @return $this + */ + public function skipEvents() + { + Events::simulate(true); + + return $this; + } + + /** + * Calls a single URI, executes it, and returns a FeatureResponse + * instance that can be used to run many assertions against. + * + * @param string $method + * @param string $path + * @param array|null $params + * + * @return \CodeIgniter\Test\FeatureResponse + * @throws \CodeIgniter\HTTP\RedirectException + * @throws \Exception + */ + public function call(string $method, string $path, array $params = null) + { + // Simulate having a blank session + $_SESSION = []; + + $request = $this->setupRequest($method, $path, $params); + + $request = $this->populateGlobals($method, $request, $params); + + $response = $this->app + ->setRequest($request) + ->run($this->routes, true); + + $featureResponse = new FeatureResponse($response); + + return $featureResponse; + } + + /** + * Performs a GET request. + * + * @param string $path + * @param array|null $params + * + * @return \CodeIgniter\Test\FeatureResponse + * @throws \CodeIgniter\HTTP\RedirectException + * @throws \Exception + */ + public function get(string $path, array $params = null) + { + return $this->call('get', $path, $params); + } + + /** + * Performs a POST request. + * + * @param string $path + * @param array|null $params + * + * @return \CodeIgniter\Test\FeatureResponse + * @throws \CodeIgniter\HTTP\RedirectException + * @throws \Exception + */ + public function post(string $path, array $params = null) + { + return $this->call('post', $path, $params); + } + + /** + * Performs a PUT request + * + * @param string $path + * @param array|null $params + * + * @return \CodeIgniter\Test\FeatureResponse + * @throws \CodeIgniter\HTTP\RedirectException + * @throws \Exception + */ + public function put(string $path, array $params = null) + { + return $this->call('put', $path, $params); + } + + /** + * Performss a PATCH request + * + * @param string $path + * @param array|null $params + * + * @return \CodeIgniter\Test\FeatureResponse + * @throws \CodeIgniter\HTTP\RedirectException + * @throws \Exception + */ + public function patch(string $path, array $params = null) + { + return $this->call('patch', $path, $params); + } + + /** + * Performs a DELETE request. + * + * @param string $path + * @param array|null $params + * + * @return \CodeIgniter\Test\FeatureResponse + * @throws \CodeIgniter\HTTP\RedirectException + * @throws \Exception + */ + public function delete(string $path, array $params = null) + { + return $this->call('delete', $path, $params); + } + + /** + * Performs an OPTIONS request. + * + * @param string $path + * @param array|null $params + * + * @return \CodeIgniter\Test\FeatureResponse + * @throws \CodeIgniter\HTTP\RedirectException + * @throws \Exception + */ + public function options(string $path, array $params = null) + { + return $this->call('delete', $path, $params); + } + + /** + * Setup a Request object to use so that CodeIgniter + * won't try to auto-populate some of the items. + * + * @param string $method + * @param string|null $path + * + * @return \CodeIgniter\HTTP\IncomingRequest + */ + protected function setupRequest(string $method, string $path=null, $params = null) + { + $config = new \Config\App(); + $uri = new URI($config->baseURL .'/'. trim($path, '/ ')); + + $request = new IncomingRequest($config, clone($uri), $params, new UserAgent()); + $request->uri = $uri; + + $request->setMethod($method); + $request->setProtocolVersion('1.1'); + + return $request; + } + + /** + * Populates the data of our Request with "global" data + * relevant to the request, like $_POST data. + * + * Always populate the GET vars based on the URI. + * + * @param string $method + * @param \CodeIgniter\HTTP\Request $request + * @param array|null $params + * + * @return \CodeIgniter\HTTP\Request + */ + protected function populateGlobals(string $method, Request $request, array $params = null) + { + $request->setGlobal('get', $this->getPrivateProperty($request->uri, 'query')); + if ($method !== 'get') + { + $request->setGlobal($method, $params); + } + + $_SESSION = $this->session; + + return $request; + } +} diff --git a/system/bootstrap.php b/system/bootstrap.php index 17b6e9bf48..0afe18ae87 100644 --- a/system/bootstrap.php +++ b/system/bootstrap.php @@ -53,36 +53,51 @@ $pos = strrpos(FCPATH, $public.DIRECTORY_SEPARATOR); /** * The path to the main application directory. Just above public. */ -define('ROOTPATH', substr_replace(FCPATH, '', $pos, strlen($public.DIRECTORY_SEPARATOR))); +if (! defined('ROOTPATH')) +{ + define('ROOTPATH', substr_replace(FCPATH, '', $pos, strlen($public.DIRECTORY_SEPARATOR))); +} /** * The path to the application directory. */ -define('APPPATH', realpath($paths->applicationDirectory).DIRECTORY_SEPARATOR); +if (! defined('APPPATH')) +{ + define('APPPATH', realpath($paths->applicationDirectory).DIRECTORY_SEPARATOR); +} /** * The path to the system directory. */ -define('BASEPATH', realpath($paths->systemDirectory).DIRECTORY_SEPARATOR); +if (! defined('BASEPATH')) +{ + define('BASEPATH', realpath($paths->systemDirectory).DIRECTORY_SEPARATOR); +} /** * The path to the writable directory. */ -define('WRITEPATH', realpath($paths->writableDirectory).DIRECTORY_SEPARATOR); +if (! defined('WRITEPATH')) +{ + define('WRITEPATH', realpath($paths->writableDirectory).DIRECTORY_SEPARATOR); +} /** * The path to the tests directory */ -define('TESTPATH', realpath($paths->testsDirectory).DIRECTORY_SEPARATOR); +if (! defined('TESTPATH')) +{ + define('TESTPATH', realpath($paths->testsDirectory).DIRECTORY_SEPARATOR); +} /* * --------------------------------------------------------------- * GRAB OUR CONSTANTS & COMMON * --------------------------------------------------------------- */ -require APPPATH.'Config/Constants.php'; +require_once APPPATH.'Config/Constants.php'; -require BASEPATH.'Common.php'; +require_once BASEPATH.'Common.php'; /* * --------------------------------------------------------------- @@ -94,12 +109,15 @@ require BASEPATH.'Common.php'; * that the config files can use the path constants. */ -require BASEPATH.'Autoloader/Autoloader.php'; -require APPPATH .'Config/Autoload.php'; -require APPPATH .'Config/Services.php'; +require_once BASEPATH.'Autoloader/Autoloader.php'; +require_once APPPATH .'Config/Autoload.php'; +require_once APPPATH .'Config/Services.php'; // Use Config\Services as CodeIgniter\Services -class_alias('Config\Services', 'CodeIgniter\Services'); +if (! class_exists('CodeIgniter\Services', false)) +{ + class_alias('Config\Services', 'CodeIgniter\Services'); +} $loader = CodeIgniter\Services::autoloader(); $loader->initialize(new Config\Autoload()); @@ -108,12 +126,12 @@ $loader->register(); // Register the loader with the SPL autoloader stack. // Now load Composer's if it's available if (file_exists(COMPOSER_PATH)) { - require COMPOSER_PATH; + require_once COMPOSER_PATH; } // Load environment settings from .env files // into $_SERVER and $_ENV -require BASEPATH . 'Config/DotEnv.php'; +require_once BASEPATH . 'Config/DotEnv.php'; $env = new \CodeIgniter\Config\DotEnv(ROOTPATH); $env->load(); diff --git a/tests/_support/_bootstrap.php b/tests/_support/_bootstrap.php index 272ef3eb25..34dd8b43cb 100644 --- a/tests/_support/_bootstrap.php +++ b/tests/_support/_bootstrap.php @@ -6,113 +6,22 @@ ini_set('display_startup_errors', '1'); // Make sure it recognizes that we're testing. $_SERVER['CI_ENVIRONMENT'] = 'testing'; -// Get our system paths -require 'application/Config/Paths.php'; -$paths = new \Config\Paths(); - // Path to the front controller (this file) define('FCPATH', getcwd().'/public'.DIRECTORY_SEPARATOR); -/* - * --------------------------------------------------------------- - * SETUP OUR PATH CONSTANTS - * --------------------------------------------------------------- - * - * The path constants provide convenient access to the folders - * throughout the application. We have to setup them up here - * so they are available in the config files that are loaded. - */ - -// Path to code root folder (just up from public) -$pos = strrpos(FCPATH, 'public'.DIRECTORY_SEPARATOR); -define('ROOTPATH', substr_replace(FCPATH, '', $pos, strlen('public'.DIRECTORY_SEPARATOR))); - -// The path to the "application" folder -define('APPPATH', realpath(FCPATH.$paths->applicationDirectory).DIRECTORY_SEPARATOR); - -// Path to the system folder -define('BASEPATH', realpath(FCPATH.$paths->systemDirectory).DIRECTORY_SEPARATOR); - -// Path to the writable directory. -define('WRITEPATH', realpath(FCPATH.$paths->writableDirectory).DIRECTORY_SEPARATOR); - // The path to the "tests" directory -define('TESTPATH', realpath(FCPATH.$paths->testsDirectory).DIRECTORY_SEPARATOR); +define('TESTPATH', realpath(__DIR__.'/../').'/'); define('SUPPORTPATH', realpath(TESTPATH.'_support/').'/'); -// Use special Services for testing. These allow -// insert mocks in place of normal services. -include BASEPATH.'Config/Services.php'; - -/* - * --------------------------------------------------------------- - * GRAB OUR CONSTANTS & COMMON - * --------------------------------------------------------------- - */ -require APPPATH.'Config/Constants.php'; - -// Use special global functions for testing. -require_once SUPPORTPATH.'MockCommon.php'; -require BASEPATH.'Common.php'; - -/* - * --------------------------------------------------------------- - * LOAD OUR AUTOLOADER - * --------------------------------------------------------------- - * - * The autoloader allows all of the pieces to work together - * in the framework. We have to load it here, though, so - * that the config files can use the path constants. - */ - -require BASEPATH.'Autoloader/Autoloader.php'; -require APPPATH .'Config/Autoload.php'; -require APPPATH .'Config/Services.php'; - -$loader = CodeIgniter\Config\Services::autoloader(); -$loader->initialize(new Config\Autoload()); -$loader->register(); // Register the loader with the SPL autoloader stack. - -// Add namespace paths to autoload mocks for testing. -$loader->addNamespace('CodeIgniter', SUPPORTPATH); -$loader->addNamespace('Config', SUPPORTPATH.'Config'); -$loader->addNamespace('Tests\Support', SUPPORTPATH); - -// Now load Composer's if it's available -if (file_exists(COMPOSER_PATH)) -{ - require COMPOSER_PATH; -} - -// Load environment settings from .env files -// into $_SERVER and $_ENV -require BASEPATH . 'Config/DotEnv.php'; - -$env = new \CodeIgniter\Config\DotEnv(ROOTPATH); -$env->load(); - // Set environment values that would otherwise stop the framework from functioning during tests. if (! isset($_SERVER['app.baseURL'])) { $_SERVER['app.baseURL'] = 'http://example.com'; } -/* - * --------------------------------------------------------------- - * GRAB OUR CODEIGNITER INSTANCE - * --------------------------------------------------------------- - * - * The CodeIgniter class contains the core functionality to make - * the application run, and does all of the dirty work to get - * the pieces all working together. - */ - -$app = new \CodeIgniter\CodeIgniter(new \Config\App()); -$app->initialize(); - //-------------------------------------------------------------------- // Load our TestCase //-------------------------------------------------------------------- -require TESTPATH.'_support/CIUnitTestCase.php'; +require __DIR__.'/CIUnitTestCase.php'; diff --git a/tests/feature/HomeTest.php b/tests/feature/HomeTest.php new file mode 100644 index 0000000000..82a822ab15 --- /dev/null +++ b/tests/feature/HomeTest.php @@ -0,0 +1,14 @@ +call('post', site_url().'?foo=bar&bar=baz', ['xxx' => 'yyy']); + + $this->assertInstanceOf(\CodeIgniter\Test\FeatureResponse::class, $response); + } + +} diff --git a/tests/system/Autoloader/AutoloaderTest.php b/tests/system/Autoloader/AutoloaderTest.php index 28586c89e0..1a2ca39f50 100644 --- a/tests/system/Autoloader/AutoloaderTest.php +++ b/tests/system/Autoloader/AutoloaderTest.php @@ -11,7 +11,7 @@ class AutoloaderTest extends \CIUnitTestCase //-------------------------------------------------------------------- - protected function setUp() + public function setUp() { $config = new Autoload(); diff --git a/tests/system/Events/EventsTest.php b/tests/system/Events/EventsTest.php index c8ef2ffa5d..7ed51464f9 100644 --- a/tests/system/Events/EventsTest.php +++ b/tests/system/Events/EventsTest.php @@ -7,7 +7,7 @@ class EventsTest extends \CIUnitTestCase */ protected $tester; - protected function setUp() + public function setUp() { Events::removeAllListeners(); } diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index eef7b028e6..bf9fda60dc 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -105,244 +105,8 @@ class IncomingRequestTest extends \CIUnitTestCase $this->assertNull($this->request->getCookie('TESTY')); } - //-------------------------------------------------------------------- - - public function testFetchGlobalReturnsSingleValue() - { - $_POST = [ - 'foo' => 'bar', - 'bar' => 'baz', - 'xxx' => 'yyy', - 'yyy' => 'zzz' - ]; - - $this->assertEquals('baz', $this->request->getPost('bar')); - } - - //-------------------------------------------------------------------- - - public function testFetchGlobalWithArrayTop() - { - $_POST = [ - 'clients' => [ - 'address' => [ - 'zipcode' => 90210 - ] - ] - ]; - - $this->assertEquals(['address' => ['zipcode' => 90210]], $this->request->getPost('clients')); - } - - public function testFetchGlobalWithArrayChildNumeric() - { - $_POST = [ - 'clients' => [ - [ - 'address' => [ - 'zipcode' => 90210 - ], - ], - [ - 'address' => [ - 'zipcode' => 60610 - ], - ], - ] - ]; - - $this->assertEquals(['zipcode' => 60610], $this->request->getPost('clients[1][address]')); - } - - public function testFetchGlobalWithArrayChildElement() - { - $_POST = [ - 'clients' => [ - 'address' => [ - 'zipcode' => 90210 - ], - ] - ]; - - $this->assertEquals(['zipcode' => 90210], $this->request->getPost('clients[address]')); - } - - public function testFetchGlobalWithArrayLastElement() - { - $_POST = [ - 'clients' => [ - 'address' => [ - 'zipcode' => 90210 - ] - ] - ]; - - $this->assertEquals(90210, $this->request->getPost('clients[address][zipcode]')); - } - - /** - * @see https://github.com/bcit-ci/CodeIgniter4/issues/353 - */ - public function testGetPostReturnsArrayValues() - { - $_POST = [ - 'ANNOUNCEMENTS' => [ - 1 => [ - 'DETAIL' => 'asdf' - ], - 2 => [ - 'DETAIL' => 'sdfg' - ] - ], - 'submit' => 'SAVE' - ]; - - $result = $this->request->getPost(); - - $this->assertEquals($_POST, $result); - $this->assertInternalType('array', $result['ANNOUNCEMENTS']); - $this->assertCount(2, $result['ANNOUNCEMENTS']); - } - //-------------------------------------------------------------------- - public function testFetchGlobalFiltersValue() - { - $_POST = [ - 'foo' => 'bar