From 5bd4fb8e3add49a4613c8082c83fe7981fe05e83 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 10 Dec 2024 12:19:42 +0800 Subject: [PATCH] MDL-83968 core: Move router test classes to correct locations --- .upgradenotes/MDL-83968-2024121005532543.yml | 16 + lib/classes/tests/route_testcase.php | 533 +---------------- .../classes/router/mocking_route_loader.php | 153 +++++ lib/tests/classes/router/route_testcase.php | 546 ++++++++++++++++++ .../fixtures/router/mocking_route_loader.php | 134 +---- 5 files changed, 725 insertions(+), 657 deletions(-) create mode 100644 .upgradenotes/MDL-83968-2024121005532543.yml create mode 100644 lib/tests/classes/router/mocking_route_loader.php create mode 100644 lib/tests/classes/router/route_testcase.php diff --git a/.upgradenotes/MDL-83968-2024121005532543.yml b/.upgradenotes/MDL-83968-2024121005532543.yml new file mode 100644 index 00000000000..8e36f6ffa76 --- /dev/null +++ b/.upgradenotes/MDL-83968-2024121005532543.yml @@ -0,0 +1,16 @@ +issueNumber: MDL-83968 +notes: + core: + - message: > + The following test classes have been moved into autoloadable locations: + + + | Old location | New classname | + + | --- | --- | + + | `\core\tests\route_testcase` | `\core\tests\router\route_testcase` | + + | `\core\router\mocking_route_loader` | + `\core\tests\router\mocking_route_loader` | + type: changed diff --git a/lib/classes/tests/route_testcase.php b/lib/classes/tests/route_testcase.php index 22b75459d86..50b8aa1186f 100644 --- a/lib/classes/tests/route_testcase.php +++ b/lib/classes/tests/route_testcase.php @@ -14,536 +14,13 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -namespace core\tests; - -use core\router; -use core\router\bridge; -use core\router\mocking_route_loader; -use core\router\route_loader_interface; -use core\router\schema\openapi_base; -use core\router\schema\referenced_object; -use core\router\schema\specification; -use stdClass; -use GuzzleHttp\Psr7\Response; -use GuzzleHttp\Psr7\ServerRequest; -use GuzzleHttp\Psr7\Uri; -use PHPUnit\Framework\ExpectationFailedException; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\StreamInterface; -use Slim\App; -use Slim\Middleware\RoutingMiddleware; -use Slim\Routing\Route; -use Slim\Routing\RouteContext; - /** - * Tests for user preference API handler. + * Helper for mocking routes. + * + * Note: This file has been deprecated and will be removed in Moodle 6.0. * * @package core - * @copyright 2023 Andrew Lyons + * @copyright Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -abstract class route_testcase extends \advanced_testcase { - /** - * Update the test route loader using the supplied callback. - * - * @param callable $modifier - */ - protected function update_test_route_loader( - callable $modifier, - ): void { - self::load_fixture('core', 'router/mocking_route_loader.php'); - - $routeloader = \core\di::get(mocking_route_loader::class); - $modifier($routeloader); - \core\di::set(route_loader_interface::class, $routeloader); - } - - /** - * Add a route from a class method. - * - * @param string $classname The class to add the route from - * @param string $methodname The method name to add - * @param null|string $grouppath The path to the route group - */ - protected function add_route_to_route_loader( - string $classname, - string $methodname, - ?string $grouppath = null, - ) { - $grouppath = $grouppath ?? $this->guess_group_path_from_classname($classname); - $this->update_test_route_loader(fn (mocking_route_loader $routeloader) => $routeloader->mock_route_from_class_method( - $grouppath, - new \ReflectionMethod($classname, $methodname), - )); - } - - /** - * Add all routes from the specified class to the test loader. - * - * Only methods within the class with a #[route] attribute will be added. - * - * @param string $classname The class to add routes from - * @param null|string $grouppath The path of the route group - */ - protected function add_class_routes_to_route_loader( - string $classname, - ?string $grouppath = null, - ): void { - $this->update_test_route_loader( - fn (mocking_route_loader $routeloader) => $routeloader->add_all_routes_in_class( - grouppath: $grouppath ?? $this->guess_group_path_from_classname($classname), - class: $classname, - ), - ); - } - - /** - * Guess the group path from a class name. - * - * @param string $classname - * @return string - */ - protected function guess_group_path_from_classname( - string $classname, - ): string { - [, , $l3] = explode('\\', $classname, 4); - - if ($l3 === 'api') { - return route_loader_interface::ROUTE_GROUP_API; - } - - throw new \coding_exception("Unable to determine route path for '{$classname}'"); - } - - /** - * Mock a route from a route attribute. - * - * @param string $grouppath - * @param \core\router\route $route - * @param string $name - * @param callable|null $callable - */ - protected function mock_route_from_route_attribute( - string $grouppath, - \core\router\route $route, - string $name = 'route', - ?callable $callable = null, - ): void { - if ($callable === null) { - $callable = fn ($request, $response) => $response->withStatus(200); - } - - $this->update_test_route_loader(fn (mocking_route_loader $routeloader) => $routeloader->mock_route_from_callable( - grouppath: $grouppath, - methods: $route->get_methods(['GET']), - pattern: $route->get_path(), - callable: $callable, - name: $name, - )); - } - - /** - * Get a fully-configured instance of the Moodle Routing Application. - * - * @return App - */ - protected function get_app(): App { - $router = $this->get_router(); - - return $router->get_app(); - } - - /** - * Get a fully-configured instance of the Moodle Routing Application. - * - * @param string $basepath The basepath for the router - * @return router - */ - protected function get_router(string $basepath = ''): router { - \core\di::set( - router::class, - \DI\autowire(router::class)->constructorParameter('basepath', $basepath), - ); - - return \core\di::get(router::class); - } - - /** - * Get an unconfigured instance of the Slim Application. - * - * @return App - */ - protected function get_simple_app(): App { - global $CFG; - require_once("{$CFG->libdir}/nikic/fast-route/src/functions.php"); - $app = bridge::create( - container: \core\di::get_container(), - ); - - return $app; - } - - /** - * Get the request for a route which is known to the router. - * - * @param \core\router\route $route - * @param string $path - * @return ServerRequestInterface - */ - protected function get_request_for_routed_route( - \core\router\route $route, - string $path, - ): ServerRequestInterface { - $this->mock_route_from_route_attribute('', $route); - - // Grab just one method. - $methods = $route->get_methods(); - $method = $methods ? reset($methods) : 'GET'; - - $request = $this->create_request( - method: $method, - path: $path, - prefix: '', - route: $route, - ); - - $request = $this->route_request( - $this->get_app(), - $request, - ); - - return $request; - } - - /** - * Create a Request object. - * - * @param string $method - * @param string $path - * @param string $prefix - * @param array $headers - * @param array $cookies - * @param array $serverparams - * @param null|\core\router\route $route - * @return ServerRequestInterface - */ - protected function create_request( - string $method, - string $path, - string $prefix = route_loader_interface::ROUTE_GROUP_API, - array $headers = ['Content-Type' => 'application/json'], - array $cookies = [], - array $serverparams = [], - ?\core\router\route $route = null, - ): ServerRequestInterface { - $uri = new Uri($prefix . $path); - - $request = new ServerRequest( - method: $method, - headers: $headers, - uri: $uri, - serverParams: $serverparams, - ); - - // Sadly Guzzle's Uri only deals with query strings, not query params. - $query = $uri->getQuery(); - if ($query) { - $queryparams = []; - foreach (explode('&', $query) as $queryparam) { - [$key, $value] = explode('=', $queryparam, 2); - $queryparams[$key] = $value; - } - $request = $request->withQueryParams($queryparams); - } - - if ($route) { - $request = $request->withAttribute(\core\router\route::class, $route); - } - - return $request - ->withCookieParams($cookies); - } - - /** - * Process a request with the app. - * - * @param string $method - * @param string $path - * @param string $prefix - * @param array $headers - * @param null|StreamInterface $body - * @param null|string $contenttype - * @param array $cookies - * @param array $serverparams - * @return ResponseInterface - */ - protected function process_request( - string $method, - string $path, - string $prefix = '', - array $headers = ['HTTP_ACCEPT' => 'application/json'], - ?StreamInterface $body = null, - ?string $contenttype = 'application/json', - array $cookies = [], - array $serverparams = [], - ): ResponseInterface { - $app = $this->get_app(); - if ($contenttype !== null) { - $headers['Content-Type'] = $contenttype; - } - $request = $this->create_request( - $method, - $path, - $prefix, - $headers, - $cookies, - $serverparams, - ); - - if ($body) { - $request = $request->withBody($body); - } - - return $app->handle($request); - } - - /** - * Process a request with the app. - * - * @param string $method - * @param string $path - * @param array $headers - * @param null|StreamInterface $body - * @param array $cookies - * @param array $serverparams - * @return ResponseInterface - */ - protected function process_api_request( - string $method, - string $path, - array $headers = ['HTTP_ACCEPT' => 'application/json'], - ?StreamInterface $body = null, - array $cookies = [], - array $serverparams = [], - ): ResponseInterface { - return $this->process_request( - method: $method, - path: $path, - prefix: route_loader_interface::ROUTE_GROUP_API, - headers: $headers, - body: $body, - cookies: $cookies, - serverparams: $serverparams, - ); - } - - /** - * Route a request within the app. - * - * @param App $app - * @param ServerRequestInterface $request - * @return ServerRequestInterface - */ - protected function route_request( - App $app, - ServerRequestInterface $request, - ): ServerRequestInterface { - $routingmiddleware = new RoutingMiddleware( - $app->getRouteResolver(), - $app->getRouteCollector()->getRouteParser(), - ); - - return $routingmiddleware->performRouting($request); - } - - /** - * Create a route and route it to create a request. - * - * @param string $routepath - * @param string $requestpath - * @return ServerRequestInterface - */ - protected function create_route( - string $routepath, - string $requestpath, - ): ServerRequestInterface { - $app = $this->get_simple_app(); - $app->get($routepath, fn () => new Response()); - $request = $this->route_request($app, new ServerRequest('GET', $requestpath)); - - return $request; - } - - /** - * Get the Slim Route object from a Request object. - * - * @param ServerRequestInterface $request - * @return Route - */ - protected function get_slim_route_from_request( - ServerRequestInterface $request, - ): Route { - return $request->getAttribute(RouteContext::ROUTE); - } - - /** - * Assert that a Response object was valid. - * - * @param ResponseInterface $response - * @param null|int $statuscode The expected status code - * @throws ExpectationFailedException - */ - protected function assert_valid_response( - ResponseInterface $response, - ?int $statuscode = 200, - ): void { - $this->assertInstanceOf(Response::class, $response); - $this->assertEquals( - $statuscode, - $response->getStatusCode(), - "Response status code is not $statuscode", - ); - } - - /** - * Assert that the supplied response related to an exception. - * - * @param ResponseInterface $response - * @param null|int $responsecode The expected response code - */ - protected function assert_exception_response( - ResponseInterface $response, - ?int $responsecode = null, - ): void { - $this->assertInstanceOf(Response::class, $response); - $this->assertNotEquals( - 200, - $response->getStatusCode(), - ); - - if ($responsecode !== null) { - $this->assertEquals( - $responsecode, - $response->getStatusCode(), - ); - } - - $payload = $this->decode_response($response); - $this->assertObjectHasProperty('message', $payload); - $this->assertObjectHasProperty('stacktrace', $payload); - } - - /** - * Assert that the supplied response was an invalid_parameter_exception response. - * - * @param ResponseInterface $response - */ - protected function assert_invalid_parameter_response( - ResponseInterface $response, - ): void { - $this->assert_exception_response($response, 400); - - $payload = $this->decode_response($response); - $this->assertObjectHasProperty('errorcode', $payload); - $this->assertEquals('invalidparameter', $payload->errorcode); - } - - /** - * Assert that the supplied response was an access_denied exception response. - * - * @param ResponseInterface $response - */ - protected function assert_access_denied_response( - ResponseInterface $response, - ): void { - $this->assert_exception_response($response, 403); - - $payload = $this->decode_response($response); - $this->assertObjectHasProperty('errorcode', $payload); - } - - /** - * Assert that the supplied response was a not_found exception response. - * - * @param \Psr\Http\Message\ResponseInterface $response - */ - protected function assert_not_found_response( - ResponseInterface $response, - ): void { - $this->assert_exception_response($response, 404); - - $payload = $this->decode_response($response); - $this->assertObjectHasProperty('errorcode', $payload); - } - - /** - * Decode the JSON response for a Response object. - * - * @param ResponseInterface $response - * @param bool $forcearray Force the contents to Array instead of Object - * @return stdClass|array - */ - protected function decode_response( - ResponseInterface $response, - bool $forcearray = false, - ): stdClass|array { - if ($forcearray) { - return json_decode( - json: (string) $response->getBody(), - associative: true, - ); - } else { - return (object) json_decode( - json: (string) $response->getBody(), - associative: false, - flags: JSON_FORCE_OBJECT, - ); - } - } - - /** - * Get the schema for an OpenAPI Component. - * - * Components include headers, parameters, responses, examples, requestBodies, and schemas. - * - * All components are subclasses of the openapi_base class and may be referenced. - * - * Any component which implements the referenced_object interface will return a reference - * to the stored internal object. - * - * @param specification $api - * @param openapi_base $component - * @return stdClass|null - */ - protected function get_api_component_schema( - specification $api, - openapi_base $component, - ): ?stdClass { - $this->assertInstanceOf(referenced_object::class, $component); - - if (is_a($component, \core\router\schema\header_object::class)) { - $type = 'headers'; - } else if (is_a($component, \core\router\schema\parameter::class)) { - $type = 'parameters'; - } else if (is_a($component, \core\router\schema\response\response::class)) { - $type = 'responses'; - } else if (is_a($component, \core\router\schema\example::class)) { - $type = 'examples'; - } else if (is_a($component, \core\router\schema\request_body::class)) { - $type = 'requestBodies'; - } else if (is_a($component, \core\router\schema\objects\type_base::class)) { - $type = 'schemas'; - } else { - $this->fail('Component is not a recognised type'); - } - - $ref = $component->get_reference(false); - - $schema = $api->get_schema(); - $components = $schema->components; - $component = $components->{$type}->{$ref} ?? null; - - return $component; - } -} +class_alias(\core\tests\router\route_testcase::class, \core\tests\route_testcase::class); diff --git a/lib/tests/classes/router/mocking_route_loader.php b/lib/tests/classes/router/mocking_route_loader.php new file mode 100644 index 00000000000..13e3d051847 --- /dev/null +++ b/lib/tests/classes/router/mocking_route_loader.php @@ -0,0 +1,153 @@ +. + +namespace core\tests\router; + +use core\router\abstract_route_loader; +use core\router\route_loader_interface; +use Slim\App; +use Slim\Routing\RouteCollectorProxy; + +/** + * A route loader containing mocked routes. + * + * @package core + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mocking_route_loader extends abstract_route_loader implements route_loader_interface { + /** @var array[] The mocked routes to configure in the loader */ + private array $groupdata = []; + + #[\Override] + public function configure_routes(App $app): array { + $routegroups = []; + + foreach ($this->groupdata as $path => $groupdata) { + $routegroups[$path] = $app->group($path, function ( + RouteCollectorProxy $group, + ) use ( + $groupdata, + ): void { + foreach ($groupdata as $data) { + $group + ->map(...$data['mapdata']) + ->setName($data['name']); + } + }); + } + + return $routegroups; + } + + /** + * Add a mocked route to the loader. + * + * @param string $grouppath The path of the RouteGroup to add the route to + * @param array $methods The HTTP methods to add the route for + * @param string $pattern The path to add the route for + * @param callable $callable The callable to add the route for + * @param string $name The name of the route + */ + public function mock_route_from_callable( + string $grouppath, + array $methods, + string $pattern, + callable $callable, + string $name, + ): void { + $this->add_groupdata( + $grouppath, + [ + 'methods' => $methods, + 'pattern' => $pattern, + 'callable' => $callable, + ], + $name, + ); + } + + /** + * Add all routes in a class to the loader. + * + * @param string $grouppath Thegroup to add the route to + * @param string|\ReflectionMethod $class The class to add to the loader + */ + public function add_all_routes_in_class( + string $grouppath, + \ReflectionMethod|string $class, + ) { + $classinfo = $class instanceof \ReflectionClass ? $class : new \ReflectionClass($class); + + $routes = $this->get_all_routes_in_class( + componentpath: '', + classinfo: $classinfo, + ); + + foreach ($routes as $mapdata) { + $this->add_groupdata( + $grouppath, + $mapdata, + implode('::', $mapdata['callable']), + ); + } + } + + /** + * Mock a route from a class method. + * + * @param string $grouppath The path to add the route to + * @param \ReflectionMethod $method The method to mock the route from + */ + public function mock_route_from_class_method( + string $grouppath, + \ReflectionMethod $method, + ) { + $mapdata = $this->get_route_data_for_method( + componentpath: '', + classinfo: $method->getDeclaringClass(), + methodinfo: $method, + ); + + $this->add_groupdata( + $grouppath, + $mapdata, + implode('::', $mapdata['callable']), + ); + } + + /** + * Add group data to the loader. + * + * @param string $grouppath The path of the RouteGroup to add the data to + * @param array $data The data to add to the group + * @param string $name The name of the group + */ + protected function add_groupdata( + string $grouppath, + array $data, + string $name, + ): void { + if (!array_key_exists($grouppath, $this->groupdata)) { + $this->groupdata[$grouppath] = []; + } + + $this->groupdata[$grouppath][] = [ + 'mapdata' => $data, + 'name' => $name, + ]; + } +} diff --git a/lib/tests/classes/router/route_testcase.php b/lib/tests/classes/router/route_testcase.php new file mode 100644 index 00000000000..04d4334d925 --- /dev/null +++ b/lib/tests/classes/router/route_testcase.php @@ -0,0 +1,546 @@ +. + +namespace core\tests\router; + +use core\router; +use core\router\bridge; +use core\router\route_loader_interface; +use core\router\schema\openapi_base; +use core\router\schema\referenced_object; +use core\router\schema\specification; +use stdClass; +use GuzzleHttp\Psr7\Response; +use GuzzleHttp\Psr7\ServerRequest; +use GuzzleHttp\Psr7\Uri; +use PHPUnit\Framework\ExpectationFailedException; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamInterface; +use Slim\App; +use Slim\Middleware\RoutingMiddleware; +use Slim\Routing\Route; +use Slim\Routing\RouteContext; + +/** + * Tests for user preference API handler. + * + * @package core + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class route_testcase extends \advanced_testcase { + /** + * Update the test route loader using the supplied callback. + * + * @param callable $modifier + */ + protected function update_test_route_loader( + callable $modifier, + ): void { + $routeloader = \core\di::get(mocking_route_loader::class); + $modifier($routeloader); + \core\di::set(route_loader_interface::class, $routeloader); + } + + /** + * Add a route from a class method. + * + * @param string $classname The class to add the route from + * @param string $methodname The method name to add + * @param null|string $grouppath The path to the route group + */ + protected function add_route_to_route_loader( + string $classname, + string $methodname, + ?string $grouppath = null, + ) { + $grouppath = $grouppath ?? $this->guess_group_path_from_classname($classname); + $this->update_test_route_loader(fn (mocking_route_loader $routeloader) => $routeloader->mock_route_from_class_method( + $grouppath, + new \ReflectionMethod($classname, $methodname), + )); + } + + /** + * Add all routes from the specified class to the test loader. + * + * Only methods within the class with a #[route] attribute will be added. + * + * @param string $classname The class to add routes from + * @param null|string $grouppath The path of the route group + */ + protected function add_class_routes_to_route_loader( + string $classname, + ?string $grouppath = null, + ): void { + $this->update_test_route_loader( + fn (mocking_route_loader $routeloader) => $routeloader->add_all_routes_in_class( + grouppath: $grouppath ?? $this->guess_group_path_from_classname($classname), + class: $classname, + ), + ); + } + + /** + * Guess the group path from a class name. + * + * @param string $classname + * @return string + */ + protected function guess_group_path_from_classname( + string $classname, + ): string { + [, , $l3] = explode('\\', $classname, 4); + + if ($l3 === 'api') { + return route_loader_interface::ROUTE_GROUP_API; + } + + throw new \coding_exception("Unable to determine route path for '{$classname}'"); + } + + /** + * Mock a route from a route attribute. + * + * @param string $grouppath + * @param \core\router\route $route + * @param string $name + * @param callable|null $callable + */ + protected function mock_route_from_route_attribute( + string $grouppath, + \core\router\route $route, + string $name = 'route', + ?callable $callable = null, + ): void { + if ($callable === null) { + $callable = fn ($request, $response) => $response->withStatus(200); + } + + $this->update_test_route_loader(fn (mocking_route_loader $routeloader) => $routeloader->mock_route_from_callable( + grouppath: $grouppath, + methods: $route->get_methods(['GET']), + pattern: $route->get_path(), + callable: $callable, + name: $name, + )); + } + + /** + * Get a fully-configured instance of the Moodle Routing Application. + * + * @return App + */ + protected function get_app(): App { + $router = $this->get_router(); + + return $router->get_app(); + } + + /** + * Get a fully-configured instance of the Moodle Routing Application. + * + * @param string $basepath The basepath for the router + * @return router + */ + protected function get_router(string $basepath = ''): router { + \core\di::set( + router::class, + \DI\autowire(router::class)->constructorParameter('basepath', $basepath), + ); + + return \core\di::get(router::class); + } + + /** + * Get an unconfigured instance of the Slim Application. + * + * @return App + */ + protected function get_simple_app(): App { + global $CFG; + require_once("{$CFG->libdir}/nikic/fast-route/src/functions.php"); + $app = bridge::create( + container: \core\di::get_container(), + ); + + return $app; + } + + /** + * Get the request for a route which is known to the router. + * + * @param \core\router\route $route + * @param string $path + * @return ServerRequestInterface + */ + protected function get_request_for_routed_route( + \core\router\route $route, + string $path, + ): ServerRequestInterface { + $this->mock_route_from_route_attribute('', $route); + + // Grab just one method. + $methods = $route->get_methods(); + $method = $methods ? reset($methods) : 'GET'; + + $request = $this->create_request( + method: $method, + path: $path, + prefix: '', + route: $route, + ); + + $request = $this->route_request( + $this->get_app(), + $request, + ); + + return $request; + } + + /** + * Create a Request object. + * + * @param string $method + * @param string $path + * @param string $prefix + * @param array $headers + * @param array $cookies + * @param array $serverparams + * @param null|\core\router\route $route + * @return ServerRequestInterface + */ + protected function create_request( + string $method, + string $path, + string $prefix = route_loader_interface::ROUTE_GROUP_API, + array $headers = ['Content-Type' => 'application/json'], + array $cookies = [], + array $serverparams = [], + ?\core\router\route $route = null, + ): ServerRequestInterface { + $uri = new Uri($prefix . $path); + + $request = new ServerRequest( + method: $method, + headers: $headers, + uri: $uri, + serverParams: $serverparams, + ); + + // Sadly Guzzle's Uri only deals with query strings, not query params. + $query = $uri->getQuery(); + if ($query) { + $queryparams = []; + foreach (explode('&', $query) as $queryparam) { + [$key, $value] = explode('=', $queryparam, 2); + $queryparams[$key] = $value; + } + $request = $request->withQueryParams($queryparams); + } + + if ($route) { + $request = $request->withAttribute(\core\router\route::class, $route); + } + + return $request + ->withCookieParams($cookies); + } + + /** + * Process a request with the app. + * + * @param string $method + * @param string $path + * @param string $prefix + * @param array $headers + * @param null|StreamInterface $body + * @param null|string $contenttype + * @param array $cookies + * @param array $serverparams + * @return ResponseInterface + */ + protected function process_request( + string $method, + string $path, + string $prefix = '', + array $headers = ['HTTP_ACCEPT' => 'application/json'], + ?StreamInterface $body = null, + ?string $contenttype = 'application/json', + array $cookies = [], + array $serverparams = [], + ): ResponseInterface { + $app = $this->get_app(); + if ($contenttype !== null) { + $headers['Content-Type'] = $contenttype; + } + $request = $this->create_request( + $method, + $path, + $prefix, + $headers, + $cookies, + $serverparams, + ); + + if ($body) { + $request = $request->withBody($body); + } + + return $app->handle($request); + } + + /** + * Process a request with the app. + * + * @param string $method + * @param string $path + * @param array $headers + * @param null|StreamInterface $body + * @param array $cookies + * @param array $serverparams + * @return ResponseInterface + */ + protected function process_api_request( + string $method, + string $path, + array $headers = ['HTTP_ACCEPT' => 'application/json'], + ?StreamInterface $body = null, + array $cookies = [], + array $serverparams = [], + ): ResponseInterface { + return $this->process_request( + method: $method, + path: $path, + prefix: route_loader_interface::ROUTE_GROUP_API, + headers: $headers, + body: $body, + cookies: $cookies, + serverparams: $serverparams, + ); + } + + /** + * Route a request within the app. + * + * @param App $app + * @param ServerRequestInterface $request + * @return ServerRequestInterface + */ + protected function route_request( + App $app, + ServerRequestInterface $request, + ): ServerRequestInterface { + $routingmiddleware = new RoutingMiddleware( + $app->getRouteResolver(), + $app->getRouteCollector()->getRouteParser(), + ); + + return $routingmiddleware->performRouting($request); + } + + /** + * Create a route and route it to create a request. + * + * @param string $routepath + * @param string $requestpath + * @return ServerRequestInterface + */ + protected function create_route( + string $routepath, + string $requestpath, + ): ServerRequestInterface { + $app = $this->get_simple_app(); + $app->get($routepath, fn () => new Response()); + $request = $this->route_request($app, new ServerRequest('GET', $requestpath)); + + return $request; + } + + /** + * Get the Slim Route object from a Request object. + * + * @param ServerRequestInterface $request + * @return Route + */ + protected function get_slim_route_from_request( + ServerRequestInterface $request, + ): Route { + return $request->getAttribute(RouteContext::ROUTE); + } + + /** + * Assert that a Response object was valid. + * + * @param ResponseInterface $response + * @param null|int $statuscode The expected status code + * @throws ExpectationFailedException + */ + protected function assert_valid_response( + ResponseInterface $response, + ?int $statuscode = 200, + ): void { + $this->assertInstanceOf(Response::class, $response); + $this->assertEquals( + $statuscode, + $response->getStatusCode(), + "Response status code is not $statuscode", + ); + } + + /** + * Assert that the supplied response related to an exception. + * + * @param ResponseInterface $response + * @param null|int $responsecode The expected response code + */ + protected function assert_exception_response( + ResponseInterface $response, + ?int $responsecode = null, + ): void { + $this->assertInstanceOf(Response::class, $response); + $this->assertNotEquals( + 200, + $response->getStatusCode(), + ); + + if ($responsecode !== null) { + $this->assertEquals( + $responsecode, + $response->getStatusCode(), + ); + } + + $payload = $this->decode_response($response); + $this->assertObjectHasProperty('message', $payload); + $this->assertObjectHasProperty('stacktrace', $payload); + } + + /** + * Assert that the supplied response was an invalid_parameter_exception response. + * + * @param ResponseInterface $response + */ + protected function assert_invalid_parameter_response( + ResponseInterface $response, + ): void { + $this->assert_exception_response($response, 400); + + $payload = $this->decode_response($response); + $this->assertObjectHasProperty('errorcode', $payload); + $this->assertEquals('invalidparameter', $payload->errorcode); + } + + /** + * Assert that the supplied response was an access_denied exception response. + * + * @param ResponseInterface $response + */ + protected function assert_access_denied_response( + ResponseInterface $response, + ): void { + $this->assert_exception_response($response, 403); + + $payload = $this->decode_response($response); + $this->assertObjectHasProperty('errorcode', $payload); + } + + /** + * Assert that the supplied response was a not_found exception response. + * + * @param \Psr\Http\Message\ResponseInterface $response + */ + protected function assert_not_found_response( + ResponseInterface $response, + ): void { + $this->assert_exception_response($response, 404); + + $payload = $this->decode_response($response); + $this->assertObjectHasProperty('errorcode', $payload); + } + + /** + * Decode the JSON response for a Response object. + * + * @param ResponseInterface $response + * @param bool $forcearray Force the contents to Array instead of Object + * @return stdClass|array + */ + protected function decode_response( + ResponseInterface $response, + bool $forcearray = false, + ): stdClass|array { + if ($forcearray) { + return json_decode( + json: (string) $response->getBody(), + associative: true, + ); + } else { + return (object) json_decode( + json: (string) $response->getBody(), + associative: false, + flags: JSON_FORCE_OBJECT, + ); + } + } + + /** + * Get the schema for an OpenAPI Component. + * + * Components include headers, parameters, responses, examples, requestBodies, and schemas. + * + * All components are subclasses of the openapi_base class and may be referenced. + * + * Any component which implements the referenced_object interface will return a reference + * to the stored internal object. + * + * @param specification $api + * @param openapi_base $component + * @return stdClass|null + */ + protected function get_api_component_schema( + specification $api, + openapi_base $component, + ): ?stdClass { + $this->assertInstanceOf(referenced_object::class, $component); + + if (is_a($component, \core\router\schema\header_object::class)) { + $type = 'headers'; + } else if (is_a($component, \core\router\schema\parameter::class)) { + $type = 'parameters'; + } else if (is_a($component, \core\router\schema\response\response::class)) { + $type = 'responses'; + } else if (is_a($component, \core\router\schema\example::class)) { + $type = 'examples'; + } else if (is_a($component, \core\router\schema\request_body::class)) { + $type = 'requestBodies'; + } else if (is_a($component, \core\router\schema\objects\type_base::class)) { + $type = 'schemas'; + } else { + $this->fail('Component is not a recognised type'); + } + + $ref = $component->get_reference(false); + + $schema = $api->get_schema(); + $components = $schema->components; + $component = $components->{$type}->{$ref} ?? null; + + return $component; + } +} diff --git a/lib/tests/fixtures/router/mocking_route_loader.php b/lib/tests/fixtures/router/mocking_route_loader.php index 7ac516b078e..fb3decf12ad 100644 --- a/lib/tests/fixtures/router/mocking_route_loader.php +++ b/lib/tests/fixtures/router/mocking_route_loader.php @@ -14,137 +14,13 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -namespace core\router; -use Slim\App; -use Slim\Routing\RouteCollectorProxy; - /** - * A route loader containing mocked routes. + * Helper for mocking routes. + * + * Note: This file has been deprecated and will be removed in Moodle 6.0. * * @package core - * @copyright 2024 Andrew Lyons + * @copyright Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class mocking_route_loader extends abstract_route_loader implements route_loader_interface { - /** @var array[] The mocked routes to configure in the loader */ - private array $groupdata = []; - - #[\Override] - public function configure_routes(App $app): array { - $routegroups = []; - - foreach ($this->groupdata as $path => $groupdata) { - $routegroups[$path] = $app->group($path, function ( - RouteCollectorProxy $group, - ) use ( - $groupdata, - ): void { - foreach ($groupdata as $data) { - $group - ->map(...$data['mapdata']) - ->setName($data['name']); - } - }); - } - - return $routegroups; - } - - /** - * Add a mocked route to the loader. - * - * @param string $grouppath The path of the RouteGroup to add the route to - * @param array $methods The HTTP methods to add the route for - * @param string $pattern The path to add the route for - * @param callable $callable The callable to add the route for - * @param string $name The name of the route - */ - public function mock_route_from_callable( - string $grouppath, - array $methods, - string $pattern, - callable $callable, - string $name, - ): void { - $this->add_groupdata( - $grouppath, - [ - 'methods' => $methods, - 'pattern' => $pattern, - 'callable' => $callable, - ], - $name, - ); - } - - /** - * Add all routes in a class to the loader. - * - * @param string $grouppath Thegroup to add the route to - * @param string|\ReflectionMethod $class The class to add to the loader - */ - public function add_all_routes_in_class( - string $grouppath, - \ReflectionMethod|string $class, - ) { - $classinfo = $class instanceof \ReflectionClass ? $class : new \ReflectionClass($class); - - $routes = $this->get_all_routes_in_class( - componentpath: '', - classinfo: $classinfo, - ); - - foreach ($routes as $mapdata) { - $this->add_groupdata( - $grouppath, - $mapdata, - implode('::', $mapdata['callable']), - ); - } - } - - /** - * Mock a route from a class method. - * - * @param string $grouppath The path to add the route to - * @param \ReflectionMethod $method The method to mock the route from - */ - public function mock_route_from_class_method( - string $grouppath, - \ReflectionMethod $method, - ) { - $mapdata = $this->get_route_data_for_method( - componentpath: '', - classinfo: $method->getDeclaringClass(), - methodinfo: $method, - ); - - $this->add_groupdata( - $grouppath, - $mapdata, - implode('::', $mapdata['callable']), - ); - } - - /** - * Add group data to the loader. - * - * @param string $grouppath The path of the RouteGroup to add the data to - * @param array $data The data to add to the group - * @param string $name The name of the group - */ - protected function add_groupdata( - string $grouppath, - array $data, - string $name, - ): void { - if (!array_key_exists($grouppath, $this->groupdata)) { - $this->groupdata[$grouppath] = []; - } - - $this->groupdata[$grouppath][] = [ - 'mapdata' => $data, - 'name' => $name, - ]; - } -} +class_alias(\core\tests\router\mocking_route_loader::class, \core\router\mocking_route_loader::class);