wiki.techinc.nl/tests/phpunit/unit/includes/Rest/Handler/HandlerTestTrait.php
daniel 79c61e80dc REST: Make module definition files more like OpenAPI specs
This splits RouteFileModule into two classes, ExtraRoutesModule and
SpecBasedModule.

ExtraRoutesModule has no module prefix and supports
only "flat" route definition files and additional routes from
extension.json.

SpecBasedModule represents a single module defined in a definition
file similar to an OpenAPI spec. The idea is that a full OpenAPI spec
can be generated by filling in any missing information based on
information provided by the Handler implementation. In particular, the
definition of parameters and request body schemas will be generated.

A JSON schema for the new file format is added under docs/rest/.

Support for the intermediate format introduced in Iebcde4645d4 is
removed. It was not included in a release and was not being used outside
core tests.

Bug: T366837
Change-Id: I4ce306b0997f80b78a3d901e38bbfa8445bed604
2024-06-24 16:42:59 +02:00

300 lines
8.4 KiB
PHP

<?php
namespace MediaWiki\Tests\Rest\Handler;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Permissions\Authority;
use MediaWiki\Rest\Handler;
use MediaWiki\Rest\HttpException;
use MediaWiki\Rest\Module\Module;
use MediaWiki\Rest\RequestInterface;
use MediaWiki\Rest\Response;
use MediaWiki\Rest\ResponseFactory;
use MediaWiki\Rest\ResponseInterface;
use MediaWiki\Rest\Router;
use MediaWiki\Rest\Validator\Validator;
use MediaWiki\Session\Session;
use MediaWiki\Tests\Rest\RestTestTrait;
use MediaWiki\Tests\Unit\DummyServicesTrait;
use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\MockObject\MockObject;
use Wikimedia\ObjectFactory\ObjectFactory;
/**
* A trait providing utility functions for testing Handler classes.
* This trait is intended to be used on subclasses of MediaWikiUnitTestCase
* or MediaWikiIntegrationTestCase.
*
* @stable to use
*/
trait HandlerTestTrait {
use RestTestTrait;
use DummyServicesTrait;
use MockAuthorityTrait;
use SessionHelperTestTrait;
/**
* Calls init() on the Handler, supplying a mock RouteUrlProvider and ResponseFactory.
*
* @param Handler $handler
* @param RequestInterface $request
* @param array $config
* @param HookContainer|array $hooks Hook container or array of hooks
* @param Authority|null $authority
* @param Session|null $session Defaults to `$this->getSession( true )`
* @param Router|Module|null $routerOrModule
*
* @internal to the trait
*/
private function initHandler(
Handler $handler,
RequestInterface $request,
$config = [],
$hooks = [],
Authority $authority = null,
Session $session = null,
$routerOrModule = null
) {
$formatter = $this->getDummyTextFormatter( true );
$responseFactory = new ResponseFactory( [ 'qqx' => $formatter ] );
$module = null;
$router = null;
if ( $routerOrModule instanceof Module ) {
$module = $routerOrModule;
$router = $module->getRouter();
}
if ( $routerOrModule instanceof Router ) {
$router = $routerOrModule;
}
if ( !$module ) {
if ( !$router ) {
$router = $this->newRouter();
}
$module = $this->newModule( [ 'router' => $router ] );
}
if ( !$request->hasBody()
&& in_array( $request->getMethod(), RequestInterface::BODY_METHODS )
) {
// Send an empty body if none was provided.
$request->setParsedBody( [] );
}
$authority ??= $this->mockAnonUltimateAuthority();
$hookContainer =
$hooks instanceof HookContainer ? $hooks : $this->createHookContainer( $hooks );
$session ??= $this->getSession( true );
$handler->initContext( $module, $config['path'] ?? 'test', $config );
$handler->initServices( $authority, $responseFactory, $hookContainer );
$handler->initSession( $session );
$handler->initForExecute( $request );
}
/**
* @return MockObject&Router
*/
private function newRouter(): Router {
$router = $this->createNoOpMock(
Router::class,
[
'getRoutePath',
'getRouteUrl'
]
);
$router->method( 'getRoutePath' )->willReturnCallback(
static function ( $route, $path = [], $query = [] ) {
foreach ( $path as $param => $value ) {
$route = str_replace(
'{' . $param . '}',
urlencode( (string)$value ),
$route
);
}
return wfAppendQuery(
'/rest' . $route,
$query
);
}
);
$router->method( 'getRouteUrl' )->willReturnCallback(
static function ( $route, $path = [], $query = [] ) use ( $router ) {
return 'https://wiki.example.com' . $router->getRoutePath(
$route,
$path,
$query
);
}
);
return $router;
}
/**
* Calls validate() on the Handler, with an appropriate Validator supplied.
*
* @internal to the trait
* @param Handler $handler
* @param null|Validator $validator
* @throws HttpException
*/
private function validateHandler(
Handler $handler,
Validator $validator = null
) {
if ( !$validator ) {
$serviceContainer = $this->getServiceContainer();
$objectFactory = new ObjectFactory( $serviceContainer );
$validator = new Validator( $objectFactory, $handler->getRequest(), $handler->getAuthority() );
}
$handler->validate( $validator );
}
/**
* Creates a mock Validator to bypass actual request query, path, and/or body param validation
*
* @internal to the trait
* @param array $queryPathParams
* @param array $bodyParams
* @return Validator|MockObject
*/
private function getMockValidator( array $queryPathParams, array $bodyParams ): Validator {
$validator = $this->createNoOpMock(
Validator::class,
[
'validateParams',
'validateBodyParams',
'validateBody',
'detectExtraneousBodyFields',
]
);
$validator->method( 'validateBody' )->willReturn( null );
$validator->method( 'validateParams' )->willReturn( $queryPathParams );
$validator->method( 'validateBodyParams' )->willReturn( $bodyParams );
return $validator;
}
/**
* Executes the given Handler on the given request.
*
* @param Handler $handler
* @param RequestInterface $request
* @param array $config
* @param HookContainer|array $hooks Hook container or array of hooks
* @param array $validatedParams Path/query params to return as already valid
* @param array $validatedBody Body params to return as already valid
* @param Authority|null $authority
* @param Session|null $session Defaults to `$this->getSession( true )`
* @param Router|Module|null $routerOrModule
*
* @return ResponseInterface
*/
private function executeHandler(
Handler $handler,
RequestInterface $request,
$config = [],
$hooks = [],
$validatedParams = [],
$validatedBody = [],
Authority $authority = null,
Session $session = null,
$routerOrModule = null
): ResponseInterface {
// supply defaults for required fields in $config
$config += [ 'path' => '/test' ];
$this->initHandler( $handler, $request, $config, $hooks, $authority, $session, $routerOrModule );
$validator = null;
if ( $validatedParams || $validatedBody ) {
/** @var Validator|MockObject $validator */
$validator = $this->getMockValidator( $validatedParams, $validatedBody );
}
$this->validateHandler( $handler, $validator );
// Check conditional request headers
$earlyResponse = $handler->checkPreconditions();
if ( $earlyResponse ) {
return $earlyResponse;
}
$ret = $handler->execute();
$response = $ret instanceof Response ? $ret
: $handler->getResponseFactory()->createFromReturnValue( $ret );
// Set Last-Modified and ETag headers in the response if available
$handler->applyConditionalResponseHeaders( $response );
return $response;
}
/**
* Executes the given Handler on the given request, parses the response body as JSON,
* and returns the result.
*
* @param Handler $handler
* @param RequestInterface $request
* @param array $config
* @param HookContainer|array $hooks Hook container or array of hooks
* @param array $validatedParams
* @param array $validatedBody
* @param Authority|null $authority
* @param Session|null $session Defaults to `$this->getSession( true )`
* @return array
*/
private function executeHandlerAndGetBodyData(
Handler $handler,
RequestInterface $request,
$config = [],
$hooks = [],
$validatedParams = [],
$validatedBody = [],
Authority $authority = null,
Session $session = null
): array {
$response = $this->executeHandler( $handler, $request, $config, $hooks,
$validatedParams, $validatedBody, $authority, $session );
$this->assertGreaterThanOrEqual( 200, $response->getStatusCode() );
$this->assertLessThan( 300, $response->getStatusCode() );
$this->assertSame( 'application/json', $response->getHeaderLine( 'Content-Type' ) );
$data = json_decode( $response->getBody(), true );
$this->assertIsArray( $data, 'Body must be a JSON array' );
return $data;
}
/**
* Executes the given Handler on the given request, and returns the HttpException thrown.
* Fails if no HttpException is thrown.
*
* @param Handler $handler
* @param RequestInterface $request
* @param array $config
* @param HookContainer|array $hooks Hook container or array of hooks
*
* @return HttpException
*/
private function executeHandlerAndGetHttpException(
Handler $handler,
RequestInterface $request,
$config = [],
$hooks = []
): HttpException {
try {
$this->executeHandler( $handler, $request, $config, $hooks );
Assert::fail( 'Expected a HttpException to be thrown' );
} catch ( HttpException $ex ) {
return $ex;
}
}
}