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);