. namespace core\router; use core\param; use core\router\schema\parameters\path_parameter; use core\router\schema\parameters\query_parameter; use core\router\schema\request_body; use core\router\schema\response\content\payload_response_type; use core\tests\router\route_testcase; use GuzzleHttp\Psr7\ServerRequest; use Psr\Http\Message\ServerRequestInterface; use Slim\Exception\HttpNotFoundException; /** * Tests for the request validator. * * @package core * @category test * @copyright Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core\router\request_validator */ final class request_validator_test extends route_testcase { /** * Request validation on a route which does not have a matching Moodle route attribute. */ public function test_validate_request_not_moodle_route(): void { $request = new ServerRequest('GET', '/example'); $validator = \core\di::get(request_validator::class); $this->assertInstanceOf( ServerRequestInterface::class, $validator->validate_request($request), ); } /** * A basic test of request validation. */ public function test_validate_request(): void { // The route being tested. $route = new route( path: '/example/{required}', pathtypes: [ new path_parameter( name: 'required', type: param::INT, ), ], ); $request = $this->get_request_for_routed_route($route, '/example/123'); $validator = \core\di::get(request_validator::class); $this->assertInstanceOf( ServerRequestInterface::class, $validator->validate_request($request), ); } /** * A basic test of request validation. */ public function test_validate_request_missing_pathtype(): void { // A route with a parameter defined in the path, but no pathtype for it. $route = new route( path: '/example/{required}', ); $request = $this->get_request_for_routed_route($route, '/example/123'); $validator = \core\di::get(request_validator::class); $this->assertInstanceOf( ServerRequestInterface::class, $validator->validate_request($request), ); } /** * When a pathtype fails to validate, it will result in an HttpNotFoundException. */ public function test_validate_request_invalid_path_component(): void { // Most of the path types are converted to regexes and will lead to a 404 before they get this far. $type = param::INT; $this->assertEmpty( $type->get_clientside_expression(), 'This test requires a type with no clientside expression. Please update the test.', ); $route = new route( path: '/example/{required}', pathtypes: [ new path_parameter( name: 'required', type: $type, ), ], ); $request = $this->get_request_for_routed_route($route, '/example/abc'); $validator = \core\di::get(request_validator::class); $this->expectException(HttpNotFoundException::class); $validator->validate_request($request); } /** * When a pathtype fails to validate, it will result in an HttpNotFoundException. */ public function test_validate_request_invalid_path_component_native(): void { // Most of the path types are converted to regexes and will lead to a 404 before they get this far. $type = param::ALPHA; $this->assertNotEmpty( $type->get_clientside_expression(), 'This test requires a type with clientside expression. Please update the test.', ); $route = new route( path: '/example/{required}', pathtypes: [ new path_parameter( name: 'required', type: $type, ), ], ); // A value which does not meet the param validation. $request = $this->get_request_for_routed_route($route, '/example/123'); $validator = \core\di::get(request_validator::class); $this->expectException(HttpNotFoundException::class); $validator->validate_request($request); } /** * Query parameter validation. */ public function test_validate_request_query_parameter_valid(): void { $type = param::INT; $value = 123; $route = new route( path: '/example', queryparams: [ new query_parameter( name: 'required', type: $type, ), ], ); $request = $this->get_request_for_routed_route($route, "/example?required={$value}"); $this->assertEquals($value, $request->getQueryParams()['required']); // Validate the request. $validator = \core\di::get(request_validator::class); $validatedrequest = $validator->validate_request($request); $this->assertInstanceOf(ServerRequestInterface::class, $validatedrequest); $this->assertEquals($value, $validatedrequest->getQueryParams()['required']); } /** * Query parameter validation failure. */ public function test_validate_request_query_parameter_invalid(): void { $type = param::INT; $value = 'abc'; $route = new route( path: '/example', queryparams: [ new query_parameter( name: 'required', type: $type, ), ], ); $request = $this->get_request_for_routed_route($route, "/example?required={$value}"); $this->assertEquals($value, $request->getQueryParams()['required']); // Validate the request. $validator = \core\di::get(request_validator::class); $this->expectException(\invalid_parameter_exception::class); $validator->validate_request($request); } /** * Validate a request body which is expected. */ public function test_validate_request_body_valid(): void { $route = new route( path: '/example', requestbody: new request_body( content: new payload_response_type( schema: new \core\router\schema\objects\schema_object( content: [ 'preferences' => new \core\router\schema\objects\array_of_strings( keyparamtype: param::TEXT, valueparamtype: param::INT, ), ], ), ), ), ); $request = $this->get_request_for_routed_route($route, "/example"); $request = $request->withParsedBody([ 'preferences' => [ 'key' => 42, ], ]); // Validate the request. $validator = \core\di::get(request_validator::class); $result = $validator->validate_request($request); } /** * Validate a request body which is expected. */ public function test_validate_request_body_invalid(): void { $route = new route( path: '/example', requestbody: new request_body( content: new payload_response_type( schema: new \core\router\schema\objects\schema_object( content: [ 'preferences' => new \core\router\schema\objects\array_of_strings( keyparamtype: param::TEXT, valueparamtype: param::INT, ), ], ), ), ), ); $request = $this->get_request_for_routed_route($route, "/example"); $request = $request->withParsedBody([ 'preferences' => [ 'key' => 'value', ], ]); // Validate the request. $validator = \core\di::get(request_validator::class); $this->expectException(\invalid_parameter_exception::class); $validator->validate_request($request); } /** * Validate a request body which is optional. */ public function test_validate_request_body_missing_optional(): void { $route = new route( path: '/example', requestbody: new request_body( content: new payload_response_type( schema: new \core\router\schema\objects\schema_object( content: [ 'preferences' => new \core\router\schema\objects\array_of_strings( keyparamtype: param::TEXT, valueparamtype: param::INT, ), ], required: false, ), ), ), ); $request = $this->get_request_for_routed_route($route, "/example"); // Validate the request. $validator = \core\di::get(request_validator::class); $result = $validator->validate_request($request); $this->assertInstanceOf(ServerRequestInterface::class, $result); $this->assertInstanceOf( ServerRequestInterface::class, $result, ); } /** * Validate a request body which is expected. */ public function test_validate_request_body_missing_required(): void { $route = new route( path: '/example', requestbody: new request_body( content: new payload_response_type( schema: new \core\router\schema\objects\schema_object( content: [ 'preferences' => new \core\router\schema\objects\array_of_strings( keyparamtype: param::TEXT, valueparamtype: param::INT, ), ], ), ), required: true, ), ); $request = $this->get_request_for_routed_route($route, "/example"); // Validate the request. $validator = \core\di::get(request_validator::class); $this->expectException(\invalid_parameter_exception::class); $validator->validate_request($request); } /** * Validate a request header is appropriately handled. */ public function test_validate_request_header_valid(): void { $route = new route( path: '/example', headerparams: [ new \core\router\schema\parameters\header_object( name: 'Accept', description: 'The media type of the response', type: param::TEXT, ), new \core\router\schema\parameters\header_object( name: 'X-Multiple', description: 'A header with multiple values', type: param::TEXT, multiple: true, ), ], ); $request = $this->get_request_for_routed_route($route, "/example") // A known header. ->withHeader('Accept', 'application/json') // An unknown header is kept. ->withHeader('X-Example', 'example') // A known header with multiple values. ->withAddedHeader('X-Multiple', 'value1') ->withAddedHeader('X-Multiple', 'value2') // An unknown header with multiple values. ->withAddedHeader('X-Unknown', 'value1') ->withAddedHeader('X-Unknown', 'value2'); // Validate the request. $validator = \core\di::get(request_validator::class); $result = $validator->validate_request($request); $this->assertInstanceOf(ServerRequestInterface::class, $result); $this->assertEquals('application/json', $result->getHeaderLine('Accept')); $this->assertEquals('example', $result->getHeaderLine('X-Example')); $this->assertEquals(['value1', 'value2'], $result->getHeader('X-Multiple')); $this->assertEquals(['value1', 'value2'], $result->getHeader('X-Unknown')); } }