3 use Wikimedia\TestingAccessWrapper
;
12 class ApiBaseTest
extends ApiTestCase
{
14 * This covers a variety of stub methods that return a fixed value.
16 * @param string|array $method Name of method, or [ name, params... ]
17 * @param string $value Expected value
19 * @dataProvider provideStubMethods
21 public function testStubMethods( $expected, $method, $args = [] ) {
22 // Some of these are protected
23 $mock = TestingAccessWrapper
::newFromObject( new MockApi() );
24 $result = call_user_func_array( [ $mock, $method ], $args );
25 $this->assertSame( $expected, $result );
28 public function provideStubMethods() {
30 [ null, 'getModuleManager' ],
31 [ null, 'getCustomPrinter' ],
32 [ [], 'getHelpUrls' ],
33 // @todo This is actually overriden by MockApi
34 // [ [], 'getAllowedParams' ],
35 [ true, 'shouldCheckMaxLag' ],
36 [ true, 'isReadMode' ],
37 [ false, 'isWriteMode' ],
38 [ false, 'mustBePosted' ],
39 [ false, 'isDeprecated' ],
40 [ false, 'isInternal' ],
41 [ false, 'needsToken' ],
42 [ null, 'getWebUITokenSalt', [ [] ] ],
43 [ null, 'getConditionalRequestData', [ 'etag' ] ],
44 [ null, 'dynamicParameterDocumentation' ],
48 public function testRequireOnlyOneParameterDefault() {
49 $mock = new MockApi();
50 $mock->requireOnlyOneParameter(
51 [ "filename" => "foo.txt", "enablechunks" => false ],
52 "filename", "enablechunks"
54 $this->assertTrue( true );
58 * @expectedException ApiUsageException
60 public function testRequireOnlyOneParameterZero() {
61 $mock = new MockApi();
62 $mock->requireOnlyOneParameter(
63 [ "filename" => "foo.txt", "enablechunks" => 0 ],
64 "filename", "enablechunks"
69 * @expectedException ApiUsageException
71 public function testRequireOnlyOneParameterTrue() {
72 $mock = new MockApi();
73 $mock->requireOnlyOneParameter(
74 [ "filename" => "foo.txt", "enablechunks" => true ],
75 "filename", "enablechunks"
79 public function testRequireOnlyOneParameterMissing() {
80 $this->setExpectedException( ApiUsageException
::class,
81 'One of the parameters "foo" and "bar" is required.' );
82 $mock = new MockApi();
83 $mock->requireOnlyOneParameter(
84 [ "filename" => "foo.txt", "enablechunks" => false ],
88 public function testRequireMaxOneParameterZero() {
89 $mock = new MockApi();
90 $mock->requireMaxOneParameter(
91 [ 'foo' => 'bar', 'baz' => 'quz' ],
93 $this->assertTrue( true );
96 public function testRequireMaxOneParameterOne() {
97 $mock = new MockApi();
98 $mock->requireMaxOneParameter(
99 [ 'foo' => 'bar', 'baz' => 'quz' ],
101 $this->assertTrue( true );
104 public function testRequireMaxOneParameterTwo() {
105 $this->setExpectedException( ApiUsageException
::class,
106 'The parameters "foo" and "baz" can not be used together.' );
107 $mock = new MockApi();
108 $mock->requireMaxOneParameter(
109 [ 'foo' => 'bar', 'baz' => 'quz' ],
113 public function testRequireAtLeastOneParameterZero() {
114 $this->setExpectedException( ApiUsageException
::class,
115 'At least one of the parameters "foo" and "bar" is required.' );
116 $mock = new MockApi();
117 $mock->requireAtLeastOneParameter(
118 [ 'a' => 'b', 'c' => 'd' ],
122 public function testRequireAtLeastOneParameterOne() {
123 $mock = new MockApi();
124 $mock->requireAtLeastOneParameter(
125 [ 'a' => 'b', 'c' => 'd' ],
127 $this->assertTrue( true );
130 public function testRequireAtLeastOneParameterTwo() {
131 $mock = new MockApi();
132 $mock->requireAtLeastOneParameter(
133 [ 'a' => 'b', 'c' => 'd' ],
135 $this->assertTrue( true );
138 public function testGetTitleOrPageIdBadParams() {
139 $this->setExpectedException( ApiUsageException
::class,
140 'The parameters "title" and "pageid" can not be used together.' );
141 $mock = new MockApi();
142 $mock->getTitleOrPageId( [ 'title' => 'a', 'pageid' => 7 ] );
145 public function testGetTitleOrPageIdTitle() {
146 $mock = new MockApi();
147 $result = $mock->getTitleOrPageId( [ 'title' => 'Foo' ] );
148 $this->assertInstanceOf( WikiPage
::class, $result );
149 $this->assertSame( 'Foo', $result->getTitle()->getPrefixedText() );
152 public function testGetTitleOrPageIdInvalidTitle() {
153 $this->setExpectedException( ApiUsageException
::class,
155 $mock = new MockApi();
156 $mock->getTitleOrPageId( [ 'title' => '|' ] );
159 public function testGetTitleOrPageIdSpecialTitle() {
160 $this->setExpectedException( ApiUsageException
::class,
161 "Namespace doesn't allow actual pages." );
162 $mock = new MockApi();
163 $mock->getTitleOrPageId( [ 'title' => 'Special:RandomPage' ] );
166 public function testGetTitleOrPageIdPageId() {
167 $result = ( new MockApi() )->getTitleOrPageId(
168 [ 'pageid' => Title
::newFromText( 'UTPage' )->getArticleId() ] );
169 $this->assertInstanceOf( WikiPage
::class, $result );
170 $this->assertSame( 'UTPage', $result->getTitle()->getPrefixedText() );
173 public function testGetTitleOrPageIdInvalidPageId() {
174 $this->setExpectedException( ApiUsageException
::class,
175 'There is no page with ID 2147483648.' );
176 $mock = new MockApi();
177 $mock->getTitleOrPageId( [ 'pageid' => 2147483648 ] );
180 public function testGetTitleFromTitleOrPageIdBadParams() {
181 $this->setExpectedException( ApiUsageException
::class,
182 'The parameters "title" and "pageid" can not be used together.' );
183 $mock = new MockApi();
184 $mock->getTitleFromTitleOrPageId( [ 'title' => 'a', 'pageid' => 7 ] );
187 public function testGetTitleFromTitleOrPageIdTitle() {
188 $mock = new MockApi();
189 $result = $mock->getTitleFromTitleOrPageId( [ 'title' => 'Foo' ] );
190 $this->assertInstanceOf( Title
::class, $result );
191 $this->assertSame( 'Foo', $result->getPrefixedText() );
194 public function testGetTitleFromTitleOrPageIdInvalidTitle() {
195 $this->setExpectedException( ApiUsageException
::class,
197 $mock = new MockApi();
198 $mock->getTitleFromTitleOrPageId( [ 'title' => '|' ] );
201 public function testGetTitleFromTitleOrPageIdPageId() {
202 $result = ( new MockApi() )->getTitleFromTitleOrPageId(
203 [ 'pageid' => Title
::newFromText( 'UTPage' )->getArticleId() ] );
204 $this->assertInstanceOf( Title
::class, $result );
205 $this->assertSame( 'UTPage', $result->getPrefixedText() );
208 public function testGetTitleFromTitleOrPageIdInvalidPageId() {
209 $this->setExpectedException( ApiUsageException
::class,
210 'There is no page with ID 298401643.' );
211 $mock = new MockApi();
212 $mock->getTitleFromTitleOrPageId( [ 'pageid' => 298401643 ] );
215 public function testGetParameter() {
216 $mock = $this->getMockBuilder( MockApi
::class )
217 ->setMethods( [ 'getAllowedParams' ] )
219 $mock->method( 'getAllowedParams' )->willReturn( [
221 ApiBase
::PARAM_TYPE
=> [ 'value' ],
224 ApiBase
::PARAM_TYPE
=> [ 'value' ],
227 $wrapper = TestingAccessWrapper
::newFromObject( $mock );
229 $context = new DerivativeContext( $mock );
230 $context->setRequest( new FauxRequest( [ 'foo' => 'bad', 'bar' => 'value' ] ) );
231 $wrapper->mMainModule
= new ApiMain( $context );
233 // Even though 'foo' is bad, getParameter( 'bar' ) must not fail
234 $this->assertSame( 'value', $wrapper->getParameter( 'bar' ) );
236 // But getParameter( 'foo' ) must throw.
238 $wrapper->getParameter( 'foo' );
239 $this->fail( 'Expected exception not thrown' );
240 } catch ( ApiUsageException
$ex ) {
241 $this->assertTrue( $this->apiExceptionHasCode( $ex, 'unknown_foo' ) );
244 // And extractRequestParams() must throw too.
246 $mock->extractRequestParams();
247 $this->fail( 'Expected exception not thrown' );
248 } catch ( ApiUsageException
$ex ) {
249 $this->assertTrue( $this->apiExceptionHasCode( $ex, 'unknown_foo' ) );
254 * @dataProvider provideGetParameterFromSettings
255 * @param string|null $input
256 * @param array $paramSettings
257 * @param mixed $expected
258 * @param array $options Key-value pairs:
259 * 'parseLimits': true|false
260 * 'apihighlimits': true|false
261 * 'internalmode': true|false
262 * @param string[] $warnings
264 public function testGetParameterFromSettings(
265 $input, $paramSettings, $expected, $warnings, $options = []
267 $mock = new MockApi();
268 $wrapper = TestingAccessWrapper
::newFromObject( $mock );
270 $context = new DerivativeContext( $mock );
271 $context->setRequest( new FauxRequest(
272 $input !== null ?
[ 'myParam' => $input ] : [] ) );
273 $wrapper->mMainModule
= new ApiMain( $context );
275 $parseLimits = isset( $options['parseLimits'] ) ?
276 $options['parseLimits'] : true;
278 if ( !empty( $options['apihighlimits'] ) ) {
279 $context->setUser( self
::$users['sysop']->getUser() );
282 if ( isset( $options['internalmode'] ) && !$options['internalmode'] ) {
283 $mainWrapper = TestingAccessWrapper
::newFromObject( $wrapper->mMainModule
);
284 $mainWrapper->mInternalMode
= false;
287 // If we're testing tags, set up some tags
288 if ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
289 $paramSettings[ApiBase
::PARAM_TYPE
] === 'tags'
291 ChangeTags
::defineTag( 'tag1' );
292 ChangeTags
::defineTag( 'tag2' );
295 if ( $expected instanceof Exception
) {
297 $wrapper->getParameterFromSettings( 'myParam', $paramSettings,
299 $this->fail( 'No exception thrown' );
300 } catch ( Exception
$ex ) {
301 $this->assertEquals( $expected, $ex );
304 $result = $wrapper->getParameterFromSettings( 'myParam',
305 $paramSettings, $parseLimits );
306 if ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
307 $paramSettings[ApiBase
::PARAM_TYPE
] === 'timestamp' &&
310 // Allow one second of fuzziness. Make sure the formats are
312 $this->assertRegExp( '/^\d{14}$/', $result );
313 $this->assertLessThanOrEqual( 1,
314 abs( wfTimestamp( TS_UNIX
, $result ) - time() ),
315 "Result $result differs from expected $expected by " .
316 'more than one second' );
318 $this->assertSame( $expected, $result );
320 $actualWarnings = array_map( function ( $warn ) {
321 return $warn instanceof Message
322 ?
array_merge( [ $warn->getKey() ], $warn->getParams() )
324 }, $mock->warnings
);
325 $this->assertSame( $warnings, $actualWarnings );
328 if ( !empty( $paramSettings[ApiBase
::PARAM_SENSITIVE
] ) ||
329 ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
330 $paramSettings[ApiBase
::PARAM_TYPE
] === 'password' )
332 $mainWrapper = TestingAccessWrapper
::newFromObject( $wrapper->getMain() );
333 $this->assertSame( [ 'myParam' ],
334 $mainWrapper->getSensitiveParams() );
338 public static function provideGetParameterFromSettings() {
340 [ 'apiwarn-badutf8', 'myParam' ],
345 for ( $i = 0; $i < 32; $i++
) {
347 $enc .= ( $i === 9 ||
$i === 10 ||
$i === 13 )
353 'Basic param' => [ 'bar', null, 'bar', [] ],
354 'Basic param, C0 controls' => [ $c0, null, $enc, $warnings ],
355 'String param' => [ 'bar', '', 'bar', [] ],
356 'String param, defaulted' => [ null, '', '', [] ],
357 'String param, empty' => [ '', 'default', '', [] ],
358 'String param, required, empty' => [
360 [ ApiBase
::PARAM_DFLT
=> 'default', ApiBase
::PARAM_REQUIRED
=> true ],
361 ApiUsageException
::newWithMessage( null,
362 [ 'apierror-missingparam', 'myParam' ] ),
365 'Multi-valued parameter' => [
367 [ ApiBase
::PARAM_ISMULTI
=> true ],
371 'Multi-valued parameter, alternative separator' => [
373 [ ApiBase
::PARAM_ISMULTI
=> true ],
377 'Multi-valued parameter, other C0 controls' => [
379 [ ApiBase
::PARAM_ISMULTI
=> true ],
383 'Multi-valued parameter, other C0 controls (2)' => [
385 [ ApiBase
::PARAM_ISMULTI
=> true ],
386 [ substr( $enc, 0, -3 ), '' ],
389 'Multi-valued parameter with limits' => [
392 ApiBase
::PARAM_ISMULTI
=> true,
393 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 3,
398 'Multi-valued parameter with exceeded limits' => [
401 ApiBase
::PARAM_ISMULTI
=> true,
402 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
404 ApiUsageException
::newWithMessage(
405 null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
409 'Multi-valued parameter with exceeded limits for non-bot' => [
412 ApiBase
::PARAM_ISMULTI
=> true,
413 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
414 ApiBase
::PARAM_ISMULTI_LIMIT2
=> 3,
416 ApiUsageException
::newWithMessage(
417 null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
421 'Multi-valued parameter with non-exceeded limits for bot' => [
424 ApiBase
::PARAM_ISMULTI
=> true,
425 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
426 ApiBase
::PARAM_ISMULTI_LIMIT2
=> 3,
430 [ 'apihighlimits' => true ],
432 'Multi-valued parameter with prohibited duplicates' => [
434 [ ApiBase
::PARAM_ISMULTI
=> true ],
435 // Note that the keys are not sequential! This matches
436 // array_unique, but might be unexpected.
437 [ 0 => 'a', 1 => 'b', 3 => 'c' ],
440 'Multi-valued parameter with allowed duplicates' => [
443 ApiBase
::PARAM_ISMULTI
=> true,
444 ApiBase
::PARAM_ALLOW_DUPLICATES
=> true,
449 'Empty boolean param' => [
451 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
455 'Boolean param 0' => [
457 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
461 'Boolean param false' => [
463 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
467 'Boolean multi-param' => [
470 ApiBase
::PARAM_TYPE
=> 'boolean',
471 ApiBase
::PARAM_ISMULTI
=> true,
474 'Internal error in ApiBase::getParameterFromSettings: ' .
475 'Multi-values not supported for myParam'
479 'Empty boolean param with non-false default' => [
482 ApiBase
::PARAM_TYPE
=> 'boolean',
483 ApiBase
::PARAM_DFLT
=> true,
486 'Internal error in ApiBase::getParameterFromSettings: ' .
487 "Boolean param myParam's default is set to '1'. " .
488 'Boolean parameters must default to false.' ),
491 'Deprecated parameter' => [
493 [ ApiBase
::PARAM_DEPRECATED
=> true ],
495 [ [ 'apiwarn-deprecation-parameter', 'myParam' ] ],
497 'Deprecated parameter value' => [
499 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => true ] ],
501 [ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ],
503 'Multiple deprecated parameter values' => [
505 [ ApiBase
::PARAM_DEPRECATED_VALUES
=>
506 [ 'b' => true, 'd' => true ],
507 ApiBase
::PARAM_ISMULTI
=> true ],
508 [ 'a', 'b', 'c', 'd' ],
510 [ 'apiwarn-deprecation-parameter', 'myParam=b' ],
511 [ 'apiwarn-deprecation-parameter', 'myParam=d' ],
514 'Deprecated parameter value with custom warning' => [
516 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => 'my-msg' ] ],
520 '"*" when wildcard not allowed' => [
522 [ ApiBase
::PARAM_ISMULTI
=> true,
523 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ] ],
525 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
526 [ 'list' => [ '*' ], 'type' => 'comma' ], 1 ] ],
531 ApiBase
::PARAM_ISMULTI
=> true,
532 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
533 ApiBase
::PARAM_ALL
=> true,
538 'Wildcard "*" with multiples not allowed' => [
541 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
542 ApiBase
::PARAM_ALL
=> true,
544 ApiUsageException
::newWithMessage( null,
545 [ 'apierror-unrecognizedvalue', 'myParam', '*' ],
549 'Wildcard "*" with unrestricted type' => [
552 ApiBase
::PARAM_ISMULTI
=> true,
553 ApiBase
::PARAM_ALL
=> true,
561 ApiBase
::PARAM_ISMULTI
=> true,
562 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
563 ApiBase
::PARAM_ALL
=> 'x',
568 'Wildcard conflicting with allowed value' => [
571 ApiBase
::PARAM_ISMULTI
=> true,
572 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
573 ApiBase
::PARAM_ALL
=> 'a',
576 'Internal error in ApiBase::getParameterFromSettings: ' .
577 'For param myParam, PARAM_ALL collides with a possible ' .
581 'Namespace with wildcard' => [
584 ApiBase
::PARAM_ISMULTI
=> true,
585 ApiBase
::PARAM_TYPE
=> 'namespace',
587 MWNamespace
::getValidNamespaces(),
590 // PARAM_ALL is ignored with namespace types.
591 'Namespace with wildcard suppressed' => [
594 ApiBase
::PARAM_ISMULTI
=> true,
595 ApiBase
::PARAM_TYPE
=> 'namespace',
596 ApiBase
::PARAM_ALL
=> false,
598 MWNamespace
::getValidNamespaces(),
601 'Namespace with wildcard "x"' => [
604 ApiBase
::PARAM_ISMULTI
=> true,
605 ApiBase
::PARAM_TYPE
=> 'namespace',
606 ApiBase
::PARAM_ALL
=> 'x',
609 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
610 [ 'list' => [ 'x' ], 'type' => 'comma' ], 1 ] ],
613 'dDy+G?e?txnr.1:(@[Ru',
614 [ ApiBase
::PARAM_TYPE
=> 'password' ],
615 'dDy+G?e?txnr.1:(@[Ru',
618 'Sensitive field' => [
619 'I am fond of pineapples',
620 [ ApiBase
::PARAM_SENSITIVE
=> true ],
621 'I am fond of pineapples',
624 'Upload with default' => [
627 ApiBase
::PARAM_TYPE
=> 'upload',
628 ApiBase
::PARAM_DFLT
=> '',
631 'Internal error in ApiBase::getParameterFromSettings: ' .
632 "File upload param myParam's default is set to ''. " .
633 'File upload parameters may not have a default.' ),
636 'Multiple upload' => [
639 ApiBase
::PARAM_TYPE
=> 'upload',
640 ApiBase
::PARAM_ISMULTI
=> true,
643 'Internal error in ApiBase::getParameterFromSettings: ' .
644 'Multi-values not supported for myParam' ),
647 // @todo Test actual upload
650 [ ApiBase
::PARAM_TYPE
=> 'namespace' ],
651 ApiUsageException
::newWithMessage( null,
652 [ 'apierror-unrecognizedvalue', 'myParam', '-1' ],
656 'Extra namespace -1' => [
659 ApiBase
::PARAM_TYPE
=> 'namespace',
660 ApiBase
::PARAM_EXTRA_NAMESPACES
=> [ '-1' ],
665 // @todo Test with PARAM_SUBMODULE_MAP unset, need
666 // getModuleManager() to return something real
667 'Nonexistent module' => [
670 ApiBase
::PARAM_TYPE
=> 'submodule',
671 ApiBase
::PARAM_SUBMODULE_MAP
=>
672 [ 'foo' => 'foo', 'bar' => 'foo+bar' ],
674 ApiUsageException
::newWithMessage(
677 'apierror-unrecognizedvalue',
685 '\\x1f with multiples not allowed' => [
688 ApiUsageException
::newWithMessage( null,
689 'apierror-badvalue-notmultivalue',
690 'badvalue_notmultivalue' ),
693 'Integer with unenforced min' => [
696 ApiBase
::PARAM_TYPE
=> 'integer',
697 ApiBase
::PARAM_MIN
=> -1,
700 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
703 'Integer with enforced min' => [
706 ApiBase
::PARAM_TYPE
=> 'integer',
707 ApiBase
::PARAM_MIN
=> -1,
708 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
710 ApiUsageException
::newWithMessage( null,
711 [ 'apierror-integeroutofrange-belowminimum', 'myParam',
712 '-1', '-2' ], 'integeroutofrange',
713 [ 'min' => -1, 'max' => null, 'botMax' => null ] ),
716 'Integer with unenforced max (internal mode)' => [
719 ApiBase
::PARAM_TYPE
=> 'integer',
720 ApiBase
::PARAM_MAX
=> 7,
725 'Integer with enforced max (internal mode)' => [
728 ApiBase
::PARAM_TYPE
=> 'integer',
729 ApiBase
::PARAM_MAX
=> 7,
730 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
735 'Integer with unenforced max (non-internal mode)' => [
738 ApiBase
::PARAM_TYPE
=> 'integer',
739 ApiBase
::PARAM_MAX
=> 7,
742 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 7, 8 ] ],
743 [ 'internalmode' => false ],
745 'Integer with enforced max (non-internal mode)' => [
748 ApiBase
::PARAM_TYPE
=> 'integer',
749 ApiBase
::PARAM_MAX
=> 7,
750 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
752 ApiUsageException
::newWithMessage(
754 [ 'apierror-integeroutofrange-abovemax', 'myParam', '7', '8' ],
756 [ 'min' => null, 'max' => 7, 'botMax' => 7 ]
759 [ 'internalmode' => false ],
761 'Array of integers' => [
764 ApiBase
::PARAM_ISMULTI
=> true,
765 ApiBase
::PARAM_TYPE
=> 'integer',
770 'Array of integers with unenforced min/max (internal mode)' => [
773 ApiBase
::PARAM_ISMULTI
=> true,
774 ApiBase
::PARAM_TYPE
=> 'integer',
775 ApiBase
::PARAM_MIN
=> 0,
776 ApiBase
::PARAM_MAX
=> 100,
779 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ] ],
781 'Array of integers with enforced min/max (internal mode)' => [
784 ApiBase
::PARAM_ISMULTI
=> true,
785 ApiBase
::PARAM_TYPE
=> 'integer',
786 ApiBase
::PARAM_MIN
=> 0,
787 ApiBase
::PARAM_MAX
=> 100,
788 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
790 ApiUsageException
::newWithMessage(
792 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ],
794 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
798 'Array of integers with unenforced min/max (non-internal mode)' => [
801 ApiBase
::PARAM_ISMULTI
=> true,
802 ApiBase
::PARAM_TYPE
=> 'integer',
803 ApiBase
::PARAM_MIN
=> 0,
804 ApiBase
::PARAM_MAX
=> 100,
808 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
809 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ]
811 [ 'internalmode' => false ],
813 'Array of integers with enforced min/max (non-internal mode)' => [
816 ApiBase
::PARAM_ISMULTI
=> true,
817 ApiBase
::PARAM_TYPE
=> 'integer',
818 ApiBase
::PARAM_MIN
=> 0,
819 ApiBase
::PARAM_MAX
=> 100,
820 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
822 ApiUsageException
::newWithMessage(
824 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
826 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
829 [ 'internalmode' => false ],
831 'Limit with parseLimits false' => [
833 [ ApiBase
::PARAM_TYPE
=> 'limit' ],
836 [ 'parseLimits' => false ],
838 'Limit with no max' => [
841 ApiBase
::PARAM_TYPE
=> 'limit',
842 ApiBase
::PARAM_MAX2
=> 10,
843 ApiBase
::PARAM_ISMULTI
=> true,
846 'Internal error in ApiBase::getParameterFromSettings: ' .
847 'MAX1 or MAX2 are not defined for the limit myParam' ),
850 'Limit with no max2' => [
853 ApiBase
::PARAM_TYPE
=> 'limit',
854 ApiBase
::PARAM_MAX
=> 10,
855 ApiBase
::PARAM_ISMULTI
=> true,
858 'Internal error in ApiBase::getParameterFromSettings: ' .
859 'MAX1 or MAX2 are not defined for the limit myParam' ),
862 'Limit with multi-value' => [
865 ApiBase
::PARAM_TYPE
=> 'limit',
866 ApiBase
::PARAM_MAX
=> 10,
867 ApiBase
::PARAM_MAX2
=> 10,
868 ApiBase
::PARAM_ISMULTI
=> true,
871 'Internal error in ApiBase::getParameterFromSettings: ' .
872 'Multi-values not supported for myParam' ),
878 ApiBase
::PARAM_TYPE
=> 'limit',
879 ApiBase
::PARAM_MAX
=> 100,
880 ApiBase
::PARAM_MAX2
=> 100,
888 ApiBase
::PARAM_TYPE
=> 'limit',
889 ApiBase
::PARAM_MAX
=> 100,
890 ApiBase
::PARAM_MAX2
=> 101,
895 'Limit max for apihighlimits' => [
898 ApiBase
::PARAM_TYPE
=> 'limit',
899 ApiBase
::PARAM_MAX
=> 100,
900 ApiBase
::PARAM_MAX2
=> 101,
904 [ 'apihighlimits' => true ],
906 'Limit too large (internal mode)' => [
909 ApiBase
::PARAM_TYPE
=> 'limit',
910 ApiBase
::PARAM_MAX
=> 100,
911 ApiBase
::PARAM_MAX2
=> 101,
916 'Limit okay for apihighlimits (internal mode)' => [
919 ApiBase
::PARAM_TYPE
=> 'limit',
920 ApiBase
::PARAM_MAX
=> 100,
921 ApiBase
::PARAM_MAX2
=> 101,
925 [ 'apihighlimits' => true ],
927 'Limit too large for apihighlimits (internal mode)' => [
930 ApiBase
::PARAM_TYPE
=> 'limit',
931 ApiBase
::PARAM_MAX
=> 100,
932 ApiBase
::PARAM_MAX2
=> 101,
936 [ 'apihighlimits' => true ],
938 'Limit too large (non-internal mode)' => [
941 ApiBase
::PARAM_TYPE
=> 'limit',
942 ApiBase
::PARAM_MAX
=> 100,
943 ApiBase
::PARAM_MAX2
=> 101,
946 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 101 ] ],
947 [ 'internalmode' => false ],
949 'Limit okay for apihighlimits (non-internal mode)' => [
952 ApiBase
::PARAM_TYPE
=> 'limit',
953 ApiBase
::PARAM_MAX
=> 100,
954 ApiBase
::PARAM_MAX2
=> 101,
958 [ 'internalmode' => false, 'apihighlimits' => true ],
960 'Limit too large for apihighlimits (non-internal mode)' => [
963 ApiBase
::PARAM_TYPE
=> 'limit',
964 ApiBase
::PARAM_MAX
=> 100,
965 ApiBase
::PARAM_MAX2
=> 101,
968 [ [ 'apierror-integeroutofrange-abovebotmax', 'myParam', 101, 102 ] ],
969 [ 'internalmode' => false, 'apihighlimits' => true ],
971 'Limit too small' => [
974 ApiBase
::PARAM_TYPE
=> 'limit',
975 ApiBase
::PARAM_MIN
=> -1,
976 ApiBase
::PARAM_MAX
=> 100,
977 ApiBase
::PARAM_MAX2
=> 100,
980 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
984 wfTimestamp( TS_UNIX
, '20211221122112' ),
985 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
991 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
994 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '0' ] ],
996 'Timestamp empty' => [
998 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1000 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '' ] ],
1002 // wfTimestamp() interprets this as Unix time
1005 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1009 'Timestamp now' => [
1011 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1015 'Invalid timestamp' => [
1017 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1018 ApiUsageException
::newWithMessage(
1020 [ 'apierror-badtimestamp', 'myParam', 'a potato' ],
1021 'badtimestamp_myParam'
1025 'Timestamp array' => [
1028 ApiBase
::PARAM_TYPE
=> 'timestamp',
1029 ApiBase
::PARAM_ISMULTI
=> 1,
1031 [ wfTimestamp( TS_MW
, 100 ), wfTimestamp( TS_MW
, 101 ) ],
1036 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1040 'User prefixed with "User:"' => [
1042 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1046 'Invalid username "|"' => [
1048 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1049 ApiUsageException
::newWithMessage( null,
1050 [ 'apierror-baduser', 'myParam', '|' ],
1051 'baduser_myParam' ),
1054 'Invalid username "300.300.300.300"' => [
1056 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1057 ApiUsageException
::newWithMessage( null,
1058 [ 'apierror-baduser', 'myParam', '300.300.300.300' ],
1059 'baduser_myParam' ),
1062 'IP range as username' => [
1064 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1068 'IPv6 as username' => [
1070 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1074 'Obsolete cloaked usemod IP address as username' => [
1076 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1080 'Invalid username containing IP address' => [
1081 'This is [not] valid 1.2.3.xxx, ha!',
1082 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1083 ApiUsageException
::newWithMessage(
1085 [ 'apierror-baduser', 'myParam', 'This is [not] valid 1.2.3.xxx, ha!' ],
1090 'External username' => [
1092 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1096 'Array of usernames' => [
1099 ApiBase
::PARAM_TYPE
=> 'user',
1100 ApiBase
::PARAM_ISMULTI
=> true,
1107 [ ApiBase
::PARAM_TYPE
=> 'tags' ],
1111 'Array of one tag' => [
1114 ApiBase
::PARAM_TYPE
=> 'tags',
1115 ApiBase
::PARAM_ISMULTI
=> true,
1120 'Array of tags' => [
1123 ApiBase
::PARAM_TYPE
=> 'tags',
1124 ApiBase
::PARAM_ISMULTI
=> true,
1131 [ ApiBase
::PARAM_TYPE
=> 'tags' ],
1132 new ApiUsageException( null,
1133 Status
::newFatal( 'tags-apply-not-allowed-one',
1134 'invalid tag', 1 ) ),
1137 'Unrecognized type' => [
1139 [ ApiBase
::PARAM_TYPE
=> 'nonexistenttype' ],
1141 'Internal error in ApiBase::getParameterFromSettings: ' .
1142 "Param myParam's type is unknown - nonexistenttype" ),
1145 'Too many bytes' => [
1148 ApiBase
::PARAM_MAX_BYTES
=> 0,
1149 ApiBase
::PARAM_MAX_CHARS
=> 0,
1151 ApiUsageException
::newWithMessage( null,
1152 [ 'apierror-maxbytes', 'myParam', 0 ] ),
1155 'Too many chars' => [
1158 ApiBase
::PARAM_MAX_BYTES
=> 4,
1159 ApiBase
::PARAM_MAX_CHARS
=> 1,
1161 ApiUsageException
::newWithMessage( null,
1162 [ 'apierror-maxchars', 'myParam', 1 ] ),
1165 'Omitted required param' => [
1167 [ ApiBase
::PARAM_REQUIRED
=> true ],
1168 ApiUsageException
::newWithMessage( null,
1169 [ 'apierror-missingparam', 'myParam' ] ),
1172 'Empty multi-value' => [
1174 [ ApiBase
::PARAM_ISMULTI
=> true ],
1178 'Multi-value \x1f' => [
1180 [ ApiBase
::PARAM_ISMULTI
=> true ],
1184 'Allowed non-multi-value with "|"' => [
1186 [ ApiBase
::PARAM_TYPE
=> [ 'a|b' ] ],
1190 'Prohibited multi-value' => [
1192 [ ApiBase
::PARAM_TYPE
=> [ 'a', 'b' ] ],
1193 ApiUsageException
::newWithMessage( null,
1195 'apierror-multival-only-one-of',
1197 Message
::listParam( [ '<kbd>a</kbd>', '<kbd>b</kbd>' ] ),
1206 // The following really just test PHP's string-to-int conversion.
1214 [ "\t1", 1, '\t1' ],
1215 [ "\r1", 1, '\r1' ],
1216 [ "\f1", 0, '\f1', 'badutf-8' ],
1217 [ "\n1", 1, '\n1' ],
1218 [ "\v1", 0, '\v1', 'badutf-8' ],
1219 [ "\e1", 0, '\e1', 'badutf-8' ],
1220 [ "\x001", 0, '\x001', 'badutf-8' ],
1223 foreach ( $integerTests as $test ) {
1224 $desc = isset( $test[2] ) ?
$test[2] : $test[0];
1225 $warnings = isset( $test[3] ) ?
1226 [ [ 'apiwarn-badutf8', 'myParam' ] ] : [];
1227 $returnArray["\"$desc\" as integer"] = [
1229 [ ApiBase
::PARAM_TYPE
=> 'integer' ],
1235 return $returnArray;
1238 public function testErrorArrayToStatus() {
1239 $mock = new MockApi();
1241 // Sanity check empty array
1242 $expect = Status
::newGood();
1243 $this->assertEquals( $expect, $mock->errorArrayToStatus( [] ) );
1245 // No blocked $user, so no special block handling
1246 $expect = Status
::newGood();
1247 $expect->fatal( 'blockedtext' );
1248 $expect->fatal( 'autoblockedtext' );
1249 $expect->fatal( 'systemblockedtext' );
1250 $expect->fatal( 'mainpage' );
1251 $expect->fatal( 'parentheses', 'foobar' );
1252 $this->assertEquals( $expect, $mock->errorArrayToStatus( [
1254 [ 'autoblockedtext' ],
1255 [ 'systemblockedtext' ],
1257 [ 'parentheses', 'foobar' ],
1260 // Has a blocked $user, so special block handling
1261 $user = $this->getMutableTestUser()->getUser();
1262 $block = new \
Block( [
1263 'address' => $user->getName(),
1264 'user' => $user->getID(),
1265 'by' => $this->getTestSysop()->getUser()->getId(),
1266 'reason' => __METHOD__
,
1267 'expiry' => time() +
100500,
1270 $blockinfo = [ 'blockinfo' => ApiQueryUserInfo
::getBlockInfo( $block ) ];
1272 $expect = Status
::newGood();
1273 $expect->fatal( ApiMessage
::create( 'apierror-blocked', 'blocked', $blockinfo ) );
1274 $expect->fatal( ApiMessage
::create( 'apierror-autoblocked', 'autoblocked', $blockinfo ) );
1275 $expect->fatal( ApiMessage
::create( 'apierror-systemblocked', 'blocked', $blockinfo ) );
1276 $expect->fatal( 'mainpage' );
1277 $expect->fatal( 'parentheses', 'foobar' );
1278 $this->assertEquals( $expect, $mock->errorArrayToStatus( [
1280 [ 'autoblockedtext' ],
1281 [ 'systemblockedtext' ],
1283 [ 'parentheses', 'foobar' ],
1287 public function testDieStatus() {
1288 $mock = new MockApi();
1290 $status = StatusValue
::newGood();
1291 $status->error( 'foo' );
1292 $status->warning( 'bar' );
1294 $mock->dieStatus( $status );
1295 $this->fail( 'Expected exception not thrown' );
1296 } catch ( ApiUsageException
$ex ) {
1297 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' );
1298 $this->assertFalse( ApiTestCase
::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' );
1301 $status = StatusValue
::newGood();
1302 $status->warning( 'foo' );
1303 $status->warning( 'bar' );
1305 $mock->dieStatus( $status );
1306 $this->fail( 'Expected exception not thrown' );
1307 } catch ( ApiUsageException
$ex ) {
1308 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' );
1309 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' );
1312 $status = StatusValue
::newGood();
1313 $status->setOk( false );
1315 $mock->dieStatus( $status );
1316 $this->fail( 'Expected exception not thrown' );
1317 } catch ( ApiUsageException
$ex ) {
1318 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'unknownerror-nocode' ),
1319 'Exception has "unknownerror-nocode"' );
1324 * @covers ApiBase::extractRequestParams
1326 public function testExtractRequestParams() {
1327 $request = new FauxRequest( [
1328 'xxexists' => 'exists!',
1329 'xxmulti' => 'a|b|c|d|{bad}',
1331 'xxtemplate-a' => 'A!',
1332 'xxtemplate-b' => 'B1|B2|B3',
1333 'xxtemplate-c' => '',
1334 'xxrecursivetemplate-b-B1' => 'X',
1335 'xxrecursivetemplate-b-B3' => 'Y',
1336 'xxrecursivetemplate-b-B4' => '?',
1337 'xxemptytemplate-' => 'nope',
1340 'errorformat' => 'raw',
1342 $context = new DerivativeContext( RequestContext
::getMain() );
1343 $context->setRequest( $request );
1344 $main = new ApiMain( $context );
1346 $mock = $this->getMockBuilder( ApiBase
::class )
1347 ->setConstructorArgs( [ $main, 'test', 'xx' ] )
1348 ->setMethods( [ 'getAllowedParams' ] )
1349 ->getMockForAbstractClass();
1350 $mock->method( 'getAllowedParams' )->willReturn( [
1351 'notexists' => null,
1354 ApiBase
::PARAM_ISMULTI
=> true,
1357 ApiBase
::PARAM_ISMULTI
=> true,
1360 ApiBase
::PARAM_ISMULTI
=> true,
1361 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'm' => 'multi' ],
1363 'recursivetemplate-{m}-{t}' => [
1364 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 't' => 'template-{m}', 'm' => 'multi' ],
1366 'emptytemplate-{m}' => [
1367 ApiBase
::PARAM_ISMULTI
=> true,
1368 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'm' => 'empty' ],
1370 'badtemplate-{e}' => [
1371 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'e' => 'exists' ],
1373 'badtemplate2-{e}' => [
1374 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'e' => 'badtemplate2-{e}' ],
1376 'badtemplate3-{x}' => [
1377 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'x' => 'foo' ],
1381 $this->assertEquals( [
1382 'notexists' => null,
1383 'exists' => 'exists!',
1384 'multi' => [ 'a', 'b', 'c', 'd', '{bad}' ],
1386 'template-a' => [ 'A!' ],
1387 'template-b' => [ 'B1', 'B2', 'B3' ],
1389 'template-d' => null,
1390 'recursivetemplate-a-A!' => null,
1391 'recursivetemplate-b-B1' => 'X',
1392 'recursivetemplate-b-B2' => null,
1393 'recursivetemplate-b-B3' => 'Y',
1394 ], $mock->extractRequestParams() );
1396 $used = TestingAccessWrapper
::newFromObject( $main )->getParamsUsed();
1398 $this->assertEquals( [
1403 'xxrecursivetemplate-a-A!',
1404 'xxrecursivetemplate-b-B1',
1405 'xxrecursivetemplate-b-B2',
1406 'xxrecursivetemplate-b-B3',
1413 $warnings = $mock->getResult()->getResultData( 'warnings', [ 'Strip' => 'all' ] );
1414 $this->assertCount( 1, $warnings );
1415 $this->assertSame( 'ignoring-invalid-templated-value', $warnings[0]['code'] );