2020-01-22 19:08:38 +00:00
|
|
|
<?php
|
|
|
|
|
|
2024-07-11 10:53:29 +00:00
|
|
|
use JsonSchema\Constraints\Constraint;
|
|
|
|
|
use JsonSchema\Validator as JsonValidator;
|
2023-09-20 07:54:42 +00:00
|
|
|
use MediaWiki\Config\HashConfig;
|
2024-02-08 14:56:54 +00:00
|
|
|
use MediaWiki\Context\DerivativeContext;
|
|
|
|
|
use MediaWiki\Context\RequestContext;
|
2022-11-18 10:00:05 +00:00
|
|
|
use MediaWiki\HookContainer\HookContainer;
|
|
|
|
|
use MediaWiki\HookContainer\StaticHookRegistry;
|
|
|
|
|
use MediaWiki\MainConfigSchema;
|
|
|
|
|
use MediaWiki\MediaWikiServices;
|
2024-06-13 21:21:02 +00:00
|
|
|
use MediaWiki\Message\Message;
|
2024-07-11 10:53:29 +00:00
|
|
|
use MediaWiki\ParamValidator\TypeDef\ArrayDef;
|
2022-11-18 10:00:05 +00:00
|
|
|
use MediaWiki\Permissions\SimpleAuthority;
|
2023-09-07 11:46:15 +00:00
|
|
|
use MediaWiki\Request\WebRequest;
|
2020-08-22 19:26:19 +00:00
|
|
|
use MediaWiki\Rest\CorsUtils;
|
2020-01-22 19:08:38 +00:00
|
|
|
use MediaWiki\Rest\EntryPoint;
|
|
|
|
|
use MediaWiki\Rest\Handler;
|
|
|
|
|
use MediaWiki\Rest\PathTemplateMatcher\PathMatcher;
|
|
|
|
|
use MediaWiki\Rest\RequestData;
|
2020-08-22 19:26:19 +00:00
|
|
|
use MediaWiki\Rest\ResponseFactory;
|
2020-01-22 19:08:38 +00:00
|
|
|
use MediaWiki\Rest\Router;
|
2024-03-27 10:40:13 +00:00
|
|
|
use MediaWiki\Rest\Validator\Validator;
|
2022-11-18 10:00:05 +00:00
|
|
|
use MediaWiki\Session\Session;
|
2022-06-29 22:34:37 +00:00
|
|
|
use MediaWiki\Tests\Unit\DummyServicesTrait;
|
2023-03-01 20:33:26 +00:00
|
|
|
use MediaWiki\Title\Title;
|
2022-11-18 10:00:05 +00:00
|
|
|
use MediaWiki\User\UserIdentityValue;
|
2024-07-09 13:37:44 +00:00
|
|
|
use Wikimedia\ObjectCache\EmptyBagOStuff;
|
2020-01-22 19:08:38 +00:00
|
|
|
use Wikimedia\ParamValidator\ParamValidator;
|
2024-06-07 06:41:15 +00:00
|
|
|
use Wikimedia\Stats\StatsFactory;
|
2020-01-22 19:08:38 +00:00
|
|
|
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
|
2022-10-07 00:45:44 +00:00
|
|
|
*
|
|
|
|
|
* @coversNothing
|
2023-08-05 23:03:25 +00:00
|
|
|
* @group Database
|
2020-01-22 19:08:38 +00:00
|
|
|
*/
|
2020-06-30 15:09:24 +00:00
|
|
|
class RestStructureTest extends MediaWikiIntegrationTestCase {
|
2022-06-29 22:34:37 +00:00
|
|
|
use DummyServicesTrait;
|
2020-01-22 19:08:38 +00:00
|
|
|
|
2022-11-18 10:00:05 +00:00
|
|
|
/** @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() ) );
|
|
|
|
|
|
2022-06-29 22:34:37 +00:00
|
|
|
$objectFactory = $this->getDummyObjectFactory();
|
2022-11-18 10:00:05 +00:00
|
|
|
$hookContainer = new HookContainer(
|
|
|
|
|
new StaticHookRegistry(),
|
|
|
|
|
$objectFactory
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$services = $this->createNoOpMock(
|
|
|
|
|
MediaWikiServices::class,
|
2022-12-13 21:00:59 +00:00
|
|
|
[
|
|
|
|
|
'getMainConfig',
|
|
|
|
|
'getHookContainer',
|
|
|
|
|
'getObjectFactory',
|
|
|
|
|
'getLocalServerObjectCache',
|
2024-06-07 06:41:15 +00:00
|
|
|
'getStatsFactory',
|
2022-12-13 21:00:59 +00:00
|
|
|
]
|
2022-11-18 10:00:05 +00:00
|
|
|
);
|
|
|
|
|
$services->method( 'getMainConfig' )->willReturn( $config );
|
|
|
|
|
$services->method( 'getHookContainer' )->willReturn( $hookContainer );
|
|
|
|
|
$services->method( 'getObjectFactory' )->willReturn( $objectFactory );
|
|
|
|
|
$services->method( 'getLocalServerObjectCache' )->willReturn( new EmptyBagOStuff() );
|
2024-06-07 06:41:15 +00:00
|
|
|
$services->method( 'getStatsFactory' )->willReturn( StatsFactory::newNull() );
|
2022-11-18 10:00:05 +00:00
|
|
|
|
|
|
|
|
return $services;
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-30 16:04:41 +00:00
|
|
|
private function getRouterForDataProviders(): Router {
|
2022-11-18 10:00:05 +00:00
|
|
|
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' ), [] );
|
|
|
|
|
|
2023-10-30 16:04:41 +00:00
|
|
|
$request = $this->createNoOpMock( WebRequest::class, [ 'getSession' ] );
|
|
|
|
|
$request->method( 'getSession' )->willReturn( $this->createNoOpMock( Session::class ) );
|
2022-11-18 10:00:05 +00:00
|
|
|
|
|
|
|
|
$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.
|
2023-10-30 16:04:41 +00:00
|
|
|
$entryPoint = TestingAccessWrapper::newFromClass( EntryPoint::class );
|
|
|
|
|
$router = $entryPoint->createRouter(
|
|
|
|
|
$services,
|
|
|
|
|
$context,
|
|
|
|
|
new RequestData(),
|
|
|
|
|
$responseFactory,
|
|
|
|
|
$cors
|
|
|
|
|
);
|
2022-11-18 10:00:05 +00:00
|
|
|
}
|
|
|
|
|
|
2023-10-30 16:04:41 +00:00
|
|
|
return $router;
|
2022-11-18 10:00:05 +00:00
|
|
|
}
|
2020-01-22 19:08:38 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize/fetch the Router instance for testing
|
2022-11-18 10:00:05 +00:00
|
|
|
* @warning Must not be called in data providers!
|
2020-01-22 19:08:38 +00:00
|
|
|
* @return Router
|
|
|
|
|
*/
|
2023-10-30 16:04:41 +00:00
|
|
|
private function getTestRouter(): Router {
|
2020-08-22 19:26:19 +00:00
|
|
|
if ( !$this->router ) {
|
2020-01-22 19:08:38 +00:00
|
|
|
$context = new DerivativeContext( RequestContext::getMain() );
|
|
|
|
|
$context->setLanguage( 'en' );
|
|
|
|
|
$context->setTitle(
|
|
|
|
|
Title::makeTitle( NS_SPECIAL, 'Badtitle/dummy title for RestStructureTest' )
|
|
|
|
|
);
|
|
|
|
|
|
2020-08-22 19:26:19 +00:00
|
|
|
$responseFactory = $this->createNoOpMock( ResponseFactory::class );
|
|
|
|
|
$cors = $this->createNoOpMock( CorsUtils::class );
|
|
|
|
|
|
2023-11-23 09:34:04 +00:00
|
|
|
$this->router = EntryPoint::createRouter(
|
|
|
|
|
$this->getServiceContainer(), $context, new RequestData(), $responseFactory, $cors
|
|
|
|
|
);
|
2020-01-22 19:08:38 +00:00
|
|
|
}
|
2020-08-22 19:26:19 +00:00
|
|
|
return $this->router;
|
2020-01-22 19:08:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2022-11-18 10:00:05 +00:00
|
|
|
* @dataProvider provideRoutes
|
2020-01-22 19:08:38 +00:00
|
|
|
*/
|
2023-10-30 16:04:41 +00:00
|
|
|
public function testPathParameters( string $moduleName, string $method, string $path ): void {
|
|
|
|
|
$router = $this->getTestRouter();
|
|
|
|
|
$module = $router->getModule( $moduleName );
|
|
|
|
|
|
|
|
|
|
$request = new RequestData( [ 'method' => $method ] );
|
|
|
|
|
$handler = $module->getHandlerForPath( $path, $request, false );
|
2020-01-22 19:08:38 +00:00
|
|
|
|
2023-10-30 16:04:41 +00:00
|
|
|
$params = $handler->getParamSettings();
|
2020-01-22 19:08:38 +00:00
|
|
|
$dataName = $this->dataName();
|
|
|
|
|
|
|
|
|
|
// Test that all parameters in the path exist and are declared as such
|
|
|
|
|
$matcher = TestingAccessWrapper::newFromObject( new PathMatcher );
|
|
|
|
|
$pathParams = [];
|
2023-10-30 16:04:41 +00:00
|
|
|
foreach ( explode( '/', $path ) as $part ) {
|
2020-01-22 19:08:38 +00:00
|
|
|
$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] )
|
|
|
|
|
) {
|
2020-02-28 15:45:22 +00:00
|
|
|
$this->assertFalse( $settings[ParamValidator::PARAM_REQUIRED] ?? false,
|
2020-01-22 19:08:38 +00:00
|
|
|
"$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 );
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-25 17:39:25 +00:00
|
|
|
/**
|
|
|
|
|
* @dataProvider provideRoutes
|
|
|
|
|
*/
|
2023-10-30 16:04:41 +00:00
|
|
|
public function testBodyParameters( string $moduleName, string $method, string $path ): void {
|
|
|
|
|
$router = $this->getTestRouter();
|
|
|
|
|
$module = $router->getModule( $moduleName );
|
|
|
|
|
|
|
|
|
|
$request = new RequestData( [ 'method' => $method ] );
|
|
|
|
|
$handler = $module->getHandlerForPath( $path, $request, false );
|
2024-04-25 17:39:25 +00:00
|
|
|
|
|
|
|
|
$bodySettings = $handler->getBodyParamSettings();
|
|
|
|
|
|
2024-06-20 13:33:35 +00:00
|
|
|
if ( !$bodySettings ) {
|
2024-04-25 17:39:25 +00:00
|
|
|
$this->addToAssertionCount( 1 );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ( $bodySettings as $settings ) {
|
|
|
|
|
$this->assertArrayHasKey( Handler::PARAM_SOURCE, $settings );
|
|
|
|
|
$this->assertSame( 'body', $settings[Handler::PARAM_SOURCE] );
|
2024-07-11 10:53:29 +00:00
|
|
|
|
|
|
|
|
if ( isset( $settings[ ArrayDef::PARAM_SCHEMA ] ) ) {
|
|
|
|
|
try {
|
|
|
|
|
self::validateSchema( $settings[ ArrayDef::PARAM_SCHEMA ] );
|
|
|
|
|
$this->addToAssertionCount( 1 );
|
|
|
|
|
} catch ( LogicException $e ) {
|
|
|
|
|
$this->fail( "Invalid JSON schema for parameter {$settings['name']}: " . $e->getMessage() );
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-04-25 17:39:25 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-20 13:33:35 +00:00
|
|
|
/**
|
|
|
|
|
* @dataProvider provideRoutes
|
|
|
|
|
*/
|
|
|
|
|
public function testBodyParametersNotInParamSettings( string $moduleName, string $method, string $path ): void {
|
|
|
|
|
$router = $this->getTestRouter();
|
|
|
|
|
$module = $router->getModule( $moduleName );
|
|
|
|
|
|
|
|
|
|
$request = new RequestData( [ 'method' => $method ] );
|
|
|
|
|
$handler = $module->getHandlerForPath( $path, $request, false );
|
|
|
|
|
|
|
|
|
|
$paramSettings = $handler->getParamSettings();
|
|
|
|
|
|
|
|
|
|
if ( !$paramSettings ) {
|
|
|
|
|
$this->addToAssertionCount( 1 );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ( $paramSettings as $settings ) {
|
|
|
|
|
$this->assertArrayHasKey( Handler::PARAM_SOURCE, $settings );
|
|
|
|
|
$this->assertNotSame( 'body', $settings[Handler::PARAM_SOURCE] );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-30 16:04:41 +00:00
|
|
|
public function provideModules(): Iterator {
|
|
|
|
|
$router = $this->getRouterForDataProviders();
|
|
|
|
|
|
|
|
|
|
foreach ( $router->getModuleNames() as $name ) {
|
|
|
|
|
yield "Module '$name'" => [ $name ];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-18 10:00:05 +00:00
|
|
|
public function provideRoutes(): Iterator {
|
2023-10-30 16:04:41 +00:00
|
|
|
$router = $this->getRouterForDataProviders();
|
|
|
|
|
|
|
|
|
|
foreach ( $router->getModuleNames() as $moduleName ) {
|
|
|
|
|
$module = $router->getModule( $moduleName );
|
2020-03-23 21:53:02 +00:00
|
|
|
|
2023-10-30 16:04:41 +00:00
|
|
|
foreach ( $module->getDefinedPaths() as $path => $methods ) {
|
|
|
|
|
|
|
|
|
|
foreach ( $methods as $method ) {
|
|
|
|
|
// NOTE: we can't use the $module object directly, since it
|
|
|
|
|
// may hold references to incorrect service instance.
|
|
|
|
|
yield "Handler in module '$moduleName' for $method $path"
|
|
|
|
|
=> [ $moduleName, $method, $path ];
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-01-22 19:08:38 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2022-11-18 10:00:05 +00:00
|
|
|
* @dataProvider provideRoutes
|
2020-01-22 19:08:38 +00:00
|
|
|
*/
|
2023-10-30 16:04:41 +00:00
|
|
|
public function testParameters( string $moduleName, string $method, string $path ): void {
|
|
|
|
|
$router = $this->getTestRouter();
|
|
|
|
|
$module = $router->getModule( $moduleName );
|
2022-11-18 10:00:05 +00:00
|
|
|
|
2023-10-30 16:04:41 +00:00
|
|
|
$request = new RequestData( [ 'method' => $method ] );
|
|
|
|
|
$handler = $module->getHandlerForPath( $path, $request, false );
|
2022-11-18 10:00:05 +00:00
|
|
|
|
|
|
|
|
$params = $handler->getParamSettings();
|
|
|
|
|
foreach ( $params as $param => $settings ) {
|
|
|
|
|
$method = $routeSpec['method'] ?? 'GET';
|
|
|
|
|
$method = implode( ",", (array)$method );
|
|
|
|
|
|
2023-10-30 16:04:41 +00:00
|
|
|
$this->assertParameter( $param, $settings, "Handler {$method} {$path}, parameter $param" );
|
2022-11-18 10:00:05 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function assertParameter( string $name, $settings, $msg ) {
|
2023-10-30 16:04:41 +00:00
|
|
|
$router = TestingAccessWrapper::newFromObject( $this->getTestRouter() );
|
2020-01-22 19:08:38 +00:00
|
|
|
|
|
|
|
|
$dataName = $this->dataName();
|
2022-11-18 10:00:05 +00:00
|
|
|
$this->assertNotSame( '', $name, "$msg: $dataName: Name cannot be empty" );
|
2020-01-22 19:08:38 +00:00
|
|
|
|
|
|
|
|
$paramValidator = TestingAccessWrapper::newFromObject( $router->restValidator )->paramValidator;
|
|
|
|
|
$ret = $paramValidator->checkSettings( $name, $settings, [ 'source' => 'unspecified' ] );
|
|
|
|
|
|
|
|
|
|
// REST-specific parameters
|
|
|
|
|
$ret['allowedKeys'][] = Handler::PARAM_SOURCE;
|
2024-03-12 10:52:13 +00:00
|
|
|
$ret['allowedKeys'][] = Handler::PARAM_DESCRIPTION;
|
2024-03-27 10:40:13 +00:00
|
|
|
if ( !in_array( $settings[Handler::PARAM_SOURCE] ?? '', Validator::KNOWN_PARAM_SOURCES, true ) ) {
|
|
|
|
|
$ret['issues'][Handler::PARAM_SOURCE] = "PARAM_SOURCE must be one of " . implode( ', ', Validator::KNOWN_PARAM_SOURCES );
|
2020-01-22 19:08:38 +00:00
|
|
|
}
|
|
|
|
|
|
2024-07-24 14:43:57 +00:00
|
|
|
// Check that "array" type is not used in getParamSettings
|
|
|
|
|
if ( isset( $settings[ParamValidator::PARAM_TYPE] ) && $settings[ParamValidator::PARAM_TYPE] === 'array' ) {
|
|
|
|
|
$this->fail( "$msg: $dataName: 'array' type is not allowed in getParamSettings" );
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-22 19:08:38 +00:00
|
|
|
// 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(
|
2022-11-18 10:00:05 +00:00
|
|
|
"$msg: $dataName: Unrecognized settings keys were used: " . implode( ', ', $keys )
|
2020-01-22 19:08:38 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( count( $ret['issues'] ) === 1 ) {
|
2022-11-18 10:00:05 +00:00
|
|
|
$this->fail( "$msg: $dataName: Validation failed: " . reset( $ret['issues'] ) );
|
2020-01-22 19:08:38 +00:00
|
|
|
} elseif ( $ret['issues'] ) {
|
2022-11-18 10:00:05 +00:00
|
|
|
$this->fail( "$msg: $dataName: Validation failed:\n* " . implode( "\n* ", $ret['issues'] ) );
|
2020-01-22 19:08:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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(),
|
2022-11-18 10:00:05 +00:00
|
|
|
"$msg: $dataName: Parameter message $key exists" );
|
2020-03-23 21:53:02 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testRoutePathAndMethodForDuplicates() {
|
2023-10-30 16:04:41 +00:00
|
|
|
$router = $this->getTestRouter();
|
2020-03-23 21:53:02 +00:00
|
|
|
$routes = [];
|
|
|
|
|
|
2023-10-30 16:04:41 +00:00
|
|
|
foreach ( $router->getModuleNames() as $moduleName ) {
|
|
|
|
|
$module = $router->getModule( $moduleName );
|
|
|
|
|
$paths = $module->getDefinedPaths();
|
2020-03-23 21:53:02 +00:00
|
|
|
|
2023-10-30 16:04:41 +00:00
|
|
|
foreach ( $paths as $path => $methods ) {
|
|
|
|
|
foreach ( $methods as $method ) {
|
|
|
|
|
// NOTE: we can't use the $module object directly, since it
|
|
|
|
|
// may hold references to incorrect service instance.
|
|
|
|
|
$key = "$moduleName: $method $path";
|
2020-03-23 21:53:02 +00:00
|
|
|
|
2023-10-30 16:04:41 +00:00
|
|
|
$this->assertArrayNotHasKey( $key, $routes, "{$key} already exists in routes" );
|
|
|
|
|
$routes[$key] = true;
|
|
|
|
|
}
|
2020-01-22 19:08:38 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-10-30 16:04:41 +00:00
|
|
|
|
2024-07-11 10:53:29 +00:00
|
|
|
/**
|
|
|
|
|
* Validate a JSON schema.
|
|
|
|
|
*
|
|
|
|
|
* @param array $schema The schema to validate.
|
|
|
|
|
* @throws LogicException if the schema is invalid
|
|
|
|
|
*/
|
|
|
|
|
public static function validateSchema( array $schema ): void {
|
|
|
|
|
$validator = new JsonValidator();
|
|
|
|
|
// Load the draft-04 schema from the local file
|
|
|
|
|
$draft04Schema = json_decode( file_get_contents( __DIR__ . '/../../../vendor/justinrainbow/json-schema/dist/schema/json-schema-draft-04.json' ) );
|
|
|
|
|
|
|
|
|
|
// Validate the schema itself against the meta-schema
|
|
|
|
|
$validator->validate( $schema, $draft04Schema, Constraint::CHECK_MODE_TYPE_CAST );
|
|
|
|
|
|
|
|
|
|
if ( !$validator->isValid() ) {
|
|
|
|
|
$errors = $validator->getErrors();
|
|
|
|
|
$messages = array_map( static function ( $error ) {
|
|
|
|
|
return sprintf( "[%s] %s", $error['property'], $error['message'] );
|
|
|
|
|
}, $errors );
|
|
|
|
|
|
|
|
|
|
throw new LogicException( "Invalid JSON schema: " . implode( "; ", $messages ) );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-22 19:08:38 +00:00
|
|
|
}
|