Initial
This commit is contained in:
24
system/Test/CIDatabaseTestCase.php
Normal file
24
system/Test/CIDatabaseTestCase.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test;
|
||||
|
||||
/**
|
||||
* CIDatabaseTestCase
|
||||
*
|
||||
* Use DatabaseTestTrait instead.
|
||||
*
|
||||
* @deprecated 4.1.2
|
||||
*/
|
||||
abstract class CIDatabaseTestCase extends CIUnitTestCase
|
||||
{
|
||||
use DatabaseTestTrait;
|
||||
}
|
||||
541
system/Test/CIUnitTestCase.php
Normal file
541
system/Test/CIUnitTestCase.php
Normal file
@@ -0,0 +1,541 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test;
|
||||
|
||||
use CodeIgniter\CodeIgniter;
|
||||
use CodeIgniter\Config\Factories;
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\Database\MigrationRunner;
|
||||
use CodeIgniter\Database\Seeder;
|
||||
use CodeIgniter\Events\Events;
|
||||
use CodeIgniter\Router\RouteCollection;
|
||||
use CodeIgniter\Session\Handlers\ArrayHandler;
|
||||
use CodeIgniter\Test\Mock\MockCache;
|
||||
use CodeIgniter\Test\Mock\MockCodeIgniter;
|
||||
use CodeIgniter\Test\Mock\MockEmail;
|
||||
use CodeIgniter\Test\Mock\MockSession;
|
||||
use Config\App;
|
||||
use Config\Autoload;
|
||||
use Config\Modules;
|
||||
use Config\Services;
|
||||
use Exception;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Framework test case for PHPUnit.
|
||||
*/
|
||||
abstract class CIUnitTestCase extends TestCase
|
||||
{
|
||||
use ReflectionHelper;
|
||||
|
||||
/**
|
||||
* @var CodeIgniter
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* Methods to run during setUp.
|
||||
*
|
||||
* @var array of methods
|
||||
*/
|
||||
protected $setUpMethods = [
|
||||
'resetFactories',
|
||||
'mockCache',
|
||||
'mockEmail',
|
||||
'mockSession',
|
||||
];
|
||||
|
||||
/**
|
||||
* Methods to run during tearDown.
|
||||
*
|
||||
* @var array of methods
|
||||
*/
|
||||
protected $tearDownMethods = [];
|
||||
|
||||
/**
|
||||
* Store of identified traits.
|
||||
*
|
||||
* @var string[]|null
|
||||
*/
|
||||
private $traits;
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Database Properties
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Should run db migration?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $migrate = true;
|
||||
|
||||
/**
|
||||
* Should run db migration only once?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $migrateOnce = false;
|
||||
|
||||
/**
|
||||
* Should run seeding only once?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $seedOnce = false;
|
||||
|
||||
/**
|
||||
* Should the db be refreshed before test?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $refresh = true;
|
||||
|
||||
/**
|
||||
* The seed file(s) used for all tests within this test case.
|
||||
* Should be fully-namespaced or relative to $basePath
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $seed = '';
|
||||
|
||||
/**
|
||||
* The path to the seeds directory.
|
||||
* Allows overriding the default application directories.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $basePath = SUPPORTPATH . 'Database';
|
||||
|
||||
/**
|
||||
* The namespace(s) to help us find the migration classes.
|
||||
* Empty is equivalent to running `spark migrate -all`.
|
||||
* Note that running "all" runs migrations in date order,
|
||||
* but specifying namespaces runs them in namespace order (then date)
|
||||
*
|
||||
* @var array|string|null
|
||||
*/
|
||||
protected $namespace = 'Tests\Support';
|
||||
|
||||
/**
|
||||
* The name of the database group to connect to.
|
||||
* If not present, will use the defaultGroup.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $DBGroup = 'tests';
|
||||
|
||||
/**
|
||||
* Our database connection.
|
||||
*
|
||||
* @var BaseConnection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* Migration Runner instance.
|
||||
*
|
||||
* @var MigrationRunner|mixed
|
||||
*/
|
||||
protected $migrations;
|
||||
|
||||
/**
|
||||
* Seeder instance
|
||||
*
|
||||
* @var Seeder
|
||||
*/
|
||||
protected $seeder;
|
||||
|
||||
/**
|
||||
* Stores information needed to remove any
|
||||
* rows inserted via $this->hasInDatabase();
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $insertCache = [];
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Feature Properties
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* If present, will override application
|
||||
* routes when using call().
|
||||
*
|
||||
* @var RouteCollection|null
|
||||
*/
|
||||
protected $routes;
|
||||
|
||||
/**
|
||||
* Values to be set in the SESSION global
|
||||
* before running the test.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $session = [];
|
||||
|
||||
/**
|
||||
* Enabled auto clean op buffer after request call
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $clean = true;
|
||||
|
||||
/**
|
||||
* Custom request's headers
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $headers = [];
|
||||
|
||||
/**
|
||||
* Allows for formatting the request body to what
|
||||
* the controller is going to expect
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bodyFormat = '';
|
||||
|
||||
/**
|
||||
* Allows for directly setting the body to what
|
||||
* it needs to be.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $requestBody = '';
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Staging
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Load the helpers.
|
||||
*/
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
helper(['url', 'test']);
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
if (! $this->app) {
|
||||
$this->app = $this->createApplication();
|
||||
}
|
||||
|
||||
foreach ($this->setUpMethods as $method) {
|
||||
$this->{$method}();
|
||||
}
|
||||
|
||||
// Check for the database trait
|
||||
if (method_exists($this, 'setUpDatabase')) {
|
||||
$this->setUpDatabase();
|
||||
}
|
||||
|
||||
// Check for other trait methods
|
||||
$this->callTraitMethods('setUp');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
|
||||
foreach ($this->tearDownMethods as $method) {
|
||||
$this->{$method}();
|
||||
}
|
||||
|
||||
// Check for the database trait
|
||||
if (method_exists($this, 'tearDownDatabase')) {
|
||||
$this->tearDownDatabase();
|
||||
}
|
||||
|
||||
// Check for other trait methods
|
||||
$this->callTraitMethods('tearDown');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for traits with corresponding
|
||||
* methods for setUp or tearDown.
|
||||
*
|
||||
* @param string $stage 'setUp' or 'tearDown'
|
||||
*/
|
||||
private function callTraitMethods(string $stage): void
|
||||
{
|
||||
if ($this->traits === null) {
|
||||
$this->traits = class_uses_recursive($this);
|
||||
}
|
||||
|
||||
foreach ($this->traits as $trait) {
|
||||
$method = $stage . class_basename($trait);
|
||||
|
||||
if (method_exists($this, $method)) {
|
||||
$this->{$method}();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Mocking
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resets shared instanced for all Factories components
|
||||
*/
|
||||
protected function resetFactories()
|
||||
{
|
||||
Factories::reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets shared instanced for all Services
|
||||
*/
|
||||
protected function resetServices()
|
||||
{
|
||||
Services::reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the mock Cache driver to prevent filesystem collisions
|
||||
*/
|
||||
protected function mockCache()
|
||||
{
|
||||
Services::injectMock('cache', new MockCache());
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the mock email driver so no emails really send
|
||||
*/
|
||||
protected function mockEmail()
|
||||
{
|
||||
Services::injectMock('email', new MockEmail(config('Email')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the mock session driver into Services
|
||||
*/
|
||||
protected function mockSession()
|
||||
{
|
||||
$_SESSION = [];
|
||||
|
||||
$config = config('App');
|
||||
$session = new MockSession(new ArrayHandler($config, '0.0.0.0'), $config);
|
||||
|
||||
Services::injectMock('session', $session);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Assertions
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Custom function to hook into CodeIgniter's Logging mechanism
|
||||
* to check if certain messages were logged during code execution.
|
||||
*
|
||||
* @param string|null $expectedMessage
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function assertLogged(string $level, $expectedMessage = null)
|
||||
{
|
||||
$result = TestLogger::didLog($level, $expectedMessage);
|
||||
|
||||
$this->assertTrue($result, sprintf(
|
||||
'Failed asserting that expected message "%s" with level "%s" was logged.',
|
||||
$expectedMessage ?? '',
|
||||
$level
|
||||
));
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks into CodeIgniter's Events system to check if a specific
|
||||
* event was triggered or not.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function assertEventTriggered(string $eventName): bool
|
||||
{
|
||||
$found = false;
|
||||
$eventName = strtolower($eventName);
|
||||
|
||||
foreach (Events::getPerformanceLogs() as $log) {
|
||||
if ($log['event'] !== $eventName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
$this->assertTrue($found);
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks into xdebug's headers capture, looking for a specific header
|
||||
* emitted
|
||||
*
|
||||
* @param string $header The leading portion of the header we are looking for
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function assertHeaderEmitted(string $header, bool $ignoreCase = false): void
|
||||
{
|
||||
$found = false;
|
||||
|
||||
if (! function_exists('xdebug_get_headers')) {
|
||||
$this->markTestSkipped('XDebug not found.');
|
||||
}
|
||||
|
||||
foreach (xdebug_get_headers() as $emitted) {
|
||||
$found = $ignoreCase ?
|
||||
(stripos($emitted, $header) === 0) :
|
||||
(strpos($emitted, $header) === 0);
|
||||
if ($found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertTrue($found, "Didn't find header for {$header}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks into xdebug's headers capture, looking for a specific header
|
||||
* emitted
|
||||
*
|
||||
* @param string $header The leading portion of the header we don't want to find
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function assertHeaderNotEmitted(string $header, bool $ignoreCase = false): void
|
||||
{
|
||||
$found = false;
|
||||
|
||||
if (! function_exists('xdebug_get_headers')) {
|
||||
$this->markTestSkipped('XDebug not found.');
|
||||
}
|
||||
|
||||
foreach (xdebug_get_headers() as $emitted) {
|
||||
$found = $ignoreCase ?
|
||||
(stripos($emitted, $header) === 0) :
|
||||
(strpos($emitted, $header) === 0);
|
||||
if ($found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$success = ! $found;
|
||||
$this->assertTrue($success, "Found header for {$header}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom function to test that two values are "close enough".
|
||||
* This is intended for extended execution time testing,
|
||||
* where the result is close but not exactly equal to the
|
||||
* expected time, for reasons beyond our control.
|
||||
*
|
||||
* @param mixed $actual
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function assertCloseEnough(int $expected, $actual, string $message = '', int $tolerance = 1)
|
||||
{
|
||||
$difference = abs($expected - (int) floor($actual));
|
||||
|
||||
$this->assertLessThanOrEqual($tolerance, $difference, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom function to test that two values are "close enough".
|
||||
* This is intended for extended execution time testing,
|
||||
* where the result is close but not exactly equal to the
|
||||
* expected time, for reasons beyond our control.
|
||||
*
|
||||
* @param mixed $expected
|
||||
* @param mixed $actual
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return bool|void
|
||||
*/
|
||||
public function assertCloseEnoughString($expected, $actual, string $message = '', int $tolerance = 1)
|
||||
{
|
||||
$expected = (string) $expected;
|
||||
$actual = (string) $actual;
|
||||
if (strlen($expected) !== strlen($actual)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$expected = (int) substr($expected, -2);
|
||||
$actual = (int) substr($actual, -2);
|
||||
$difference = abs($expected - $actual);
|
||||
|
||||
$this->assertLessThanOrEqual($tolerance, $difference, $message);
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Utility
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Loads up an instance of CodeIgniter
|
||||
* and gets the environment setup.
|
||||
*
|
||||
* @return CodeIgniter
|
||||
*/
|
||||
protected function createApplication()
|
||||
{
|
||||
// Initialize the autoloader.
|
||||
Services::autoloader()->initialize(new Autoload(), new Modules());
|
||||
|
||||
$app = new MockCodeIgniter(new App());
|
||||
$app->initialize();
|
||||
|
||||
return $app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return first matching emitted header.
|
||||
*
|
||||
* @param string $header Identifier of the header of interest
|
||||
*
|
||||
* @return string|null The value of the header found, null if not found
|
||||
*/
|
||||
protected function getHeaderEmitted(string $header, bool $ignoreCase = false): ?string
|
||||
{
|
||||
if (! function_exists('xdebug_get_headers')) {
|
||||
$this->markTestSkipped('XDebug not found.');
|
||||
}
|
||||
|
||||
foreach (xdebug_get_headers() as $emitted) {
|
||||
$found = $ignoreCase ?
|
||||
(stripos($emitted, $header) === 0) :
|
||||
(strpos($emitted, $header) === 0);
|
||||
if ($found) {
|
||||
return $emitted;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
118
system/Test/Constraints/SeeInDatabase.php
Normal file
118
system/Test/Constraints/SeeInDatabase.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Constraints;
|
||||
|
||||
use CodeIgniter\Database\ConnectionInterface;
|
||||
use PHPUnit\Framework\Constraint\Constraint;
|
||||
|
||||
class SeeInDatabase extends Constraint
|
||||
{
|
||||
/**
|
||||
* The number of results that will show in the database
|
||||
* in case of failure.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $show = 3;
|
||||
|
||||
/**
|
||||
* @var ConnectionInterface
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* Data used to compare results against.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* SeeInDatabase constructor.
|
||||
*/
|
||||
public function __construct(ConnectionInterface $db, array $data)
|
||||
{
|
||||
$this->db = $db;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if data is found in the table
|
||||
*
|
||||
* @param mixed $table
|
||||
*/
|
||||
protected function matches($table): bool
|
||||
{
|
||||
return $this->db->table($table)->where($this->data)->countAllResults() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description of the failure
|
||||
*
|
||||
* @param mixed $table
|
||||
*/
|
||||
protected function failureDescription($table): string
|
||||
{
|
||||
return sprintf(
|
||||
"a row in the table [%s] matches the attributes \n%s\n\n%s",
|
||||
$table,
|
||||
$this->toString(JSON_PRETTY_PRINT),
|
||||
$this->getAdditionalInfo($table)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets additional records similar to $data.
|
||||
*/
|
||||
protected function getAdditionalInfo(string $table): string
|
||||
{
|
||||
$builder = $this->db->table($table);
|
||||
|
||||
$similar = $builder->where(
|
||||
array_key_first($this->data),
|
||||
$this->data[array_key_first($this->data)]
|
||||
)->limit($this->show)->get()->getResultArray();
|
||||
|
||||
if ($similar !== []) {
|
||||
$description = 'Found similar results: ' . json_encode($similar, JSON_PRETTY_PRINT);
|
||||
} else {
|
||||
// Does the table have any results at all?
|
||||
$results = $this->db->table($table)
|
||||
->limit($this->show)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
if ($results !== []) {
|
||||
return 'The table is empty.';
|
||||
}
|
||||
|
||||
$description = 'Found: ' . json_encode($results, JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
$total = $this->db->table($table)->countAll();
|
||||
if ($total > $this->show) {
|
||||
$description .= sprintf(' and %s others', $total - $this->show);
|
||||
}
|
||||
|
||||
return $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a string representation of the constraint
|
||||
*
|
||||
* @param int $options
|
||||
*/
|
||||
public function toString($options = 0): string
|
||||
{
|
||||
return json_encode($this->data, $options);
|
||||
}
|
||||
}
|
||||
99
system/Test/ControllerResponse.php
Normal file
99
system/Test/ControllerResponse.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test;
|
||||
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\Services;
|
||||
|
||||
/**
|
||||
* Testable response from a controller
|
||||
*
|
||||
* @deprecated Use TestResponse directly
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class ControllerResponse extends TestResponse
|
||||
{
|
||||
/**
|
||||
* The message payload.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @deprecated Use $response->getBody() instead
|
||||
*/
|
||||
protected $body;
|
||||
|
||||
/**
|
||||
* DOM for the body.
|
||||
*
|
||||
* @var DOMParser
|
||||
*
|
||||
* @deprecated Use $domParser instead
|
||||
*/
|
||||
protected $dom;
|
||||
|
||||
/**
|
||||
* Maintains the deprecated $dom property.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(Services::response());
|
||||
|
||||
$this->dom = &$this->domParser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the response.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @deprecated Will revert to parent::setResponse() in a future release (no $body updates)
|
||||
*/
|
||||
public function setResponse(ResponseInterface $response)
|
||||
{
|
||||
parent::setResponse($response);
|
||||
|
||||
$this->body = $response->getBody() ?? '';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the body and updates the DOM.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @deprecated Use response()->setBody() instead
|
||||
*/
|
||||
public function setBody(string $body)
|
||||
{
|
||||
$this->body = $body;
|
||||
|
||||
if ($body !== '') {
|
||||
$this->domParser->withString($body);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the body.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @deprecated Use response()->getBody() instead
|
||||
*/
|
||||
public function getBody()
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
}
|
||||
297
system/Test/ControllerTestTrait.php
Normal file
297
system/Test/ControllerTestTrait.php
Normal file
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\HTTP\Exceptions\HTTPException;
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
use CodeIgniter\HTTP\Response;
|
||||
use CodeIgniter\HTTP\URI;
|
||||
use Config\App;
|
||||
use Config\Services;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Controller Test Trait
|
||||
*
|
||||
* Provides features that make testing controllers simple and fluent.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* $this->withRequest($request)
|
||||
* ->withResponse($response)
|
||||
* ->withURI($uri)
|
||||
* ->withBody($body)
|
||||
* ->controller('App\Controllers\Home')
|
||||
* ->execute('methodName');
|
||||
*/
|
||||
trait ControllerTestTrait
|
||||
{
|
||||
/**
|
||||
* Controller configuration.
|
||||
*
|
||||
* @var App
|
||||
*/
|
||||
protected $appConfig;
|
||||
|
||||
/**
|
||||
* Request.
|
||||
*
|
||||
* @var IncomingRequest
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* Response.
|
||||
*
|
||||
* @var Response
|
||||
*/
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* Message logger.
|
||||
*
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* Initialized controller.
|
||||
*
|
||||
* @var Controller
|
||||
*/
|
||||
protected $controller;
|
||||
|
||||
/**
|
||||
* URI of this request.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $uri = 'http://example.com';
|
||||
|
||||
/**
|
||||
* Request body.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $body;
|
||||
|
||||
/**
|
||||
* Initializes required components.
|
||||
*/
|
||||
protected function setUpControllerTestTrait(): void
|
||||
{
|
||||
// The URL helper is always loaded by the system so ensure it is available.
|
||||
helper('url');
|
||||
|
||||
if (empty($this->appConfig)) {
|
||||
$this->appConfig = config('App');
|
||||
}
|
||||
|
||||
if (! $this->uri instanceof URI) {
|
||||
$this->uri = Services::uri($this->appConfig->baseURL ?? 'http://example.com/', false);
|
||||
}
|
||||
|
||||
if (empty($this->request)) {
|
||||
// Do some acrobatics so we can use the Request service with our own URI
|
||||
$tempUri = Services::uri();
|
||||
Services::injectMock('uri', $this->uri);
|
||||
|
||||
$this->withRequest(Services::request($this->appConfig, false)->setBody($this->body));
|
||||
|
||||
// Restore the URI service
|
||||
Services::injectMock('uri', $tempUri);
|
||||
}
|
||||
|
||||
if (empty($this->response)) {
|
||||
$this->response = Services::response($this->appConfig, false);
|
||||
}
|
||||
|
||||
if (empty($this->logger)) {
|
||||
$this->logger = Services::logger();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the specified controller, and generates any needed dependencies.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function controller(string $name)
|
||||
{
|
||||
if (! class_exists($name)) {
|
||||
throw new InvalidArgumentException('Invalid Controller: ' . $name);
|
||||
}
|
||||
|
||||
$this->controller = new $name();
|
||||
$this->controller->initController($this->request, $this->response, $this->logger);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the specified method on the controller and returns the results.
|
||||
*
|
||||
* @param array $params
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return TestResponse
|
||||
*/
|
||||
public function execute(string $method, ...$params)
|
||||
{
|
||||
if (! method_exists($this->controller, $method) || ! is_callable([$this->controller, $method])) {
|
||||
throw new InvalidArgumentException('Method does not exist or is not callable in controller: ' . $method);
|
||||
}
|
||||
|
||||
$response = null;
|
||||
|
||||
try {
|
||||
ob_start();
|
||||
$response = $this->controller->{$method}(...$params);
|
||||
} catch (Throwable $e) {
|
||||
$code = $e->getCode();
|
||||
|
||||
// If code is not a valid HTTP status then assume there is an error
|
||||
if ($code < 100 || $code >= 600) {
|
||||
throw $e;
|
||||
}
|
||||
} finally {
|
||||
$output = ob_get_clean();
|
||||
}
|
||||
|
||||
// If the controller returned a view then add it to the output
|
||||
if (is_string($response)) {
|
||||
$output = is_string($output) ? $output . $response : $response;
|
||||
}
|
||||
|
||||
// If the controller did not return a response then start one
|
||||
if (! $response instanceof Response) {
|
||||
$response = $this->response;
|
||||
}
|
||||
|
||||
// Check for output to set or prepend
|
||||
// @see \CodeIgniter\CodeIgniter::gatherOutput()
|
||||
if (is_string($output)) {
|
||||
if (is_string($response->getBody())) {
|
||||
$response->setBody($output . $response->getBody());
|
||||
} else {
|
||||
$response->setBody($output);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for an overriding code from exceptions
|
||||
if (isset($code)) {
|
||||
$response->setStatusCode($code);
|
||||
}
|
||||
// Otherwise ensure there is a status code
|
||||
else {
|
||||
// getStatusCode() throws for empty codes
|
||||
try {
|
||||
$response->getStatusCode();
|
||||
} catch (HTTPException $e) {
|
||||
// If no code has been set then assume success
|
||||
$response->setStatusCode(200);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the result and add the Request for reference
|
||||
return (new TestResponse($response))->setRequest($this->request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set controller's config, with method chaining.
|
||||
*
|
||||
* @param mixed $appConfig
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function withConfig($appConfig)
|
||||
{
|
||||
$this->appConfig = $appConfig;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set controller's request, with method chaining.
|
||||
*
|
||||
* @param mixed $request
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function withRequest($request)
|
||||
{
|
||||
$this->request = $request;
|
||||
|
||||
// Make sure it's available for other classes
|
||||
Services::injectMock('request', $request);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set controller's response, with method chaining.
|
||||
*
|
||||
* @param mixed $response
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function withResponse($response)
|
||||
{
|
||||
$this->response = $response;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set controller's logger, with method chaining.
|
||||
*
|
||||
* @param mixed $logger
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function withLogger($logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the controller's URI, with method chaining.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function withUri(string $uri)
|
||||
{
|
||||
$this->uri = new URI($uri);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the method's body, with method chaining.
|
||||
*
|
||||
* @param string|null $body
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function withBody($body)
|
||||
{
|
||||
$this->body = $body;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
293
system/Test/ControllerTester.php
Normal file
293
system/Test/ControllerTester.php
Normal file
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
use CodeIgniter\HTTP\Response;
|
||||
use CodeIgniter\HTTP\URI;
|
||||
use Config\App;
|
||||
use Config\Services;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* ControllerTester Trait
|
||||
*
|
||||
* Provides features that make testing controllers simple and fluent.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* $this->withRequest($request)
|
||||
* ->withResponse($response)
|
||||
* ->withURI($uri)
|
||||
* ->withBody($body)
|
||||
* ->controller('App\Controllers\Home')
|
||||
* ->execute('methodName');
|
||||
*
|
||||
* @deprecated Use ControllerTestTrait instead
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
trait ControllerTester
|
||||
{
|
||||
/**
|
||||
* Controller configuration.
|
||||
*
|
||||
* @var App
|
||||
*/
|
||||
protected $appConfig;
|
||||
|
||||
/**
|
||||
* Request.
|
||||
*
|
||||
* @var IncomingRequest
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* Response.
|
||||
*
|
||||
* @var Response
|
||||
*/
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* Message logger.
|
||||
*
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* Initialized controller.
|
||||
*
|
||||
* @var Controller
|
||||
*/
|
||||
protected $controller;
|
||||
|
||||
/**
|
||||
* URI of this request.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $uri = 'http://example.com';
|
||||
|
||||
/**
|
||||
* Request or response body.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $body;
|
||||
|
||||
/**
|
||||
* Initializes required components.
|
||||
*/
|
||||
protected function setUpControllerTester(): void
|
||||
{
|
||||
if (empty($this->appConfig)) {
|
||||
$this->appConfig = config('App');
|
||||
}
|
||||
|
||||
if (! $this->uri instanceof URI) {
|
||||
$this->uri = Services::uri($this->appConfig->baseURL ?? 'http://example.com/', false);
|
||||
}
|
||||
|
||||
if (empty($this->request)) {
|
||||
// Do some acrobatics so we can use the Request service with our own URI
|
||||
$tempUri = Services::uri();
|
||||
Services::injectMock('uri', $this->uri);
|
||||
|
||||
$this->withRequest(Services::request($this->appConfig, false)->setBody($this->body));
|
||||
|
||||
// Restore the URI service
|
||||
Services::injectMock('uri', $tempUri);
|
||||
}
|
||||
|
||||
if (empty($this->response)) {
|
||||
$this->response = Services::response($this->appConfig, false);
|
||||
}
|
||||
|
||||
if (empty($this->logger)) {
|
||||
$this->logger = Services::logger();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the specified controller, and generates any needed dependencies.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function controller(string $name)
|
||||
{
|
||||
if (! class_exists($name)) {
|
||||
throw new InvalidArgumentException('Invalid Controller: ' . $name);
|
||||
}
|
||||
|
||||
$this->controller = new $name();
|
||||
$this->controller->initController($this->request, $this->response, $this->logger);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the specified method on the controller and returns the results.
|
||||
*
|
||||
* @param array $params
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return ControllerResponse
|
||||
*/
|
||||
public function execute(string $method, ...$params)
|
||||
{
|
||||
if (! method_exists($this->controller, $method) || ! is_callable([$this->controller, $method])) {
|
||||
throw new InvalidArgumentException('Method does not exist or is not callable in controller: ' . $method);
|
||||
}
|
||||
|
||||
// The URL helper is always loaded by the system
|
||||
// so ensure it's available.
|
||||
helper('url');
|
||||
|
||||
$result = (new ControllerResponse())
|
||||
->setRequest($this->request)
|
||||
->setResponse($this->response);
|
||||
|
||||
$response = null;
|
||||
|
||||
try {
|
||||
ob_start();
|
||||
|
||||
$response = $this->controller->{$method}(...$params);
|
||||
} catch (Throwable $e) {
|
||||
$code = $e->getCode();
|
||||
|
||||
// If code is not a valid HTTP status then assume there is an error
|
||||
if ($code < 100 || $code >= 600) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$result->response()->setStatusCode($code);
|
||||
} finally {
|
||||
$output = ob_get_clean();
|
||||
|
||||
// If the controller returned a response, use it
|
||||
if (isset($response) && $response instanceof Response) {
|
||||
$result->setResponse($response);
|
||||
}
|
||||
|
||||
// check if controller returned a view rather than echoing it
|
||||
if (is_string($response)) {
|
||||
$output = $response;
|
||||
$result->response()->setBody($output);
|
||||
$result->setBody($output);
|
||||
} elseif (! empty($response) && ! empty($response->getBody())) {
|
||||
$result->setBody($response->getBody());
|
||||
} else {
|
||||
$result->setBody('');
|
||||
}
|
||||
}
|
||||
|
||||
// If not response code has been sent, assume a success
|
||||
if (empty($result->response()->getStatusCode())) {
|
||||
$result->response()->setStatusCode(200);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set controller's config, with method chaining.
|
||||
*
|
||||
* @param mixed $appConfig
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function withConfig($appConfig)
|
||||
{
|
||||
$this->appConfig = $appConfig;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set controller's request, with method chaining.
|
||||
*
|
||||
* @param mixed $request
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function withRequest($request)
|
||||
{
|
||||
$this->request = $request;
|
||||
|
||||
// Make sure it's available for other classes
|
||||
Services::injectMock('request', $request);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set controller's response, with method chaining.
|
||||
*
|
||||
* @param mixed $response
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function withResponse($response)
|
||||
{
|
||||
$this->response = $response;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set controller's logger, with method chaining.
|
||||
*
|
||||
* @param mixed $logger
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function withLogger($logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the controller's URI, with method chaining.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function withUri(string $uri)
|
||||
{
|
||||
$this->uri = new URI($uri);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the method's body, with method chaining.
|
||||
*
|
||||
* @param string|null $body
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function withBody($body)
|
||||
{
|
||||
$this->body = $body;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
283
system/Test/DOMParser.php
Normal file
283
system/Test/DOMParser.php
Normal file
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test;
|
||||
|
||||
use BadMethodCallException;
|
||||
use DOMDocument;
|
||||
use DOMNodeList;
|
||||
use DOMXPath;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Load a response into a DOMDocument for testing assertions based on that
|
||||
*/
|
||||
class DOMParser
|
||||
{
|
||||
/**
|
||||
* DOM for the body,
|
||||
*
|
||||
* @var DOMDocument
|
||||
*/
|
||||
protected $dom;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @throws BadMethodCallException
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
if (! extension_loaded('DOM')) {
|
||||
// always there in travis-ci
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new BadMethodCallException('DOM extension is required, but not currently loaded.');
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
$this->dom = new DOMDocument('1.0', 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the body of the current document.
|
||||
*/
|
||||
public function getBody(): string
|
||||
{
|
||||
return $this->dom->saveHTML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a string as the body that we want to work with.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withString(string $content)
|
||||
{
|
||||
// converts all special characters to utf-8
|
||||
$content = mb_convert_encoding($content, 'HTML-ENTITIES', 'UTF-8');
|
||||
|
||||
// turning off some errors
|
||||
libxml_use_internal_errors(true);
|
||||
|
||||
if (! $this->dom->loadHTML($content)) {
|
||||
// unclear how we would get here, given that we are trapping libxml errors
|
||||
// @codeCoverageIgnoreStart
|
||||
libxml_clear_errors();
|
||||
|
||||
throw new BadMethodCallException('Invalid HTML');
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
// ignore the whitespace.
|
||||
$this->dom->preserveWhiteSpace = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the contents of a file as a string
|
||||
* so that we can work with it.
|
||||
*
|
||||
* @return DOMParser
|
||||
*/
|
||||
public function withFile(string $path)
|
||||
{
|
||||
if (! is_file($path)) {
|
||||
throw new InvalidArgumentException(basename($path) . ' is not a valid file.');
|
||||
}
|
||||
|
||||
$content = file_get_contents($path);
|
||||
|
||||
return $this->withString($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the text is found within the result.
|
||||
*
|
||||
* @param string $search
|
||||
* @param string $element
|
||||
*/
|
||||
public function see(?string $search = null, ?string $element = null): bool
|
||||
{
|
||||
// If Element is null, we're just scanning for text
|
||||
if ($element === null) {
|
||||
$content = $this->dom->saveHTML($this->dom->documentElement);
|
||||
|
||||
return mb_strpos($content, $search) !== false;
|
||||
}
|
||||
|
||||
$result = $this->doXPath($search, $element);
|
||||
|
||||
return (bool) $result->length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the text is NOT found within the result.
|
||||
*
|
||||
* @param string $search
|
||||
*/
|
||||
public function dontSee(?string $search = null, ?string $element = null): bool
|
||||
{
|
||||
return ! $this->see($search, $element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if an element with the matching CSS specifier
|
||||
* is found within the current DOM.
|
||||
*/
|
||||
public function seeElement(string $element): bool
|
||||
{
|
||||
return $this->see(null, $element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the element is available within the result.
|
||||
*/
|
||||
public function dontSeeElement(string $element): bool
|
||||
{
|
||||
return $this->dontSee(null, $element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a link with the specified text is found
|
||||
* within the results.
|
||||
*/
|
||||
public function seeLink(string $text, ?string $details = null): bool
|
||||
{
|
||||
return $this->see($text, 'a' . $details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for an input named $field with a value of $value.
|
||||
*/
|
||||
public function seeInField(string $field, string $value): bool
|
||||
{
|
||||
$result = $this->doXPath(null, 'input', ["[@value=\"{$value}\"][@name=\"{$field}\"]"]);
|
||||
|
||||
return (bool) $result->length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for checkboxes that are currently checked.
|
||||
*/
|
||||
public function seeCheckboxIsChecked(string $element): bool
|
||||
{
|
||||
$result = $this->doXPath(null, 'input' . $element, [
|
||||
'[@type="checkbox"]',
|
||||
'[@checked="checked"]',
|
||||
]);
|
||||
|
||||
return (bool) $result->length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the DOM using an XPath expression.
|
||||
*
|
||||
* @return DOMNodeList
|
||||
*/
|
||||
protected function doXPath(?string $search, string $element, array $paths = [])
|
||||
{
|
||||
// Otherwise, grab any elements that match
|
||||
// the selector
|
||||
$selector = $this->parseSelector($element);
|
||||
|
||||
$path = '';
|
||||
|
||||
// By ID
|
||||
if (! empty($selector['id'])) {
|
||||
$path = empty($selector['tag'])
|
||||
? "id(\"{$selector['id']}\")"
|
||||
: "//{$selector['tag']}[@id=\"{$selector['id']}\"]";
|
||||
}
|
||||
// By Class
|
||||
elseif (! empty($selector['class'])) {
|
||||
$path = empty($selector['tag'])
|
||||
? "//*[@class=\"{$selector['class']}\"]"
|
||||
: "//{$selector['tag']}[@class=\"{$selector['class']}\"]";
|
||||
}
|
||||
// By tag only
|
||||
elseif (! empty($selector['tag'])) {
|
||||
$path = "//{$selector['tag']}";
|
||||
}
|
||||
|
||||
if (! empty($selector['attr'])) {
|
||||
foreach ($selector['attr'] as $key => $value) {
|
||||
$path .= "[@{$key}=\"{$value}\"]";
|
||||
}
|
||||
}
|
||||
|
||||
// $paths might contain a number of different
|
||||
// ready to go xpath portions to tack on.
|
||||
if (! empty($paths) && is_array($paths)) {
|
||||
foreach ($paths as $extra) {
|
||||
$path .= $extra;
|
||||
}
|
||||
}
|
||||
|
||||
if ($search !== null) {
|
||||
$path .= "[contains(., \"{$search}\")]";
|
||||
}
|
||||
|
||||
$xpath = new DOMXPath($this->dom);
|
||||
|
||||
return $xpath->query($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for the a selector in the passed text.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function parseSelector(string $selector)
|
||||
{
|
||||
$id = null;
|
||||
$class = null;
|
||||
$attr = null;
|
||||
|
||||
// ID?
|
||||
if (strpos($selector, '#') !== false) {
|
||||
[$tag, $id] = explode('#', $selector);
|
||||
}
|
||||
// Attribute
|
||||
elseif (strpos($selector, '[') !== false && strpos($selector, ']') !== false) {
|
||||
$open = strpos($selector, '[');
|
||||
$close = strpos($selector, ']');
|
||||
|
||||
$tag = substr($selector, 0, $open);
|
||||
$text = substr($selector, $open + 1, $close - 2);
|
||||
|
||||
// We only support a single attribute currently
|
||||
$text = explode(',', $text);
|
||||
$text = trim(array_shift($text));
|
||||
|
||||
[$name, $value] = explode('=', $text);
|
||||
|
||||
$name = trim($name);
|
||||
$value = trim($value);
|
||||
$attr = [$name => trim($value, '] ')];
|
||||
}
|
||||
// Class?
|
||||
elseif (strpos($selector, '.') !== false) {
|
||||
[$tag, $class] = explode('.', $selector);
|
||||
}
|
||||
// Otherwise, assume the entire string is our tag
|
||||
else {
|
||||
$tag = $selector;
|
||||
}
|
||||
|
||||
return [
|
||||
'tag' => $tag,
|
||||
'id' => $id,
|
||||
'class' => $class,
|
||||
'attr' => $attr,
|
||||
];
|
||||
}
|
||||
}
|
||||
326
system/Test/DatabaseTestTrait.php
Normal file
326
system/Test/DatabaseTestTrait.php
Normal file
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test;
|
||||
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Test\Constraints\SeeInDatabase;
|
||||
use Config\Database;
|
||||
use Config\Migrations;
|
||||
use Config\Services;
|
||||
|
||||
/**
|
||||
* DatabaseTestTrait
|
||||
*
|
||||
* Provides functionality for refreshing/seeding
|
||||
* the database during testing.
|
||||
*
|
||||
* @mixin CIUnitTestCase
|
||||
*/
|
||||
trait DatabaseTestTrait
|
||||
{
|
||||
/**
|
||||
* Is db migration done once or more than once?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $doneMigration = false;
|
||||
|
||||
/**
|
||||
* Is seeding done once or more than once?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $doneSeed = false;
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Staging
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Runs the trait set up methods.
|
||||
*/
|
||||
protected function setUpDatabase()
|
||||
{
|
||||
$this->loadDependencies();
|
||||
$this->setUpMigrate();
|
||||
$this->setUpSeed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the trait set up methods.
|
||||
*/
|
||||
protected function tearDownDatabase()
|
||||
{
|
||||
$this->clearInsertCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load any database test dependencies.
|
||||
*/
|
||||
public function loadDependencies()
|
||||
{
|
||||
if ($this->db === null) {
|
||||
$this->db = Database::connect($this->DBGroup);
|
||||
$this->db->initialize();
|
||||
}
|
||||
|
||||
if ($this->migrations === null) {
|
||||
// Ensure that we can run migrations
|
||||
$config = new Migrations();
|
||||
$config->enabled = true;
|
||||
|
||||
$this->migrations = Services::migrations($config, $this->db);
|
||||
$this->migrations->setSilent(false);
|
||||
}
|
||||
|
||||
if ($this->seeder === null) {
|
||||
$this->seeder = Database::seeder($this->DBGroup);
|
||||
$this->seeder->setSilent(true);
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Migrations
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Migrate on setUp
|
||||
*/
|
||||
protected function setUpMigrate()
|
||||
{
|
||||
if ($this->migrateOnce === false || self::$doneMigration === false) {
|
||||
if ($this->refresh === true) {
|
||||
$this->regressDatabase();
|
||||
|
||||
// Reset counts on faked items
|
||||
Fabricator::resetCounts();
|
||||
}
|
||||
|
||||
$this->migrateDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Regress migrations as defined by the class
|
||||
*/
|
||||
protected function regressDatabase()
|
||||
{
|
||||
if ($this->migrate === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If no namespace was specified then rollback all
|
||||
if (empty($this->namespace)) {
|
||||
$this->migrations->setNamespace(null);
|
||||
$this->migrations->regress(0, 'tests');
|
||||
}
|
||||
|
||||
// Regress each specified namespace
|
||||
else {
|
||||
$namespaces = is_array($this->namespace) ? $this->namespace : [$this->namespace];
|
||||
|
||||
foreach ($namespaces as $namespace) {
|
||||
$this->migrations->setNamespace($namespace);
|
||||
$this->migrations->regress(0, 'tests');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run migrations as defined by the class
|
||||
*/
|
||||
protected function migrateDatabase()
|
||||
{
|
||||
if ($this->migrate === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If no namespace was specified then migrate all
|
||||
if (empty($this->namespace)) {
|
||||
$this->migrations->setNamespace(null);
|
||||
$this->migrations->latest('tests');
|
||||
self::$doneMigration = true;
|
||||
}
|
||||
// Run migrations for each specified namespace
|
||||
else {
|
||||
$namespaces = is_array($this->namespace) ? $this->namespace : [$this->namespace];
|
||||
|
||||
foreach ($namespaces as $namespace) {
|
||||
$this->migrations->setNamespace($namespace);
|
||||
$this->migrations->latest('tests');
|
||||
self::$doneMigration = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Seeds
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Seed on setUp
|
||||
*/
|
||||
protected function setUpSeed()
|
||||
{
|
||||
if ($this->seedOnce === false || self::$doneSeed === false) {
|
||||
$this->runSeeds();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run seeds as defined by the class
|
||||
*/
|
||||
protected function runSeeds()
|
||||
{
|
||||
if (! empty($this->seed)) {
|
||||
if (! empty($this->basePath)) {
|
||||
$this->seeder->setPath(rtrim($this->basePath, '/') . '/Seeds');
|
||||
}
|
||||
|
||||
$seeds = is_array($this->seed) ? $this->seed : [$this->seed];
|
||||
|
||||
foreach ($seeds as $seed) {
|
||||
$this->seed($seed);
|
||||
}
|
||||
}
|
||||
|
||||
self::$doneSeed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds that database with a specific seeder.
|
||||
*/
|
||||
public function seed(string $name)
|
||||
{
|
||||
$this->seeder->call($name);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Utility
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Reset $doneMigration and $doneSeed
|
||||
*
|
||||
* @afterClass
|
||||
*/
|
||||
public static function resetMigrationSeedCount()
|
||||
{
|
||||
self::$doneMigration = false;
|
||||
self::$doneSeed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any rows inserted via $this->hasInDatabase()
|
||||
*/
|
||||
protected function clearInsertCache()
|
||||
{
|
||||
foreach ($this->insertCache as $row) {
|
||||
$this->db->table($row[0])
|
||||
->where($row[1])
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the Builder class appropriate for the current database.
|
||||
*
|
||||
* @return BaseBuilder
|
||||
*/
|
||||
public function loadBuilder(string $tableName)
|
||||
{
|
||||
$builderClass = str_replace('Connection', 'Builder', get_class($this->db));
|
||||
|
||||
return new $builderClass($tableName, $this->db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a single column from a database row with criteria
|
||||
* matching $where.
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function grabFromDatabase(string $table, string $column, array $where)
|
||||
{
|
||||
$query = $this->db->table($table)
|
||||
->select($column)
|
||||
->where($where)
|
||||
->get();
|
||||
|
||||
$query = $query->getRow();
|
||||
|
||||
return $query->{$column} ?? false;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Assertions
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Asserts that records that match the conditions in $where DO
|
||||
* exist in the database.
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function seeInDatabase(string $table, array $where)
|
||||
{
|
||||
$constraint = new SeeInDatabase($this->db, $where);
|
||||
static::assertThat($table, $constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that records that match the conditions in $where do
|
||||
* not exist in the database.
|
||||
*/
|
||||
public function dontSeeInDatabase(string $table, array $where)
|
||||
{
|
||||
$count = $this->db->table($table)
|
||||
->where($where)
|
||||
->countAllResults();
|
||||
|
||||
$this->assertTrue($count === 0, 'Row was found in database');
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a row into to the database. This row will be removed
|
||||
* after the test has run.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasInDatabase(string $table, array $data)
|
||||
{
|
||||
$this->insertCache[] = [
|
||||
$table,
|
||||
$data,
|
||||
];
|
||||
|
||||
return $this->db->table($table)->insert($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the number of rows in the database that match $where
|
||||
* is equal to $expected.
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function seeNumRecords(int $expected, string $table, array $where)
|
||||
{
|
||||
$count = $this->db->table($table)
|
||||
->where($where)
|
||||
->countAllResults();
|
||||
|
||||
$this->assertEquals($expected, $count, 'Wrong number of matching rows in database.');
|
||||
}
|
||||
}
|
||||
542
system/Test/Fabricator.php
Normal file
542
system/Test/Fabricator.php
Normal file
@@ -0,0 +1,542 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test;
|
||||
|
||||
use CodeIgniter\Exceptions\FrameworkException;
|
||||
use CodeIgniter\Model;
|
||||
use Faker\Factory;
|
||||
use Faker\Generator;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Fabricator
|
||||
*
|
||||
* Bridge class for using Faker to create example data based on
|
||||
* model specifications.
|
||||
*/
|
||||
class Fabricator
|
||||
{
|
||||
/**
|
||||
* Array of counts for fabricated items
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $tableCounts = [];
|
||||
|
||||
/**
|
||||
* Locale-specific Faker instance
|
||||
*
|
||||
* @var Generator
|
||||
*/
|
||||
protected $faker;
|
||||
|
||||
/**
|
||||
* Model instance (can be non-framework if it follows framework design)
|
||||
*
|
||||
* @var Model|object
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* Locale used to initialize Faker
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $locale;
|
||||
|
||||
/**
|
||||
* Map of properties and their formatter to use
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
protected $formatters;
|
||||
|
||||
/**
|
||||
* Date fields present in the model
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dateFields = [];
|
||||
|
||||
/**
|
||||
* Array of data to add or override faked versions
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $overrides = [];
|
||||
|
||||
/**
|
||||
* Array of single-use data to override faked versions
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
protected $tempOverrides;
|
||||
|
||||
/**
|
||||
* Default formatter to use when nothing is detected
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $defaultFormatter = 'word';
|
||||
|
||||
/**
|
||||
* Store the model instance and initialize Faker to the locale.
|
||||
*
|
||||
* @param object|string $model Instance or classname of the model to use
|
||||
* @param array|null $formatters Array of property => formatter
|
||||
* @param string|null $locale Locale for Faker provider
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct($model, ?array $formatters = null, ?string $locale = null)
|
||||
{
|
||||
if (is_string($model)) {
|
||||
// Create a new model instance
|
||||
$model = model($model, false);
|
||||
}
|
||||
|
||||
if (! is_object($model)) {
|
||||
throw new InvalidArgumentException(lang('Fabricator.invalidModel'));
|
||||
}
|
||||
|
||||
$this->model = $model;
|
||||
|
||||
// If no locale was specified then use the App default
|
||||
if ($locale === null) {
|
||||
$locale = config('App')->defaultLocale;
|
||||
}
|
||||
|
||||
// There is no easy way to retrieve the locale from Faker so we will store it
|
||||
$this->locale = $locale;
|
||||
|
||||
// Create the locale-specific Generator
|
||||
$this->faker = Factory::create($this->locale);
|
||||
|
||||
// Determine eligible date fields
|
||||
foreach (['createdField', 'updatedField', 'deletedField'] as $field) {
|
||||
if (! empty($this->model->{$field})) {
|
||||
$this->dateFields[] = $this->model->{$field};
|
||||
}
|
||||
}
|
||||
|
||||
// Set the formatters
|
||||
$this->setFormatters($formatters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset internal counts
|
||||
*/
|
||||
public static function resetCounts()
|
||||
{
|
||||
self::$tableCounts = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count for a specific table
|
||||
*
|
||||
* @param string $table Name of the target table
|
||||
*/
|
||||
public static function getCount(string $table): int
|
||||
{
|
||||
return empty(self::$tableCounts[$table]) ? 0 : self::$tableCounts[$table];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the count for a specific table
|
||||
*
|
||||
* @param string $table Name of the target table
|
||||
* @param int $count Count value
|
||||
*
|
||||
* @return int The new count value
|
||||
*/
|
||||
public static function setCount(string $table, int $count): int
|
||||
{
|
||||
self::$tableCounts[$table] = $count;
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the count for a table
|
||||
*
|
||||
* @param string $table Name of the target table
|
||||
*
|
||||
* @return int The new count value
|
||||
*/
|
||||
public static function upCount(string $table): int
|
||||
{
|
||||
return self::setCount($table, self::getCount($table) + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the count for a table
|
||||
*
|
||||
* @param string $table Name of the target table
|
||||
*
|
||||
* @return int The new count value
|
||||
*/
|
||||
public static function downCount(string $table): int
|
||||
{
|
||||
return self::setCount($table, self::getCount($table) - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the model instance
|
||||
*
|
||||
* @return object Framework or compatible model
|
||||
*/
|
||||
public function getModel()
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale
|
||||
*/
|
||||
public function getLocale(): string
|
||||
{
|
||||
return $this->locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Faker generator
|
||||
*/
|
||||
public function getFaker(): Generator
|
||||
{
|
||||
return $this->faker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return and reset tempOverrides
|
||||
*/
|
||||
public function getOverrides(): array
|
||||
{
|
||||
$overrides = $this->tempOverrides ?? $this->overrides;
|
||||
|
||||
$this->tempOverrides = $this->overrides;
|
||||
|
||||
return $overrides;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the overrides, once or persistent
|
||||
*
|
||||
* @param array $overrides Array of [field => value]
|
||||
* @param bool $persist Whether these overrides should persist through the next operation
|
||||
*/
|
||||
public function setOverrides(array $overrides = [], $persist = true): self
|
||||
{
|
||||
if ($persist) {
|
||||
$this->overrides = $overrides;
|
||||
}
|
||||
|
||||
$this->tempOverrides = $overrides;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current formatters
|
||||
*/
|
||||
public function getFormatters(): ?array
|
||||
{
|
||||
return $this->formatters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the formatters to use. Will attempt to autodetect if none are available.
|
||||
*
|
||||
* @param array|null $formatters Array of [field => formatter], or null to detect
|
||||
*/
|
||||
public function setFormatters(?array $formatters = null): self
|
||||
{
|
||||
if ($formatters !== null) {
|
||||
$this->formatters = $formatters;
|
||||
} elseif (method_exists($this->model, 'fake')) {
|
||||
$this->formatters = null;
|
||||
} else {
|
||||
$this->detectFormatters();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to identify the appropriate Faker formatter for each field.
|
||||
*/
|
||||
protected function detectFormatters(): self
|
||||
{
|
||||
$this->formatters = [];
|
||||
|
||||
if (! empty($this->model->allowedFields)) {
|
||||
foreach ($this->model->allowedFields as $field) {
|
||||
$this->formatters[$field] = $this->guessFormatter($field);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guess at the correct formatter to match a field name.
|
||||
*
|
||||
* @param string $field Name of the field
|
||||
*
|
||||
* @return string Name of the formatter
|
||||
*/
|
||||
protected function guessFormatter($field): string
|
||||
{
|
||||
// First check for a Faker formatter of the same name - covers things like "email"
|
||||
try {
|
||||
$this->faker->getFormatter($field);
|
||||
|
||||
return $field;
|
||||
} catch (InvalidArgumentException $e) {
|
||||
// No match, keep going
|
||||
}
|
||||
|
||||
// Next look for known model fields
|
||||
if (in_array($field, $this->dateFields, true)) {
|
||||
switch ($this->model->dateFormat) {
|
||||
case 'datetime':
|
||||
case 'date':
|
||||
return 'date';
|
||||
|
||||
case 'int':
|
||||
return 'unixTime';
|
||||
}
|
||||
} elseif ($field === $this->model->primaryKey) {
|
||||
return 'numberBetween';
|
||||
}
|
||||
|
||||
// Check some common partials
|
||||
foreach (['email', 'name', 'title', 'text', 'date', 'url'] as $term) {
|
||||
if (stripos($field, $term) !== false) {
|
||||
return $term;
|
||||
}
|
||||
}
|
||||
|
||||
if (stripos($field, 'phone') !== false) {
|
||||
return 'phoneNumber';
|
||||
}
|
||||
|
||||
// Nothing left, use the default
|
||||
return $this->defaultFormatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate new entities with faked data
|
||||
*
|
||||
* @param int|null $count Optional number to create a collection
|
||||
*
|
||||
* @return array|object An array or object (based on returnType), or an array of returnTypes
|
||||
*/
|
||||
public function make(?int $count = null)
|
||||
{
|
||||
// If a singleton was requested then go straight to it
|
||||
if ($count === null) {
|
||||
return $this->model->returnType === 'array'
|
||||
? $this->makeArray()
|
||||
: $this->makeObject();
|
||||
}
|
||||
|
||||
$return = [];
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$return[] = $this->model->returnType === 'array'
|
||||
? $this->makeArray()
|
||||
: $this->makeObject();
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an array of faked data
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*
|
||||
* @return array An array of faked data
|
||||
*/
|
||||
public function makeArray()
|
||||
{
|
||||
if ($this->formatters !== null) {
|
||||
$result = [];
|
||||
|
||||
foreach ($this->formatters as $field => $formatter) {
|
||||
$result[$field] = $this->faker->{$formatter};
|
||||
}
|
||||
}
|
||||
// If no formatters were defined then look for a model fake() method
|
||||
elseif (method_exists($this->model, 'fake')) {
|
||||
$result = $this->model->fake($this->faker);
|
||||
|
||||
$result = is_object($result) && method_exists($result, 'toArray')
|
||||
// This should cover entities
|
||||
? $result->toArray()
|
||||
// Try to cast it
|
||||
: (array) $result;
|
||||
}
|
||||
// Nothing left to do but give up
|
||||
else {
|
||||
throw new RuntimeException(lang('Fabricator.missingFormatters'));
|
||||
}
|
||||
|
||||
// Replace overridden fields
|
||||
return array_merge($result, $this->getOverrides());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an object of faked data
|
||||
*
|
||||
* @param string|null $className Class name of the object to create; null to use model default
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*
|
||||
* @return object An instance of the class with faked data
|
||||
*/
|
||||
public function makeObject(?string $className = null): object
|
||||
{
|
||||
if ($className === null) {
|
||||
if ($this->model->returnType === 'object' || $this->model->returnType === 'array') {
|
||||
$className = 'stdClass';
|
||||
} else {
|
||||
$className = $this->model->returnType;
|
||||
}
|
||||
}
|
||||
|
||||
// If using the model's fake() method then check it for the correct return type
|
||||
if ($this->formatters === null && method_exists($this->model, 'fake')) {
|
||||
$result = $this->model->fake($this->faker);
|
||||
|
||||
if ($result instanceof $className) {
|
||||
// Set overrides manually
|
||||
foreach ($this->getOverrides() as $key => $value) {
|
||||
$result->{$key} = $value;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the array values and apply them to the object
|
||||
$array = $this->makeArray();
|
||||
$object = new $className();
|
||||
|
||||
// Check for the entity method
|
||||
if (method_exists($object, 'fill')) {
|
||||
$object->fill($array);
|
||||
} else {
|
||||
foreach ($array as $key => $value) {
|
||||
$object->{$key} = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate new entities from the database
|
||||
*
|
||||
* @param int|null $count Optional number to create a collection
|
||||
* @param bool $mock Whether to execute or mock the insertion
|
||||
*
|
||||
* @throws FrameworkException
|
||||
*
|
||||
* @return array|object An array or object (based on returnType), or an array of returnTypes
|
||||
*/
|
||||
public function create(?int $count = null, bool $mock = false)
|
||||
{
|
||||
// Intercept mock requests
|
||||
if ($mock) {
|
||||
return $this->createMock($count);
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
|
||||
// Iterate over new entities and insert each one, storing insert IDs
|
||||
foreach ($this->make($count ?? 1) as $result) {
|
||||
if ($id = $this->model->insert($result, true)) {
|
||||
$ids[] = $id;
|
||||
self::upCount($this->model->table);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
throw FrameworkException::forFabricatorCreateFailed($this->model->table, implode(' ', $this->model->errors() ?? []));
|
||||
}
|
||||
|
||||
// If the model defines a "withDeleted" method for handling soft deletes then use it
|
||||
if (method_exists($this->model, 'withDeleted')) {
|
||||
$this->model->withDeleted();
|
||||
}
|
||||
|
||||
return $this->model->find($count === null ? reset($ids) : $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate new database entities without actually inserting them
|
||||
*
|
||||
* @param int|null $count Optional number to create a collection
|
||||
*
|
||||
* @return array|object An array or object (based on returnType), or an array of returnTypes
|
||||
*/
|
||||
protected function createMock(?int $count = null)
|
||||
{
|
||||
switch ($this->model->dateFormat) {
|
||||
case 'datetime':
|
||||
$datetime = date('Y-m-d H:i:s');
|
||||
break;
|
||||
|
||||
case 'date':
|
||||
$datetime = date('Y-m-d');
|
||||
break;
|
||||
|
||||
default:
|
||||
$datetime = time();
|
||||
}
|
||||
|
||||
// Determine which fields we will need
|
||||
$fields = [];
|
||||
|
||||
if (! empty($this->model->useTimestamps)) {
|
||||
$fields[$this->model->createdField] = $datetime;
|
||||
$fields[$this->model->updatedField] = $datetime;
|
||||
}
|
||||
|
||||
if (! empty($this->model->useSoftDeletes)) {
|
||||
$fields[$this->model->deletedField] = null;
|
||||
}
|
||||
|
||||
// Iterate over new entities and add the necessary fields
|
||||
$return = [];
|
||||
|
||||
foreach ($this->make($count ?? 1) as $i => $result) {
|
||||
// Set the ID
|
||||
$fields[$this->model->primaryKey] = $i;
|
||||
|
||||
// Merge fields
|
||||
if (is_array($result)) {
|
||||
$result = array_merge($result, $fields);
|
||||
} else {
|
||||
foreach ($fields as $key => $value) {
|
||||
$result->{$key} = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$return[] = $result;
|
||||
}
|
||||
|
||||
return $count === null ? reset($return) : $return;
|
||||
}
|
||||
}
|
||||
25
system/Test/FeatureResponse.php
Normal file
25
system/Test/FeatureResponse.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test;
|
||||
|
||||
/**
|
||||
* Assertions for a response
|
||||
*
|
||||
* @deprecated Use TestResponse directly
|
||||
*/
|
||||
class FeatureResponse extends TestResponse
|
||||
{
|
||||
/**
|
||||
* @deprecated Will be protected in a future release, use response() instead
|
||||
*/
|
||||
public $response;
|
||||
}
|
||||
401
system/Test/FeatureTestCase.php
Normal file
401
system/Test/FeatureTestCase.php
Normal file
@@ -0,0 +1,401 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test;
|
||||
|
||||
use CodeIgniter\Events\Events;
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
use CodeIgniter\HTTP\Request;
|
||||
use CodeIgniter\HTTP\URI;
|
||||
use CodeIgniter\HTTP\UserAgent;
|
||||
use CodeIgniter\Router\Exceptions\RedirectException;
|
||||
use CodeIgniter\Router\RouteCollection;
|
||||
use Config\App;
|
||||
use Config\Services;
|
||||
use Exception;
|
||||
use ReflectionException;
|
||||
|
||||
/**
|
||||
* Class FeatureTestCase
|
||||
*
|
||||
* Provides a base class with the trait for doing full HTTP testing
|
||||
* against your application.
|
||||
*
|
||||
* @no-final
|
||||
*
|
||||
* @deprecated Use FeatureTestTrait instead
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class FeatureTestCase extends CIUnitTestCase
|
||||
{
|
||||
use DatabaseTestTrait;
|
||||
|
||||
/**
|
||||
* Sets a RouteCollection that will override
|
||||
* the application's route collection.
|
||||
*
|
||||
* Example routes:
|
||||
* [
|
||||
* ['get', 'home', 'Home::index']
|
||||
* ]
|
||||
*
|
||||
* @param array $routes
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function withRoutes(?array $routes = null)
|
||||
{
|
||||
$collection = Services::routes();
|
||||
|
||||
if ($routes) {
|
||||
$collection->resetRoutes();
|
||||
|
||||
foreach ($routes as $route) {
|
||||
$collection->{$route[0]}($route[1], $route[2]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->routes = $collection;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets any values that should exist during this session.
|
||||
*
|
||||
* @param array|null $values Array of values, or null to use the current $_SESSION
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withSession(?array $values = null)
|
||||
{
|
||||
$this->session = $values ?? $_SESSION;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set request's headers
|
||||
*
|
||||
* Example of use
|
||||
* withHeaders([
|
||||
* 'Authorization' => 'Token'
|
||||
* ])
|
||||
*
|
||||
* @param array $headers Array of headers
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withHeaders(array $headers = [])
|
||||
{
|
||||
$this->headers = $headers;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the format the request's body should have.
|
||||
*
|
||||
* @param string $format The desired format. Currently supported formats: xml, json
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withBodyFormat(string $format)
|
||||
{
|
||||
$this->bodyFormat = $format;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the raw body for the request
|
||||
*
|
||||
* @param mixed $body
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withBody($body)
|
||||
{
|
||||
$this->requestBody = $body;
|
||||
|
||||
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.
|
||||
*
|
||||
* @throws Exception
|
||||
* @throws RedirectException
|
||||
*
|
||||
* @return FeatureResponse
|
||||
*/
|
||||
public function call(string $method, string $path, ?array $params = null)
|
||||
{
|
||||
$buffer = \ob_get_level();
|
||||
|
||||
// Clean up any open output buffers
|
||||
// not relevant to unit testing
|
||||
// @codeCoverageIgnoreStart
|
||||
if (\ob_get_level() > 0 && (! isset($this->clean) || $this->clean === true)) {
|
||||
\ob_end_clean();
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
// Simulate having a blank session
|
||||
$_SESSION = [];
|
||||
$_SERVER['REQUEST_METHOD'] = $method;
|
||||
|
||||
$request = $this->setupRequest($method, $path);
|
||||
$request = $this->setupHeaders($request);
|
||||
$request = $this->populateGlobals($method, $request, $params);
|
||||
$request = $this->setRequestBody($request);
|
||||
|
||||
// Initialize the RouteCollection
|
||||
if (! $routes = $this->routes) {
|
||||
require APPPATH . 'Config/Routes.php';
|
||||
|
||||
/**
|
||||
* @var RouteCollection $routes
|
||||
*/
|
||||
$routes->getRoutes('*');
|
||||
}
|
||||
|
||||
$routes->setHTTPVerb($method);
|
||||
|
||||
// Make sure any other classes that might call the request
|
||||
// instance get the right one.
|
||||
Services::injectMock('request', $request);
|
||||
|
||||
// Make sure filters are reset between tests
|
||||
Services::injectMock('filters', Services::filters(null, false));
|
||||
|
||||
$response = $this->app
|
||||
->setRequest($request)
|
||||
->run($routes, true);
|
||||
|
||||
$output = \ob_get_contents();
|
||||
if (empty($response->getBody()) && ! empty($output)) {
|
||||
$response->setBody($output);
|
||||
}
|
||||
|
||||
// Reset directory if it has been set
|
||||
Services::router()->setDirectory(null);
|
||||
|
||||
// Ensure the output buffer is identical so no tests are risky
|
||||
// @codeCoverageIgnoreStart
|
||||
while (\ob_get_level() > $buffer) {
|
||||
\ob_end_clean();
|
||||
}
|
||||
|
||||
while (\ob_get_level() < $buffer) {
|
||||
\ob_start();
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
return new FeatureResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a GET request.
|
||||
*
|
||||
* @throws Exception
|
||||
* @throws RedirectException
|
||||
*
|
||||
* @return FeatureResponse
|
||||
*/
|
||||
public function get(string $path, ?array $params = null)
|
||||
{
|
||||
return $this->call('get', $path, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a POST request.
|
||||
*
|
||||
* @throws Exception
|
||||
* @throws RedirectException
|
||||
*
|
||||
* @return FeatureResponse
|
||||
*/
|
||||
public function post(string $path, ?array $params = null)
|
||||
{
|
||||
return $this->call('post', $path, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a PUT request
|
||||
*
|
||||
* @throws Exception
|
||||
* @throws RedirectException
|
||||
*
|
||||
* @return FeatureResponse
|
||||
*/
|
||||
public function put(string $path, ?array $params = null)
|
||||
{
|
||||
return $this->call('put', $path, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performss a PATCH request
|
||||
*
|
||||
* @throws Exception
|
||||
* @throws RedirectException
|
||||
*
|
||||
* @return FeatureResponse
|
||||
*/
|
||||
public function patch(string $path, ?array $params = null)
|
||||
{
|
||||
return $this->call('patch', $path, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a DELETE request.
|
||||
*
|
||||
* @throws Exception
|
||||
* @throws RedirectException
|
||||
*
|
||||
* @return FeatureResponse
|
||||
*/
|
||||
public function delete(string $path, ?array $params = null)
|
||||
{
|
||||
return $this->call('delete', $path, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an OPTIONS request.
|
||||
*
|
||||
* @throws Exception
|
||||
* @throws RedirectException
|
||||
*
|
||||
* @return FeatureResponse
|
||||
*/
|
||||
public function options(string $path, ?array $params = null)
|
||||
{
|
||||
return $this->call('options', $path, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup a Request object to use so that CodeIgniter
|
||||
* won't try to auto-populate some of the items.
|
||||
*/
|
||||
protected function setupRequest(string $method, ?string $path = null): IncomingRequest
|
||||
{
|
||||
$config = config(App::class);
|
||||
$uri = new URI(rtrim($config->baseURL, '/') . '/' . trim($path, '/ '));
|
||||
|
||||
$request = new IncomingRequest($config, clone $uri, null, new UserAgent());
|
||||
$request->uri = $uri;
|
||||
|
||||
$request->setMethod($method);
|
||||
$request->setProtocolVersion('1.1');
|
||||
|
||||
if ($config->forceGlobalSecureRequests) {
|
||||
$_SERVER['HTTPS'] = 'test';
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the custom request's headers
|
||||
*
|
||||
* @return IncomingRequest
|
||||
*/
|
||||
protected function setupHeaders(IncomingRequest $request)
|
||||
{
|
||||
foreach ($this->headers as $name => $value) {
|
||||
$request->setHeader($name, $value);
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*
|
||||
* @return Request
|
||||
*/
|
||||
protected function populateGlobals(string $method, Request $request, ?array $params = null)
|
||||
{
|
||||
// $params should set the query vars if present,
|
||||
// otherwise set it from the URL.
|
||||
$get = ! empty($params) && $method === 'get'
|
||||
? $params
|
||||
: $this->getPrivateProperty($request->uri, 'query');
|
||||
|
||||
$request->setGlobal('get', $get);
|
||||
if ($method !== 'get') {
|
||||
$request->setGlobal($method, $params);
|
||||
}
|
||||
|
||||
$request->setGlobal('request', $params);
|
||||
|
||||
$_SESSION = $this->session ?? [];
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the request's body formatted according to the value in $this->bodyFormat.
|
||||
* 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.
|
||||
*/
|
||||
protected function setRequestBody(Request $request, ?array $params = null): Request
|
||||
{
|
||||
if (isset($this->requestBody) && $this->requestBody !== '') {
|
||||
$request->setBody($this->requestBody);
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
if (isset($this->bodyFormat) && $this->bodyFormat !== '') {
|
||||
if (empty($params)) {
|
||||
$params = $request->fetchGlobal('request');
|
||||
}
|
||||
$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);
|
||||
$request->setHeader('Content-Type', $formatMime);
|
||||
}
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
}
|
||||
396
system/Test/FeatureTestTrait.php
Normal file
396
system/Test/FeatureTestTrait.php
Normal file
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test;
|
||||
|
||||
use CodeIgniter\Events\Events;
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
use CodeIgniter\HTTP\Request;
|
||||
use CodeIgniter\HTTP\URI;
|
||||
use CodeIgniter\HTTP\UserAgent;
|
||||
use CodeIgniter\Router\Exceptions\RedirectException;
|
||||
use CodeIgniter\Router\RouteCollection;
|
||||
use Config\App;
|
||||
use Config\Services;
|
||||
use Exception;
|
||||
use ReflectionException;
|
||||
|
||||
/**
|
||||
* Trait FeatureTestTrait
|
||||
*
|
||||
* Provides additional utilities for doing full HTTP testing
|
||||
* against your application in trait format.
|
||||
*/
|
||||
trait FeatureTestTrait
|
||||
{
|
||||
/**
|
||||
* Sets a RouteCollection that will override
|
||||
* the application's route collection.
|
||||
*
|
||||
* Example routes:
|
||||
* [
|
||||
* ['get', 'home', 'Home::index']
|
||||
* ]
|
||||
*
|
||||
* @param array $routes
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function withRoutes(?array $routes = null)
|
||||
{
|
||||
$collection = Services::routes();
|
||||
|
||||
if ($routes) {
|
||||
$collection->resetRoutes();
|
||||
|
||||
foreach ($routes as $route) {
|
||||
$collection->{$route[0]}($route[1], $route[2]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->routes = $collection;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets any values that should exist during this session.
|
||||
*
|
||||
* @param array|null $values Array of values, or null to use the current $_SESSION
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withSession(?array $values = null)
|
||||
{
|
||||
$this->session = $values ?? $_SESSION;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set request's headers
|
||||
*
|
||||
* Example of use
|
||||
* withHeaders([
|
||||
* 'Authorization' => 'Token'
|
||||
* ])
|
||||
*
|
||||
* @param array $headers Array of headers
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withHeaders(array $headers = [])
|
||||
{
|
||||
$this->headers = $headers;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the format the request's body should have.
|
||||
*
|
||||
* @param string $format The desired format. Currently supported formats: xml, json
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withBodyFormat(string $format)
|
||||
{
|
||||
$this->bodyFormat = $format;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the raw body for the request
|
||||
*
|
||||
* @param mixed $body
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withBody($body)
|
||||
{
|
||||
$this->requestBody = $body;
|
||||
|
||||
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 TestResponse
|
||||
* instance that can be used to run many assertions against.
|
||||
*
|
||||
* @throws RedirectException
|
||||
* @throws Exception
|
||||
*
|
||||
* @return TestResponse
|
||||
*/
|
||||
public function call(string $method, string $path, ?array $params = null)
|
||||
{
|
||||
$buffer = \ob_get_level();
|
||||
|
||||
// Clean up any open output buffers
|
||||
// not relevant to unit testing
|
||||
// @codeCoverageIgnoreStart
|
||||
if (\ob_get_level() > 0 && (! isset($this->clean) || $this->clean === true)) {
|
||||
\ob_end_clean();
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
// Simulate having a blank session
|
||||
$_SESSION = [];
|
||||
$_SERVER['REQUEST_METHOD'] = $method;
|
||||
|
||||
$request = $this->setupRequest($method, $path);
|
||||
$request = $this->setupHeaders($request);
|
||||
$request = $this->populateGlobals($method, $request, $params);
|
||||
$request = $this->setRequestBody($request);
|
||||
|
||||
// Initialize the RouteCollection
|
||||
if (! $routes = $this->routes) {
|
||||
require APPPATH . 'Config/Routes.php';
|
||||
|
||||
/**
|
||||
* @var RouteCollection $routes
|
||||
*/
|
||||
$routes->getRoutes('*');
|
||||
}
|
||||
|
||||
$routes->setHTTPVerb($method);
|
||||
|
||||
// Make sure any other classes that might call the request
|
||||
// instance get the right one.
|
||||
Services::injectMock('request', $request);
|
||||
|
||||
// Make sure filters are reset between tests
|
||||
Services::injectMock('filters', Services::filters(null, false));
|
||||
|
||||
$response = $this->app
|
||||
->setRequest($request)
|
||||
->run($routes, true);
|
||||
|
||||
$output = \ob_get_contents();
|
||||
if (empty($response->getBody()) && ! empty($output)) {
|
||||
$response->setBody($output);
|
||||
}
|
||||
|
||||
// Reset directory if it has been set
|
||||
Services::router()->setDirectory(null);
|
||||
|
||||
// Ensure the output buffer is identical so no tests are risky
|
||||
// @codeCoverageIgnoreStart
|
||||
while (\ob_get_level() > $buffer) {
|
||||
\ob_end_clean();
|
||||
}
|
||||
|
||||
while (\ob_get_level() < $buffer) {
|
||||
\ob_start();
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
return new TestResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a GET request.
|
||||
*
|
||||
* @throws RedirectException
|
||||
* @throws Exception
|
||||
*
|
||||
* @return TestResponse
|
||||
*/
|
||||
public function get(string $path, ?array $params = null)
|
||||
{
|
||||
return $this->call('get', $path, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a POST request.
|
||||
*
|
||||
* @throws RedirectException
|
||||
* @throws Exception
|
||||
*
|
||||
* @return TestResponse
|
||||
*/
|
||||
public function post(string $path, ?array $params = null)
|
||||
{
|
||||
return $this->call('post', $path, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a PUT request
|
||||
*
|
||||
* @throws RedirectException
|
||||
* @throws Exception
|
||||
*
|
||||
* @return TestResponse
|
||||
*/
|
||||
public function put(string $path, ?array $params = null)
|
||||
{
|
||||
return $this->call('put', $path, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performss a PATCH request
|
||||
*
|
||||
* @throws RedirectException
|
||||
* @throws Exception
|
||||
*
|
||||
* @return TestResponse
|
||||
*/
|
||||
public function patch(string $path, ?array $params = null)
|
||||
{
|
||||
return $this->call('patch', $path, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a DELETE request.
|
||||
*
|
||||
* @throws RedirectException
|
||||
* @throws Exception
|
||||
*
|
||||
* @return TestResponse
|
||||
*/
|
||||
public function delete(string $path, ?array $params = null)
|
||||
{
|
||||
return $this->call('delete', $path, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an OPTIONS request.
|
||||
*
|
||||
* @throws RedirectException
|
||||
* @throws Exception
|
||||
*
|
||||
* @return TestResponse
|
||||
*/
|
||||
public function options(string $path, ?array $params = null)
|
||||
{
|
||||
return $this->call('options', $path, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup a Request object to use so that CodeIgniter
|
||||
* won't try to auto-populate some of the items.
|
||||
*/
|
||||
protected function setupRequest(string $method, ?string $path = null): IncomingRequest
|
||||
{
|
||||
$path = URI::removeDotSegments($path);
|
||||
$config = config(App::class);
|
||||
$request = new IncomingRequest($config, new URI(), null, new UserAgent());
|
||||
|
||||
// $path may have a query in it
|
||||
$parts = explode('?', $path);
|
||||
$_SERVER['QUERY_STRING'] = $parts[1] ?? '';
|
||||
|
||||
$request->setPath($parts[0]);
|
||||
$request->setMethod($method);
|
||||
$request->setProtocolVersion('1.1');
|
||||
|
||||
if ($config->forceGlobalSecureRequests) {
|
||||
$_SERVER['HTTPS'] = 'test';
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the custom request's headers
|
||||
*
|
||||
* @return IncomingRequest
|
||||
*/
|
||||
protected function setupHeaders(IncomingRequest $request)
|
||||
{
|
||||
if (! empty($this->headers)) {
|
||||
foreach ($this->headers as $name => $value) {
|
||||
$request->setHeader($name, $value);
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*
|
||||
* @return Request
|
||||
*/
|
||||
protected function populateGlobals(string $method, Request $request, ?array $params = null)
|
||||
{
|
||||
// $params should set the query vars if present,
|
||||
// otherwise set it from the URL.
|
||||
$get = ! empty($params) && $method === 'get'
|
||||
? $params
|
||||
: $this->getPrivateProperty($request->uri, 'query');
|
||||
|
||||
$request->setGlobal('get', $get);
|
||||
if ($method !== 'get') {
|
||||
$request->setGlobal($method, $params);
|
||||
}
|
||||
|
||||
$request->setGlobal('request', $params);
|
||||
|
||||
$_SESSION = $this->session ?? [];
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the request's body formatted according to the value in $this->bodyFormat.
|
||||
* 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.
|
||||
*/
|
||||
protected function setRequestBody(Request $request, ?array $params = null): Request
|
||||
{
|
||||
if (isset($this->requestBody) && $this->requestBody !== '') {
|
||||
$request->setBody($this->requestBody);
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
if (isset($this->bodyFormat) && $this->bodyFormat !== '') {
|
||||
if (empty($params)) {
|
||||
$params = $request->fetchGlobal('request');
|
||||
}
|
||||
$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);
|
||||
$request->setHeader('Content-Type', $formatMime);
|
||||
}
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
}
|
||||
270
system/Test/FilterTestTrait.php
Normal file
270
system/Test/FilterTestTrait.php
Normal file
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test;
|
||||
|
||||
use Closure;
|
||||
use CodeIgniter\Filters\Exceptions\FilterException;
|
||||
use CodeIgniter\Filters\FilterInterface;
|
||||
use CodeIgniter\Filters\Filters;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use CodeIgniter\Router\RouteCollection;
|
||||
use Config\Filters as FiltersConfig;
|
||||
use Config\Services;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Filter Test Trait
|
||||
*
|
||||
* Provides functionality for testing
|
||||
* filters and their route associations.
|
||||
*
|
||||
* @mixin CIUnitTestCase
|
||||
*/
|
||||
trait FilterTestTrait
|
||||
{
|
||||
/**
|
||||
* Have the one-time classes been instantiated?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $doneFilterSetUp = false;
|
||||
|
||||
/**
|
||||
* The active IncomingRequest or CLIRequest
|
||||
*
|
||||
* @var RequestInterface
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* The active Response instance
|
||||
*
|
||||
* @var ResponseInterface
|
||||
*/
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* The Filters configuration to use.
|
||||
* Extracted for access to aliases
|
||||
* during Filters::discoverFilters().
|
||||
*
|
||||
* @var FiltersConfig|null
|
||||
*/
|
||||
protected $filtersConfig;
|
||||
|
||||
/**
|
||||
* The prepared Filters library.
|
||||
*
|
||||
* @var Filters|null
|
||||
*/
|
||||
protected $filters;
|
||||
|
||||
/**
|
||||
* The default App and discovered
|
||||
* routes to check for filters.
|
||||
*
|
||||
* @var RouteCollection|null
|
||||
*/
|
||||
protected $collection;
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Staging
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Initializes dependencies once.
|
||||
*/
|
||||
protected function setUpFilterTestTrait(): void
|
||||
{
|
||||
if ($this->doneFilterSetUp === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create our own Request and Response so we can
|
||||
// use the same ones for Filters and FilterInterface
|
||||
// yet isolate them from outside influence
|
||||
$this->request = $this->request ?? clone Services::request();
|
||||
$this->response = $this->response ?? clone Services::response();
|
||||
|
||||
// Create our config and Filters instance to reuse for performance
|
||||
$this->filtersConfig = $this->filtersConfig ?? config('Filters');
|
||||
$this->filters = $this->filters ?? new Filters($this->filtersConfig, $this->request, $this->response);
|
||||
|
||||
if ($this->collection === null) {
|
||||
// Load the RouteCollection from Config to gather App route info
|
||||
// (creates $routes using the Service as a starting point)
|
||||
require APPPATH . 'Config/Routes.php';
|
||||
|
||||
$routes->getRoutes('*'); // Triggers discovery
|
||||
$this->collection = $routes;
|
||||
}
|
||||
|
||||
$this->doneFilterSetUp = true;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Utility
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns a callable method for a filter position
|
||||
* using the local HTTP instances.
|
||||
*
|
||||
* @param FilterInterface|string $filter The filter instance, class, or alias
|
||||
* @param string $position "before" or "after"
|
||||
*/
|
||||
protected function getFilterCaller($filter, string $position): Closure
|
||||
{
|
||||
if (! in_array($position, ['before', 'after'], true)) {
|
||||
throw new InvalidArgumentException('Invalid filter position passed: ' . $position);
|
||||
}
|
||||
|
||||
if (is_string($filter)) {
|
||||
// Check for an alias (no namespace)
|
||||
if (strpos($filter, '\\') === false) {
|
||||
if (! isset($this->filtersConfig->aliases[$filter])) {
|
||||
throw new RuntimeException("No filter found with alias '{$filter}'");
|
||||
}
|
||||
|
||||
$filter = $this->filtersConfig->aliases[$filter];
|
||||
}
|
||||
|
||||
// Get an instance
|
||||
$filter = new $filter();
|
||||
}
|
||||
|
||||
if (! $filter instanceof FilterInterface) {
|
||||
throw FilterException::forIncorrectInterface(get_class($filter));
|
||||
}
|
||||
|
||||
$request = clone $this->request;
|
||||
|
||||
if ($position === 'before') {
|
||||
return static function (?array $params = null) use ($filter, $request) {
|
||||
return $filter->before($request, $params);
|
||||
};
|
||||
}
|
||||
|
||||
$response = clone $this->response;
|
||||
|
||||
return static function (?array $params = null) use ($filter, $request, $response) {
|
||||
return $filter->after($request, $response, $params);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of filter aliases enabled
|
||||
* for the given route at position.
|
||||
*
|
||||
* @param string $route The route to test
|
||||
* @param string $position "before" or "after"
|
||||
*
|
||||
* @return string[] The filter aliases
|
||||
*/
|
||||
protected function getFiltersForRoute(string $route, string $position): array
|
||||
{
|
||||
if (! in_array($position, ['before', 'after'], true)) {
|
||||
throw new InvalidArgumentException('Invalid filter position passed:' . $position);
|
||||
}
|
||||
|
||||
$this->filters->reset();
|
||||
|
||||
if ($routeFilter = $this->collection->getFilterForRoute($route)) {
|
||||
$this->filters->enableFilter($routeFilter, $position);
|
||||
}
|
||||
|
||||
$aliases = $this->filters->initialize($route)->getFilters();
|
||||
|
||||
$this->filters->reset();
|
||||
|
||||
return $aliases[$position];
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Assertions
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Asserts that the given route at position uses
|
||||
* the filter (by its alias).
|
||||
*
|
||||
* @param string $route The route to test
|
||||
* @param string $position "before" or "after"
|
||||
* @param string $alias Alias for the anticipated filter
|
||||
*/
|
||||
protected function assertFilter(string $route, string $position, string $alias): void
|
||||
{
|
||||
$filters = $this->getFiltersForRoute($route, $position);
|
||||
|
||||
$this->assertContains(
|
||||
$alias,
|
||||
$filters,
|
||||
"Filter '{$alias}' does not apply to '{$route}'.",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the given route at position does not
|
||||
* use the filter (by its alias).
|
||||
*
|
||||
* @param string $route The route to test
|
||||
* @param string $position "before" or "after"
|
||||
* @param string $alias Alias for the anticipated filter
|
||||
*/
|
||||
protected function assertNotFilter(string $route, string $position, string $alias)
|
||||
{
|
||||
$filters = $this->getFiltersForRoute($route, $position);
|
||||
|
||||
$this->assertNotContains(
|
||||
$alias,
|
||||
$filters,
|
||||
"Filter '{$alias}' applies to '{$route}' when it should not.",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the given route at position has
|
||||
* at least one filter set.
|
||||
*
|
||||
* @param string $route The route to test
|
||||
* @param string $position "before" or "after"
|
||||
*/
|
||||
protected function assertHasFilters(string $route, string $position)
|
||||
{
|
||||
$filters = $this->getFiltersForRoute($route, $position);
|
||||
|
||||
$this->assertNotEmpty(
|
||||
$filters,
|
||||
"No filters found for '{$route}' when at least one was expected.",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the given route at position has
|
||||
* no filters set.
|
||||
*
|
||||
* @param string $route The route to test
|
||||
* @param string $position "before" or "after"
|
||||
*/
|
||||
protected function assertNotHasFilters(string $route, string $position)
|
||||
{
|
||||
$filters = $this->getFiltersForRoute($route, $position);
|
||||
|
||||
$this->assertSame(
|
||||
[],
|
||||
$filters,
|
||||
"Found filters for '{$route}' when none were expected: " . implode(', ', $filters) . '.',
|
||||
);
|
||||
}
|
||||
}
|
||||
50
system/Test/Filters/CITestStreamFilter.php
Normal file
50
system/Test/Filters/CITestStreamFilter.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Filters;
|
||||
|
||||
use php_user_filter;
|
||||
|
||||
/**
|
||||
* Used to capture output during unit testing, so that it can
|
||||
* be used in assertions.
|
||||
*/
|
||||
class CITestStreamFilter extends php_user_filter
|
||||
{
|
||||
/**
|
||||
* Buffer to capture stream content.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $buffer = '';
|
||||
|
||||
/**
|
||||
* This method is called whenever data is read from or written to the
|
||||
* attached stream (such as with fread() or fwrite()).
|
||||
*
|
||||
* @param resource $in
|
||||
* @param resource $out
|
||||
* @param int $consumed
|
||||
* @param bool $closing
|
||||
*/
|
||||
public function filter($in, $out, &$consumed, $closing): int
|
||||
{
|
||||
while ($bucket = stream_bucket_make_writeable($in)) {
|
||||
static::$buffer .= $bucket->data;
|
||||
|
||||
$consumed += $bucket->datalen;
|
||||
}
|
||||
|
||||
return PSFS_PASS_ON;
|
||||
}
|
||||
}
|
||||
|
||||
stream_filter_register('CITestStreamFilter', CITestStreamFilter::class); // @codeCoverageIgnore
|
||||
84
system/Test/Interfaces/FabricatorModel.php
Normal file
84
system/Test/Interfaces/FabricatorModel.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Interfaces;
|
||||
|
||||
use Faker\Generator;
|
||||
use ReflectionException;
|
||||
|
||||
/**
|
||||
* FabricatorModel
|
||||
*
|
||||
* An interface defining the required methods and properties
|
||||
* needed for a model to qualify for use with the Fabricator class.
|
||||
* While interfaces cannot enforce properties, the following
|
||||
* are required for use with Fabricator:
|
||||
*
|
||||
* @property string $returnType
|
||||
* @property string $primaryKey
|
||||
* @property string $dateFormat
|
||||
*/
|
||||
interface FabricatorModel
|
||||
{
|
||||
/**
|
||||
* Fetches the row of database from $this->table with a primary key
|
||||
* matching $id.
|
||||
*
|
||||
* @param array|mixed|null $id One primary key or an array of primary keys
|
||||
*
|
||||
* @return array|object|null The resulting row of data, or null.
|
||||
*/
|
||||
public function find($id = null);
|
||||
|
||||
/**
|
||||
* Inserts data into the current table. If an object is provided,
|
||||
* it will attempt to convert it to an array.
|
||||
*
|
||||
* @param array|object $data
|
||||
* @param bool $returnID Whether insert ID should be returned or not.
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*
|
||||
* @return bool|int|string
|
||||
*/
|
||||
public function insert($data = null, bool $returnID = true);
|
||||
|
||||
/**
|
||||
* The following properties and methods are optional, but if present should
|
||||
* adhere to their definitions.
|
||||
*
|
||||
* @property array $allowedFields
|
||||
* @property string $useSoftDeletes
|
||||
* @property string $useTimestamps
|
||||
* @property string $createdField
|
||||
* @property string $updatedField
|
||||
* @property string $deletedField
|
||||
*/
|
||||
|
||||
/*
|
||||
* Sets $useSoftDeletes value so that we can temporarily override
|
||||
* the softdeletes settings. Can be used for all find* methods.
|
||||
*
|
||||
* @param boolean $val
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
// public function withDeleted($val = true);
|
||||
|
||||
/**
|
||||
* Faked data for Fabricator.
|
||||
*
|
||||
* @param Generator $faker
|
||||
*
|
||||
* @return array|object
|
||||
*/
|
||||
// public function fake(Generator &$faker);
|
||||
}
|
||||
42
system/Test/Mock/MockAppConfig.php
Normal file
42
system/Test/Mock/MockAppConfig.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use Config\App;
|
||||
|
||||
class MockAppConfig extends App
|
||||
{
|
||||
public $baseURL = 'http://example.com/';
|
||||
public $uriProtocol = 'REQUEST_URI';
|
||||
public $cookiePrefix = '';
|
||||
public $cookieDomain = '';
|
||||
public $cookiePath = '/';
|
||||
public $cookieSecure = false;
|
||||
public $cookieHTTPOnly = false;
|
||||
public $cookieSameSite = 'Lax';
|
||||
public $proxyIPs = '';
|
||||
public $CSRFTokenName = 'csrf_test_name';
|
||||
public $CSRFHeaderName = 'X-CSRF-TOKEN';
|
||||
public $CSRFCookieName = 'csrf_cookie_name';
|
||||
public $CSRFExpire = 7200;
|
||||
public $CSRFRegenerate = true;
|
||||
public $CSRFExcludeURIs = ['http://example.com'];
|
||||
public $CSRFRedirect = false;
|
||||
public $CSRFSameSite = 'Lax';
|
||||
public $CSPEnabled = false;
|
||||
public $defaultLocale = 'en';
|
||||
public $negotiateLocale = false;
|
||||
public $supportedLocales = [
|
||||
'en',
|
||||
'es',
|
||||
];
|
||||
}
|
||||
26
system/Test/Mock/MockAutoload.php
Normal file
26
system/Test/Mock/MockAutoload.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use Config\Autoload;
|
||||
|
||||
class MockAutoload extends Autoload
|
||||
{
|
||||
public $psr4 = [];
|
||||
public $classmap = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Don't call the parent since we don't want the default mappings.
|
||||
// parent::__construct();
|
||||
}
|
||||
}
|
||||
18
system/Test/Mock/MockBuilder.php
Normal file
18
system/Test/Mock/MockBuilder.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
|
||||
class MockBuilder extends BaseBuilder
|
||||
{
|
||||
}
|
||||
40
system/Test/Mock/MockCLIConfig.php
Normal file
40
system/Test/Mock/MockCLIConfig.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use Config\App;
|
||||
|
||||
class MockCLIConfig extends App
|
||||
{
|
||||
public $baseURL = 'http://example.com/';
|
||||
public $uriProtocol = 'REQUEST_URI';
|
||||
public $cookiePrefix = '';
|
||||
public $cookieDomain = '';
|
||||
public $cookiePath = '/';
|
||||
public $cookieSecure = false;
|
||||
public $cookieHTTPOnly = false;
|
||||
public $cookieSameSite = 'Lax';
|
||||
public $proxyIPs = '';
|
||||
public $CSRFTokenName = 'csrf_test_name';
|
||||
public $CSRFCookieName = 'csrf_cookie_name';
|
||||
public $CSRFExpire = 7200;
|
||||
public $CSRFRegenerate = true;
|
||||
public $CSRFExcludeURIs = ['http://example.com'];
|
||||
public $CSRFSameSite = 'Lax';
|
||||
public $CSPEnabled = false;
|
||||
public $defaultLocale = 'en';
|
||||
public $negotiateLocale = false;
|
||||
public $supportedLocales = [
|
||||
'en',
|
||||
'es',
|
||||
];
|
||||
}
|
||||
54
system/Test/Mock/MockCURLRequest.php
Normal file
54
system/Test/Mock/MockCURLRequest.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use CodeIgniter\HTTP\CURLRequest;
|
||||
|
||||
/**
|
||||
* Class MockCURLRequest
|
||||
*
|
||||
* Simply allows us to not actually call cURL during the
|
||||
* test runs. Instead, we can set the desired output
|
||||
* and get back the set options.
|
||||
*/
|
||||
class MockCURLRequest extends CURLRequest
|
||||
{
|
||||
public $curl_options;
|
||||
protected $output = '';
|
||||
|
||||
public function setOutput($output)
|
||||
{
|
||||
$this->output = $output;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function sendRequest(array $curlOptions = []): string
|
||||
{
|
||||
// Save so we can access later.
|
||||
$this->curl_options = $curlOptions;
|
||||
|
||||
return $this->output;
|
||||
}
|
||||
|
||||
// for testing purposes only
|
||||
public function getBaseURI()
|
||||
{
|
||||
return $this->baseURI;
|
||||
}
|
||||
|
||||
// for testing purposes only
|
||||
public function getDelay()
|
||||
{
|
||||
return $this->delay;
|
||||
}
|
||||
}
|
||||
297
system/Test/Mock/MockCache.php
Normal file
297
system/Test/Mock/MockCache.php
Normal file
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use Closure;
|
||||
use CodeIgniter\Cache\CacheInterface;
|
||||
use CodeIgniter\Cache\Handlers\BaseHandler;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class MockCache extends BaseHandler implements CacheInterface
|
||||
{
|
||||
/**
|
||||
* Mock cache storage.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $cache = [];
|
||||
|
||||
/**
|
||||
* Expiration times.
|
||||
*
|
||||
* @var ?int[]
|
||||
*/
|
||||
protected $expirations = [];
|
||||
|
||||
/**
|
||||
* If true, will not cache any data.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $bypass = false;
|
||||
|
||||
/**
|
||||
* Takes care of any handler-specific setup that must be done.
|
||||
*/
|
||||
public function initialize()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to fetch an item from the cache store.
|
||||
*
|
||||
* @param string $key Cache item name
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $key)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
return array_key_exists($key, $this->cache) ? $this->cache[$key] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an item from the cache, or execute the given Closure and store the result.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function remember(string $key, int $ttl, Closure $callback)
|
||||
{
|
||||
$value = $this->get($key);
|
||||
|
||||
if ($value !== null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$this->save($key, $value = $callback(), $ttl);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves an item to the cache store.
|
||||
*
|
||||
* The $raw parameter is only utilized by Mamcache in order to
|
||||
* allow usage of increment() and decrement().
|
||||
*
|
||||
* @param string $key Cache item name
|
||||
* @param mixed $value the data to save
|
||||
* @param int $ttl Time To Live, in seconds (default 60)
|
||||
* @param bool $raw Whether to store the raw value.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function save(string $key, $value, int $ttl = 60, bool $raw = false)
|
||||
{
|
||||
if ($this->bypass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
$this->cache[$key] = $value;
|
||||
$this->expirations[$key] = $ttl > 0 ? time() + $ttl : null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a specific item from the cache store.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(string $key)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
if (! isset($this->cache[$key])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unset($this->cache[$key], $this->expirations[$key]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes items from the cache store matching a given pattern.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function deleteMatching(string $pattern)
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
foreach (array_keys($this->cache) as $key) {
|
||||
if (fnmatch($pattern, $key)) {
|
||||
$count++;
|
||||
unset($this->cache[$key], $this->expirations[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs atomic incrementation of a raw stored value.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function increment(string $key, int $offset = 1)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
$data = $this->cache[$key] ?: null;
|
||||
|
||||
if (empty($data)) {
|
||||
$data = 0;
|
||||
} elseif (! is_int($data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->save($key, $data + $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs atomic decrementation of a raw stored value.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function decrement(string $key, int $offset = 1)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
$data = $this->cache[$key] ?: null;
|
||||
|
||||
if (empty($data)) {
|
||||
$data = 0;
|
||||
} elseif (! is_int($data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->save($key, $data - $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will delete all items in the entire cache.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function clean()
|
||||
{
|
||||
$this->cache = [];
|
||||
$this->expirations = [];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns information on the entire cache.
|
||||
*
|
||||
* The information returned and the structure of the data
|
||||
* varies depending on the handler.
|
||||
*
|
||||
* @return string[] Keys currently present in the store
|
||||
*/
|
||||
public function getCacheInfo()
|
||||
{
|
||||
return array_keys($this->cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns detailed information about the specific item in the cache.
|
||||
*
|
||||
* @return array|null Returns null if the item does not exist, otherwise array<string, mixed>
|
||||
* with at least the 'expire' key for absolute epoch expiry (or null).
|
||||
*/
|
||||
public function getMetaData(string $key)
|
||||
{
|
||||
// Misses return null
|
||||
if (! array_key_exists($key, $this->expirations)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Count expired items as a miss
|
||||
if (is_int($this->expirations[$key]) && $this->expirations[$key] > time()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ['expire' => $this->expirations[$key]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the driver is supported on this system.
|
||||
*/
|
||||
public function isSupported(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Test Helpers
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Instructs the class to ignore all
|
||||
* requests to cache an item, and always "miss"
|
||||
* when checked for existing data.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function bypass(bool $bypass = true)
|
||||
{
|
||||
$this->clean();
|
||||
$this->bypass = $bypass;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Additional Assertions
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Asserts that the cache has an item named $key.
|
||||
* The value is not checked since storing false or null
|
||||
* values is valid.
|
||||
*/
|
||||
public function assertHas(string $key)
|
||||
{
|
||||
Assert::assertNotNull($this->get($key), "The cache does not have an item named: `{$key}`");
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the cache has an item named $key with a value matching $value.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function assertHasValue(string $key, $value = null)
|
||||
{
|
||||
$item = $this->get($key);
|
||||
|
||||
// Let assertHas handle throwing the error for consistency
|
||||
// if the key is not found
|
||||
if (empty($item)) {
|
||||
$this->assertHas($key);
|
||||
}
|
||||
|
||||
Assert::assertSame($value, $this->get($key), "The cached item `{$key}` does not equal match expectation. Found: " . print_r($value, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the cache does NOT have an item named $key.
|
||||
*/
|
||||
public function assertMissing(string $key)
|
||||
{
|
||||
Assert::assertArrayNotHasKey($key, $this->cache, "The cached item named `{$key}` exists.");
|
||||
}
|
||||
}
|
||||
22
system/Test/Mock/MockCodeIgniter.php
Normal file
22
system/Test/Mock/MockCodeIgniter.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use CodeIgniter\CodeIgniter;
|
||||
|
||||
class MockCodeIgniter extends CodeIgniter
|
||||
{
|
||||
protected function callExit($code)
|
||||
{
|
||||
// Do not call exit() in testing.
|
||||
}
|
||||
}
|
||||
32
system/Test/Mock/MockCommon.php
Normal file
32
system/Test/Mock/MockCommon.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
if (! function_exists('is_cli')) {
|
||||
/**
|
||||
* Is CLI?
|
||||
*
|
||||
* Test to see if a request was made from the command line.
|
||||
* You can set the return value for testing.
|
||||
*
|
||||
* @param bool $newReturn return value to set
|
||||
*/
|
||||
function is_cli(?bool $newReturn = null): bool
|
||||
{
|
||||
// PHPUnit always runs via CLI.
|
||||
static $returnValue = true;
|
||||
|
||||
if ($newReturn !== null) {
|
||||
$returnValue = $newReturn;
|
||||
}
|
||||
|
||||
return $returnValue;
|
||||
}
|
||||
}
|
||||
233
system/Test/Mock/MockConnection.php
Normal file
233
system/Test/Mock/MockConnection.php
Normal file
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use CodeIgniter\CodeIgniter;
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\Database\BaseResult;
|
||||
use CodeIgniter\Database\Query;
|
||||
|
||||
class MockConnection extends BaseConnection
|
||||
{
|
||||
protected $returnValues = [];
|
||||
public $database;
|
||||
public $lastQuery;
|
||||
|
||||
public function shouldReturn(string $method, $return)
|
||||
{
|
||||
$this->returnValues[$method] = $return;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Orchestrates a query against the database. Queries must use
|
||||
* Database\Statement objects to store the query and build it.
|
||||
* This method works with the cache.
|
||||
*
|
||||
* Should automatically handle different connections for read/write
|
||||
* queries if needed.
|
||||
*
|
||||
* @param mixed ...$binds
|
||||
*
|
||||
* @return BaseResult|bool|Query
|
||||
*
|
||||
* @todo BC set $queryClass default as null in 4.1
|
||||
*/
|
||||
public function query(string $sql, $binds = null, bool $setEscapeFlags = true, string $queryClass = '')
|
||||
{
|
||||
$queryClass = str_replace('Connection', 'Query', static::class);
|
||||
|
||||
$query = new $queryClass($this);
|
||||
|
||||
$query->setQuery($sql, $binds, $setEscapeFlags);
|
||||
|
||||
if (! empty($this->swapPre) && ! empty($this->DBPrefix)) {
|
||||
$query->swapPrefix($this->DBPrefix, $this->swapPre);
|
||||
}
|
||||
|
||||
$startTime = microtime(true);
|
||||
|
||||
$this->lastQuery = $query;
|
||||
|
||||
// Run the query
|
||||
if (false === ($this->resultID = $this->simpleQuery($query->getQuery()))) {
|
||||
$query->setDuration($startTime, $startTime);
|
||||
|
||||
// @todo deal with errors
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$query->setDuration($startTime);
|
||||
|
||||
// resultID is not false, so it must be successful
|
||||
if ($query->isWriteType()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// query is not write-type, so it must be read-type query; return QueryResult
|
||||
$resultClass = str_replace('Connection', 'Result', static::class);
|
||||
|
||||
return new $resultClass($this->connID, $this->resultID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the database.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function connect(bool $persistent = false)
|
||||
{
|
||||
$return = $this->returnValues['connect'] ?? true;
|
||||
|
||||
if (is_array($return)) {
|
||||
// By removing the top item here, we can
|
||||
// get a different value for, say, testing failover connections.
|
||||
$return = array_shift($this->returnValues['connect']);
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep or establish the connection if no queries have been sent for
|
||||
* a length of time exceeding the server's idle timeout.
|
||||
*/
|
||||
public function reconnect(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a specific database table to use.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function setDatabase(string $databaseName)
|
||||
{
|
||||
$this->database = $databaseName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing the version of the database being used.
|
||||
*/
|
||||
public function getVersion(): string
|
||||
{
|
||||
return CodeIgniter::CI_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the query against the database.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function execute(string $sql)
|
||||
{
|
||||
return $this->returnValues['execute'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of rows affected by this query.
|
||||
*/
|
||||
public function affectedRows(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last error code and message.
|
||||
*
|
||||
* Must return an array with keys 'code' and 'message':
|
||||
*
|
||||
* return ['code' => null, 'message' => null);
|
||||
*/
|
||||
public function error(): array
|
||||
{
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert ID
|
||||
*/
|
||||
public function insertID(): int
|
||||
{
|
||||
return $this->connID->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the SQL for listing tables in a platform-dependent manner.
|
||||
*/
|
||||
protected function _listTables(bool $constrainByPrefix = false): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific query string so that the column names can be fetched.
|
||||
*/
|
||||
protected function _listColumns(string $table = ''): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function _fieldData(string $table): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function _indexData(string $table): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function _foreignKeyData(string $table): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the connection.
|
||||
*/
|
||||
protected function _close()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin Transaction
|
||||
*/
|
||||
protected function _transBegin(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit Transaction
|
||||
*/
|
||||
protected function _transCommit(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback Transaction
|
||||
*/
|
||||
protected function _transRollback(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
40
system/Test/Mock/MockEmail.php
Normal file
40
system/Test/Mock/MockEmail.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use CodeIgniter\Email\Email;
|
||||
use CodeIgniter\Events\Events;
|
||||
|
||||
class MockEmail extends Email
|
||||
{
|
||||
/**
|
||||
* Value to return from mocked send().
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $returnValue = true;
|
||||
|
||||
public function send($autoClear = true)
|
||||
{
|
||||
if ($this->returnValue) {
|
||||
$this->setArchiveValues();
|
||||
|
||||
if ($autoClear) {
|
||||
$this->clear();
|
||||
}
|
||||
|
||||
Events::trigger('email', $this->archive);
|
||||
}
|
||||
|
||||
return $this->returnValue;
|
||||
}
|
||||
}
|
||||
40
system/Test/Mock/MockEvents.php
Normal file
40
system/Test/Mock/MockEvents.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use CodeIgniter\Events\Events;
|
||||
|
||||
/**
|
||||
* Events
|
||||
*/
|
||||
class MockEvents extends Events
|
||||
{
|
||||
public function getListeners()
|
||||
{
|
||||
return self::$listeners;
|
||||
}
|
||||
|
||||
public function getEventsFile()
|
||||
{
|
||||
return self::$files;
|
||||
}
|
||||
|
||||
public function getSimulate()
|
||||
{
|
||||
return self::$simulate;
|
||||
}
|
||||
|
||||
public function unInitialize()
|
||||
{
|
||||
static::$initialized = false;
|
||||
}
|
||||
}
|
||||
34
system/Test/Mock/MockFileLogger.php
Normal file
34
system/Test/Mock/MockFileLogger.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use CodeIgniter\Log\Handlers\FileHandler;
|
||||
|
||||
/**
|
||||
* Class MockFileLogger
|
||||
*
|
||||
* Extends FileHandler, exposing some inner workings
|
||||
*/
|
||||
class MockFileLogger extends FileHandler
|
||||
{
|
||||
/**
|
||||
* Where would the log be written?
|
||||
*/
|
||||
public $destination;
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
parent::__construct($config);
|
||||
$this->handles = $config['handles'] ?? [];
|
||||
$this->destination = $this->path . 'log-' . date('Y-m-d') . '.' . $this->fileExtension;
|
||||
}
|
||||
}
|
||||
22
system/Test/Mock/MockIncomingRequest.php
Normal file
22
system/Test/Mock/MockIncomingRequest.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
|
||||
class MockIncomingRequest extends IncomingRequest
|
||||
{
|
||||
protected function detectURI($protocol, $baseURL)
|
||||
{
|
||||
// Do nothing...
|
||||
}
|
||||
}
|
||||
56
system/Test/Mock/MockLanguage.php
Normal file
56
system/Test/Mock/MockLanguage.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use CodeIgniter\Language\Language;
|
||||
|
||||
class MockLanguage extends Language
|
||||
{
|
||||
/**
|
||||
* Stores the data that should be
|
||||
* returned by the 'requireFile()' method.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* Sets the data that should be returned by the
|
||||
* 'requireFile()' method to allow easy overrides
|
||||
* during testing.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setData(string $file, array $data, ?string $locale = null)
|
||||
{
|
||||
$this->language[$locale ?? $this->locale][$file] = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an override that allows us to set custom
|
||||
* data to be returned easily during testing.
|
||||
*/
|
||||
protected function requireFile(string $path): array
|
||||
{
|
||||
return $this->data ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Arbitrarily turnoff internationalization support for testing
|
||||
*/
|
||||
public function disableIntlSupport()
|
||||
{
|
||||
$this->intlSupport = false;
|
||||
}
|
||||
}
|
||||
101
system/Test/Mock/MockLogger.php
Normal file
101
system/Test/Mock/MockLogger.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
class MockLogger
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Error Logging Threshold
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You can enable error logging by setting a threshold over zero. The
|
||||
| threshold determines what gets logged. Any values below or equal to the
|
||||
| threshold will be logged. Threshold options are:
|
||||
|
|
||||
| 0 = Disables logging, Error logging TURNED OFF
|
||||
| 1 = Emergency Messages - System is unusable
|
||||
| 2 = Alert Messages - Action Must Be Taken Immediately
|
||||
| 3 = Critical Messages - Application component unavailable, unexpected exception.
|
||||
| 4 = Runtime Errors - Don't need immediate action, but should be monitored.
|
||||
| 5 = Warnings - Exceptional occurrences that are not errors.
|
||||
| 6 = Notices - Normal but significant events.
|
||||
| 7 = Info - Interesting events, like user logging in, etc.
|
||||
| 8 = Debug - Detailed debug information.
|
||||
| 9 = All Messages
|
||||
|
|
||||
| You can also pass an array with threshold levels to show individual error types
|
||||
|
|
||||
| array(1, 2, 3, 8) = Emergency, Alert, Critical, and Debug messages
|
||||
|
|
||||
| For a live site you'll usually enable Critical or higher (3) to be logged otherwise
|
||||
| your log files will fill up very fast.
|
||||
|
|
||||
*/
|
||||
|
||||
public $threshold = 9;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Date Format for Logs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Each item that is logged has an associated date. You can use PHP date
|
||||
| codes to set your own date formatting
|
||||
|
|
||||
*/
|
||||
public $dateFormat = 'Y-m-d';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log Handlers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The logging system supports multiple actions to be taken when something
|
||||
| is logged. This is done by allowing for multiple Handlers, special classes
|
||||
| designed to write the log to their chosen destinations, whether that is
|
||||
| a file on the getServer, a cloud-based service, or even taking actions such
|
||||
| as emailing the dev team.
|
||||
|
|
||||
| Each handler is defined by the class name used for that handler, and it
|
||||
| MUST implement the CodeIgniter\Log\Handlers\HandlerInterface interface.
|
||||
|
|
||||
| The value of each key is an array of configuration items that are sent
|
||||
| to the constructor of each handler. The only required configuration item
|
||||
| is the 'handles' element, which must be an array of integer log levels.
|
||||
| This is most easily handled by using the constants defined in the
|
||||
| Psr\Log\LogLevel class.
|
||||
|
|
||||
| Handlers are executed in the order defined in this array, starting with
|
||||
| the handler on top and continuing down.
|
||||
|
|
||||
*/
|
||||
public $handlers = [
|
||||
// File Handler
|
||||
'Tests\Support\Log\Handlers\TestHandler' => [
|
||||
// The log levels that this handler will handle.
|
||||
'handles' => [
|
||||
'critical',
|
||||
'alert',
|
||||
'emergency',
|
||||
'debug',
|
||||
'error',
|
||||
'info',
|
||||
'notice',
|
||||
'warning',
|
||||
],
|
||||
|
||||
// Logging Directory Path
|
||||
'path' => '',
|
||||
],
|
||||
];
|
||||
}
|
||||
18
system/Test/Mock/MockQuery.php
Normal file
18
system/Test/Mock/MockQuery.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use CodeIgniter\Database\Query;
|
||||
|
||||
class MockQuery extends Query
|
||||
{
|
||||
}
|
||||
32
system/Test/Mock/MockResourceController.php
Normal file
32
system/Test/Mock/MockResourceController.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use CodeIgniter\RESTful\ResourceController;
|
||||
|
||||
class MockResourceController extends ResourceController
|
||||
{
|
||||
public function getModel()
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
public function getModelName()
|
||||
{
|
||||
return $this->modelName;
|
||||
}
|
||||
|
||||
public function getFormat()
|
||||
{
|
||||
return $this->format;
|
||||
}
|
||||
}
|
||||
35
system/Test/Mock/MockResourcePresenter.php
Normal file
35
system/Test/Mock/MockResourcePresenter.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use CodeIgniter\RESTful\ResourcePresenter;
|
||||
|
||||
class MockResourcePresenter extends ResourcePresenter
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
public function getModel()
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
public function getModelName()
|
||||
{
|
||||
return $this->modelName;
|
||||
}
|
||||
|
||||
public function getFormat()
|
||||
{
|
||||
return $this->format;
|
||||
}
|
||||
}
|
||||
39
system/Test/Mock/MockResponse.php
Normal file
39
system/Test/Mock/MockResponse.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use CodeIgniter\HTTP\Response;
|
||||
|
||||
/**
|
||||
* Class MockResponse
|
||||
*/
|
||||
class MockResponse extends Response
|
||||
{
|
||||
/**
|
||||
* If true, will not write output. Useful during testing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $pretend = true;
|
||||
|
||||
// for testing
|
||||
public function getPretend()
|
||||
{
|
||||
return $this->pretend;
|
||||
}
|
||||
|
||||
// artificial error for testing
|
||||
public function misbehave()
|
||||
{
|
||||
$this->statusCode = 0;
|
||||
}
|
||||
}
|
||||
96
system/Test/Mock/MockResult.php
Normal file
96
system/Test/Mock/MockResult.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use CodeIgniter\Database\BaseResult;
|
||||
|
||||
class MockResult extends BaseResult
|
||||
{
|
||||
/**
|
||||
* Gets the number of fields in the result set.
|
||||
*/
|
||||
public function getFieldCount(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of column names in the result set.
|
||||
*/
|
||||
public function getFieldNames(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of objects representing field meta-data.
|
||||
*/
|
||||
public function getFieldData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the current result.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function freeResult()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal pointer to the desired offset. This is called
|
||||
* internally before fetching results to make sure the result set
|
||||
* starts at zero.
|
||||
*
|
||||
* @param int $n
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function dataSeek($n = 0)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an array.
|
||||
*
|
||||
* Overridden by driver classes.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function fetchAssoc()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an object.
|
||||
*
|
||||
* Overridden by child classes.
|
||||
*
|
||||
* @param string $className
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
protected function fetchObject($className = 'stdClass')
|
||||
{
|
||||
return new $className();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of fields in the result set.
|
||||
*/
|
||||
public function getNumRows(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
30
system/Test/Mock/MockSecurity.php
Normal file
30
system/Test/Mock/MockSecurity.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use CodeIgniter\Security\Security;
|
||||
|
||||
class MockSecurity extends Security
|
||||
{
|
||||
protected function doSendCookie(): void
|
||||
{
|
||||
$_COOKIE['csrf_cookie_name'] = $this->hash;
|
||||
}
|
||||
|
||||
protected function randomize(string $hash): string
|
||||
{
|
||||
$keyBinary = hex2bin('005513c290126d34d41bf41c5265e0f1');
|
||||
$hashBinary = hex2bin($hash);
|
||||
|
||||
return bin2hex(($hashBinary ^ $keyBinary) . $keyBinary);
|
||||
}
|
||||
}
|
||||
31
system/Test/Mock/MockSecurityConfig.php
Normal file
31
system/Test/Mock/MockSecurityConfig.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use Config\Security as Security;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class MockSecurityConfig extends Security
|
||||
{
|
||||
public $tokenName = 'csrf_test_name';
|
||||
public $headerName = 'X-CSRF-TOKEN';
|
||||
public $cookieName = 'csrf_cookie_name';
|
||||
public $expires = 7200;
|
||||
public $regenerate = true;
|
||||
public $redirect = false;
|
||||
public $samesite = 'Lax';
|
||||
public $excludeURIs = ['http://example.com'];
|
||||
}
|
||||
34
system/Test/Mock/MockServices.php
Normal file
34
system/Test/Mock/MockServices.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use CodeIgniter\Autoloader\FileLocator;
|
||||
use CodeIgniter\Config\BaseService;
|
||||
|
||||
class MockServices extends BaseService
|
||||
{
|
||||
public $psr4 = [
|
||||
'Tests/Support' => TESTPATH . '_support/',
|
||||
];
|
||||
public $classmap = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Don't call the parent since we don't want the default mappings.
|
||||
// parent::__construct();
|
||||
}
|
||||
|
||||
public static function locator(bool $getShared = true)
|
||||
{
|
||||
return new FileLocator(static::autoloader());
|
||||
}
|
||||
}
|
||||
70
system/Test/Mock/MockSession.php
Normal file
70
system/Test/Mock/MockSession.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use CodeIgniter\Cookie\Cookie;
|
||||
use CodeIgniter\Session\Session;
|
||||
|
||||
/**
|
||||
* Class MockSession
|
||||
*
|
||||
* Provides a safe way to test the Session class itself,
|
||||
* that doesn't interact with the session or cookies at all.
|
||||
*/
|
||||
class MockSession extends Session
|
||||
{
|
||||
/**
|
||||
* Holds our "cookie" data.
|
||||
*
|
||||
* @var Cookie[]
|
||||
*/
|
||||
public $cookies = [];
|
||||
|
||||
public $didRegenerate = false;
|
||||
|
||||
/**
|
||||
* Sets the driver as the session handler in PHP.
|
||||
* Extracted for easier testing.
|
||||
*/
|
||||
protected function setSaveHandler()
|
||||
{
|
||||
// session_set_save_handler($this->driver, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the session.
|
||||
* Extracted for testing reasons.
|
||||
*/
|
||||
protected function startSession()
|
||||
{
|
||||
// session_start();
|
||||
$this->setCookie();
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes care of setting the cookie on the client side.
|
||||
* Extracted for testing reasons.
|
||||
*/
|
||||
protected function setCookie()
|
||||
{
|
||||
$expiration = $this->sessionExpiration === 0 ? 0 : time() + $this->sessionExpiration;
|
||||
$this->cookie = $this->cookie->withValue(session_id())->withExpires($expiration);
|
||||
|
||||
$this->cookies[] = $this->cookie;
|
||||
}
|
||||
|
||||
public function regenerate(bool $destroy = false)
|
||||
{
|
||||
$this->didRegenerate = true;
|
||||
$_SESSION['__ci_last_regenerate'] = time();
|
||||
}
|
||||
}
|
||||
28
system/Test/Mock/MockTable.php
Normal file
28
system/Test/Mock/MockTable.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test\Mock;
|
||||
|
||||
use BadMethodCallException;
|
||||
use CodeIgniter\View\Table;
|
||||
|
||||
class MockTable extends Table
|
||||
{
|
||||
// Override inaccessible protected method
|
||||
public function __call($method, $params)
|
||||
{
|
||||
if (is_callable([$this, '_' . $method])) {
|
||||
return call_user_func_array([$this, '_' . $method], $params);
|
||||
}
|
||||
|
||||
throw new BadMethodCallException('Method ' . $method . ' was not found');
|
||||
}
|
||||
}
|
||||
98
system/Test/ReflectionHelper.php
Normal file
98
system/Test/ReflectionHelper.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test;
|
||||
|
||||
use Closure;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use ReflectionMethod;
|
||||
use ReflectionObject;
|
||||
use ReflectionProperty;
|
||||
|
||||
/**
|
||||
* Testing helper.
|
||||
*/
|
||||
trait ReflectionHelper
|
||||
{
|
||||
/**
|
||||
* Find a private method invoker.
|
||||
*
|
||||
* @param object|string $obj object or class name
|
||||
* @param string $method method name
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
public static function getPrivateMethodInvoker($obj, $method)
|
||||
{
|
||||
$refMethod = new ReflectionMethod($obj, $method);
|
||||
$refMethod->setAccessible(true);
|
||||
$obj = (gettype($obj) === 'object') ? $obj : null;
|
||||
|
||||
return static function (...$args) use ($obj, $refMethod) {
|
||||
return $refMethod->invokeArgs($obj, $args);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an accessible property.
|
||||
*
|
||||
* @param object|string $obj
|
||||
* @param string $property
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*
|
||||
* @return ReflectionProperty
|
||||
*/
|
||||
private static function getAccessibleRefProperty($obj, $property)
|
||||
{
|
||||
$refClass = is_object($obj) ? new ReflectionObject($obj) : new ReflectionClass($obj);
|
||||
|
||||
$refProperty = $refClass->getProperty($property);
|
||||
$refProperty->setAccessible(true);
|
||||
|
||||
return $refProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a private property.
|
||||
*
|
||||
* @param object|string $obj object or class name
|
||||
* @param string $property property name
|
||||
* @param mixed $value value
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public static function setPrivateProperty($obj, $property, $value)
|
||||
{
|
||||
$refProperty = self::getAccessibleRefProperty($obj, $property);
|
||||
$refProperty->setValue($obj, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a private property.
|
||||
*
|
||||
* @param object|string $obj object or class name
|
||||
* @param string $property property name
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*
|
||||
* @return mixed value
|
||||
*/
|
||||
public static function getPrivateProperty($obj, $property)
|
||||
{
|
||||
$refProperty = self::getAccessibleRefProperty($obj, $property);
|
||||
|
||||
return is_string($obj) ? $refProperty->getValue() : $refProperty->getValue($obj);
|
||||
}
|
||||
}
|
||||
78
system/Test/TestLogger.php
Normal file
78
system/Test/TestLogger.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test;
|
||||
|
||||
use CodeIgniter\Log\Logger;
|
||||
|
||||
class TestLogger extends Logger
|
||||
{
|
||||
protected static $op_logs = [];
|
||||
|
||||
/**
|
||||
* The log method is overridden so that we can store log history during
|
||||
* the tests to allow us to check ->assertLogged() methods.
|
||||
*
|
||||
* @param string $level
|
||||
* @param string $message
|
||||
*/
|
||||
public function log($level, $message, array $context = []): bool
|
||||
{
|
||||
// While this requires duplicate work, we want to ensure
|
||||
// we have the final message to test against.
|
||||
$logMessage = $this->interpolate($message, $context);
|
||||
|
||||
// Determine the file and line by finding the first
|
||||
// backtrace that is not part of our logging system.
|
||||
$trace = debug_backtrace();
|
||||
$file = null;
|
||||
|
||||
foreach ($trace as $row) {
|
||||
if (! in_array($row['function'], ['log', 'log_message'], true)) {
|
||||
$file = basename($row['file'] ?? '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self::$op_logs[] = [
|
||||
'level' => $level,
|
||||
'message' => $logMessage,
|
||||
'file' => $file,
|
||||
];
|
||||
|
||||
// Let the parent do it's thing.
|
||||
return parent::log($level, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by CIUnitTestCase class to provide ->assertLogged() methods.
|
||||
*
|
||||
* @param string $message
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function didLog(string $level, $message)
|
||||
{
|
||||
foreach (self::$op_logs as $log) {
|
||||
if (strtolower($log['level']) === strtolower($level) && $message === $log['message']) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Expose cleanFileNames()
|
||||
public function cleanup($file)
|
||||
{
|
||||
return $this->cleanFileNames($file);
|
||||
}
|
||||
}
|
||||
496
system/Test/TestResponse.php
Normal file
496
system/Test/TestResponse.php
Normal file
@@ -0,0 +1,496 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Test;
|
||||
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\Services;
|
||||
use Exception;
|
||||
use PHPUnit\Framework\Constraint\IsEqual;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Test Response Class
|
||||
*
|
||||
* Consolidated response processing
|
||||
* for test results.
|
||||
*
|
||||
* @no-final
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class TestResponse extends TestCase
|
||||
{
|
||||
/**
|
||||
* The request.
|
||||
*
|
||||
* @var RequestInterface|null
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* The response.
|
||||
*
|
||||
* @var ResponseInterface
|
||||
*/
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* DOM for the body.
|
||||
*
|
||||
* @var DOMParser
|
||||
*/
|
||||
protected $domParser;
|
||||
|
||||
/**
|
||||
* Stores or the Response and parses the body in the DOM.
|
||||
*/
|
||||
public function __construct(ResponseInterface $response)
|
||||
{
|
||||
$this->setResponse($response);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Getters / Setters
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Sets the request.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setRequest(RequestInterface $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Response and updates the DOM.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setResponse(ResponseInterface $response)
|
||||
{
|
||||
$this->response = $response;
|
||||
$this->domParser = new DOMParser();
|
||||
|
||||
$body = $response->getBody();
|
||||
if (is_string($body) && $body !== '') {
|
||||
$this->domParser->withString($body);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request accessor.
|
||||
*
|
||||
* @return RequestInterface|null
|
||||
*/
|
||||
public function request()
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response accessor.
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function response()
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Status Checks
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Boils down the possible responses into a boolean valid/not-valid
|
||||
* response type.
|
||||
*/
|
||||
public function isOK(): bool
|
||||
{
|
||||
$status = $this->response->getStatusCode();
|
||||
|
||||
// Only 200 and 300 range status codes
|
||||
// are considered valid.
|
||||
if ($status >= 400 || $status < 200) {
|
||||
return false;
|
||||
}
|
||||
// Empty bodies are not considered valid, unless in redirects
|
||||
return ! ($status < 300 && empty($this->response->getBody()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the status is a specific value.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function assertStatus(int $code)
|
||||
{
|
||||
$this->assertSame($code, $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.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the Response is considered OK.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function assertNotOK()
|
||||
{
|
||||
$this->assertFalse($this->isOK(), "{$this->response->getStatusCode()} is an unexpected successful status code, or the Response has body content.");
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Redirection
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns whether or not the Response was a redirect or RedirectResponse
|
||||
*/
|
||||
public function isRedirect(): bool
|
||||
{
|
||||
return $this->response instanceof RedirectResponse
|
||||
|| $this->response->hasHeader('Location')
|
||||
|| $this->response->hasHeader('Refresh');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given response was a redirect.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function assertRedirect()
|
||||
{
|
||||
$this->assertTrue($this->isRedirect(), 'Response is not a redirect or RedirectResponse.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that a given response was a redirect
|
||||
* and it was redirect to a specific URI.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function assertRedirectTo(string $uri)
|
||||
{
|
||||
$this->assertRedirect();
|
||||
|
||||
$uri = trim(strtolower($uri));
|
||||
$redirectUri = strtolower($this->getRedirectUrl());
|
||||
|
||||
$matches = $uri === $redirectUri
|
||||
|| strtolower(site_url($uri)) === $redirectUri
|
||||
|| $uri === site_url($redirectUri);
|
||||
|
||||
$this->assertTrue($matches, "Redirect URL `{$uri}` does not match `{$redirectUri}`");
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given response was not a redirect.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function assertNotRedirect()
|
||||
{
|
||||
$this->assertFalse($this->isRedirect(), 'Response is an unexpected redirect or RedirectResponse.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL set for redirection.
|
||||
*/
|
||||
public function getRedirectUrl(): ?string
|
||||
{
|
||||
if (! $this->isRedirect()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->response->hasHeader('Location')) {
|
||||
return $this->response->getHeaderLine('Location');
|
||||
}
|
||||
|
||||
if ($this->response->hasHeader('Refresh')) {
|
||||
return str_replace('0;url=', '', $this->response->getHeaderLine('Refresh'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Session
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Asserts that an SESSION key has been set and, optionally, test it's value.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function assertSessionHas(string $key, $value = null)
|
||||
{
|
||||
$this->assertArrayHasKey($key, $_SESSION, "'{$key}' is not in the current \$_SESSION");
|
||||
|
||||
if ($value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_scalar($value)) {
|
||||
$this->assertSame($value, $_SESSION[$key], "The value of '{$key}' ({$value}) does not match expected value.");
|
||||
} else {
|
||||
$this->assertSame($value, $_SESSION[$key], "The value of '{$key}' does not match expected value.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the session is missing $key.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function assertSessionMissing(string $key)
|
||||
{
|
||||
$this->assertArrayNotHasKey($key, $_SESSION, "'{$key}' should not be present in \$_SESSION.");
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Headers
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Asserts that the Response contains a specific header.
|
||||
*
|
||||
* @param string|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->assertSame($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.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function assertHeaderMissing(string $key)
|
||||
{
|
||||
$this->assertFalse($this->response->hasHeader($key), "'{$key}' should not be in the Response headers.");
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Cookies
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Asserts that the response has the specified cookie.
|
||||
*
|
||||
* @param string|null $value
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @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)->getExpiresTimestamp());
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// JSON
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the response's body as JSON
|
||||
*
|
||||
* @return false|mixed
|
||||
*/
|
||||
public function getJSON()
|
||||
{
|
||||
$response = $this->response->getJSON();
|
||||
|
||||
if ($response === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the response contains a matching JSON fragment.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function assertJSONFragment(array $fragment, bool $strict = false)
|
||||
{
|
||||
$json = json_decode($this->getJSON(), true);
|
||||
$this->assertIsArray($json, 'Response does not have valid json');
|
||||
$patched = array_replace_recursive($json, $fragment);
|
||||
|
||||
if ($strict) {
|
||||
$this->assertSame($json, $patched, 'Response does not contain a matching JSON fragment.');
|
||||
} else {
|
||||
$this->assertThat($patched, new IsEqual($json), 'Response does not contain a matching JSON fragment.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the JSON exactly matches the passed in data.
|
||||
* If the value being passed in is a string, it must be a json_encoded string.
|
||||
*
|
||||
* @param array|string $test
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function assertJSONExact($test)
|
||||
{
|
||||
$json = $this->getJSON();
|
||||
|
||||
if (is_object($test)) {
|
||||
$test = method_exists($test, 'toArray') ? $test->toArray() : (array) $test;
|
||||
}
|
||||
|
||||
if (is_array($test)) {
|
||||
$test = Services::format()->getFormatter('application/json')->format($test);
|
||||
}
|
||||
|
||||
$this->assertJsonStringEqualsJsonString($test, $json, 'Response does not contain matching JSON.');
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// XML Methods
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the response' body as XML
|
||||
*
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function getXML()
|
||||
{
|
||||
return $this->response->getXML();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// DomParser
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Assert that the desired text can be found in the result body.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @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.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward any unrecognized method calls to our DOMParser instance.
|
||||
*
|
||||
* @param string $function Method name
|
||||
* @param mixed $params Any method parameters
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call($function, $params)
|
||||
{
|
||||
if (method_exists($this->domParser, $function)) {
|
||||
return $this->domParser->{$function}(...$params);
|
||||
}
|
||||
}
|
||||
}
|
||||
106
system/Test/bootstrap.php
Normal file
106
system/Test/bootstrap.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use CodeIgniter\Config\DotEnv;
|
||||
use CodeIgniter\Router\RouteCollection;
|
||||
use CodeIgniter\Services;
|
||||
use Config\Autoload;
|
||||
use Config\Modules;
|
||||
use Config\Paths;
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
ini_set('display_startup_errors', '1');
|
||||
|
||||
// Make sure it recognizes that we're testing.
|
||||
$_SERVER['CI_ENVIRONMENT'] = 'testing';
|
||||
define('ENVIRONMENT', 'testing');
|
||||
defined('CI_DEBUG') || define('CI_DEBUG', true);
|
||||
|
||||
// Often these constants are pre-defined, but query the current directory structure as a fallback
|
||||
defined('HOMEPATH') || define('HOMEPATH', realpath(rtrim(getcwd(), '\\/ ')) . DIRECTORY_SEPARATOR);
|
||||
$source = is_dir(HOMEPATH . 'app')
|
||||
? HOMEPATH
|
||||
: (is_dir('vendor/codeigniter4/framework/')
|
||||
? 'vendor/codeigniter4/framework/'
|
||||
: 'vendor/codeigniter4/codeigniter4/');
|
||||
defined('CONFIGPATH') || define('CONFIGPATH', realpath($source . 'app/Config') . DIRECTORY_SEPARATOR);
|
||||
defined('PUBLICPATH') || define('PUBLICPATH', realpath($source . 'public') . DIRECTORY_SEPARATOR);
|
||||
unset($source);
|
||||
|
||||
// Load framework paths from their config file
|
||||
require CONFIGPATH . 'Paths.php';
|
||||
$paths = new Paths();
|
||||
|
||||
// Define necessary framework path constants
|
||||
defined('APPPATH') || define('APPPATH', realpath(rtrim($paths->appDirectory, '\\/ ')) . DIRECTORY_SEPARATOR);
|
||||
defined('WRITEPATH') || define('WRITEPATH', realpath(rtrim($paths->writableDirectory, '\\/ ')) . DIRECTORY_SEPARATOR);
|
||||
defined('SYSTEMPATH') || define('SYSTEMPATH', realpath(rtrim($paths->systemDirectory, '\\/')) . DIRECTORY_SEPARATOR);
|
||||
defined('ROOTPATH') || define('ROOTPATH', realpath(APPPATH . '../') . DIRECTORY_SEPARATOR);
|
||||
defined('CIPATH') || define('CIPATH', realpath(SYSTEMPATH . '../') . DIRECTORY_SEPARATOR);
|
||||
defined('FCPATH') || define('FCPATH', realpath(PUBLICPATH) . DIRECTORY_SEPARATOR);
|
||||
defined('TESTPATH') || define('TESTPATH', realpath(HOMEPATH . 'tests/') . DIRECTORY_SEPARATOR);
|
||||
defined('SUPPORTPATH') || define('SUPPORTPATH', realpath(TESTPATH . '_support/') . DIRECTORY_SEPARATOR);
|
||||
defined('COMPOSER_PATH') || define('COMPOSER_PATH', realpath(HOMEPATH . 'vendor/autoload.php'));
|
||||
defined('VENDORPATH') || define('VENDORPATH', realpath(HOMEPATH . 'vendor') . DIRECTORY_SEPARATOR);
|
||||
|
||||
// Load Common.php from App then System
|
||||
if (file_exists(APPPATH . 'Common.php')) {
|
||||
require_once APPPATH . 'Common.php';
|
||||
}
|
||||
|
||||
require_once SYSTEMPATH . 'Common.php';
|
||||
|
||||
// Set environment values that would otherwise stop the framework from functioning during tests.
|
||||
if (! isset($_SERVER['app.baseURL'])) {
|
||||
$_SERVER['app.baseURL'] = 'http://example.com/';
|
||||
}
|
||||
|
||||
// Load necessary components
|
||||
require_once SYSTEMPATH . 'Config/AutoloadConfig.php';
|
||||
require_once APPPATH . 'Config/Autoload.php';
|
||||
require_once APPPATH . 'Config/Constants.php';
|
||||
require_once SYSTEMPATH . 'Modules/Modules.php';
|
||||
require_once APPPATH . 'Config/Modules.php';
|
||||
|
||||
require_once SYSTEMPATH . 'Autoloader/Autoloader.php';
|
||||
require_once SYSTEMPATH . 'Config/BaseService.php';
|
||||
require_once SYSTEMPATH . 'Config/Services.php';
|
||||
require_once APPPATH . 'Config/Services.php';
|
||||
|
||||
// Use Config\Services as CodeIgniter\Services
|
||||
if (! class_exists('CodeIgniter\Services', false)) {
|
||||
class_alias('Config\Services', 'CodeIgniter\Services');
|
||||
}
|
||||
|
||||
// Initialize and register the loader with the SPL autoloader stack.
|
||||
Services::autoloader()->initialize(new Autoload(), new Modules())->register();
|
||||
|
||||
// Now load Composer's if it's available
|
||||
if (is_file(COMPOSER_PATH)) {
|
||||
require_once COMPOSER_PATH;
|
||||
}
|
||||
|
||||
// Load environment settings from .env files into $_SERVER and $_ENV
|
||||
require_once SYSTEMPATH . 'Config/DotEnv.php';
|
||||
|
||||
$env = new DotEnv(ROOTPATH);
|
||||
$env->load();
|
||||
|
||||
// Always load the URL helper, it should be used in most of apps.
|
||||
helper('url');
|
||||
|
||||
require_once APPPATH . 'Config/Routes.php';
|
||||
|
||||
/**
|
||||
* @var RouteCollection $routes
|
||||
*/
|
||||
$routes->getRoutes('*');
|
||||
Reference in New Issue
Block a user