From ebfbd2d42a57e6cf87feda0d46b0c1bd2c00c2c5 Mon Sep 17 00:00:00 2001 From: Brad Jorsch Date: Wed, 12 Jun 2019 15:51:59 -0400 Subject: [PATCH] rest: Use ParamValidator library, add BodyValidator Parameter validation is based on parameter definitions like those in the Action API, using the new ParamValidator library. Handlers should use the provided Handler methods to access parameters rather than fetching them directly from the RequestInterface. Body validation allows the handler to have the (non-form-data) body of a request parsed and validated. The only validator included in this patch ignores the body entirely; future patches may implement validation for JSON bodies based on JSON schemas, or the like. Bug: T223239 Change-Id: I3c37ea2b432840514b6bff90007c8403989225d5 --- includes/Rest/EntryPoint.php | 9 +- includes/Rest/Handler.php | 73 ++++++++ includes/Rest/Handler/HelloHandler.php | 11 ++ includes/Rest/HttpException.php | 14 +- includes/Rest/ResponseFactory.php | 9 +- includes/Rest/Router.php | 20 ++- includes/Rest/SimpleHandler.php | 21 ++- includes/Rest/Validator/BodyValidator.php | 26 +++ includes/Rest/Validator/NullBodyValidator.php | 16 ++ .../Validator/ParamValidatorCallbacks.php | 82 +++++++++ includes/Rest/Validator/Validator.php | 163 ++++++++++++++++++ .../MWBasicRequestAuthorizerTest.php | 22 ++- .../phpunit/includes/Rest/EntryPointTest.php | 26 ++- .../Rest/Handler/HelloHandlerTest.php | 15 +- .../phpunit/unit/includes/Rest/RouterTest.php | 25 ++- 15 files changed, 504 insertions(+), 28 deletions(-) create mode 100644 includes/Rest/Validator/BodyValidator.php create mode 100644 includes/Rest/Validator/NullBodyValidator.php create mode 100644 includes/Rest/Validator/ParamValidatorCallbacks.php create mode 100644 includes/Rest/Validator/Validator.php diff --git a/includes/Rest/EntryPoint.php b/includes/Rest/EntryPoint.php index f28b4ea80c..ee3441e595 100644 --- a/includes/Rest/EntryPoint.php +++ b/includes/Rest/EntryPoint.php @@ -6,6 +6,7 @@ use ExtensionRegistry; use MediaWiki; use MediaWiki\MediaWikiServices; use MediaWiki\Rest\BasicAccess\MWBasicAuthorizer; +use MediaWiki\Rest\Validator\Validator; use RequestContext; use Title; use WebResponse; @@ -36,6 +37,7 @@ class EntryPoint { $services = MediaWikiServices::getInstance(); $conf = $services->getMainConfig(); + $objectFactory = $services->getObjectFactory(); if ( !$conf->get( 'EnableRestAPI' ) ) { wfHttpError( 403, 'Access Denied', @@ -51,6 +53,9 @@ class EntryPoint { $authorizer = new MWBasicAuthorizer( $context->getUser(), $services->getPermissionManager() ); + // @phan-suppress-next-line PhanAccessMethodInternal + $restValidator = new Validator( $objectFactory, $request, RequestContext::getMain()->getUser() ); + global $IP; $router = new Router( [ "$IP/includes/Rest/coreRoutes.json" ], @@ -58,7 +63,9 @@ class EntryPoint { $conf->get( 'RestPath' ), $services->getLocalServerObjectCache(), new ResponseFactory, - $authorizer + $authorizer, + $objectFactory, + $restValidator ); $entryPoint = new self( diff --git a/includes/Rest/Handler.php b/includes/Rest/Handler.php index c05d8e774a..efe2b7e9e2 100644 --- a/includes/Rest/Handler.php +++ b/includes/Rest/Handler.php @@ -2,7 +2,18 @@ namespace MediaWiki\Rest; +use MediaWiki\Rest\Validator\BodyValidator; +use MediaWiki\Rest\Validator\NullBodyValidator; +use MediaWiki\Rest\Validator\Validator; + abstract class Handler { + + /** + * (string) ParamValidator constant to specify the source of the parameter. + * Value must be 'path', 'query', or 'post'. + */ + const PARAM_SOURCE = 'rest-param-source'; + /** @var Router */ private $router; @@ -15,6 +26,12 @@ abstract class Handler { /** @var ResponseFactory */ private $responseFactory; + /** @var array|null */ + private $validatedParams; + + /** @var mixed */ + private $validatedBody; + /** * Initialise with dependencies from the Router. This is called after construction. * @internal @@ -68,6 +85,62 @@ abstract class Handler { return $this->responseFactory; } + /** + * Validate the request parameters/attributes and body. If there is a validation + * failure, a response with an error message should be returned or an + * HttpException should be thrown. + * + * @param Validator $restValidator + * @throws HttpException On validation failure. + */ + public function validate( Validator $restValidator ) { + $validatedParams = $restValidator->validateParams( $this->getParamSettings() ); + $validatedBody = $restValidator->validateBody( $this->request, $this ); + $this->validatedParams = $validatedParams; + $this->validatedBody = $validatedBody; + } + + /** + * Fetch ParamValidator settings for parameters + * + * Every setting must include self::PARAM_SOURCE to specify which part of + * the request is to contain the parameter. + * + * @return array[] Associative array mapping parameter names to + * ParamValidator settings arrays + */ + public function getParamSettings() { + return []; + } + + /** + * Fetch the BodyValidator + * @param string $contentType Content type of the request. + * @return BodyValidator + */ + public function getBodyValidator( $contentType ) { + return new NullBodyValidator(); + } + + /** + * Fetch the validated parameters + * + * @return array|null Array mapping parameter names to validated values, + * or null if validateParams() was not called yet or validation failed. + */ + public function getValidatedParams() { + return $this->validatedParams; + } + + /** + * Fetch the validated body + * @return mixed Value returned by the body validator, or null if validateParams() was + * not called yet, validation failed, there was no body, or the body was form data. + */ + public function getValidatedBody() { + return $this->validatedBody; + } + /** * The subclass should override this to provide the maximum last modified * timestamp for the current request. This is called before execute() in diff --git a/includes/Rest/Handler/HelloHandler.php b/includes/Rest/Handler/HelloHandler.php index 34faee26d3..495b10139a 100644 --- a/includes/Rest/Handler/HelloHandler.php +++ b/includes/Rest/Handler/HelloHandler.php @@ -2,6 +2,7 @@ namespace MediaWiki\Rest\Handler; +use Wikimedia\ParamValidator\ParamValidator; use MediaWiki\Rest\SimpleHandler; /** @@ -16,4 +17,14 @@ class HelloHandler extends SimpleHandler { public function needsWriteAccess() { return false; } + + public function getParamSettings() { + return [ + 'name' => [ + self::PARAM_SOURCE => 'path', + ParamValidator::PARAM_TYPE => 'string', + ParamValidator::PARAM_REQUIRED => true, + ], + ]; + } } diff --git a/includes/Rest/HttpException.php b/includes/Rest/HttpException.php index ae6dde2b3f..bcc414fdf1 100644 --- a/includes/Rest/HttpException.php +++ b/includes/Rest/HttpException.php @@ -8,7 +8,19 @@ namespace MediaWiki\Rest; * error response. */ class HttpException extends \Exception { - public function __construct( $message, $code = 500 ) { + + /** @var array|null */ + private $errorData = null; + + public function __construct( $message, $code = 500, $errorData = null ) { parent::__construct( $message, $code ); + $this->errorData = $errorData; + } + + /** + * @return array|null + */ + public function getErrorData() { + return $this->errorData; } } diff --git a/includes/Rest/ResponseFactory.php b/includes/Rest/ResponseFactory.php index d18cdb5d6b..5e5a19852d 100644 --- a/includes/Rest/ResponseFactory.php +++ b/includes/Rest/ResponseFactory.php @@ -175,8 +175,13 @@ class ResponseFactory { public function createFromException( $exception ) { if ( $exception instanceof HttpException ) { // FIXME can HttpException represent 2xx or 3xx responses? - $response = $this->createHttpError( $exception->getCode(), - [ 'message' => $exception->getMessage() ] ); + $response = $this->createHttpError( + $exception->getCode(), + array_merge( + [ 'message' => $exception->getMessage() ], + (array)$exception->getErrorData() + ) + ); } else { $response = $this->createHttpError( 500, [ 'message' => 'Error: exception of type ' . get_class( $exception ), diff --git a/includes/Rest/Router.php b/includes/Rest/Router.php index 961da01471..a520130d06 100644 --- a/includes/Rest/Router.php +++ b/includes/Rest/Router.php @@ -6,6 +6,7 @@ use AppendIterator; use BagOStuff; use MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface; use MediaWiki\Rest\PathTemplateMatcher\PathMatcher; +use MediaWiki\Rest\Validator\Validator; use Wikimedia\ObjectFactory; /** @@ -44,6 +45,12 @@ class Router { /** @var BasicAuthorizerInterface */ private $basicAuth; + /** @var ObjectFactory */ + private $objectFactory; + + /** @var Validator */ + private $restValidator; + /** * @param string[] $routeFiles List of names of JSON files containing routes * @param array $extraRoutes Extension route array @@ -51,10 +58,13 @@ class Router { * @param BagOStuff $cacheBag A cache in which to store the matcher trees * @param ResponseFactory $responseFactory * @param BasicAuthorizerInterface $basicAuth + * @param ObjectFactory $objectFactory + * @param Validator $restValidator */ public function __construct( $routeFiles, $extraRoutes, $rootPath, BagOStuff $cacheBag, ResponseFactory $responseFactory, - BasicAuthorizerInterface $basicAuth + BasicAuthorizerInterface $basicAuth, ObjectFactory $objectFactory, + Validator $restValidator ) { $this->routeFiles = $routeFiles; $this->extraRoutes = $extraRoutes; @@ -62,6 +72,8 @@ class Router { $this->cacheBag = $cacheBag; $this->responseFactory = $responseFactory; $this->basicAuth = $basicAuth; + $this->objectFactory = $objectFactory; + $this->restValidator = $restValidator; } /** @@ -245,9 +257,10 @@ class Router { $request->setPathParams( array_map( 'rawurldecode', $match['params'] ) ); $spec = $match['userData']; $objectFactorySpec = array_intersect_key( $spec, + // @todo ObjectFactory supports more keys than this. [ 'factory' => true, 'class' => true, 'args' => true ] ); /** @var $handler Handler (annotation for PHPStorm) */ - $handler = ObjectFactory::getObjectFromSpec( $objectFactorySpec ); + $handler = $this->objectFactory->createObject( $objectFactorySpec ); $handler->init( $this, $request, $spec, $this->responseFactory ); try { @@ -268,6 +281,9 @@ class Router { if ( $authResult ) { return $this->responseFactory->createHttpError( 403, [ 'error' => $authResult ] ); } + + $handler->validate( $this->restValidator ); + $response = $handler->execute(); if ( !( $response instanceof ResponseInterface ) ) { $response = $this->responseFactory->createFromReturnValue( $response ); diff --git a/includes/Rest/SimpleHandler.php b/includes/Rest/SimpleHandler.php index 3718d66b93..3c19e48e87 100644 --- a/includes/Rest/SimpleHandler.php +++ b/includes/Rest/SimpleHandler.php @@ -14,7 +14,26 @@ namespace MediaWiki\Rest; */ class SimpleHandler extends Handler { public function execute() { - $params = array_values( $this->getRequest()->getPathParams() ); + $paramSettings = $this->getParamSettings(); + $validatedParams = $this->getValidatedParams(); + $unvalidatedParams = []; + $params = []; + foreach ( $this->getRequest()->getPathParams() as $name => $value ) { + $source = $paramSettings[$name][self::PARAM_SOURCE] ?? 'unknown'; + if ( $source !== 'path' ) { + $unvalidatedParams[] = $name; + $params[] = $value; + } else { + $params[] = $validatedParams[$name]; + } + } + + if ( $unvalidatedParams ) { + throw new \LogicException( + 'Path parameters were not validated: ' . implode( ', ', $unvalidatedParams ) + ); + } + // @phan-suppress-next-line PhanUndeclaredMethod return $this->run( ...$params ); } diff --git a/includes/Rest/Validator/BodyValidator.php b/includes/Rest/Validator/BodyValidator.php new file mode 100644 index 0000000000..0147fa880c --- /dev/null +++ b/includes/Rest/Validator/BodyValidator.php @@ -0,0 +1,26 @@ +request = $request; + $this->user = $user; + } + + /** + * Get the raw parameters from a source in the request + * @param string $source 'path', 'query', or 'post' + * @return array + */ + private function getParamsFromSource( $source ) { + switch ( $source ) { + case 'path': + return $this->request->getPathParams(); + + case 'query': + return $this->request->getQueryParams(); + + case 'post': + return $this->request->getPostParams(); + + default: + throw new InvalidArgumentException( __METHOD__ . ": Invalid source '$source'" ); + } + } + + public function hasParam( $name, array $options ) { + $params = $this->getParamsFromSource( $options['source'] ); + return isset( $params[$name] ); + } + + public function getValue( $name, $default, array $options ) { + $params = $this->getParamsFromSource( $options['source'] ); + return $params[$name] ?? $default; + // @todo Should normalization to NFC UTF-8 be done here (much like in the + // action API and the rest of MW), or should it be left to handlers to + // do whatever normalization they need? + } + + public function hasUpload( $name, array $options ) { + if ( $options['source'] !== 'post' ) { + return false; + } + return $this->getUploadedFile( $name, $options ) !== null; + } + + public function getUploadedFile( $name, array $options ) { + if ( $options['source'] !== 'post' ) { + return null; + } + $upload = $this->request->getUploadedFiles()[$name] ?? null; + return $upload instanceof UploadedFileInterface ? $upload : null; + } + + public function recordCondition( ValidationException $condition, array $options ) { + // @todo Figure out how to handle warnings + } + + public function useHighLimits( array $options ) { + return $this->user->isAllowed( 'apihighlimits' ); + } + +} diff --git a/includes/Rest/Validator/Validator.php b/includes/Rest/Validator/Validator.php new file mode 100644 index 0000000000..cee1cdb359 --- /dev/null +++ b/includes/Rest/Validator/Validator.php @@ -0,0 +1,163 @@ + [ 'class' => BooleanDef::class ], + 'enum' => [ 'class' => EnumDef::class ], + 'integer' => [ 'class' => IntegerDef::class ], + 'float' => [ 'class' => FloatDef::class ], + 'double' => [ 'class' => FloatDef::class ], + 'NULL' => [ + 'class' => StringDef::class, + 'args' => [ [ + 'allowEmptyWhenRequired' => true, + ] ], + ], + 'password' => [ 'class' => PasswordDef::class ], + 'string' => [ 'class' => StringDef::class ], + 'timestamp' => [ 'class' => TimestampDef::class ], + 'upload' => [ 'class' => UploadDef::class ], + ]; + + /** @var string[] HTTP request methods that we expect never to have a payload */ + private static $noBodyMethods = [ 'GET', 'HEAD', 'DELETE' ]; + + /** @var string[] HTTP request methods that we expect always to have a payload */ + private static $bodyMethods = [ 'POST', 'PUT' ]; + + /** @var string[] Content types handled via $_POST */ + private static $formDataContentTypes = [ + 'application/x-www-form-urlencoded', + 'multipart/form-data', + ]; + + /** @var ParamValidator */ + private $paramValidator; + + /** + * @internal + * @param ObjectFactory $objectFactory + * @param RequestInterface $request + * @param User $user + */ + public function __construct( + ObjectFactory $objectFactory, RequestInterface $request, User $user + ) { + $this->paramValidator = new ParamValidator( + new ParamValidatorCallbacks( $request, $user ), + $objectFactory, + [ + 'typeDefs' => self::$typeDefs, + ] + ); + } + + /** + * Validate parameters + * @param array[] $paramSettings Parameter settings + * @return array Validated parameters + * @throws HttpException on validaton failure + */ + public function validateParams( array $paramSettings ) { + $validatedParams = []; + foreach ( $paramSettings as $name => $settings ) { + try { + $validatedParams[$name] = $this->paramValidator->getValue( $name, $settings, [ + 'source' => $settings[Handler::PARAM_SOURCE] ?? 'unspecified', + ] ); + } catch ( ValidationException $e ) { + throw new HttpException( 'Parameter validation failed', 400, [ + 'error' => 'parameter-validation-failed', + 'name' => $e->getParamName(), + 'value' => $e->getParamValue(), + 'failureCode' => $e->getFailureCode(), + 'failureData' => $e->getFailureData(), + ] ); + } + } + return $validatedParams; + } + + /** + * Validate the body of a request. + * + * This may return a data structure representing the parsed body. When used + * in the context of Handler::validateParams(), the returned value will be + * available to the handler via Handler::getValidatedBody(). + * + * @param RequestInterface $request + * @param Handler $handler Used to call getBodyValidator() + * @return mixed May be null + * @throws HttpException on validation failure + */ + public function validateBody( RequestInterface $request, Handler $handler ) { + $method = strtoupper( trim( $request->getMethod() ) ); + + // If the method should never have a body, don't bother validating. + if ( in_array( $method, self::$noBodyMethods, true ) ) { + return null; + } + + // Get the content type + list( $ct ) = explode( ';', $request->getHeaderLine( 'Content-Type' ), 2 ); + $ct = strtolower( trim( $ct ) ); + if ( $ct === '' ) { + // No Content-Type was supplied. RFC 7231 § 3.1.1.5 allows this, but since it's probably a + // client error let's return a 415. But don't 415 for unknown methods and an empty body. + if ( !in_array( $method, self::$bodyMethods, true ) ) { + $body = $request->getBody(); + $size = $body->getSize(); + if ( $size === null ) { + // No size available. Try reading 1 byte. + if ( $body->isSeekable() ) { + $body->rewind(); + } + $size = $body->read( 1 ) === '' ? 0 : 1; + } + if ( $size === 0 ) { + return null; + } + } + throw new HttpException( "A Content-Type header must be supplied with a request payload.", 415, [ + 'error' => 'no-content-type', + ] ); + } + + // Form data is parsed into $_POST and $_FILES by PHP and from there is accessed as parameters, + // don't bother trying to handle these via BodyValidator too. + if ( in_array( $ct, self::$formDataContentTypes, true ) ) { + return null; + } + + // Validate the body. BodyValidator throws an HttpException on failure. + return $handler->getBodyValidator( $ct )->validateBody( $request ); + } + +} diff --git a/tests/phpunit/includes/Rest/BasicAccess/MWBasicRequestAuthorizerTest.php b/tests/phpunit/includes/Rest/BasicAccess/MWBasicRequestAuthorizerTest.php index 3c6573ac1f..2d1fd986cc 100644 --- a/tests/phpunit/includes/Rest/BasicAccess/MWBasicRequestAuthorizerTest.php +++ b/tests/phpunit/includes/Rest/BasicAccess/MWBasicRequestAuthorizerTest.php @@ -9,8 +9,11 @@ use MediaWiki\Rest\Handler; use MediaWiki\Rest\RequestData; use MediaWiki\Rest\ResponseFactory; use MediaWiki\Rest\Router; +use MediaWiki\Rest\Validator\Validator; use MediaWikiTestCase; +use Psr\Container\ContainerInterface; use User; +use Wikimedia\ObjectFactory; /** * @group Database @@ -21,7 +24,7 @@ use User; * @covers \MediaWiki\Rest\BasicAccess\MWBasicRequestAuthorizer */ class MWBasicRequestAuthorizerTest extends MediaWikiTestCase { - private function createRouter( $userRights ) { + private function createRouter( $userRights, $request ) { $user = User::newFromName( 'Test user' ); // Don't allow the rights to everybody so that user rights kick in. $this->mergeMwGlobalArrayValue( 'wgGroupPermissions', [ '*' => $userRights ] ); @@ -34,18 +37,25 @@ class MWBasicRequestAuthorizerTest extends MediaWikiTestCase { global $IP; + $objectFactory = new ObjectFactory( + $this->getMockForAbstractClass( ContainerInterface::class ) + ); + return new Router( [ "$IP/tests/phpunit/unit/includes/Rest/testRoutes.json" ], [], '/rest', new \EmptyBagOStuff(), new ResponseFactory(), - new MWBasicAuthorizer( $user, MediaWikiServices::getInstance()->getPermissionManager() ) ); + new MWBasicAuthorizer( $user, MediaWikiServices::getInstance()->getPermissionManager() ), + $objectFactory, + new Validator( $objectFactory, $request, $user ) + ); } public function testReadDenied() { - $router = $this->createRouter( [ 'read' => false ] ); $request = new RequestData( [ 'uri' => new Uri( '/rest/user/joe/hello' ) ] ); + $router = $this->createRouter( [ 'read' => false ], $request ); $response = $router->execute( $request ); $this->assertSame( 403, $response->getStatusCode() ); @@ -56,8 +66,8 @@ class MWBasicRequestAuthorizerTest extends MediaWikiTestCase { } public function testReadAllowed() { - $router = $this->createRouter( [ 'read' => true ] ); $request = new RequestData( [ 'uri' => new Uri( '/rest/user/joe/hello' ) ] ); + $router = $this->createRouter( [ 'read' => true ], $request ); $response = $router->execute( $request ); $this->assertSame( 200, $response->getStatusCode() ); } @@ -75,10 +85,10 @@ class MWBasicRequestAuthorizerTest extends MediaWikiTestCase { } public function testWriteDenied() { - $router = $this->createRouter( [ 'read' => true, 'writeapi' => false ] ); $request = new RequestData( [ 'uri' => new Uri( '/rest/mock/MWBasicRequestAuthorizerTest/write' ) ] ); + $router = $this->createRouter( [ 'read' => true, 'writeapi' => false ], $request ); $response = $router->execute( $request ); $this->assertSame( 403, $response->getStatusCode() ); @@ -89,10 +99,10 @@ class MWBasicRequestAuthorizerTest extends MediaWikiTestCase { } public function testWriteAllowed() { - $router = $this->createRouter( [ 'read' => true, 'writeapi' => true ] ); $request = new RequestData( [ 'uri' => new Uri( '/rest/mock/MWBasicRequestAuthorizerTest/write' ) ] ); + $router = $this->createRouter( [ 'read' => true, 'writeapi' => true ], $request ); $response = $router->execute( $request ); $this->assertSame( 200, $response->getStatusCode() ); diff --git a/tests/phpunit/includes/Rest/EntryPointTest.php b/tests/phpunit/includes/Rest/EntryPointTest.php index b599e9d6a4..b984895281 100644 --- a/tests/phpunit/includes/Rest/EntryPointTest.php +++ b/tests/phpunit/includes/Rest/EntryPointTest.php @@ -9,10 +9,15 @@ use MediaWiki\Rest\BasicAccess\StaticBasicAuthorizer; use MediaWiki\Rest\Handler; use MediaWiki\Rest\EntryPoint; use MediaWiki\Rest\RequestData; +use MediaWiki\Rest\RequestInterface; use MediaWiki\Rest\ResponseFactory; use MediaWiki\Rest\Router; +use MediaWiki\Rest\Validator\Validator; +use Psr\Container\ContainerInterface; use RequestContext; use WebResponse; +use Wikimedia\ObjectFactory; +use User; /** * @covers \MediaWiki\Rest\EntryPoint @@ -21,16 +26,23 @@ use WebResponse; class EntryPointTest extends \MediaWikiTestCase { private static $mockHandler; - private function createRouter() { + private function createRouter( RequestInterface $request ) { global $IP; + $objectFactory = new ObjectFactory( + $this->getMockForAbstractClass( ContainerInterface::class ) + ); + return new Router( [ "$IP/tests/phpunit/unit/includes/Rest/testRoutes.json" ], [], '/rest', new EmptyBagOStuff(), new ResponseFactory(), - new StaticBasicAuthorizer() ); + new StaticBasicAuthorizer(), + $objectFactory, + new Validator( $objectFactory, $request, new User ) + ); } private function createWebResponse() { @@ -58,11 +70,12 @@ class EntryPointTest extends \MediaWikiTestCase { [ 'Foo: Bar', true, null ] ); + $request = new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/header' ) ] ); $entryPoint = new EntryPoint( RequestContext::getMain(), - new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/header' ) ] ), + $request, $webResponse, - $this->createRouter() ); + $this->createRouter( $request ) ); $entryPoint->execute(); $this->assertTrue( true ); } @@ -83,11 +96,12 @@ class EntryPointTest extends \MediaWikiTestCase { * Make sure EntryPoint rewinds a seekable body stream before reading. */ public function testBodyRewind() { + $request = new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/bodyRewind' ) ] ); $entryPoint = new EntryPoint( RequestContext::getMain(), - new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/bodyRewind' ) ] ), + $request, $this->createWebResponse(), - $this->createRouter() ); + $this->createRouter( $request ) ); ob_start(); $entryPoint->execute(); $this->assertSame( 'hello', ob_get_clean() ); diff --git a/tests/phpunit/unit/includes/Rest/Handler/HelloHandlerTest.php b/tests/phpunit/unit/includes/Rest/Handler/HelloHandlerTest.php index 188629f8eb..91652a253a 100644 --- a/tests/phpunit/unit/includes/Rest/Handler/HelloHandlerTest.php +++ b/tests/phpunit/unit/includes/Rest/Handler/HelloHandlerTest.php @@ -8,6 +8,10 @@ use MediaWiki\Rest\BasicAccess\StaticBasicAuthorizer; use MediaWiki\Rest\RequestData; use MediaWiki\Rest\ResponseFactory; use MediaWiki\Rest\Router; +use MediaWiki\Rest\Validator\Validator; +use Psr\Container\ContainerInterface; +use Wikimedia\ObjectFactory; +use User; /** * @covers \MediaWiki\Rest\Handler\HelloHandler @@ -48,14 +52,21 @@ class HelloHandlerTest extends \MediaWikiUnitTestCase { /** @dataProvider provideTestViaRouter */ public function testViaRouter( $requestInfo, $responseInfo ) { + $objectFactory = new ObjectFactory( + $this->getMockForAbstractClass( ContainerInterface::class ) + ); + + $request = new RequestData( $requestInfo ); $router = new Router( [ __DIR__ . '/../testRoutes.json' ], [], '/rest', new EmptyBagOStuff(), new ResponseFactory(), - new StaticBasicAuthorizer() ); - $request = new RequestData( $requestInfo ); + new StaticBasicAuthorizer(), + $objectFactory, + new Validator( $objectFactory, $request, new User ) + ); $response = $router->execute( $request ); if ( isset( $responseInfo['statusCode'] ) ) { $this->assertSame( $responseInfo['statusCode'], $response->getStatusCode() ); diff --git a/tests/phpunit/unit/includes/Rest/RouterTest.php b/tests/phpunit/unit/includes/Rest/RouterTest.php index cacccb9649..e16ea25c9a 100644 --- a/tests/phpunit/unit/includes/Rest/RouterTest.php +++ b/tests/phpunit/unit/includes/Rest/RouterTest.php @@ -7,37 +7,48 @@ use MediaWiki\Rest\BasicAccess\StaticBasicAuthorizer; use MediaWiki\Rest\Handler; use MediaWiki\Rest\HttpException; use MediaWiki\Rest\RequestData; +use MediaWiki\Rest\RequestInterface; use MediaWiki\Rest\ResponseFactory; use MediaWiki\Rest\Router; +use MediaWiki\Rest\Validator\Validator; +use Psr\Container\ContainerInterface; +use Wikimedia\ObjectFactory; +use User; /** * @covers \MediaWiki\Rest\Router */ class RouterTest extends \MediaWikiUnitTestCase { /** @return Router */ - private function createRouter( $authError = null ) { + private function createRouter( RequestInterface $request, $authError = null ) { + $objectFactory = new ObjectFactory( + $this->getMockForAbstractClass( ContainerInterface::class ) + ); return new Router( [ __DIR__ . '/testRoutes.json' ], [], '/rest', new \EmptyBagOStuff(), new ResponseFactory(), - new StaticBasicAuthorizer( $authError ) ); + new StaticBasicAuthorizer( $authError ), + $objectFactory, + new Validator( $objectFactory, $request, new User ) + ); } public function testPrefixMismatch() { - $router = $this->createRouter(); $request = new RequestData( [ 'uri' => new Uri( '/bogus' ) ] ); + $router = $this->createRouter( $request ); $response = $router->execute( $request ); $this->assertSame( 404, $response->getStatusCode() ); } public function testWrongMethod() { - $router = $this->createRouter(); $request = new RequestData( [ 'uri' => new Uri( '/rest/user/joe/hello' ), 'method' => 'OPTIONS' ] ); + $router = $this->createRouter( $request ); $response = $router->execute( $request ); $this->assertSame( 405, $response->getStatusCode() ); $this->assertSame( 'Method Not Allowed', $response->getReasonPhrase() ); @@ -45,8 +56,8 @@ class RouterTest extends \MediaWikiUnitTestCase { } public function testNoMatch() { - $router = $this->createRouter(); $request = new RequestData( [ 'uri' => new Uri( '/rest/bogus' ) ] ); + $router = $this->createRouter( $request ); $response = $router->execute( $request ); $this->assertSame( 404, $response->getStatusCode() ); // TODO: add more information to the response body and test for its presence here @@ -61,8 +72,8 @@ class RouterTest extends \MediaWikiUnitTestCase { } public function testException() { - $router = $this->createRouter(); $request = new RequestData( [ 'uri' => new Uri( '/rest/mock/RouterTest/throw' ) ] ); + $router = $this->createRouter( $request ); $response = $router->execute( $request ); $this->assertSame( 555, $response->getStatusCode() ); $body = $response->getBody(); @@ -72,9 +83,9 @@ class RouterTest extends \MediaWikiUnitTestCase { } public function testBasicAccess() { - $router = $this->createRouter( 'test-error' ); // Using the throwing handler is a way to assert that the handler is not executed $request = new RequestData( [ 'uri' => new Uri( '/rest/mock/RouterTest/throw' ) ] ); + $router = $this->createRouter( $request, 'test-error' ); $response = $router->execute( $request ); $this->assertSame( 403, $response->getStatusCode() ); $body = $response->getBody(); -- 2.20.1