3 namespace MediaWiki\Tests\Api\Validator
;
9 use MediaWiki\Api\Validator\ApiParamValidator
;
10 use MediaWiki\Message\Message
;
11 use MediaWiki\Request\FauxRequest
;
12 use MediaWiki\Tests\Api\ApiTestCase
;
13 use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait
;
14 use Wikimedia\Message\DataMessageValue
;
15 use Wikimedia\Message\MessageValue
;
16 use Wikimedia\ParamValidator\ParamValidator
;
17 use Wikimedia\ParamValidator\TypeDef\EnumDef
;
18 use Wikimedia\ParamValidator\TypeDef\IntegerDef
;
19 use Wikimedia\TestingAccessWrapper
;
22 * @covers \MediaWiki\Api\Validator\ApiParamValidator
26 class ApiParamValidatorTest
extends ApiTestCase
{
27 use MockAuthorityTrait
;
29 private function getValidator( FauxRequest
$request ): array {
30 $context = $this->apiContext
->newTestContext( $request, $this->mockRegisteredUltimateAuthority() );
31 $main = new ApiMain( $context );
33 new ApiParamValidator( $main, $this->getServiceContainer()->getObjectFactory() ),
38 public function testKnownTypes(): void
{
39 [ $validator ] = $this->getValidator( new FauxRequest( [] ) );
42 'boolean', 'enum', 'expiry', 'integer', 'limit', 'namespace', 'NULL', 'password',
43 'raw', 'string', 'submodule', 'tags', 'text', 'timestamp', 'title', 'user', 'upload',
45 $validator->knownTypes()
50 * @dataProvider provideNormalizeSettings
51 * @param array|mixed $settings
52 * @param array $expect
54 public function testNormalizeSettings( $settings, array $expect ): void
{
55 [ $validator ] = $this->getValidator( new FauxRequest( [] ) );
56 $this->assertEquals( $expect, $validator->normalizeSettings( $settings ) );
59 public static function provideNormalizeSettings(): array {
64 ParamValidator
::PARAM_IGNORE_UNRECOGNIZED_VALUES
=> true,
65 IntegerDef
::PARAM_IGNORE_RANGE
=> true,
66 ParamValidator
::PARAM_TYPE
=> 'NULL',
69 'Explicit ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES' => [
71 ParamValidator
::PARAM_IGNORE_UNRECOGNIZED_VALUES
=> false,
74 ParamValidator
::PARAM_IGNORE_UNRECOGNIZED_VALUES
=> false,
75 IntegerDef
::PARAM_IGNORE_RANGE
=> true,
76 ParamValidator
::PARAM_TYPE
=> 'NULL',
79 'Explicit IntegerDef::PARAM_IGNORE_RANGE' => [
81 IntegerDef
::PARAM_IGNORE_RANGE
=> false,
84 ParamValidator
::PARAM_IGNORE_UNRECOGNIZED_VALUES
=> true,
85 IntegerDef
::PARAM_IGNORE_RANGE
=> false,
86 ParamValidator
::PARAM_TYPE
=> 'NULL',
89 'Handle ApiBase::PARAM_RANGE_ENFORCE' => [
91 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
94 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
95 ParamValidator
::PARAM_IGNORE_UNRECOGNIZED_VALUES
=> true,
96 IntegerDef
::PARAM_IGNORE_RANGE
=> false,
97 ParamValidator
::PARAM_TYPE
=> 'NULL',
100 'Handle EnumDef::PARAM_DEPRECATED_VALUES, null' => [
102 EnumDef
::PARAM_DEPRECATED_VALUES
=> [
105 'string' => 'some-message',
106 'array' => [ 'some-message', 'with', 'params' ],
107 'Message' => ApiMessage
::create(
108 [ 'api-message', 'with', 'params' ], 'somecode', [ 'some-data' ]
110 'MessageValue' => MessageValue
::new( 'message-value', [ 'with', 'params' ] ),
111 'DataMessageValue' => DataMessageValue
::new(
112 'data-message-value', [ 'with', 'params' ], 'somecode', [ 'some-data' ]
117 ParamValidator
::PARAM_IGNORE_UNRECOGNIZED_VALUES
=> true,
118 IntegerDef
::PARAM_IGNORE_RANGE
=> true,
119 EnumDef
::PARAM_DEPRECATED_VALUES
=> [
122 'string' => DataMessageValue
::new( 'some-message', [], 'bogus', [ '💩' => 'back-compat' ] ),
123 'array' => DataMessageValue
::new(
124 'some-message', [ 'with', 'params' ], 'bogus', [ '💩' => 'back-compat' ]
126 'Message' => DataMessageValue
::new(
127 'api-message', [ 'with', 'params' ], 'bogus', [ '💩' => 'back-compat' ]
129 'MessageValue' => MessageValue
::new( 'message-value', [ 'with', 'params' ] ),
130 'DataMessageValue' => DataMessageValue
::new(
131 'data-message-value', [ 'with', 'params' ], 'somecode', [ 'some-data' ]
134 ParamValidator
::PARAM_TYPE
=> 'NULL',
141 * @dataProvider provideCheckSettings
142 * @param array $params All module parameters.
143 * @param string $name Parameter to test.
144 * @param array $expect
146 public function testCheckSettings( array $params, string $name, array $expect ): void
{
147 [ $validator, $main ] = $this->getValidator( new FauxRequest( [] ) );
148 $module = $main->getModuleFromPath( 'query+allpages' );
150 $mock = $this->getMockBuilder( ParamValidator
::class )
151 ->disableOriginalConstructor()
152 ->onlyMethods( [ 'checkSettings' ] )
154 $mock->expects( $this->once() )->method( 'checkSettings' )
155 ->willReturnCallback( function ( $n, $settings, $options ) use ( $name, $module ) {
156 $this->assertSame( "ap$name", $n );
157 $this->assertSame( [ 'module' => $module ], $options );
159 $ret = [ 'issues' => [ 'X' ], 'allowedKeys' => [ 'Y' ], 'messages' => [] ];
160 $stack = is_array( $settings ) ?
[ &$settings ] : [];
162 foreach ( $stack[0] as $k => $v ) {
163 if ( $v instanceof MessageValue
) {
164 $ret['messages'][] = $v;
165 } elseif ( is_array( $v ) ) {
166 $stack[] = &$stack[0][$k];
169 array_shift( $stack );
173 TestingAccessWrapper
::newFromObject( $validator )->paramValidator
= $mock;
175 $this->assertEquals( $expect, $validator->checkSettings( $module, $params, $name, [] ) );
178 public static function provideCheckSettings() {
180 'Y', ApiBase
::PARAM_RANGE_ENFORCE
, ApiBase
::PARAM_HELP_MSG
, ApiBase
::PARAM_HELP_MSG_APPEND
,
181 ApiBase
::PARAM_HELP_MSG_INFO
, ApiBase
::PARAM_HELP_MSG_PER_VALUE
, ApiBase
::PARAM_TEMPLATE_VARS
,
190 'allowedKeys' => $keys,
192 MessageValue
::new( 'apihelp-query+allpages-param-test' ),
196 'Message mapping' => [
198 EnumDef
::PARAM_DEPRECATED_VALUES
=> [
201 'c' => [ 'ccc', 'p1', 'p2' ],
202 'd' => Message
::newFromKey( 'ddd' )->plaintextParams( 'p1', 'p2' ),
208 'allowedKeys' => $keys,
210 DataMessageValue
::new( 'bbb', [], 'bogus', [ '💩' => 'back-compat' ] ),
211 DataMessageValue
::new( 'ccc', [], 'bogus', [ '💩' => 'back-compat' ] )
212 ->params( 'p1', 'p2' ),
213 DataMessageValue
::new( 'ddd', [], 'bogus', [ '💩' => 'back-compat' ] )
214 ->plaintextParams( 'p1', 'p2' ),
215 MessageValue
::new( 'apihelp-query+allpages-param-test' ),
219 'Test everything' => [
222 ParamValidator
::PARAM_TYPE
=> 'not tested here',
223 ParamValidator
::PARAM_ISMULTI
=> true
226 ParamValidator
::PARAM_TYPE
=> [],
227 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
228 ApiBase
::PARAM_HELP_MSG
=> 'foo',
229 ApiBase
::PARAM_HELP_MSG_APPEND
=> [],
230 ApiBase
::PARAM_HELP_MSG_INFO
=> [],
231 ApiBase
::PARAM_HELP_MSG_PER_VALUE
=> [],
232 ApiBase
::PARAM_TEMPLATE_VARS
=> [
240 'allowedKeys' => $keys,
242 MessageValue
::new( 'foo' ),
248 ApiBase
::PARAM_RANGE_ENFORCE
=> 1,
249 ApiBase
::PARAM_HELP_MSG
=> false,
250 ApiBase
::PARAM_HELP_MSG_APPEND
=> 'foo',
251 ApiBase
::PARAM_HELP_MSG_INFO
=> 'bar',
252 ApiBase
::PARAM_HELP_MSG_PER_VALUE
=> true,
253 ApiBase
::PARAM_TEMPLATE_VARS
=> false,
259 ApiBase
::PARAM_RANGE_ENFORCE
=> 'PARAM_RANGE_ENFORCE must be boolean, got integer',
260 'Message specification for PARAM_HELP_MSG is not valid',
261 ApiBase
::PARAM_HELP_MSG_APPEND
=> 'PARAM_HELP_MSG_APPEND must be an array, got string',
262 ApiBase
::PARAM_HELP_MSG_INFO
=> 'PARAM_HELP_MSG_INFO must be an array, got string',
263 ApiBase
::PARAM_HELP_MSG_PER_VALUE
=> 'PARAM_HELP_MSG_PER_VALUE must be an array, got boolean',
264 ApiBase
::PARAM_TEMPLATE_VARS
=> 'PARAM_TEMPLATE_VARS must be an array, got boolean',
266 'allowedKeys' => $keys,
270 'PARAM_HELP_MSG (string)' => [
272 ApiBase
::PARAM_HELP_MSG
=> 'foo',
277 'allowedKeys' => $keys,
279 MessageValue
::new( 'foo' ),
283 'PARAM_HELP_MSG (array)' => [
285 ApiBase
::PARAM_HELP_MSG
=> [ 'foo', 'bar' ],
290 'allowedKeys' => $keys,
292 MessageValue
::new( 'foo', [ 'bar' ] ),
296 'PARAM_HELP_MSG (Message)' => [
298 ApiBase
::PARAM_HELP_MSG
=> Message
::newFromKey( 'foo' )->numParams( 123 ),
303 'allowedKeys' => $keys,
305 MessageValue
::new( 'foo' )->numParams( 123 ),
309 'PARAM_HELP_MSG_APPEND' => [
310 [ 'test' => [ ApiBase
::PARAM_HELP_MSG_APPEND
=> [
313 [ 'bar', 'p1', 'p2' ],
314 Message
::newFromKey( 'baz' )->numParams( 123 ),
320 'Message specification for PARAM_HELP_MSG_APPEND[1] is not valid',
322 'allowedKeys' => $keys,
324 MessageValue
::new( 'apihelp-query+allpages-param-test' ),
325 MessageValue
::new( 'foo' ),
326 MessageValue
::new( 'bar', [ 'p1', 'p2' ] ),
327 MessageValue
::new( 'baz' )->numParams( 123 ),
331 'PARAM_HELP_MSG_INFO' => [
332 [ 'test' => [ ApiBase
::PARAM_HELP_MSG_INFO
=> [
336 [ 'bar', 'p1', 'p2' ],
342 'PARAM_HELP_MSG_INFO[0] must be an array, got string',
343 'PARAM_HELP_MSG_INFO[1][0] must be a string, got boolean',
345 'allowedKeys' => $keys,
347 MessageValue
::new( 'apihelp-query+allpages-param-test' ),
348 MessageValue
::new( 'apihelp-query+allpages-paraminfo-foo' ),
349 MessageValue
::new( 'apihelp-query+allpages-paraminfo-bar', [ 'p1', 'p2' ] ),
353 'PARAM_HELP_MSG_PER_VALUE for non-array type' => [
355 ParamValidator
::PARAM_TYPE
=> 'namespace',
356 ApiBase
::PARAM_HELP_MSG_PER_VALUE
=> [],
362 ApiBase
::PARAM_HELP_MSG_PER_VALUE
363 => 'PARAM_HELP_MSG_PER_VALUE can only be used with PARAM_TYPE as an array',
365 'allowedKeys' => $keys,
367 MessageValue
::new( 'apihelp-query+allpages-param-test' ),
371 'PARAM_HELP_MSG_PER_VALUE' => [
373 ParamValidator
::PARAM_TYPE
=> [ 'a', 'b', 'c', 'd' ],
374 ApiBase
::PARAM_HELP_MSG_PER_VALUE
=> [
377 'c' => [ 'ccc', 'p1', 'p2' ],
378 'd' => Message
::newFromKey( 'ddd' )->numParams( 123 ),
386 'Message specification for PARAM_HELP_MSG_PER_VALUE[a] is not valid',
387 'PARAM_HELP_MSG_PER_VALUE contains "e", which is not in PARAM_TYPE.',
389 'allowedKeys' => $keys,
391 MessageValue
::new( 'apihelp-query+allpages-param-test' ),
392 MessageValue
::new( 'bbb' ),
393 MessageValue
::new( 'ccc', [ 'p1', 'p2' ] ),
394 MessageValue
::new( 'ddd' )->numParams( 123 ),
395 MessageValue
::new( 'eee' ),
399 'Template-style parameter name without PARAM_TEMPLATE_VARS' => [
400 [ 'test{x}' => null ],
405 "Parameter name may not contain '{' or '}' without PARAM_TEMPLATE_VARS",
407 'allowedKeys' => $keys,
409 MessageValue
::new( 'apihelp-query+allpages-param-test{x}' ),
413 'PARAM_TEMPLATE_VARS cannot be empty' => [
415 ApiBase
::PARAM_TEMPLATE_VARS
=> [],
421 ApiBase
::PARAM_TEMPLATE_VARS
=> 'PARAM_TEMPLATE_VARS cannot be the empty array',
423 'allowedKeys' => $keys,
425 MessageValue
::new( 'apihelp-query+allpages-param-test{x}' ),
429 'PARAM_TEMPLATE_VARS, ok' => [
432 ParamValidator
::PARAM_ISMULTI
=> true,
434 'ok-templated-{x}' => [
435 ParamValidator
::PARAM_ISMULTI
=> true,
436 ApiBase
::PARAM_TEMPLATE_VARS
=> [
441 ApiBase
::PARAM_TEMPLATE_VARS
=> [
443 'b' => 'ok-templated-{x}',
450 'allowedKeys' => $keys,
452 MessageValue
::new( 'apihelp-query+allpages-param-test-{a}-{b}' ),
456 'PARAM_TEMPLATE_VARS simple errors' => [
459 ParamValidator
::PARAM_ISMULTI
=> true,
461 'not-multi' => false,
462 'test-{a}-{b}-{c}' => [
463 ApiBase
::PARAM_TEMPLATE_VARS
=> [
465 'not-in-name' => 'ok',
476 "PARAM_TEMPLATE_VARS keys may not contain '{' or '}', got \"{x}\"",
477 'Parameter name must contain PARAM_TEMPLATE_VARS key {not-in-name}',
478 'PARAM_TEMPLATE_VARS[a] has invalid target type boolean',
479 'PARAM_TEMPLATE_VARS[b] target parameter "missing" does not exist',
480 'PARAM_TEMPLATE_VARS[c] target parameter "not-multi" must have PARAM_ISMULTI = true',
482 'allowedKeys' => $keys,
484 MessageValue
::new( 'apihelp-query+allpages-param-test-{a}-{b}-{c}' ),
488 'PARAM_TEMPLATE_VARS no recursion' => [
491 ParamValidator
::PARAM_ISMULTI
=> true,
492 ApiBase
::PARAM_TEMPLATE_VARS
=> [
501 'PARAM_TEMPLATE_VARS[a] cannot target the parameter itself'
503 'allowedKeys' => $keys,
505 MessageValue
::new( 'apihelp-query+allpages-param-test-{a}' ),
509 'PARAM_TEMPLATE_VARS targeting another template, target must be a subset' => [
511 'ok1' => [ ParamValidator
::PARAM_ISMULTI
=> true ],
512 'ok2' => [ ParamValidator
::PARAM_ISMULTI
=> true ],
514 ApiBase
::PARAM_TEMPLATE_VARS
=> [
519 ParamValidator
::PARAM_ISMULTI
=> true,
520 ApiBase
::PARAM_TEMPLATE_VARS
=> [
529 'PARAM_TEMPLATE_VARS[a]: Target\'s PARAM_TEMPLATE_VARS must be a subset of the original',
531 'allowedKeys' => $keys,
533 MessageValue
::new( 'apihelp-query+allpages-param-test1-{a}' ),
541 * @dataProvider provideGetValue
543 public function testGetValue( ?
string $data, $settings, $expect ): void
{
544 [ $validator, $main ] = $this->getValidator( new FauxRequest( [ 'aptest' => $data ] ) );
545 $module = $main->getModuleFromPath( 'query+allpages' );
547 'parse-limit' => false,
548 'raw' => ( $settings[ParamValidator
::PARAM_TYPE
] ??
'' ) === 'raw',
551 if ( $expect instanceof ApiUsageException
) {
553 $validator->getValue( $module, 'test', $settings, $options );
554 $this->fail( 'Expected exception not thrown' );
555 } catch ( ApiUsageException
$e ) {
556 $this->assertSame( $module->getModulePath(), $e->getModulePath() );
557 $this->assertEquals( $expect->getStatusValue(), $e->getStatusValue() );
560 $this->assertEquals( $expect, $validator->getValue( $module, 'test', $settings, $options ) );
564 public static function provideGetValue(): array {
569 ParamValidator
::PARAM_TYPE
=> 'integer',
573 'Test for default' => [
581 ParamValidator
::PARAM_TYPE
=> 'integer',
585 'Test boolean (false)' => [
590 'Test boolean (true)' => [
595 // The 'string' type will be NFC normalized (in this case,
596 // U+2001 will be converted to U+2003; see Figure 5 of
597 // of https://unicode.org/reports/tr15 for more examples).
598 'Test string (Unicode NFC)' => [
601 ParamValidator
::PARAM_TYPE
=> 'string',
605 // The 'raw' type bypasses Unicode NFC normalization.
606 'Test string (raw)' => [
609 ParamValidator
::PARAM_TYPE
=> 'raw',
613 'Validation failure' => [
616 ParamValidator
::PARAM_TYPE
=> 'integer',
618 ApiUsageException
::newWithMessage( null, [
619 'paramvalidator-badinteger',
620 Message
::plaintextParam( 'aptest' ),
621 Message
::plaintextParam( 'xyz' ),
628 * @dataProvider provideValidateValue
630 public function testValidateValue( $value, $settings, $expect ): void
{
631 [ $validator, $main ] = $this->getValidator( new FauxRequest() );
632 $module = $main->getModuleFromPath( 'query+allpages' );
634 if ( $expect instanceof ApiUsageException
) {
636 $validator->validateValue( $module, 'test', $value, $settings, [] );
637 $this->fail( 'Expected exception not thrown' );
638 } catch ( ApiUsageException
$e ) {
639 $this->assertSame( $module->getModulePath(), $e->getModulePath() );
640 $this->assertEquals( $expect->getStatusValue(), $e->getStatusValue() );
645 $validator->validateValue( $module, 'test', $value, $settings, [] )
650 public static function provideValidateValue(): array {
655 ParamValidator
::PARAM_TYPE
=> 'integer',
659 'Validation failure' => [
662 ParamValidator
::PARAM_TYPE
=> 'integer',
663 IntegerDef
::PARAM_IGNORE_RANGE
=> false,
664 IntegerDef
::PARAM_MAX
=> 10,
666 ApiUsageException
::newWithMessage( null, [
667 'paramvalidator-outofrange-max',
668 Message
::plaintextParam( 'aptest' ),
669 Message
::plaintextParam( 1234 ),
670 Message
::numParam( '' ),
671 Message
::numParam( 10 ),
672 ], 'outofrange', [ 'min' => null, 'curmax' => 10, 'max' => 10, 'highmax' => 10 ] ),
677 public function testGetParamInfo() {
678 [ $validator, $main ] = $this->getValidator( new FauxRequest() );
679 $module = $main->getModuleFromPath( 'query+allpages' );
689 $mock = $this->getMockBuilder( ParamValidator
::class )
690 ->disableOriginalConstructor()
691 ->onlyMethods( [ 'getParamInfo' ] )
693 $mock->expects( $this->once() )->method( 'getParamInfo' )
695 $this->identicalTo( 'aptest' ),
696 $this->identicalTo( $settings ),
697 $this->identicalTo( $options +
[ 'module' => $module ] )
699 ->willReturn( [ $dummy ] );
701 TestingAccessWrapper
::newFromObject( $validator )->paramValidator
= $mock;
702 $this->assertSame( [ $dummy ], $validator->getParamInfo( $module, 'test', $settings, $options ) );
705 public function testGetHelpInfo() {
706 [ $validator, $main ] = $this->getValidator( new FauxRequest() );
707 $module = $main->getModuleFromPath( 'query+allpages' );
716 $mock = $this->getMockBuilder( ParamValidator
::class )
717 ->disableOriginalConstructor()
718 ->onlyMethods( [ 'getHelpInfo' ] )
720 $mock->expects( $this->once() )->method( 'getHelpInfo' )
722 $this->identicalTo( 'aptest' ),
723 $this->identicalTo( $settings ),
724 $this->identicalTo( $options +
[ 'module' => $module ] )
727 'mv1' => MessageValue
::new( 'parentheses', [ 'foobar' ] ),
728 'mv2' => MessageValue
::new( 'paramvalidator-help-continue' ),
731 TestingAccessWrapper
::newFromObject( $validator )->paramValidator
= $mock;
732 $ret = $validator->getHelpInfo( $module, 'test', $settings, $options );
733 $this->assertArrayHasKey( 'mv1', $ret );
734 $this->assertInstanceOf( Message
::class, $ret['mv1'] );
735 $this->assertEquals( '(parentheses: foobar)', $ret['mv1']->inLanguage( 'qqx' )->plain() );
736 $this->assertArrayHasKey( 'mv2', $ret );
737 $this->assertInstanceOf( Message
::class, $ret['mv2'] );
739 [ 'api-help-param-continue', 'paramvalidator-help-continue' ],
740 $ret['mv2']->getKeysToTry()
742 $this->assertCount( 2, $ret );