Merge remote-tracking branch 'upstream/develop' into 4.4

This commit is contained in:
kenjis 2023-07-05 10:53:48 +09:00
commit 37449d3d3d
No known key found for this signature in database
GPG Key ID: BD254878922AF198
7 changed files with 305 additions and 34 deletions

View File

@ -1326,6 +1326,11 @@ class RouteCollection implements RouteCollectionInterface
// Build our resulting string, inserting the $params in
// the appropriate places.
foreach ($matches[0] as $index => $pattern) {
if (! isset($params[$index])) {
throw new InvalidArgumentException(
'Missing argument for "' . $pattern . '" in route "' . $from . '".'
);
}
if (! preg_match('#^' . $pattern . '$#u', $params[$index])) {
throw RouterException::forInvalidParameterType();
}

View File

@ -109,7 +109,7 @@ trait FeatureTestTrait
/**
* Set the raw body for the request
*
* @param mixed $body
* @param string $body
*
* @return $this
*/
@ -136,6 +136,8 @@ trait FeatureTestTrait
* Calls a single URI, executes it, and returns a TestResponse
* instance that can be used to run many assertions against.
*
* @param string $method HTTP verb
*
* @return TestResponse
*/
public function call(string $method, string $path, ?array $params = null)
@ -147,7 +149,7 @@ trait FeatureTestTrait
$request = $this->setupRequest($method, $path);
$request = $this->setupHeaders($request);
$request = $this->populateGlobals($method, $request, $params);
$request = $this->setRequestBody($request);
$request = $this->setRequestBody($request, $params);
// Initialize the RouteCollection
if (! $routes = $this->routes) {
@ -258,6 +260,8 @@ trait FeatureTestTrait
/**
* Setup a Request object to use so that CodeIgniter
* won't try to auto-populate some of the items.
*
* @param string $method HTTP verb
*/
protected function setupRequest(string $method, ?string $path = null): IncomingRequest
{
@ -302,6 +306,8 @@ trait FeatureTestTrait
*
* Always populate the GET vars based on the URI.
*
* @param string $method HTTP verb
*
* @return Request
*
* @throws ReflectionException
@ -310,16 +316,23 @@ trait FeatureTestTrait
{
// $params should set the query vars if present,
// otherwise set it from the URL.
$get = ! empty($params) && $method === 'get'
$get = (! empty($params) && $method === 'get')
? $params
: $this->getPrivateProperty($request->getUri(), 'query');
$request->setGlobal('get', $get);
if ($method !== 'get') {
$request->setGlobal($method, $params);
if ($method === 'get') {
$request->setGlobal('request', $request->fetchGlobal('get'));
}
$request->setGlobal('request', $params);
if ($method === 'post') {
$request->setGlobal($method, $params);
$request->setGlobal(
'request',
$request->fetchGlobal('post') + $request->fetchGlobal('get')
);
}
$_SESSION = $this->session ?? [];
@ -331,32 +344,31 @@ trait FeatureTestTrait
* This allows the body to be formatted in a way that the controller is going to
* expect as in the case of testing a JSON or XML API.
*
* @param array|null $params The parameters to be formatted and put in the body. If this is empty, it will get the
* what has been loaded into the request global of the request class.
* @param array|null $params The parameters to be formatted and put in the body.
*/
protected function setRequestBody(Request $request, ?array $params = null): Request
{
if (isset($this->requestBody) && $this->requestBody !== '') {
if ($this->requestBody !== '') {
$request->setBody($this->requestBody);
return $request;
}
if (isset($this->bodyFormat) && $this->bodyFormat !== '') {
if (empty($params)) {
$params = $request->fetchGlobal('request');
}
if ($this->bodyFormat !== '') {
$formatMime = '';
if ($this->bodyFormat === 'json') {
$formatMime = 'application/json';
} elseif ($this->bodyFormat === 'xml') {
$formatMime = 'application/xml';
}
if (! empty($formatMime) && ! empty($params)) {
$formatted = Services::format()->getFormatter($formatMime)->format($params);
$request->setBody($formatted);
if ($formatMime !== '') {
$request->setHeader('Content-Type', $formatMime);
}
if ($params !== null && $formatMime !== '') {
$formatted = Services::format()->getFormatter($formatMime)->format($params);
// "withBodyFormat() and $params of call()" has higher priority than withBody().
$request->setBody($formatted);
}
}
return $request;

View File

@ -17,6 +17,7 @@ use CodeIgniter\HTTP\URI;
use CodeIgniter\Router\Exceptions\RouterException;
use CodeIgniter\Test\CIUnitTestCase;
use Config\App;
use InvalidArgumentException;
/**
* @backupGlobals enabled
@ -900,4 +901,20 @@ final class MiscUrlTest extends CIUnitTestCase
url_to('path-to', 'string', 13, 'en')
);
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/7651
*/
public function testUrlToMissingArgument()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Missing argument for "([a-zA-Z]+)" in route "([a-zA-Z]+)/login".');
$routes = Services::routes();
$routes->group('(:alpha)', static function ($routes) {
$routes->match(['get'], 'login', 'Common\LoginController::loginView', ['as' => 'loginURL']);
});
url_to('loginURL');
}
}

View File

@ -376,6 +376,162 @@ final class FeatureTestTraitTest extends CIUnitTestCase
$this->assertTrue($response->isOK());
}
public function testCallGetWithParams()
{
$this->withRoutes([
[
'get',
'home',
static fn () => json_encode(Services::request()->getGet()),
],
]);
$data = [
'true' => true,
'false' => false,
'int' => 2,
'null' => null,
'float' => 1.23,
'string' => 'foo',
];
$response = $this->get('home', $data);
$response->assertOK();
$this->assertStringContainsString(
// All GET values will be strings.
'{"true":"1","false":"","int":"2","null":"","float":"1.23","string":"foo"}',
$response->getBody()
);
}
public function testCallGetWithParamsAndREQUEST()
{
$this->withRoutes([
[
'get',
'home',
static fn () => json_encode(Services::request()->fetchGlobal('request')),
],
]);
$data = [
'true' => true,
'false' => false,
'int' => 2,
'null' => null,
'float' => 1.23,
'string' => 'foo',
];
$response = $this->get('home', $data);
$response->assertOK();
$this->assertStringContainsString(
// All GET values will be strings.
'{"true":"1","false":"","int":"2","null":"","float":"1.23","string":"foo"}',
$response->getBody()
);
}
public function testCallPostWithParams()
{
$this->withRoutes([
[
'post',
'home',
static fn () => json_encode(Services::request()->getPost()),
],
]);
$data = [
'true' => true,
'false' => false,
'int' => 2,
'null' => null,
'float' => 1.23,
'string' => 'foo',
];
$response = $this->post('home', $data);
$response->assertOK();
$this->assertStringContainsString(
// All POST values will be strings.
'{"true":"1","false":"","int":"2","null":"","float":"1.23","string":"foo"}',
$response->getBody()
);
}
public function testCallPostWithParamsAndREQUEST()
{
$this->withRoutes([
[
'post',
'home',
static fn () => json_encode(Services::request()->fetchGlobal('request')),
],
]);
$data = [
'true' => true,
'false' => false,
'int' => 2,
'null' => null,
'float' => 1.23,
'string' => 'foo',
];
$response = $this->post('home', $data);
$response->assertOK();
$this->assertStringContainsString(
// All POST values will be strings.
'{"true":"1","false":"","int":"2","null":"","float":"1.23","string":"foo"}',
$response->getBody()
);
}
public function testCallPutWithJsonRequest()
{
$this->withRoutes([
[
'put',
'home',
'\Tests\Support\Controllers\Popcorn::echoJson',
],
]);
$data = [
'true' => true,
'false' => false,
'int' => 2,
'null' => null,
'float' => 1.23,
'string' => 'foo',
];
$response = $this->withBodyFormat('json')
->call('put', 'home', $data);
$response->assertOK();
$response->assertJSONExact($data);
}
public function testCallPutWithJsonRequestAndREQUEST()
{
$this->withRoutes([
[
'put',
'home',
static fn () => json_encode(Services::request()->fetchGlobal('request')),
],
]);
$data = [
'true' => true,
'false' => false,
'int' => 2,
'null' => null,
'float' => 1.23,
'string' => 'foo',
];
$response = $this->withBodyFormat('json')
->call('put', 'home', $data);
$response->assertOK();
$this->assertStringContainsString('[]', $response->getBody());
}
public function testCallWithJsonRequest()
{
$this->withRoutes([
@ -385,9 +541,19 @@ final class FeatureTestTraitTest extends CIUnitTestCase
'\Tests\Support\Controllers\Popcorn::echoJson',
],
]);
$response = $this->withBodyFormat('json')->call('post', 'home', ['foo' => 'bar']);
$data = [
'true' => true,
'false' => false,
'int' => 2,
'null' => null,
'float' => 1.23,
'string' => 'foo',
];
$response = $this->withBodyFormat('json')
->call('post', 'home', $data);
$response->assertOK();
$response->assertJSONExact(['foo' => 'bar']);
$response->assertJSONExact($data);
}
public function testSetupRequestBodyWithParams()
@ -400,14 +566,39 @@ final class FeatureTestTraitTest extends CIUnitTestCase
$this->assertSame('application/json', $request->header('Content-Type')->getValue());
}
public function testSetupJSONRequestBodyWithBody()
{
$request = $this->setupRequest('post', 'home');
$request = $this->withBodyFormat('json')
->withBody(json_encode(['foo1' => 'bar1']))
->setRequestBody($request);
$this->assertJsonStringEqualsJsonString(
json_encode(['foo1' => 'bar1']),
$request->getBody()
);
$this->assertSame(
'application/json',
$request->header('Content-Type')->getValue()
);
}
public function testSetupRequestBodyWithXml()
{
$request = $this->setupRequest('post', 'home');
$request = $this->withBodyFormat('xml')->setRequestBody($request, ['foo' => 'bar']);
$data = [
'true' => true,
'false' => false,
'int' => 2,
'null' => null,
'float' => 1.23,
'string' => 'foo',
];
$request = $this->withBodyFormat('xml')->setRequestBody($request, $data);
$expectedXml = '<?xml version="1.0"?>
<response><foo>bar</foo></response>
<response><true>1</true><false/><int>2</int><null/><float>1.23</float><string>foo</string></response>
';
$this->assertSame($expectedXml, $request->getBody());

View File

@ -12,6 +12,10 @@ Release Date: Unreleased
BREAKING
********
- **FeatureTestTrait:** When using :ref:`withBodyFormat() <feature-formatting-the-request>`,
the priority of the request body has been changed.
See :ref:`Upgrading Guide <upgrade-437-feature-testing>` for details.
Message Changes
***************

View File

@ -18,6 +18,27 @@ Mandatory File Changes
Breaking Changes
****************
.. _upgrade-437-feature-testing:
Feature Testing Request Body
============================
If you call:
1. :ref:`withBody() <feature-setting-the-body>`
2. and :ref:`withBodyFormat() <feature-formatting-the-request>`
3. and pass the ``$params`` to :ref:`call() <feature-requesting-a-page>` (or shorthand methods)
the priority for a Request body has been changed. In the unlikely event that you
have test code affected by this change, modify it.
For example, now the ``$params`` is used to build the request body, and the ``$body``
is not used::
$this->withBody($body)->withBodyFormat('json')->call('post', $params)
Previously, the ``$body`` was used for the request body.
Breaking Enhancements
*********************

View File

@ -21,23 +21,34 @@ are called if you implement your own methods.
.. literalinclude:: feature/001.php
Requesting A Page
.. _feature-requesting-a-page:
Requesting a Page
=================
Essentially, feature tests simply allows you to call an endpoint on your application and get the results back.
to do this, you use the ``call()`` method. The first parameter is the HTTP method to use (most frequently either GET or POST).
The second parameter is the path on your site to test. The third parameter accepts an array that is used to populate the
superglobal variables for the HTTP verb you are using. So, a method of **GET** would have the **$_GET** variable
populated, while a **post** request would have the **$_POST** array populated.
To do this, you use the ``call()`` method.
1. The first parameter is the HTTP method to use (most frequently either GET or POST).
2. The second parameter is the URI path on your site to test.
3. The third parameter ``$params`` accepts an array that is used to populate the
superglobal variables for the HTTP verb you are using. So, a method of **GET**
would have the **$_GET** variable populated, while a **POST** request would
have the **$_POST** array populated. The ``$params`` is also used in
:ref:`feature-formatting-the-request`.
.. note:: The ``$params`` array does not make sense for every HTTP verb, but is
included for consistency.
.. literalinclude:: feature/002.php
Shorthand Methods
-----------------
Shorthand methods for each of the HTTP verbs exist to ease typing and make things clearer:
.. literalinclude:: feature/003.php
.. note:: The ``$params`` array does not make sense for every HTTP verb, but is included for consistency.
Setting Different Routes
------------------------
@ -74,22 +85,32 @@ to send out emails. You can tell the system to skip any event handling with the
.. literalinclude:: feature/007.php
Formatting The Request
.. _feature-formatting-the-request:
Formatting the Request
-----------------------
You can set the format of your request's body using the ``withBodyFormat()`` method. Currently this supports either
`json` or `xml`. This will take the parameters passed into ``call()``, ``post()``, ``get()``... and assign them to the
body of the request in the given format. This will also set the `Content-Type` header for your request accordingly.
``json`` or ``xml``.
This is useful when testing JSON or XML APIs so that you can set the request in the form that the controller will expect.
This will take the parameters passed into ``call()``, ``post()``, ``get()``... and assign them to the
body of the request in the given format.
This will also set the `Content-Type` header for your request accordingly.
.. literalinclude:: feature/008.php
.. _feature-setting-the-body:
Setting the Body
----------------
You can set the body of your request with the ``withBody()`` method. This allows you to format the body how you want
to format it. It is recommended that you use this if you have more complicated XMLs to test. This will also not set
the Content-Type header for you so if you need that, you can set it with the ``withHeaders()`` method.
to format it. It is recommended that you use this if you have more complicated XMLs to test.
This will not set
the `Content-Type` header for you. If you need that, you can set it with the ``withHeaders()`` method.
Checking the Response
=====================