3
declare(strict_types=1);
6
* This file is part of CodeIgniter 4 framework.
8
* (c) CodeIgniter Foundation <admin@codeigniter.com>
10
* For the full copyright and license information, please view
11
* the LICENSE file that was distributed with this source code.
14
namespace CodeIgniter\Test;
16
use CodeIgniter\Events\Events;
17
use CodeIgniter\HTTP\Exceptions\RedirectException;
18
use CodeIgniter\HTTP\IncomingRequest;
19
use CodeIgniter\HTTP\Method;
20
use CodeIgniter\HTTP\Request;
21
use CodeIgniter\HTTP\SiteURI;
22
use CodeIgniter\HTTP\URI;
26
use ReflectionException;
29
* Trait FeatureTestTrait
31
* Provides additional utilities for doing full HTTP testing
32
* against your application in trait format.
37
* Sets a RouteCollection that will override
38
* the application's route collection.
42
* ['GET', 'home', 'Home::index'],
45
* @param array|null $routes Array to set routes
49
protected function withRoutes(?array $routes = null)
51
$collection = service('routes');
53
if ($routes !== null) {
54
$collection->resetRoutes();
56
foreach ($routes as $route) {
57
if ($route[0] === strtolower($route[0])) {
59
'Passing lowercase HTTP method "' . $route[0] . '" is deprecated.'
60
. ' Use uppercase HTTP method like "' . strtoupper($route[0]) . '".',
66
* @TODO For backward compatibility. Remove strtolower() in the future.
69
$method = strtolower($route[0]);
71
if (isset($route[3])) {
72
$collection->{$method}($route[1], $route[2], $route[3]);
74
$collection->{$method}($route[1], $route[2]);
79
$this->routes = $collection;
85
* Sets any values that should exist during this session.
87
* @param array|null $values Array of values, or null to use the current $_SESSION
91
public function withSession(?array $values = null)
93
$this->session = $values ?? $_SESSION;
99
* Set request's headers
103
* 'Authorization' => 'Token'
106
* @param array $headers Array of headers
110
public function withHeaders(array $headers = [])
112
$this->headers = $headers;
118
* Set the format the request's body should have.
120
* @param string $format The desired format. Currently supported formats: xml, json
124
public function withBodyFormat(string $format)
126
$this->bodyFormat = $format;
132
* Set the raw body for the request
134
* @param string $body
138
public function withBody($body)
140
$this->requestBody = $body;
146
* Don't run any events while running this test.
150
public function skipEvents()
152
Events::simulate(true);
158
* Calls a single URI, executes it, and returns a TestResponse
159
* instance that can be used to run many assertions against.
161
* @param string $method HTTP verb
163
* @return TestResponse
165
public function call(string $method, string $path, ?array $params = null)
167
if ($method === strtolower($method)) {
169
'Passing lowercase HTTP method "' . $method . '" is deprecated.'
170
. ' Use uppercase HTTP method like "' . strtoupper($method) . '".',
177
* @TODO remove this in the future.
179
$method = strtoupper($method);
181
// Simulate having a blank session
183
$_SERVER['REQUEST_METHOD'] = $method;
185
$request = $this->setupRequest($method, $path);
186
$request = $this->setupHeaders($request);
187
$name = strtolower($method);
188
$request = $this->populateGlobals($name, $request, $params);
189
$request = $this->setRequestBody($request, $params);
191
// Initialize the RouteCollection
192
if (! $routes = $this->routes) {
193
$routes = service('routes')->loadRoutes();
196
$routes->setHTTPVerb($method);
198
// Make sure any other classes that might call the request
199
// instance get the right one.
200
Services::injectMock('request', $request);
202
// Make sure filters are reset between tests
203
Services::injectMock('filters', Services::filters(null, false));
205
// Make sure validation is reset between tests
206
Services::injectMock('validation', Services::validation(null, false));
208
$response = $this->app
210
->setRequest($request)
211
->run($routes, true);
213
// Reset directory if it has been set
214
service('router')->setDirectory(null);
216
return new TestResponse($response);
220
* Performs a GET request.
222
* @param string $path URI path relative to baseURL. May include query.
224
* @return TestResponse
226
* @throws RedirectException
229
public function get(string $path, ?array $params = null)
231
return $this->call(Method::GET, $path, $params);
235
* Performs a POST request.
237
* @return TestResponse
239
* @throws RedirectException
242
public function post(string $path, ?array $params = null)
244
return $this->call(Method::POST, $path, $params);
248
* Performs a PUT request
250
* @return TestResponse
252
* @throws RedirectException
255
public function put(string $path, ?array $params = null)
257
return $this->call(Method::PUT, $path, $params);
261
* Performss a PATCH request
263
* @return TestResponse
265
* @throws RedirectException
268
public function patch(string $path, ?array $params = null)
270
return $this->call(Method::PATCH, $path, $params);
274
* Performs a DELETE request.
276
* @return TestResponse
278
* @throws RedirectException
281
public function delete(string $path, ?array $params = null)
283
return $this->call(Method::DELETE, $path, $params);
287
* Performs an OPTIONS request.
289
* @return TestResponse
291
* @throws RedirectException
294
public function options(string $path, ?array $params = null)
296
return $this->call(Method::OPTIONS, $path, $params);
300
* Setup a Request object to use so that CodeIgniter
301
* won't try to auto-populate some of the items.
303
* @param string $method HTTP verb
305
protected function setupRequest(string $method, ?string $path = null): IncomingRequest
307
$config = config(App::class);
308
$uri = new SiteURI($config);
310
// $path may have a query in it
311
$path = URI::removeDotSegments($path);
312
$parts = explode('?', $path);
314
$query = $parts[1] ?? '';
316
$superglobals = service('superglobals');
317
$superglobals->setServer('QUERY_STRING', $query);
319
$uri->setPath($path);
320
$uri->setQuery($query);
322
Services::injectMock('uri', $uri);
324
$request = Services::incomingrequest($config, false);
326
$request->setMethod($method);
327
$request->setProtocolVersion('1.1');
329
if ($config->forceGlobalSecureRequests) {
330
$_SERVER['HTTPS'] = 'test';
331
$server = $request->getServer();
332
$server['HTTPS'] = 'test';
333
$request->setGlobal('server', $server);
340
* Setup the custom request's headers
342
* @return IncomingRequest
344
protected function setupHeaders(IncomingRequest $request)
346
if (! empty($this->headers)) {
347
foreach ($this->headers as $name => $value) {
348
$request->setHeader($name, $value);
356
* Populates the data of our Request with "global" data
357
* relevant to the request, like $_POST data.
359
* Always populate the GET vars based on the URI.
361
* @param string $name Superglobal name (lowercase)
362
* @param non-empty-array|null $params
366
* @throws ReflectionException
368
protected function populateGlobals(string $name, Request $request, ?array $params = null)
370
// $params should set the query vars if present,
371
// otherwise set it from the URL.
372
$get = ($params !== null && $params !== [] && $name === 'get')
374
: $this->getPrivateProperty($request->getUri(), 'query');
376
$request->setGlobal('get', $get);
378
if ($name === 'get') {
379
$request->setGlobal('request', $request->fetchGlobal('get'));
382
if ($name === 'post') {
383
$request->setGlobal($name, $params);
386
$request->fetchGlobal('post') + $request->fetchGlobal('get')
390
$_SESSION = $this->session ?? [];
396
* Set the request's body formatted according to the value in $this->bodyFormat.
397
* This allows the body to be formatted in a way that the controller is going to
398
* expect as in the case of testing a JSON or XML API.
400
* @param array|null $params The parameters to be formatted and put in the body.
402
protected function setRequestBody(Request $request, ?array $params = null): Request
404
if ($this->requestBody !== '') {
405
$request->setBody($this->requestBody);
408
if ($this->bodyFormat !== '') {
410
if ($this->bodyFormat === 'json') {
411
$formatMime = 'application/json';
412
} elseif ($this->bodyFormat === 'xml') {
413
$formatMime = 'application/xml';
416
if ($formatMime !== '') {
417
$request->setHeader('Content-Type', $formatMime);
420
if ($params !== null && $formatMime !== '') {
421
$formatted = service('format')->getFormatter($formatMime)->format($params);
422
// "withBodyFormat() and $params of call()" has higher priority than withBody().
423
$request->setBody($formatted);