wiki.techinc.nl/tests/phpunit/structure/RestStructureTest.php
Daniel Kinzler f2e9d5108d REST: collect metrics on endpoint access
This is a modified version of Ie282bc5b5f5df0bbd6a40c8362ba73fcbbf36c2e
which was reverted in 5c7cca8776.

Bug: T321969
Change-Id: I566d54a473aa51c4cdaada21a49d63c0624aab93
2023-01-12 14:50:58 +00:00

267 lines
8.6 KiB
PHP

<?php
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\StaticHookRegistry;
use MediaWiki\MainConfigSchema;
use MediaWiki\MediaWikiServices;
use MediaWiki\Permissions\SimpleAuthority;
use MediaWiki\Rest\CorsUtils;
use MediaWiki\Rest\EntryPoint;
use MediaWiki\Rest\Handler;
use MediaWiki\Rest\PathTemplateMatcher\PathMatcher;
use MediaWiki\Rest\RequestData;
use MediaWiki\Rest\ResponseFactory;
use MediaWiki\Rest\Router;
use MediaWiki\Session\Session;
use MediaWiki\User\UserIdentityValue;
use Wikimedia\ObjectFactory\ObjectFactory;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\TestingAccessWrapper;
/**
* Checks that all REST Handlers, core and extensions, conform to the conventions:
* - parameters in path have correct PARAM_SOURCE
* - path parameters not in path are not required
* - do not have inconsistencies in the parameter definitions
*
* @coversNothing
*/
class RestStructureTest extends MediaWikiIntegrationTestCase {
/** @var ?Router */
private $router = null;
/**
* Constructs a fake MediaWikiServices instance for use in data providers.
*
* @return MediaWikiServices
*/
private function getFakeServiceContainer(): MediaWikiServices {
$config = new HashConfig( iterator_to_array( MainConfigSchema::listDefaultValues() ) );
$objectFactory = $this->createNoOpMock( ObjectFactory::class );
$hookContainer = new HookContainer(
new StaticHookRegistry(),
$objectFactory
);
$services = $this->createNoOpMock(
MediaWikiServices::class,
[
'getMainConfig',
'getHookContainer',
'getObjectFactory',
'getLocalServerObjectCache',
'getStatsdDataFactory',
]
);
$services->method( 'getMainConfig' )->willReturn( $config );
$services->method( 'getHookContainer' )->willReturn( $hookContainer );
$services->method( 'getObjectFactory' )->willReturn( $objectFactory );
$services->method( 'getLocalServerObjectCache' )->willReturn( new EmptyBagOStuff() );
$services->method( 'getStatsdDataFactory' )->willReturn( new NullStatsdDataFactory() );
return $services;
}
/**
* Return all routes. Safe to use in data providers.
* @return Iterator<array>
*/
private function getAllRoutes(): Iterator {
static $router = null;
if ( !$router ) {
$language = $this->createNoOpMock( Language::class, [ 'getCode' ] );
$language->method( 'getCode' )->willReturn( 'en' );
$title = Title::makeTitle( NS_SPECIAL, 'Badtitle/dummy title for RestStructureTest' );
$authority = new SimpleAuthority( new UserIdentityValue( 0, 'Testor' ), [] );
$request = $this->createNoOpMock(
WebRequest::class,
[ 'getSession' ]
);
$request->method( 'getSession' )->willReturn(
$this->createNoOpMock( Session::class )
);
$context = $this->createNoOpMock(
RequestContext::class,
[ 'getLanguage', 'getTitle', 'getAuthority', 'getRequest' ]
);
$context->method( 'getLanguage' )->willReturn( $language );
$context->method( 'getTitle' )->willReturn( $title );
$context->method( 'getAuthority' )->willReturn( $authority );
$context->method( 'getRequest' )->willReturn( $request );
$responseFactory = $this->createNoOpMock( ResponseFactory::class );
$cors = $this->createNoOpMock( CorsUtils::class );
$services = $this->getFakeServiceContainer();
// NOTE: createRouter() implements the logic for determining the list of route files to load.
$entryPoint = TestingAccessWrapper::newFromClass( EntryPoint::class );
$router = $entryPoint->createRouter( $services, $context, new RequestData(), $responseFactory, $cors );
$router = TestingAccessWrapper::newFromObject( $router );
}
return $router->getAllRoutes();
}
/**
* Initialize/fetch the Router instance for testing
* @warning Must not be called in data providers!
* @return Router
*/
private function getRouter(): Router {
if ( !$this->router ) {
$context = new DerivativeContext( RequestContext::getMain() );
$context->setLanguage( 'en' );
$context->setTitle(
Title::makeTitle( NS_SPECIAL, 'Badtitle/dummy title for RestStructureTest' )
);
$responseFactory = $this->createNoOpMock( ResponseFactory::class );
$cors = $this->createNoOpMock( CorsUtils::class );
$this->router = TestingAccessWrapper::newFromClass( EntryPoint::class )
->createRouter( $this->getServiceContainer(), $context, new RequestData(), $responseFactory, $cors );
}
return $this->router;
}
/**
* @dataProvider provideRoutes
*/
public function testPathParameters( array $spec ): void {
$router = TestingAccessWrapper::newFromObject( $this->getRouter() );
$request = new RequestData();
$handler = $router->createHandler( $request, $spec );
$params = $handler->getParamSettings();
$dataName = $this->dataName();
// Test that all parameters in the path exist and are declared as such
$matcher = TestingAccessWrapper::newFromObject( new PathMatcher );
$pathParams = [];
foreach ( explode( '/', $spec['path'] ) as $part ) {
$param = $matcher->getParamName( $part );
if ( $param !== false ) {
$this->assertArrayHasKey( $param, $params, "Path parameter $param exists" );
$this->assertSame( 'path', $params[$param][Handler::PARAM_SOURCE] ?? null,
"$dataName: Path parameter {{$param}} must have PARAM_SOURCE = 'path'" );
$pathParams[$param] = true;
}
}
// Test that any path parameters not in the path aren't marked as required
foreach ( $params as $param => $settings ) {
if ( ( $settings[Handler::PARAM_SOURCE] ?? null ) === 'path' &&
!isset( $pathParams[$param] )
) {
$this->assertFalse( $settings[ParamValidator::PARAM_REQUIRED] ?? false,
"$dataName, parameter $param: PARAM_REQUIRED cannot be true for a path parameter "
. 'not in the path'
);
}
}
// In case there were no path parameters
$this->addToAssertionCount( 1 );
}
public function provideRoutes(): Iterator {
foreach ( $this->getAllRoutes() as $spec ) {
$method = $spec['method'] ?? 'GET';
$method = implode( ",", (array)$method );
yield "Handler {$method} {$spec['path']}" => [ $spec ];
}
}
/**
* @dataProvider provideRoutes
*/
public function testParameters( array $routeSpec ): void {
$router = TestingAccessWrapper::newFromObject( $this->getRouter() );
$request = new RequestData();
$handler = $router->createHandler( $request, $routeSpec );
$params = $handler->getParamSettings();
foreach ( $params as $param => $settings ) {
$method = $routeSpec['method'] ?? 'GET';
$method = implode( ",", (array)$method );
$this->assertParameter( $param, $settings, "Handler {$method} {$routeSpec['path']}, parameter $param" );
}
}
private function assertParameter( string $name, $settings, $msg ) {
static $sources = [ 'path', 'query', 'post' ];
$router = TestingAccessWrapper::newFromObject( $this->getRouter() );
$dataName = $this->dataName();
$this->assertNotSame( '', $name, "$msg: $dataName: Name cannot be empty" );
$paramValidator = TestingAccessWrapper::newFromObject( $router->restValidator )->paramValidator;
$ret = $paramValidator->checkSettings( $name, $settings, [ 'source' => 'unspecified' ] );
// REST-specific parameters
$ret['allowedKeys'][] = Handler::PARAM_SOURCE;
if ( !in_array( $settings[Handler::PARAM_SOURCE] ?? '', $sources, true ) ) {
$ret['issues'][Handler::PARAM_SOURCE] = "PARAM_SOURCE must be 'path', 'query', or 'post'";
}
// Warn about unknown keys. Don't fail, they might be for forward- or back-compat.
if ( is_array( $settings ) ) {
$keys = array_diff(
array_keys( $settings ),
$ret['allowedKeys']
);
if ( $keys ) {
$this->addWarning(
"$msg: $dataName: Unrecognized settings keys were used: " . implode( ', ', $keys )
);
}
}
if ( count( $ret['issues'] ) === 1 ) {
$this->fail( "$msg: $dataName: Validation failed: " . reset( $ret['issues'] ) );
} elseif ( $ret['issues'] ) {
$this->fail( "$msg: $dataName: Validation failed:\n* " . implode( "\n* ", $ret['issues'] ) );
}
// Check message existence
$done = [];
foreach ( $ret['messages'] as $msg ) {
// We don't really care about the parameters, so do it simply
$key = $msg->getKey();
if ( !isset( $done[$key] ) ) {
$done[$key] = true;
$this->assertTrue( Message::newFromKey( $key )->exists(),
"$msg: $dataName: Parameter message $key exists" );
}
}
}
public function testRoutePathAndMethodForDuplicates() {
$router = TestingAccessWrapper::newFromObject( $this->getRouter() );
$routes = [];
foreach ( $router->getAllRoutes() as $spec ) {
$method = $spec['method'] ?? 'GET';
$method = (array)$method;
foreach ( $method as $m ) {
$key = "{$m} {$spec['path']}";
$this->assertArrayNotHasKey( $key, $routes, "{$key} already exists in routes" );
$routes[$key] = true;
}
}
}
}