3 namespace MediaWiki\Tests\Api
;
7 use MediaWiki\Api\ApiBase
;
8 use MediaWiki\Api\ApiBlockInfoTrait
;
9 use MediaWiki\Api\ApiMain
;
10 use MediaWiki\Api\ApiMessage
;
11 use MediaWiki\Api\ApiUsageException
;
12 use MediaWiki\Api\Validator\SubmoduleDef
;
13 use MediaWiki\Block\DatabaseBlock
;
14 use MediaWiki\Context\DerivativeContext
;
15 use MediaWiki\Context\RequestContext
;
16 use MediaWiki\MediaWikiServices
;
17 use MediaWiki\Message\Message
;
18 use MediaWiki\ParamValidator\TypeDef\NamespaceDef
;
19 use MediaWiki\Permissions\PermissionStatus
;
20 use MediaWiki\Request\FauxRequest
;
21 use MediaWiki\Status\Status
;
22 use MediaWiki\Title\Title
;
25 use Wikimedia\Message\MessageSpecifier
;
26 use Wikimedia\ParamValidator\ParamValidator
;
27 use Wikimedia\ParamValidator\TypeDef\EnumDef
;
28 use Wikimedia\ParamValidator\TypeDef\IntegerDef
;
29 use Wikimedia\ParamValidator\TypeDef\StringDef
;
30 use Wikimedia\TestingAccessWrapper
;
38 * @covers \MediaWiki\Api\ApiBase
40 class ApiBaseTest
extends ApiTestCase
{
42 protected function setUp(): void
{
44 $this->setGroupPermissions( [
48 'apihighlimits' => false,
54 * This covers a variety of stub methods that return a fixed value.
56 * @dataProvider provideStubMethods
58 public function testStubMethods( $expected, $method, $args = [] ) {
59 // Some of these are protected
60 $mock = TestingAccessWrapper
::newFromObject( new MockApi() );
61 $result = $mock->$method( ...$args );
62 $this->assertSame( $expected, $result );
65 public static function provideStubMethods() {
67 [ null, 'getModuleManager' ],
68 [ null, 'getCustomPrinter' ],
69 [ [], 'getHelpUrls' ],
70 // @todo This is actually overridden by MockApi
71 // [ [], 'getAllowedParams' ],
72 [ true, 'shouldCheckMaxLag' ],
73 [ true, 'isReadMode' ],
74 [ false, 'isWriteMode' ],
75 [ false, 'mustBePosted' ],
76 [ false, 'isDeprecated' ],
77 [ false, 'isInternal' ],
78 [ false, 'needsToken' ],
79 [ null, 'getWebUITokenSalt', [ [] ] ],
80 [ null, 'getConditionalRequestData', [ 'etag' ] ],
81 [ null, 'dynamicParameterDocumentation' ],
85 public function testRequireOnlyOneParameterDefault() {
86 $mock = new MockApi();
87 $mock->requireOnlyOneParameter(
88 [ "filename" => "foo.txt", "enablechunks" => false ],
89 "filename", "enablechunks"
91 $this->assertTrue( true );
94 public function testRequireOnlyOneParameterZero() {
95 $mock = new MockApi();
96 $this->expectException( ApiUsageException
::class );
97 $mock->requireOnlyOneParameter(
98 [ "filename" => "foo.txt", "enablechunks" => 0 ],
99 "filename", "enablechunks"
103 public function testRequireOnlyOneParameterTrue() {
104 $mock = new MockApi();
105 $this->expectException( ApiUsageException
::class );
106 $mock->requireOnlyOneParameter(
107 [ "filename" => "foo.txt", "enablechunks" => true ],
108 "filename", "enablechunks"
112 public function testRequireOnlyOneParameterMissing() {
113 $this->expectApiErrorCode( 'missingparam' );
114 $mock = new MockApi();
115 $mock->requireOnlyOneParameter(
116 [ "filename" => "foo.txt", "enablechunks" => false ],
120 public function testRequireMaxOneParameterZero() {
121 $mock = new MockApi();
122 $mock->requireMaxOneParameter(
123 [ 'foo' => 'bar', 'baz' => 'quz' ],
125 $this->assertTrue( true );
128 public function testRequireMaxOneParameterOne() {
129 $mock = new MockApi();
130 $mock->requireMaxOneParameter(
131 [ 'foo' => 'bar', 'baz' => 'quz' ],
133 $this->assertTrue( true );
136 public function testRequireMaxOneParameterTwo() {
137 $this->expectApiErrorCode( 'invalidparammix' );
138 $mock = new MockApi();
139 $mock->requireMaxOneParameter(
140 [ 'foo' => 'bar', 'baz' => 'quz' ],
144 public function testRequireAtLeastOneParameterZero() {
145 $this->expectApiErrorCode( 'missingparam' );
146 $mock = new MockApi();
147 $mock->requireAtLeastOneParameter(
148 [ 'a' => 'b', 'c' => 'd' ],
152 public function testRequireAtLeastOneParameterOne() {
153 $mock = new MockApi();
154 $mock->requireAtLeastOneParameter(
155 [ 'a' => 'b', 'c' => 'd' ],
157 $this->assertTrue( true );
160 public function testRequireAtLeastOneParameterTwo() {
161 $mock = new MockApi();
162 $mock->requireAtLeastOneParameter(
163 [ 'a' => 'b', 'c' => 'd' ],
165 $this->assertTrue( true );
168 public function testGetTitleOrPageIdBadParams() {
169 $this->expectApiErrorCode( 'invalidparammix' );
170 $mock = new MockApi();
171 $mock->getTitleOrPageId( [ 'title' => 'a', 'pageid' => 7 ] );
174 public function testGetTitleOrPageIdTitle() {
175 $mock = new MockApi();
176 $result = $mock->getTitleOrPageId( [ 'title' => 'Foo' ] );
177 $this->assertInstanceOf( WikiPage
::class, $result );
178 $this->assertSame( 'Foo', $result->getTitle()->getPrefixedText() );
181 public function testGetTitleOrPageIdInvalidTitle() {
182 $this->expectApiErrorCode( 'invalidtitle' );
183 $mock = new MockApi();
184 $mock->getTitleOrPageId( [ 'title' => '|' ] );
187 public function testGetTitleOrPageIdSpecialTitle() {
188 $this->expectApiErrorCode( 'pagecannotexist' );
189 $mock = new MockApi();
190 $mock->getTitleOrPageId( [ 'title' => 'Special:RandomPage' ] );
193 public function testGetTitleOrPageIdPageId() {
194 $page = $this->getExistingTestPage();
195 $result = ( new MockApi() )->getTitleOrPageId(
196 [ 'pageid' => $page->getId() ] );
197 $this->assertInstanceOf( WikiPage
::class, $result );
199 $page->getTitle()->getPrefixedText(),
200 $result->getTitle()->getPrefixedText()
204 public function testGetTitleOrPageIdInvalidPageId() {
205 $this->expectApiErrorCode( 'nosuchpageid' );
206 $mock = new MockApi();
207 $mock->getTitleOrPageId( [ 'pageid' => 2147483648 ] );
210 public function testGetTitleFromTitleOrPageIdBadParams() {
211 $this->expectApiErrorCode( 'invalidparammix' );
212 $mock = new MockApi();
213 $mock->getTitleFromTitleOrPageId( [ 'title' => 'a', 'pageid' => 7 ] );
216 public function testGetTitleFromTitleOrPageIdTitle() {
217 $mock = new MockApi();
218 $result = $mock->getTitleFromTitleOrPageId( [ 'title' => 'Foo' ] );
219 $this->assertInstanceOf( Title
::class, $result );
220 $this->assertSame( 'Foo', $result->getPrefixedText() );
223 public function testGetTitleFromTitleOrPageIdInvalidTitle() {
224 $this->expectApiErrorCode( 'invalidtitle' );
225 $mock = new MockApi();
226 $mock->getTitleFromTitleOrPageId( [ 'title' => '|' ] );
229 public function testGetTitleFromTitleOrPageIdPageId() {
230 $page = $this->getExistingTestPage();
231 $result = ( new MockApi() )->getTitleFromTitleOrPageId(
232 [ 'pageid' => $page->getId() ] );
233 $this->assertInstanceOf( Title
::class, $result );
234 $this->assertSame( $page->getTitle()->getPrefixedText(), $result->getPrefixedText() );
237 public function testGetTitleFromTitleOrPageIdInvalidPageId() {
238 $this->expectApiErrorCode( 'nosuchpageid' );
239 $mock = new MockApi();
240 $mock->getTitleFromTitleOrPageId( [ 'pageid' => 298401643 ] );
243 public function testGetParameter() {
244 $mock = $this->getMockBuilder( MockApi
::class )
245 ->onlyMethods( [ 'getAllowedParams' ] )
247 $mock->method( 'getAllowedParams' )->willReturn( [
249 ParamValidator
::PARAM_TYPE
=> [ 'value' ],
252 ParamValidator
::PARAM_TYPE
=> [ 'value' ],
255 $wrapper = TestingAccessWrapper
::newFromObject( $mock );
257 $context = new DerivativeContext( $mock );
258 $context->setRequest( new FauxRequest( [ 'foo' => 'bad', 'bar' => 'value' ] ) );
259 $wrapper->mMainModule
= new ApiMain( $context );
261 // Even though 'foo' is bad, getParameter( 'bar' ) must not fail
262 $this->assertSame( 'value', $wrapper->getParameter( 'bar' ) );
264 // But getParameter( 'foo' ) must throw.
266 $wrapper->getParameter( 'foo' );
267 $this->fail( 'Expected exception not thrown' );
268 } catch ( ApiUsageException
$ex ) {
269 $this->assertApiErrorCode( 'badvalue', $ex );
272 // And extractRequestParams() must throw too.
274 $mock->extractRequestParams();
275 $this->fail( 'Expected exception not thrown' );
276 } catch ( ApiUsageException
$ex ) {
277 $this->assertApiErrorCode( 'badvalue', $ex );
282 * @param string|null $input
283 * @param array $paramSettings
284 * @param mixed $expected
285 * @param string[] $warnings
286 * @param array $options Key-value pairs:
287 * 'parseLimits': true|false
288 * 'apihighlimits': true|false
289 * 'prefix': true|false
291 private function doGetParameterFromSettings(
292 $input, $paramSettings, $expected, $warnings, $options = []
294 $mock = new MockApi();
295 $wrapper = TestingAccessWrapper
::newFromObject( $mock );
296 if ( $options['prefix'] ) {
297 $wrapper->mModulePrefix
= 'my';
298 $paramName = 'Param';
300 $paramName = 'myParam';
303 $context = new DerivativeContext( $mock );
304 $context->setRequest( new FauxRequest(
305 $input !== null ?
[ 'myParam' => $input ] : [] ) );
306 $wrapper->mMainModule
= new ApiMain( $context );
308 $parseLimits = $options['parseLimits'] ??
true;
310 if ( !empty( $options['apihighlimits'] ) ) {
311 $context->setUser( $this->getTestSysop()->getUser() );
314 // If we're testing tags, set up some tags
315 if ( isset( $paramSettings[ParamValidator
::PARAM_TYPE
] ) &&
316 $paramSettings[ParamValidator
::PARAM_TYPE
] === 'tags'
318 $changeTagStore = $this->getServiceContainer()->getChangeTagsStore();
319 $changeTagStore->defineTag( 'tag1' );
320 $changeTagStore->defineTag( 'tag2' );
323 if ( $expected instanceof Exception
) {
325 $wrapper->getParameterFromSettings( $paramName, $paramSettings,
327 $this->fail( 'No exception thrown' );
328 } catch ( Exception
$ex ) {
329 $this->assertInstanceOf( get_class( $expected ), $ex );
330 if ( $ex instanceof ApiUsageException
) {
331 $this->assertEquals( $expected->getModulePath(), $ex->getModulePath() );
332 $this->assertEquals( $expected->getStatusValue(), $ex->getStatusValue() );
334 $this->assertEquals( $expected->getMessage(), $ex->getMessage() );
335 $this->assertEquals( $expected->getCode(), $ex->getCode() );
339 $result = $wrapper->getParameterFromSettings( $paramName,
340 $paramSettings, $parseLimits );
341 if ( isset( $paramSettings[ParamValidator
::PARAM_TYPE
] ) &&
342 $paramSettings[ParamValidator
::PARAM_TYPE
] === 'timestamp' &&
345 // Allow one second of fuzziness. Make sure the formats are
347 $this->assertMatchesRegularExpression( '/^\d{14}$/', $result );
348 $this->assertLessThanOrEqual( 1,
349 abs( wfTimestamp( TS_UNIX
, $result ) - time() ),
350 "Result $result differs from expected $expected by " .
351 'more than one second' );
353 $this->assertSame( $expected, $result );
355 $actualWarnings = array_map( static function ( $warn ) {
356 return $warn instanceof MessageSpecifier
357 ?
[ $warn->getKey(), ...$warn->getParams() ]
359 }, $mock->warnings
);
360 $this->assertEquals( $warnings, $actualWarnings );
363 if ( !empty( $paramSettings[ParamValidator
::PARAM_SENSITIVE
] ) ||
364 ( isset( $paramSettings[ParamValidator
::PARAM_TYPE
] ) &&
365 $paramSettings[ParamValidator
::PARAM_TYPE
] === 'password' )
367 $mainWrapper = TestingAccessWrapper
::newFromObject( $wrapper->getMain() );
368 $this->assertSame( [ 'myParam' ],
369 $mainWrapper->getSensitiveParams() );
374 * @dataProvider provideGetParameterFromSettings
375 * @see self::doGetParameterFromSettings()
377 public function testGetParameterFromSettings_noprefix(
378 $input, $paramSettings, $expected, $warnings, $options = []
380 $options['prefix'] = false;
381 $this->doGetParameterFromSettings( $input, $paramSettings, $expected, $warnings, $options );
385 * @dataProvider provideGetParameterFromSettings
386 * @see self::doGetParameterFromSettings()
388 public function testGetParameterFromSettings_prefix(
389 $input, $paramSettings, $expected, $warnings, $options = []
391 $options['prefix'] = true;
392 $this->doGetParameterFromSettings( $input, $paramSettings, $expected, $warnings, $options );
395 public static function provideGetParameterFromSettings() {
397 [ 'apiwarn-badutf8', 'myParam' ],
402 for ( $i = 0; $i < 32; $i++
) {
404 $enc .= ( $i === 9 ||
$i === 10 ||
$i === 13 )
409 $namespaces = MediaWikiServices
::getInstance()->getNamespaceInfo()->getValidNamespaces();
412 'Basic param' => [ 'bar', null, 'bar', [] ],
413 'Basic param, C0 controls' => [ $c0, null, $enc, $warnings ],
414 'String param' => [ 'bar', '', 'bar', [] ],
415 'String param, defaulted' => [ null, '', '', [] ],
416 'String param, empty' => [ '', 'default', '', [] ],
417 'String param, required, empty' => [
419 [ ParamValidator
::PARAM_DEFAULT
=> 'default', ParamValidator
::PARAM_REQUIRED
=> true ],
420 ApiUsageException
::newWithMessage( null, [
421 'paramvalidator-missingparam',
422 Message
::plaintextParam( 'myParam' ),
423 Message
::plaintextParam( '' ),
427 'Multi-valued parameter' => [
429 [ ParamValidator
::PARAM_ISMULTI
=> true ],
433 'Multi-valued parameter, alternative separator' => [
435 [ ParamValidator
::PARAM_ISMULTI
=> true ],
439 'Multi-valued parameter, other C0 controls' => [
441 [ ParamValidator
::PARAM_ISMULTI
=> true ],
445 'Multi-valued parameter, other C0 controls (2)' => [
447 [ ParamValidator
::PARAM_ISMULTI
=> true ],
448 [ substr( $enc, 0, -3 ), '' ],
451 'Multi-valued parameter with limits' => [
454 ParamValidator
::PARAM_ISMULTI
=> true,
455 ParamValidator
::PARAM_ISMULTI_LIMIT1
=> 3,
460 'Multi-valued parameter with exceeded limits' => [
463 ParamValidator
::PARAM_ISMULTI
=> true,
464 ParamValidator
::PARAM_ISMULTI_LIMIT1
=> 2,
466 ApiUsageException
::newWithMessage( null, [
467 'paramvalidator-toomanyvalues',
468 Message
::plaintextParam( 'myParam' ),
469 Message
::numParam( 2 ),
470 ], 'toomanyvalues', [
471 'parameter' => 'myParam',
478 'Multi-valued parameter with exceeded limits for non-bot' => [
481 ParamValidator
::PARAM_ISMULTI
=> true,
482 ParamValidator
::PARAM_ISMULTI_LIMIT1
=> 2,
483 ParamValidator
::PARAM_ISMULTI_LIMIT2
=> 3,
485 ApiUsageException
::newWithMessage( null, [
486 'paramvalidator-toomanyvalues',
487 Message
::plaintextParam( 'myParam' ),
488 Message
::numParam( 2 ),
489 ], 'toomanyvalues', [
490 'parameter' => 'myParam',
497 'Multi-valued parameter with non-exceeded limits for bot' => [
500 ParamValidator
::PARAM_ISMULTI
=> true,
501 ParamValidator
::PARAM_ISMULTI_LIMIT1
=> 2,
502 ParamValidator
::PARAM_ISMULTI_LIMIT2
=> 3,
506 [ 'apihighlimits' => true ],
508 'Multi-valued parameter with prohibited duplicates' => [
510 [ ParamValidator
::PARAM_ISMULTI
=> true ],
514 'Multi-valued parameter with allowed duplicates' => [
517 ParamValidator
::PARAM_ISMULTI
=> true,
518 ParamValidator
::PARAM_ALLOW_DUPLICATES
=> true,
523 'Empty boolean param' => [
525 [ ParamValidator
::PARAM_TYPE
=> 'boolean' ],
529 'Boolean param 0' => [
531 [ ParamValidator
::PARAM_TYPE
=> 'boolean' ],
535 'Boolean param false' => [
537 [ ParamValidator
::PARAM_TYPE
=> 'boolean' ],
541 'Deprecated parameter' => [
543 [ ParamValidator
::PARAM_DEPRECATED
=> true ],
546 'paramvalidator-param-deprecated',
547 Message
::plaintextParam( 'myParam' ),
548 Message
::plaintextParam( 'foo' )
551 'Deprecated parameter with default, unspecified' => [
553 [ ParamValidator
::PARAM_DEPRECATED
=> true, ParamValidator
::PARAM_DEFAULT
=> 'foo' ],
557 'Deprecated parameter with default, specified' => [
559 [ ParamValidator
::PARAM_DEPRECATED
=> true, ParamValidator
::PARAM_DEFAULT
=> 'foo' ],
562 'paramvalidator-param-deprecated',
563 Message
::plaintextParam( 'myParam' ),
564 Message
::plaintextParam( 'foo' )
567 'Deprecated parameter value' => [
569 [ ParamValidator
::PARAM_TYPE
=> [ 'a' ], EnumDef
::PARAM_DEPRECATED_VALUES
=> [ 'a' => true ] ],
572 'paramvalidator-deprecated-value',
573 Message
::plaintextParam( 'myParam' ),
574 Message
::plaintextParam( 'a' )
577 'Deprecated parameter value as default, unspecified' => [
580 ParamValidator
::PARAM_TYPE
=> [ 'a' ],
581 EnumDef
::PARAM_DEPRECATED_VALUES
=> [ 'a' => true ],
582 ParamValidator
::PARAM_DEFAULT
=> 'a'
587 'Deprecated parameter value as default, specified' => [
590 ParamValidator
::PARAM_TYPE
=> [ 'a' ],
591 EnumDef
::PARAM_DEPRECATED_VALUES
=> [ 'a' => true ],
592 ParamValidator
::PARAM_DEFAULT
=> 'a'
596 'paramvalidator-deprecated-value',
597 Message
::plaintextParam( 'myParam' ),
598 Message
::plaintextParam( 'a' )
601 'Multiple deprecated parameter values' => [
604 ParamValidator
::PARAM_TYPE
=> [ 'a', 'b', 'c', 'd' ],
605 EnumDef
::PARAM_DEPRECATED_VALUES
=> [ 'b' => true, 'd' => true ],
606 ParamValidator
::PARAM_ISMULTI
=> true,
608 [ 'a', 'b', 'c', 'd' ],
611 'paramvalidator-deprecated-value',
612 Message
::plaintextParam( 'myParam' ),
613 Message
::plaintextParam( 'b' )
616 'paramvalidator-deprecated-value',
617 Message
::plaintextParam( 'myParam' ),
618 Message
::plaintextParam( 'd' )
622 'Deprecated parameter value with custom warning' => [
624 [ ParamValidator
::PARAM_TYPE
=> [ 'a' ], EnumDef
::PARAM_DEPRECATED_VALUES
=> [ 'a' => 'my-msg' ] ],
628 '"*" when wildcard not allowed' => [
631 ParamValidator
::PARAM_ISMULTI
=> true,
632 ParamValidator
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
636 'paramvalidator-unrecognizedvalues',
637 Message
::plaintextParam( 'myParam' ),
638 Message
::plaintextParam( '*' ),
639 Message
::listParam( [ Message
::plaintextParam( '*' ) ], 'comma' ),
640 Message
::numParam( 1 ),
646 ParamValidator
::PARAM_ISMULTI
=> true,
647 ParamValidator
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
648 ParamValidator
::PARAM_ALL
=> true,
653 'Wildcard "*" with multiples not allowed' => [
656 ParamValidator
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
657 ParamValidator
::PARAM_ALL
=> true,
659 ApiUsageException
::newWithMessage( null, [
660 'paramvalidator-badvalue-enumnotmulti',
661 Message
::plaintextParam( 'myParam' ),
662 Message
::plaintextParam( '*' ),
663 Message
::listParam( [
664 Message
::plaintextParam( 'a' ),
665 Message
::plaintextParam( 'b' ),
666 Message
::plaintextParam( 'c' ),
668 Message
::numParam( 3 ),
672 'Wildcard "*" with unrestricted type' => [
675 ParamValidator
::PARAM_ISMULTI
=> true,
676 ParamValidator
::PARAM_ALL
=> true,
684 ParamValidator
::PARAM_ISMULTI
=> true,
685 ParamValidator
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
686 ParamValidator
::PARAM_ALL
=> 'x',
691 'Namespace with wildcard' => [
694 ParamValidator
::PARAM_ISMULTI
=> true,
695 ParamValidator
::PARAM_TYPE
=> 'namespace',
700 // PARAM_ALL is ignored with namespace types.
701 'Namespace with wildcard suppressed' => [
704 ParamValidator
::PARAM_ISMULTI
=> true,
705 ParamValidator
::PARAM_TYPE
=> 'namespace',
706 ParamValidator
::PARAM_ALL
=> false,
711 'Namespace with wildcard "x"' => [
714 ParamValidator
::PARAM_ISMULTI
=> true,
715 ParamValidator
::PARAM_TYPE
=> 'namespace',
716 ParamValidator
::PARAM_ALL
=> 'x',
720 'paramvalidator-unrecognizedvalues',
721 Message
::plaintextParam( 'myParam' ),
722 Message
::plaintextParam( 'x' ),
723 Message
::listParam( [ Message
::plaintextParam( 'x' ) ], 'comma' ),
724 Message
::numParam( 1 ),
728 'dDy+G?e?txnr.1:(@Ru',
729 [ ParamValidator
::PARAM_TYPE
=> 'password' ],
730 'dDy+G?e?txnr.1:(@Ru',
733 'Sensitive field' => [
734 'I am fond of pineapples',
735 [ ParamValidator
::PARAM_SENSITIVE
=> true ],
736 'I am fond of pineapples',
739 // @todo Test actual upload
742 [ ParamValidator
::PARAM_TYPE
=> 'namespace' ],
743 ApiUsageException
::newWithMessage( null, [
744 'paramvalidator-badvalue-enumnotmulti',
745 Message
::plaintextParam( 'myParam' ),
746 Message
::plaintextParam( '-1' ),
747 Message
::listParam( array_map( [ Message
::class, 'plaintextParam' ], $namespaces ) ),
748 Message
::numParam( count( $namespaces ) ),
752 'Extra namespace -1' => [
755 ParamValidator
::PARAM_TYPE
=> 'namespace',
756 NamespaceDef
::PARAM_EXTRA_NAMESPACES
=> [ -1 ],
761 // @todo Test with PARAM_SUBMODULE_MAP unset, need
762 // getModuleManager() to return something real
763 'Nonexistent module' => [
766 ParamValidator
::PARAM_TYPE
=> 'submodule',
767 SubmoduleDef
::PARAM_SUBMODULE_MAP
=>
768 [ 'foo' => 'foo', 'bar' => 'foo+bar' ],
770 ApiUsageException
::newWithMessage( null, [
771 'paramvalidator-badvalue-enumnotmulti',
772 Message
::plaintextParam( 'myParam' ),
773 Message
::plaintextParam( 'not-a-module-name' ),
774 Message
::listParam( [
775 Message
::plaintextParam( 'foo' ),
776 Message
::plaintextParam( 'bar' ),
778 Message
::numParam( 2 ),
782 '\\x1f with multiples not allowed' => [
785 ApiUsageException
::newWithMessage( null, [
786 'paramvalidator-notmulti',
787 Message
::plaintextParam( 'myParam' ),
788 Message
::plaintextParam( "\x1f" ),
792 'Integer with unenforced min' => [
795 ParamValidator
::PARAM_TYPE
=> 'integer',
796 IntegerDef
::PARAM_MIN
=> -1,
800 'paramvalidator-outofrange-min',
801 Message
::plaintextParam( 'myParam' ),
802 Message
::plaintextParam( '-2' ),
803 Message
::numParam( -1 ),
804 Message
::numParam( '' ),
807 'Integer with enforced min' => [
810 ParamValidator
::PARAM_TYPE
=> 'integer',
811 IntegerDef
::PARAM_MIN
=> -1,
812 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
814 ApiUsageException
::newWithMessage( null, [
815 'paramvalidator-outofrange-min',
816 Message
::plaintextParam( 'myParam' ),
817 Message
::plaintextParam( '-2' ),
818 Message
::numParam( -1 ),
819 Message
::numParam( '' ),
820 ], 'outofrange', [ 'min' => -1, 'curmax' => null, 'max' => null, 'highmax' => null ] ),
823 'Integer with unenforced max' => [
826 ParamValidator
::PARAM_TYPE
=> 'integer',
827 IntegerDef
::PARAM_MAX
=> 7,
831 'paramvalidator-outofrange-max',
832 Message
::plaintextParam( 'myParam' ),
833 Message
::plaintextParam( '8' ),
834 Message
::numParam( '' ),
835 Message
::numParam( 7 ),
838 'Integer with enforced max' => [
841 ParamValidator
::PARAM_TYPE
=> 'integer',
842 IntegerDef
::PARAM_MAX
=> 7,
843 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
845 ApiUsageException
::newWithMessage( null, [
846 'paramvalidator-outofrange-max',
847 Message
::plaintextParam( 'myParam' ),
848 Message
::plaintextParam( '8' ),
849 Message
::numParam( '' ),
850 Message
::numParam( 7 ),
851 ], 'outofrange', [ 'min' => null, 'curmax' => 7, 'max' => 7, 'highmax' => 7 ] ),
854 'Array of integers' => [
857 ParamValidator
::PARAM_ISMULTI
=> true,
858 ParamValidator
::PARAM_TYPE
=> 'integer',
863 'Array of integers with unenforced min/max' => [
866 ParamValidator
::PARAM_ISMULTI
=> true,
867 ParamValidator
::PARAM_TYPE
=> 'integer',
868 IntegerDef
::PARAM_MIN
=> 0,
869 IntegerDef
::PARAM_MAX
=> 100,
874 'paramvalidator-outofrange-minmax',
875 Message
::plaintextParam( 'myParam' ),
876 Message
::plaintextParam( '966' ),
877 Message
::numParam( 0 ),
878 Message
::numParam( 100 ),
881 'paramvalidator-outofrange-minmax',
882 Message
::plaintextParam( 'myParam' ),
883 Message
::plaintextParam( '-1' ),
884 Message
::numParam( 0 ),
885 Message
::numParam( 100 ),
889 'Array of integers with enforced min/max' => [
892 ParamValidator
::PARAM_ISMULTI
=> true,
893 ParamValidator
::PARAM_TYPE
=> 'integer',
894 IntegerDef
::PARAM_MIN
=> 0,
895 IntegerDef
::PARAM_MAX
=> 100,
896 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
898 ApiUsageException
::newWithMessage( null, [
899 'paramvalidator-outofrange-minmax',
900 Message
::plaintextParam( 'myParam' ),
901 Message
::plaintextParam( '966' ),
902 Message
::numParam( 0 ),
903 Message
::numParam( 100 ),
904 ], 'outofrange', [ 'min' => 0, 'curmax' => 100, 'max' => 100, 'highmax' => 100 ] ),
907 'Limit with parseLimits false (numeric)' => [
909 [ ParamValidator
::PARAM_TYPE
=> 'limit' ],
912 [ 'parseLimits' => false ],
914 'Limit with parseLimits false (max)' => [
916 [ ParamValidator
::PARAM_TYPE
=> 'limit' ],
919 [ 'parseLimits' => false ],
921 'Limit with parseLimits false (invalid)' => [
923 [ ParamValidator
::PARAM_TYPE
=> 'limit' ],
924 ApiUsageException
::newWithMessage( null, [
925 'paramvalidator-badinteger',
926 Message
::plaintextParam( 'myParam' ),
927 Message
::plaintextParam( 'kitten' ),
930 [ 'parseLimits' => false ],
932 'Limit with no max, supplied "max"' => [
935 ParamValidator
::PARAM_TYPE
=> 'limit',
943 ParamValidator
::PARAM_TYPE
=> 'limit',
944 IntegerDef
::PARAM_MAX
=> 100,
945 IntegerDef
::PARAM_MAX2
=> 100,
953 ParamValidator
::PARAM_TYPE
=> 'limit',
954 IntegerDef
::PARAM_MAX
=> 100,
955 IntegerDef
::PARAM_MAX2
=> 101,
960 'Limit max for apihighlimits' => [
963 ParamValidator
::PARAM_TYPE
=> 'limit',
964 IntegerDef
::PARAM_MAX
=> 100,
965 IntegerDef
::PARAM_MAX2
=> 101,
969 [ 'apihighlimits' => true ],
971 'Limit too large' => [
974 ParamValidator
::PARAM_TYPE
=> 'limit',
975 IntegerDef
::PARAM_MAX
=> 100,
976 IntegerDef
::PARAM_MAX2
=> 101,
980 'paramvalidator-outofrange-minmax',
981 Message
::plaintextParam( 'myParam' ),
982 Message
::plaintextParam( '101' ),
983 Message
::numParam( 0 ),
984 Message
::numParam( 100 ),
987 'Limit okay for apihighlimits' => [
990 ParamValidator
::PARAM_TYPE
=> 'limit',
991 IntegerDef
::PARAM_MAX
=> 100,
992 IntegerDef
::PARAM_MAX2
=> 101,
996 [ 'apihighlimits' => true ],
998 'Limit too large for apihighlimits (non-internal mode)' => [
1001 ParamValidator
::PARAM_TYPE
=> 'limit',
1002 IntegerDef
::PARAM_MAX
=> 100,
1003 IntegerDef
::PARAM_MAX2
=> 101,
1007 'paramvalidator-outofrange-minmax',
1008 Message
::plaintextParam( 'myParam' ),
1009 Message
::plaintextParam( '102' ),
1010 Message
::numParam( 0 ),
1011 Message
::numParam( 101 ),
1013 [ 'apihighlimits' => true ],
1015 'Limit too small' => [
1018 ParamValidator
::PARAM_TYPE
=> 'limit',
1019 IntegerDef
::PARAM_MIN
=> -1,
1020 IntegerDef
::PARAM_MAX
=> 100,
1021 IntegerDef
::PARAM_MAX2
=> 100,
1025 'paramvalidator-outofrange-minmax',
1026 Message
::plaintextParam( 'myParam' ),
1027 Message
::plaintextParam( '-2' ),
1028 Message
::numParam( -1 ),
1029 Message
::numParam( 100 ),
1033 wfTimestamp( TS_UNIX
, '20211221122112' ),
1034 [ ParamValidator
::PARAM_TYPE
=> 'timestamp' ],
1040 [ ParamValidator
::PARAM_TYPE
=> 'timestamp' ],
1044 'paramvalidator-unclearnowtimestamp',
1045 Message
::plaintextParam( 'myParam' ),
1046 Message
::plaintextParam( '0' ),
1049 'Timestamp empty' => [
1051 [ ParamValidator
::PARAM_TYPE
=> 'timestamp' ],
1054 'paramvalidator-unclearnowtimestamp',
1055 Message
::plaintextParam( 'myParam' ),
1056 Message
::plaintextParam( '' ),
1059 // wfTimestamp() interprets this as Unix time
1062 [ ParamValidator
::PARAM_TYPE
=> 'timestamp' ],
1066 'Timestamp now' => [
1068 [ ParamValidator
::PARAM_TYPE
=> 'timestamp' ],
1072 'Invalid timestamp' => [
1074 [ ParamValidator
::PARAM_TYPE
=> 'timestamp' ],
1075 ApiUsageException
::newWithMessage( null, [
1076 'paramvalidator-badtimestamp',
1077 Message
::plaintextParam( 'myParam' ),
1078 Message
::plaintextParam( 'a potato' ),
1079 ], 'badtimestamp' ),
1082 'Timestamp array' => [
1085 ParamValidator
::PARAM_TYPE
=> 'timestamp',
1086 ParamValidator
::PARAM_ISMULTI
=> 1,
1088 [ wfTimestamp( TS_MW
, 100 ), wfTimestamp( TS_MW
, 101 ) ],
1092 '99990123123456|8888-01-23 12:34:56|indefinite',
1094 ParamValidator
::PARAM_TYPE
=> 'expiry',
1095 ParamValidator
::PARAM_ISMULTI
=> 1,
1097 [ '9999-01-23T12:34:56Z', '8888-01-23T12:34:56Z', 'infinity' ],
1102 [ ParamValidator
::PARAM_TYPE
=> 'user' ],
1106 'User prefixed with "User:"' => [
1108 [ ParamValidator
::PARAM_TYPE
=> 'user' ],
1112 'Invalid username "|"' => [
1114 [ ParamValidator
::PARAM_TYPE
=> 'user' ],
1115 ApiUsageException
::newWithMessage( null, [
1116 'paramvalidator-baduser',
1117 Message
::plaintextParam( 'myParam' ),
1118 Message
::plaintextParam( '|' ),
1122 'Invalid username "300.300.300.300"' => [
1124 [ ParamValidator
::PARAM_TYPE
=> 'user' ],
1125 ApiUsageException
::newWithMessage( null, [
1126 'paramvalidator-baduser',
1127 Message
::plaintextParam( 'myParam' ),
1128 Message
::plaintextParam( '300.300.300.300' ),
1132 'IP range as username' => [
1134 [ ParamValidator
::PARAM_TYPE
=> 'user' ],
1138 'IPv6 as username' => [
1140 [ ParamValidator
::PARAM_TYPE
=> 'user' ],
1144 'Obsolete cloaked usemod IP address as username' => [
1146 [ ParamValidator
::PARAM_TYPE
=> 'user' ],
1150 'Invalid username containing IP address' => [
1151 'This is [not] valid 1.2.3.xxx, ha!',
1152 [ ParamValidator
::PARAM_TYPE
=> 'user' ],
1153 ApiUsageException
::newWithMessage( null, [
1154 'paramvalidator-baduser',
1155 Message
::plaintextParam( 'myParam' ),
1156 Message
::plaintextParam( 'This is [not] valid 1.2.3.xxx, ha!' ),
1160 'External username' => [
1162 [ ParamValidator
::PARAM_TYPE
=> 'user' ],
1166 'Array of usernames' => [
1169 ParamValidator
::PARAM_TYPE
=> 'user',
1170 ParamValidator
::PARAM_ISMULTI
=> true,
1177 [ ParamValidator
::PARAM_TYPE
=> 'tags' ],
1181 'Array of one tag' => [
1184 ParamValidator
::PARAM_TYPE
=> 'tags',
1185 ParamValidator
::PARAM_ISMULTI
=> true,
1190 'Array of tags' => [
1193 ParamValidator
::PARAM_TYPE
=> 'tags',
1194 ParamValidator
::PARAM_ISMULTI
=> true,
1201 [ ParamValidator
::PARAM_TYPE
=> 'tags' ],
1202 ApiUsageException
::newWithMessage(
1204 [ 'tags-apply-not-allowed-one', 'invalid tag', 1 ],
1206 [ 'disallowedtags' => [ 'invalid tag' ] ]
1210 'Unrecognized type' => [
1212 [ ParamValidator
::PARAM_TYPE
=> 'nonexistenttype' ],
1213 new DomainException( "Param myParam's type is unknown - nonexistenttype" ),
1216 'Too many bytes' => [
1219 StringDef
::PARAM_MAX_BYTES
=> 0,
1220 StringDef
::PARAM_MAX_CHARS
=> 0,
1222 ApiUsageException
::newWithMessage( null, [
1223 'paramvalidator-maxbytes',
1224 Message
::plaintextParam( 'myParam' ),
1225 Message
::plaintextParam( '1' ),
1226 Message
::numParam( 0 ),
1227 Message
::numParam( 1 ),
1228 ], 'maxbytes', [ 'maxbytes' => 0, 'maxchars' => 0 ] ),
1231 'Too many chars' => [
1234 StringDef
::PARAM_MAX_BYTES
=> 4,
1235 StringDef
::PARAM_MAX_CHARS
=> 1,
1237 ApiUsageException
::newWithMessage( null, [
1238 'paramvalidator-maxchars',
1239 Message
::plaintextParam( 'myParam' ),
1240 Message
::plaintextParam( '§§' ),
1241 Message
::numParam( 1 ),
1242 Message
::numParam( 2 ),
1243 ], 'maxchars', [ 'maxbytes' => 4, 'maxchars' => 1 ] ),
1246 'Omitted required param' => [
1248 [ ParamValidator
::PARAM_REQUIRED
=> true ],
1249 ApiUsageException
::newWithMessage( null, [
1250 'paramvalidator-missingparam',
1251 Message
::plaintextParam( 'myParam' )
1252 ], 'missingparam' ),
1255 'Empty multi-value' => [
1257 [ ParamValidator
::PARAM_ISMULTI
=> true ],
1261 'Multi-value \x1f' => [
1263 [ ParamValidator
::PARAM_ISMULTI
=> true ],
1267 'Allowed non-multi-value with "|"' => [
1269 [ ParamValidator
::PARAM_TYPE
=> [ 'a|b' ] ],
1273 'Prohibited multi-value' => [
1275 [ ParamValidator
::PARAM_TYPE
=> [ 'a', 'b' ] ],
1276 ApiUsageException
::newWithMessage( null, [
1277 'paramvalidator-badvalue-enumnotmulti',
1278 Message
::plaintextParam( 'myParam' ),
1279 Message
::plaintextParam( 'a|b' ),
1280 Message
::listParam( [ Message
::plaintextParam( 'a' ), Message
::plaintextParam( 'b' ) ] ),
1281 Message
::numParam( 2 ),
1294 [ "\t1", null, '\t1' ],
1295 [ "\r1", null, '\r1' ],
1296 [ "\f1", null, '\f1', 'badutf-8' ],
1297 [ "\n1", null, '\n1' ],
1298 [ "\v1", null, '\v1', 'badutf-8' ],
1299 [ "\e1", null, '\e1', 'badutf-8' ],
1300 [ "\x001", null, '\x001', 'badutf-8' ],
1303 foreach ( $integerTests as $test ) {
1304 $desc = $test[2] ??
$test[0];
1305 $warnings = isset( $test[3] ) ?
1306 [ [ 'apiwarn-badutf8', 'myParam' ] ] : [];
1307 $returnArray["\"$desc\" as integer"] = [
1309 [ ParamValidator
::PARAM_TYPE
=> 'integer' ],
1310 $test[1] ?? ApiUsageException
::newWithMessage( null, [
1311 'paramvalidator-badinteger',
1312 Message
::plaintextParam( 'myParam' ),
1313 Message
::plaintextParam( preg_replace( "/[\f\v\e\\0]/", '�', $test[0] ) ),
1319 return $returnArray;
1323 * @dataProvider provideGetFinalParamDescription
1325 public function testGetFinalParamDescription( $paramSettings, $expectedMessages ) {
1326 $mock = $this->getMockBuilder( MockApi
::class )
1327 ->onlyMethods( [ 'getAllowedParams', 'getModulePath' ] )
1329 $mock->method( 'getAllowedParams' )->willReturn( [
1330 'param' => $paramSettings,
1332 $mock->method( 'getModulePath' )->willReturn( 'test' );
1333 if ( $expectedMessages instanceof Exception
) {
1334 $this->expectExceptionObject( $expectedMessages );
1336 $paramDescription = $mock->getFinalParamDescription();
1337 $this->assertArrayHasKey( 'param', $paramDescription );
1338 $messages = $paramDescription['param'];
1339 $messageKeys = array_map( static fn ( MessageSpecifier
$m ) => $m->getKey(), $messages );
1340 $this->assertSame( $expectedMessages, $messageKeys );
1343 public static function provideGetFinalParamDescription() {
1345 'default message' => [
1347 'messages' => [ 'apihelp-test-param-param' ],
1349 'custom message' => [
1350 'settings' => [ ApiBase
::PARAM_HELP_MSG
=> 'foo' ],
1351 'messages' => [ 'foo' ],
1353 'default per-value message' => [
1355 ParamValidator
::PARAM_TYPE
=> [ 'a', 'b' ],
1356 ApiBase
::PARAM_HELP_MSG_PER_VALUE
=> [],
1359 'apihelp-test-param-param',
1360 'apihelp-test-paramvalue-param-a',
1361 'apihelp-test-paramvalue-param-b',
1364 'custom per-value message' => [
1366 ParamValidator
::PARAM_TYPE
=> [ 'a', 'b' ],
1367 ApiBase
::PARAM_HELP_MSG_PER_VALUE
=> [
1373 'apihelp-test-param-param',
1378 'custom per-value message for strings' => [
1380 ParamValidator
::PARAM_TYPE
=> 'string',
1381 ParamValidator
::PARAM_ISMULTI
=> true,
1382 ApiBase
::PARAM_HELP_MSG_PER_VALUE
=> [
1388 'apihelp-test-param-param',
1393 'must be multi-valued for per-value message' => [
1395 ParamValidator
::PARAM_TYPE
=> 'string',
1396 ApiBase
::PARAM_HELP_MSG_PER_VALUE
=> [],
1398 'messages' => new MWException(
1399 'Internal error in ' . ApiBase
::class . '::getFinalParamDescription: '
1400 . 'ApiBase::PARAM_HELP_MSG_PER_VALUE may only be used when '
1401 . "ParamValidator::PARAM_TYPE is an array or it is 'string' "
1402 . 'and ParamValidator::PARAM_ISMULTI is true'
1408 public function testAddBlockInfoToStatus() {
1409 $mock = new MockApi();
1411 $msg = new Message( 'mainpage' );
1413 // Check empty array
1414 $expect = Status
::newGood();
1415 $test = Status
::newGood();
1416 $mock->addBlockInfoToStatus( $test );
1417 $this->assertEquals( $expect, $test );
1419 // No blocked $user, so no special block handling
1420 $expect = Status
::newGood();
1421 $expect->fatal( 'blockedtext' );
1422 $expect->fatal( 'autoblockedtext' );
1423 $expect->fatal( 'systemblockedtext' );
1424 $expect->fatal( 'mainpage' );
1425 $expect->fatal( $msg );
1426 $expect->fatal( 'parentheses', 'foobar' );
1427 $test = clone $expect;
1428 $mock->addBlockInfoToStatus( $test );
1429 $this->assertEquals( $expect, $test );
1431 // Has a blocked $user, so special block handling
1432 $user = $this->getMutableTestUser()->getUser();
1433 $block = new DatabaseBlock( [
1435 'by' => $this->getTestSysop()->getUser(),
1436 'reason' => __METHOD__
,
1437 'expiry' => time() +
100500,
1439 $this->getServiceContainer()->getDatabaseBlockStore()->insertBlock( $block );
1441 $mockTrait = $this->getMockForTrait( ApiBlockInfoTrait
::class );
1442 $language = $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'en' );
1443 $mockTrait->method( 'getLanguage' )->willReturn( $language );
1444 $userInfoTrait = TestingAccessWrapper
::newFromObject( $mockTrait );
1445 $blockinfo = [ 'blockinfo' => $userInfoTrait->getBlockDetails( $block ) ];
1447 $expect = Status
::newGood();
1448 $expect->fatal( ApiMessage
::create( 'blockedtext', 'blocked', $blockinfo ) );
1449 // This would normally use the 'autoblocked' code, but the codes are computed from $blockinfo
1450 // now rather than the message, and we're not faking it well enough
1451 $expect->fatal( ApiMessage
::create( 'autoblockedtext', 'blocked', $blockinfo ) );
1452 $expect->fatal( ApiMessage
::create( 'systemblockedtext', 'blocked', $blockinfo ) );
1453 $expect->fatal( 'mainpage' );
1454 $expect->fatal( $msg );
1455 $expect->fatal( 'parentheses', 'foobar' );
1456 $test = Status
::newGood();
1457 $test->fatal( 'blockedtext' );
1458 $test->fatal( 'autoblockedtext' );
1459 $test->fatal( 'systemblockedtext' );
1460 $test->fatal( 'mainpage' );
1461 $test->fatal( $msg );
1462 $test->fatal( 'parentheses', 'foobar' );
1463 $mock->addBlockInfoToStatus( $test, $user );
1464 $this->assertEquals( $expect, $test );
1467 public static function provideDieStatus() {
1468 $status = StatusValue
::newGood();
1469 $status->error( 'foo' );
1470 $status->warning( 'bar' );
1471 yield
[ $status, [ 'foo' => true, 'bar' => false ] ];
1473 $status = StatusValue
::newGood();
1474 $status->warning( 'foo' );
1475 $status->warning( 'bar' );
1476 yield
[ $status, [ 'foo' => true, 'bar' => true ] ];
1478 $status = StatusValue
::newGood();
1479 $status->setOK( false );
1480 yield
[ $status, [ 'unknownerror-nocode' => true ] ];
1482 $status = PermissionStatus
::newEmpty();
1483 $status->setRateLimitExceeded();
1484 yield
[ $status, [ 'ratelimited' => true ] ];
1486 $status = StatusValue
::newFatal( 'actionthrottledtext' );
1487 yield
[ $status, [ 'ratelimited' => true ] ];
1489 $status = StatusValue
::newFatal( 'actionthrottled' );
1490 yield
[ $status, [ 'ratelimited' => true ] ];
1492 $status = StatusValue
::newFatal( 'blockedtext' );
1493 yield
[ $status, [ 'blocked' => true ] ];
1495 $status = StatusValue
::newFatal( 'autoblockedtext' );
1496 yield
[ $status, [ 'autoblocked' => true ] ];
1500 * @dataProvider provideDieStatus
1502 * @param StatusValue $status
1503 * @param array $expected
1505 public function testDieStatus( $status, $expected ) {
1506 $mock = new MockApi();
1509 $mock->dieStatus( $status );
1510 $this->fail( 'Expected exception not thrown' );
1511 } catch ( ApiUsageException
$ex ) {
1512 foreach ( $expected as $key => $has ) {
1513 $this->assertSame( $has, ApiTestCase
::apiExceptionHasCode( $ex, $key ), "Exception has '$key'" );
1519 * @covers \MediaWiki\Api\ApiBase::extractRequestParams
1521 public function testExtractRequestParams() {
1522 $request = new FauxRequest( [
1523 'xxexists' => 'exists!',
1524 'xxmulti' => 'a|b|c|d|{bad}',
1526 'xxtemplate-a' => 'A!',
1527 'xxtemplate-b' => 'B1|B2|B3',
1528 'xxtemplate-c' => '',
1529 'xxrecursivetemplate-b-B1' => 'X',
1530 'xxrecursivetemplate-b-B3' => 'Y',
1531 'xxrecursivetemplate-b-B4' => '?',
1532 'xxemptytemplate-' => 'nope',
1535 'errorformat' => 'raw',
1537 $context = new DerivativeContext( RequestContext
::getMain() );
1538 $context->setRequest( $request );
1539 $main = new ApiMain( $context );
1541 $mock = $this->getMockBuilder( ApiBase
::class )
1542 ->setConstructorArgs( [ $main, 'test', 'xx' ] )
1543 ->onlyMethods( [ 'getAllowedParams' ] )
1544 ->getMockForAbstractClass();
1545 $mock->method( 'getAllowedParams' )->willReturn( [
1546 'notexists' => null,
1549 ParamValidator
::PARAM_ISMULTI
=> true,
1552 ParamValidator
::PARAM_ISMULTI
=> true,
1555 ParamValidator
::PARAM_ISMULTI
=> true,
1556 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'm' => 'multi' ],
1558 'recursivetemplate-{m}-{t}' => [
1559 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 't' => 'template-{m}', 'm' => 'multi' ],
1561 'emptytemplate-{m}' => [
1562 ParamValidator
::PARAM_ISMULTI
=> true,
1563 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'm' => 'empty' ],
1565 'badtemplate-{e}' => [
1566 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'e' => 'exists' ],
1568 'badtemplate2-{e}' => [
1569 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'e' => 'badtemplate2-{e}' ],
1571 'badtemplate3-{x}' => [
1572 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'x' => 'foo' ],
1576 $this->assertEquals( [
1577 'notexists' => null,
1578 'exists' => 'exists!',
1579 'multi' => [ 'a', 'b', 'c', 'd', '{bad}' ],
1581 'template-a' => [ 'A!' ],
1582 'template-b' => [ 'B1', 'B2', 'B3' ],
1584 'template-d' => null,
1585 'recursivetemplate-a-A!' => null,
1586 'recursivetemplate-b-B1' => 'X',
1587 'recursivetemplate-b-B2' => null,
1588 'recursivetemplate-b-B3' => 'Y',
1589 ], $mock->extractRequestParams() );
1591 $used = TestingAccessWrapper
::newFromObject( $main )->getParamsUsed();
1593 $this->assertEquals( [
1598 'xxrecursivetemplate-a-A!',
1599 'xxrecursivetemplate-b-B1',
1600 'xxrecursivetemplate-b-B2',
1601 'xxrecursivetemplate-b-B3',
1608 $warnings = $mock->getResult()->getResultData( 'warnings', [ 'Strip' => 'all' ] );
1609 $this->assertCount( 1, $warnings );
1610 $this->assertSame( 'ignoring-invalid-templated-value', $warnings[0]['code'] );