Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace core\tests\router;
18
 
19
use core\router;
20
use core\router\bridge;
21
use core\router\route_loader_interface;
22
use core\router\schema\openapi_base;
23
use core\router\schema\referenced_object;
24
use core\router\schema\specification;
25
use stdClass;
26
use GuzzleHttp\Psr7\Response;
27
use GuzzleHttp\Psr7\ServerRequest;
28
use GuzzleHttp\Psr7\Uri;
29
use PHPUnit\Framework\ExpectationFailedException;
30
use Psr\Http\Message\ResponseInterface;
31
use Psr\Http\Message\ServerRequestInterface;
32
use Psr\Http\Message\StreamInterface;
33
use Slim\App;
34
use Slim\Middleware\RoutingMiddleware;
35
use Slim\Routing\Route;
36
use Slim\Routing\RouteContext;
37
 
38
/**
39
 * Tests for user preference API handler.
40
 *
41
 * @package    core
42
 * @copyright  Andrew Lyons <andrew@nicols.co.uk>
43
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44
 */
45
abstract class route_testcase extends \advanced_testcase {
46
    /**
47
     * Update the test route loader using the supplied callback.
48
     *
49
     * @param callable $modifier
50
     */
51
    protected function update_test_route_loader(
52
        callable $modifier,
53
    ): void {
54
        $routeloader = \core\di::get(mocking_route_loader::class);
55
        $modifier($routeloader);
56
        \core\di::set(route_loader_interface::class, $routeloader);
57
    }
58
 
59
    /**
60
     * Add a route from a class method.
61
     *
62
     * @param string $classname The class to add the route from
63
     * @param string $methodname The method name to add
64
     * @param null|string $grouppath The path to the route group
65
     */
66
    protected function add_route_to_route_loader(
67
        string $classname,
68
        string $methodname,
69
        ?string $grouppath = null,
70
    ) {
71
        $grouppath = $grouppath ?? $this->guess_group_path_from_classname($classname);
72
        $this->update_test_route_loader(fn (mocking_route_loader $routeloader) => $routeloader->mock_route_from_class_method(
73
            $grouppath,
74
            new \ReflectionMethod($classname, $methodname),
75
        ));
76
    }
77
 
78
    /**
79
     * Add all routes from the specified class to the test loader.
80
     *
81
     * Only methods within the class with a #[route] attribute will be added.
82
     *
83
     * @param string $classname The class to add routes from
84
     * @param null|string $grouppath The path of the route group
85
     */
86
    protected function add_class_routes_to_route_loader(
87
        string $classname,
88
        ?string $grouppath = null,
89
    ): void {
90
        $this->update_test_route_loader(
91
            fn (mocking_route_loader $routeloader) => $routeloader->add_all_routes_in_class(
92
                grouppath: $grouppath ?? $this->guess_group_path_from_classname($classname),
93
                class: $classname,
94
            ),
95
        );
96
    }
97
 
98
    /**
99
     * Guess the group path from a class name.
100
     *
101
     * @param string $classname
102
     * @return string
103
     */
104
    protected function guess_group_path_from_classname(
105
        string $classname,
106
    ): string {
107
        [, , $l3] = explode('\\', $classname, 4);
108
 
109
        if ($l3 === 'api') {
110
            return route_loader_interface::ROUTE_GROUP_API;
111
        }
112
 
113
        throw new \coding_exception("Unable to determine route path for '{$classname}'");
114
    }
115
 
116
    /**
117
     * Mock a route from a route attribute.
118
     *
119
     * @param string $grouppath
120
     * @param \core\router\route $route
121
     * @param string $name
122
     * @param callable|null $callable
123
     */
124
    protected function mock_route_from_route_attribute(
125
        string $grouppath,
126
        \core\router\route $route,
127
        string $name = 'route',
128
        ?callable $callable = null,
129
    ): void {
130
        if ($callable === null) {
131
            $callable = fn ($request, $response) => $response->withStatus(200);
132
        }
133
 
134
        $this->update_test_route_loader(fn (mocking_route_loader $routeloader) => $routeloader->mock_route_from_callable(
135
            grouppath: $grouppath,
136
            methods: $route->get_methods(['GET']),
137
            pattern: $route->get_path(),
138
            callable: $callable,
139
            name: $name,
140
        ));
141
    }
142
 
143
    /**
144
     * Get a fully-configured instance of the Moodle Routing Application.
145
     *
146
     * @return App
147
     */
148
    protected function get_app(): App {
149
        $router = $this->get_router();
150
 
151
        return $router->get_app();
152
    }
153
 
154
    /**
155
     * Get a fully-configured instance of the Moodle Routing Application.
156
     *
157
     * @param string $basepath The basepath for the router
158
     * @return router
159
     */
160
    protected function get_router(string $basepath = ''): router {
161
        \core\di::set(
162
            router::class,
163
            \DI\autowire(router::class)->constructorParameter('basepath', $basepath),
164
        );
165
 
166
        return \core\di::get(router::class);
167
    }
168
 
169
    /**
170
     * Get an unconfigured instance of the Slim Application.
171
     *
172
     * @return App
173
     */
174
    protected function get_simple_app(): App {
175
        return bridge::create(
176
            container: \core\di::get_container(),
177
        );
178
    }
179
 
180
    /**
181
     * Get the request for a route which is known to the router.
182
     *
183
     * @param \core\router\route $route
184
     * @param string $path
185
     * @return ServerRequestInterface
186
     */
187
    protected function get_request_for_routed_route(
188
        \core\router\route $route,
189
        string $path,
190
    ): ServerRequestInterface {
191
        $this->mock_route_from_route_attribute('', $route);
192
 
193
        // Grab just one method.
194
        $methods = $route->get_methods();
195
        $method = $methods ? reset($methods) : 'GET';
196
 
197
        $request = $this->create_request(
198
            method: $method,
199
            path: $path,
200
            prefix: '',
201
            route: $route,
202
        );
203
 
204
        $request = $this->route_request(
205
            $this->get_app(),
206
            $request,
207
        );
208
 
209
        return $request;
210
    }
211
 
212
    /**
213
     * Create a Request object.
214
     *
215
     * @param string $method
216
     * @param string $path
217
     * @param string $prefix
218
     * @param array  $headers
219
     * @param array  $cookies
220
     * @param array  $serverparams
221
     * @param null|\core\router\route $route
222
     * @return ServerRequestInterface
223
     */
224
    protected function create_request(
225
        string $method,
226
        string $path,
227
        string $prefix = route_loader_interface::ROUTE_GROUP_API,
228
        array $headers = ['Content-Type' => 'application/json'],
229
        array $cookies = [],
230
        array $serverparams = [],
231
        ?\core\router\route $route = null,
232
    ): ServerRequestInterface {
233
        $uri = new Uri($prefix . $path);
234
 
235
        $request = new ServerRequest(
236
            method: $method,
237
            uri: $uri,
238
            headers: $headers,
239
            serverParams: $serverparams,
240
        );
241
 
242
        // Sadly Guzzle's Uri only deals with query strings, not query params.
243
        $query = $uri->getQuery();
244
        if ($query) {
245
            $queryparams = [];
246
            foreach (explode('&', $query) as $queryparam) {
247
                [$key, $value] = explode('=', $queryparam, 2);
248
                $queryparams[$key] = $value;
249
            }
250
            $request = $request->withQueryParams($queryparams);
251
        }
252
 
253
        if ($route) {
254
            $request = $request->withAttribute(\core\router\route::class, $route);
255
        }
256
 
257
        return $request
258
            ->withCookieParams($cookies);
259
    }
260
 
261
    /**
262
     * Process a request with the app.
263
     *
264
     * @param string $method
265
     * @param string $path
266
     * @param string $prefix
267
     * @param array  $headers
268
     * @param null|StreamInterface $body
269
     * @param null|string $contenttype
270
     * @param array  $cookies
271
     * @param array  $serverparams
272
     * @return ResponseInterface
273
     */
274
    protected function process_request(
275
        string $method,
276
        string $path,
277
        string $prefix = '',
278
        array $headers = ['HTTP_ACCEPT' => 'application/json'],
279
        ?StreamInterface $body = null,
280
        ?string $contenttype = 'application/json',
281
        array $cookies = [],
282
        array $serverparams = [],
283
    ): ResponseInterface {
284
        $app = $this->get_app();
285
        if ($contenttype !== null) {
286
            $headers['Content-Type'] = $contenttype;
287
        }
288
        $request = $this->create_request(
289
            $method,
290
            $path,
291
            $prefix,
292
            $headers,
293
            $cookies,
294
            $serverparams,
295
        );
296
 
297
        if ($body) {
298
            $request = $request->withBody($body);
299
        }
300
 
301
        return $app->handle($request);
302
    }
303
 
304
    /**
305
     * Process a request with the app.
306
     *
307
     * @param string $method
308
     * @param string $path
309
     * @param array  $headers
310
     * @param null|StreamInterface $body
311
     * @param array  $cookies
312
     * @param array  $serverparams
313
     * @return ResponseInterface
314
     */
315
    protected function process_api_request(
316
        string $method,
317
        string $path,
318
        array $headers = ['HTTP_ACCEPT' => 'application/json'],
319
        ?StreamInterface $body = null,
320
        array $cookies = [],
321
        array $serverparams = [],
322
    ): ResponseInterface {
323
        return $this->process_request(
324
            method: $method,
325
            path: $path,
326
            prefix: route_loader_interface::ROUTE_GROUP_API,
327
            headers: $headers,
328
            body: $body,
329
            cookies: $cookies,
330
            serverparams: $serverparams,
331
        );
332
    }
333
 
334
    /**
335
     * Route a request within the app.
336
     *
337
     * @param App $app
338
     * @param ServerRequestInterface $request
339
     * @return ServerRequestInterface
340
     */
341
    protected function route_request(
342
        App $app,
343
        ServerRequestInterface $request,
344
    ): ServerRequestInterface {
345
        $routingmiddleware = new RoutingMiddleware(
346
            $app->getRouteResolver(),
347
            $app->getRouteCollector()->getRouteParser(),
348
        );
349
 
350
        return $routingmiddleware->performRouting($request);
351
    }
352
 
353
    /**
354
     * Create a route and route it to create a request.
355
     *
356
     * @param string $routepath
357
     * @param string $requestpath
358
     * @return ServerRequestInterface
359
     */
360
    protected function create_route(
361
        string $routepath,
362
        string $requestpath,
363
    ): ServerRequestInterface {
364
        $app = $this->get_simple_app();
365
        $app->get($routepath, fn () => new Response());
366
        $request = $this->route_request($app, new ServerRequest('GET', $requestpath));
367
 
368
        return $request;
369
    }
370
 
371
    /**
372
     * Get the Slim Route object from a Request object.
373
     *
374
     * @param ServerRequestInterface $request
375
     * @return Route
376
     */
377
    protected function get_slim_route_from_request(
378
        ServerRequestInterface $request,
379
    ): Route {
380
        return $request->getAttribute(RouteContext::ROUTE);
381
    }
382
 
383
    /**
384
     * Assert that a Response object was valid.
385
     *
386
     * @param ResponseInterface $response
387
     * @param null|int $statuscode The expected status code
388
     * @throws ExpectationFailedException
389
     */
390
    protected function assert_valid_response(
391
        ResponseInterface $response,
392
        ?int $statuscode = 200,
393
    ): void {
394
        $this->assertInstanceOf(Response::class, $response);
395
        $this->assertEquals(
396
            $statuscode,
397
            $response->getStatusCode(),
398
            "Response status code is not $statuscode",
399
        );
400
    }
401
 
402
    /**
403
     * Assert that the supplied response related to an exception.
404
     *
405
     * @param ResponseInterface $response
406
     * @param null|int $responsecode The expected response code
407
     */
408
    protected function assert_exception_response(
409
        ResponseInterface $response,
410
        ?int $responsecode = null,
411
    ): void {
412
        $this->assertInstanceOf(Response::class, $response);
413
        $this->assertNotEquals(
414
            200,
415
            $response->getStatusCode(),
416
        );
417
 
418
        if ($responsecode !== null) {
419
            $this->assertEquals(
420
                $responsecode,
421
                $response->getStatusCode(),
422
            );
423
        }
424
 
425
        $payload = $this->decode_response($response);
426
        $this->assertObjectHasProperty('message', $payload);
427
        $this->assertObjectHasProperty('stacktrace', $payload);
428
        foreach ($payload->stacktrace as $frame) {
429
            $this->assertObjectNotHasProperty('args', $frame);
430
        }
431
    }
432
 
433
    /**
434
     * Assert that the supplied response was an invalid_parameter_exception response.
435
     *
436
     * @param ResponseInterface $response
437
     */
438
    protected function assert_invalid_parameter_response(
439
        ResponseInterface $response,
440
    ): void {
441
        $this->assert_exception_response($response, 400);
442
 
443
        $payload = $this->decode_response($response);
444
        $this->assertObjectHasProperty('errorcode', $payload);
445
        $this->assertEquals('invalidparameter', $payload->errorcode);
446
    }
447
 
448
    /**
449
     * Assert that the supplied response was an access_denied exception response.
450
     *
451
     * @param ResponseInterface $response
452
     */
453
    protected function assert_access_denied_response(
454
        ResponseInterface $response,
455
    ): void {
456
        $this->assert_exception_response($response, 403);
457
 
458
        $payload = $this->decode_response($response);
459
        $this->assertObjectHasProperty('errorcode', $payload);
460
    }
461
 
462
    /**
463
     * Assert that the supplied response was a not_found exception response.
464
     *
465
     * @param \Psr\Http\Message\ResponseInterface $response
466
     */
467
    protected function assert_not_found_response(
468
        ResponseInterface $response,
469
    ): void {
470
        $this->assert_exception_response($response, 404);
471
 
472
        $payload = $this->decode_response($response);
473
        $this->assertObjectHasProperty('errorcode', $payload);
474
    }
475
 
476
    /**
477
     * Decode the JSON response for a Response object.
478
     *
479
     * @param ResponseInterface $response
480
     * @param bool $forcearray Force the contents to Array instead of Object
481
     * @return stdClass|array
482
     */
483
    protected function decode_response(
484
        ResponseInterface $response,
485
        bool $forcearray = false,
486
    ): stdClass|array {
487
        if ($forcearray) {
488
            return json_decode(
489
                json: (string) $response->getBody(),
490
                associative: true,
491
            );
492
        } else {
493
            return (object) json_decode(
494
                json: (string) $response->getBody(),
495
                associative: false,
496
                flags: JSON_FORCE_OBJECT,
497
            );
498
        }
499
    }
500
 
501
    /**
502
     * Get the schema for an OpenAPI Component.
503
     *
504
     * Components include headers, parameters, responses, examples, requestBodies, and schemas.
505
     *
506
     * All components are subclasses of the openapi_base class and may be referenced.
507
     *
508
     * Any component which implements the referenced_object interface will return a reference
509
     * to the stored internal object.
510
     *
511
     * @param specification $api
512
     * @param openapi_base $component
513
     * @return stdClass|null
514
     */
515
    protected function get_api_component_schema(
516
        specification $api,
517
        openapi_base $component,
518
    ): ?stdClass {
519
        $this->assertInstanceOf(referenced_object::class, $component);
520
 
521
        if (is_a($component, \core\router\schema\header_object::class)) {
522
            $type = 'headers';
523
        } else if (is_a($component, \core\router\schema\parameter::class)) {
524
            $type = 'parameters';
525
        } else if (is_a($component, \core\router\schema\response\response::class)) {
526
            $type = 'responses';
527
        } else if (is_a($component, \core\router\schema\example::class)) {
528
            $type = 'examples';
529
        } else if (is_a($component, \core\router\schema\request_body::class)) {
530
            $type = 'requestBodies';
531
        } else if (is_a($component, \core\router\schema\objects\type_base::class)) {
532
            $type = 'schemas';
533
        } else {
534
            $this->fail('Component is not a recognised type');
535
        }
536
 
537
        $ref = $component->get_reference(false);
538
 
539
        $schema = $api->get_schema();
540
        $components = $schema->components;
541
        $component = $components->{$type}->{$ref} ?? null;
542
 
543
        return $component;
544
    }
545
}