CodeIgniter4/system/Test/FilterTestTrait.php
2024-12-29 00:21:53 +08:00

314 lines
9.1 KiB
PHP

<?php
declare(strict_types=1);
/**
* 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\Exceptions\InvalidArgumentException;
use CodeIgniter\Exceptions\RuntimeException;
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;
/**
* 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 ??= clone service('request');
$this->response ??= clone service('response');
// Create our config and Filters instance to reuse for performance
$this->filtersConfig ??= config(FiltersConfig::class);
$this->filters ??= new Filters($this->filtersConfig, $this->request, $this->response);
if ($this->collection === null) {
$this->collection = service('routes')->loadRoutes();
}
$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"
*
* @phpstan-return Closure(list<string>|null=): mixed
*/
protected function getFilterCaller($filter, string $position): Closure
{
if (! in_array($position, ['before', 'after'], true)) {
throw new InvalidArgumentException('Invalid filter position passed: ' . $position);
}
if ($filter instanceof FilterInterface) {
$filterInstances = [$filter];
}
if (is_string($filter)) {
// Check for an alias (no namespace)
if (! str_contains($filter, '\\')) {
if (! isset($this->filtersConfig->aliases[$filter])) {
throw new RuntimeException("No filter found with alias '{$filter}'");
}
$filterClasses = (array) $this->filtersConfig->aliases[$filter];
} else {
// FQCN
$filterClasses = [$filter];
}
$filterInstances = [];
foreach ($filterClasses as $class) {
// Get an instance
$filter = new $class();
if (! $filter instanceof FilterInterface) {
throw FilterException::forIncorrectInterface($filter::class);
}
$filterInstances[] = $filter;
}
}
$request = clone $this->request;
if ($position === 'before') {
return static function (?array $params = null) use ($filterInstances, $request) {
foreach ($filterInstances as $filter) {
$result = $filter->before($request, $params);
// @TODO The following logic is in Filters class.
// Should use Filters class.
if ($result instanceof RequestInterface) {
$request = $result;
continue;
}
if ($result instanceof ResponseInterface) {
return $result;
}
if (empty($result)) {
continue;
}
}
return $result;
};
}
$response = clone $this->response;
return static function (?array $params = null) use ($filterInstances, $request, $response) {
foreach ($filterInstances as $filter) {
$result = $filter->after($request, $response, $params);
// @TODO The following logic is in Filters class.
// Should use Filters class.
if ($result instanceof ResponseInterface) {
$response = $result;
continue;
}
}
return $result;
};
}
/**
* 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 list<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();
$routeFilters = $this->collection->getFiltersForRoute($route);
if ($routeFilters !== []) {
$this->filters->enableFilters($routeFilters, $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) . '.',
);
}
}