Merge "Fix article counting logic in DerivedPageDataUpdater"
[lhc/web/wiklou.git] / tests / phpunit / includes / api / ApiBaseTest.php
1 <?php
2
3 use Wikimedia\TestingAccessWrapper;
4
5 /**
6 * @group API
7 * @group Database
8 * @group medium
9 *
10 * @covers ApiBase
11 */
12 class ApiBaseTest extends ApiTestCase {
13 /**
14 * This covers a variety of stub methods that return a fixed value.
15 *
16 * @param string|array $method Name of method, or [ name, params... ]
17 * @param string $value Expected value
18 *
19 * @dataProvider provideStubMethods
20 */
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 );
26 }
27
28 public function provideStubMethods() {
29 return [
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' ],
45 ];
46 }
47
48 public function testRequireOnlyOneParameterDefault() {
49 $mock = new MockApi();
50 $mock->requireOnlyOneParameter(
51 [ "filename" => "foo.txt", "enablechunks" => false ],
52 "filename", "enablechunks"
53 );
54 $this->assertTrue( true );
55 }
56
57 /**
58 * @expectedException ApiUsageException
59 */
60 public function testRequireOnlyOneParameterZero() {
61 $mock = new MockApi();
62 $mock->requireOnlyOneParameter(
63 [ "filename" => "foo.txt", "enablechunks" => 0 ],
64 "filename", "enablechunks"
65 );
66 }
67
68 /**
69 * @expectedException ApiUsageException
70 */
71 public function testRequireOnlyOneParameterTrue() {
72 $mock = new MockApi();
73 $mock->requireOnlyOneParameter(
74 [ "filename" => "foo.txt", "enablechunks" => true ],
75 "filename", "enablechunks"
76 );
77 }
78
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 ],
85 "foo", "bar" );
86 }
87
88 public function testRequireMaxOneParameterZero() {
89 $mock = new MockApi();
90 $mock->requireMaxOneParameter(
91 [ 'foo' => 'bar', 'baz' => 'quz' ],
92 'squirrel' );
93 $this->assertTrue( true );
94 }
95
96 public function testRequireMaxOneParameterOne() {
97 $mock = new MockApi();
98 $mock->requireMaxOneParameter(
99 [ 'foo' => 'bar', 'baz' => 'quz' ],
100 'foo', 'squirrel' );
101 $this->assertTrue( true );
102 }
103
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' ],
110 'foo', 'baz' );
111 }
112
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' ],
119 'foo', 'bar' );
120 }
121
122 public function testRequireAtLeastOneParameterOne() {
123 $mock = new MockApi();
124 $mock->requireAtLeastOneParameter(
125 [ 'a' => 'b', 'c' => 'd' ],
126 'foo', 'a' );
127 $this->assertTrue( true );
128 }
129
130 public function testRequireAtLeastOneParameterTwo() {
131 $mock = new MockApi();
132 $mock->requireAtLeastOneParameter(
133 [ 'a' => 'b', 'c' => 'd' ],
134 'a', 'c' );
135 $this->assertTrue( true );
136 }
137
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 ] );
143 }
144
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() );
150 }
151
152 public function testGetTitleOrPageIdInvalidTitle() {
153 $this->setExpectedException( ApiUsageException::class,
154 'Bad title "|".' );
155 $mock = new MockApi();
156 $mock->getTitleOrPageId( [ 'title' => '|' ] );
157 }
158
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' ] );
164 }
165
166 public function testGetTitleOrPageIdPageId() {
167 $page = $this->getExistingTestPage();
168 $result = ( new MockApi() )->getTitleOrPageId(
169 [ 'pageid' => $page->getId() ] );
170 $this->assertInstanceOf( WikiPage::class, $result );
171 $this->assertSame(
172 $page->getTitle()->getPrefixedText(),
173 $result->getTitle()->getPrefixedText()
174 );
175 }
176
177 public function testGetTitleOrPageIdInvalidPageId() {
178 // FIXME: fails under postgres
179 $this->markTestSkippedIfDbType( 'postgres' );
180
181 $this->setExpectedException( ApiUsageException::class,
182 'There is no page with ID 2147483648.' );
183 $mock = new MockApi();
184 $mock->getTitleOrPageId( [ 'pageid' => 2147483648 ] );
185 }
186
187 public function testGetTitleFromTitleOrPageIdBadParams() {
188 $this->setExpectedException( ApiUsageException::class,
189 'The parameters "title" and "pageid" can not be used together.' );
190 $mock = new MockApi();
191 $mock->getTitleFromTitleOrPageId( [ 'title' => 'a', 'pageid' => 7 ] );
192 }
193
194 public function testGetTitleFromTitleOrPageIdTitle() {
195 $mock = new MockApi();
196 $result = $mock->getTitleFromTitleOrPageId( [ 'title' => 'Foo' ] );
197 $this->assertInstanceOf( Title::class, $result );
198 $this->assertSame( 'Foo', $result->getPrefixedText() );
199 }
200
201 public function testGetTitleFromTitleOrPageIdInvalidTitle() {
202 $this->setExpectedException( ApiUsageException::class,
203 'Bad title "|".' );
204 $mock = new MockApi();
205 $mock->getTitleFromTitleOrPageId( [ 'title' => '|' ] );
206 }
207
208 public function testGetTitleFromTitleOrPageIdPageId() {
209 $page = $this->getExistingTestPage();
210 $result = ( new MockApi() )->getTitleFromTitleOrPageId(
211 [ 'pageid' => $page->getId() ] );
212 $this->assertInstanceOf( Title::class, $result );
213 $this->assertSame( $page->getTitle()->getPrefixedText(), $result->getPrefixedText() );
214 }
215
216 public function testGetTitleFromTitleOrPageIdInvalidPageId() {
217 $this->setExpectedException( ApiUsageException::class,
218 'There is no page with ID 298401643.' );
219 $mock = new MockApi();
220 $mock->getTitleFromTitleOrPageId( [ 'pageid' => 298401643 ] );
221 }
222
223 public function testGetParameter() {
224 $mock = $this->getMockBuilder( MockApi::class )
225 ->setMethods( [ 'getAllowedParams' ] )
226 ->getMock();
227 $mock->method( 'getAllowedParams' )->willReturn( [
228 'foo' => [
229 ApiBase::PARAM_TYPE => [ 'value' ],
230 ],
231 'bar' => [
232 ApiBase::PARAM_TYPE => [ 'value' ],
233 ],
234 ] );
235 $wrapper = TestingAccessWrapper::newFromObject( $mock );
236
237 $context = new DerivativeContext( $mock );
238 $context->setRequest( new FauxRequest( [ 'foo' => 'bad', 'bar' => 'value' ] ) );
239 $wrapper->mMainModule = new ApiMain( $context );
240
241 // Even though 'foo' is bad, getParameter( 'bar' ) must not fail
242 $this->assertSame( 'value', $wrapper->getParameter( 'bar' ) );
243
244 // But getParameter( 'foo' ) must throw.
245 try {
246 $wrapper->getParameter( 'foo' );
247 $this->fail( 'Expected exception not thrown' );
248 } catch ( ApiUsageException $ex ) {
249 $this->assertTrue( $this->apiExceptionHasCode( $ex, 'unknown_foo' ) );
250 }
251
252 // And extractRequestParams() must throw too.
253 try {
254 $mock->extractRequestParams();
255 $this->fail( 'Expected exception not thrown' );
256 } catch ( ApiUsageException $ex ) {
257 $this->assertTrue( $this->apiExceptionHasCode( $ex, 'unknown_foo' ) );
258 }
259 }
260
261 /**
262 * @param string|null $input
263 * @param array $paramSettings
264 * @param mixed $expected
265 * @param array $options Key-value pairs:
266 * 'parseLimits': true|false
267 * 'apihighlimits': true|false
268 * 'internalmode': true|false
269 * 'prefix': true|false
270 * @param string[] $warnings
271 */
272 private function doGetParameterFromSettings(
273 $input, $paramSettings, $expected, $warnings, $options = []
274 ) {
275 $mock = new MockApi();
276 $wrapper = TestingAccessWrapper::newFromObject( $mock );
277 if ( $options['prefix'] ) {
278 $wrapper->mModulePrefix = 'my';
279 $paramName = 'Param';
280 } else {
281 $paramName = 'myParam';
282 }
283
284 $context = new DerivativeContext( $mock );
285 $context->setRequest( new FauxRequest(
286 $input !== null ? [ 'myParam' => $input ] : [] ) );
287 $wrapper->mMainModule = new ApiMain( $context );
288
289 $parseLimits = $options['parseLimits'] ?? true;
290
291 if ( !empty( $options['apihighlimits'] ) ) {
292 $context->setUser( self::$users['sysop']->getUser() );
293 }
294
295 if ( isset( $options['internalmode'] ) && !$options['internalmode'] ) {
296 $mainWrapper = TestingAccessWrapper::newFromObject( $wrapper->mMainModule );
297 $mainWrapper->mInternalMode = false;
298 }
299
300 // If we're testing tags, set up some tags
301 if ( isset( $paramSettings[ApiBase::PARAM_TYPE] ) &&
302 $paramSettings[ApiBase::PARAM_TYPE] === 'tags'
303 ) {
304 ChangeTags::defineTag( 'tag1' );
305 ChangeTags::defineTag( 'tag2' );
306 }
307
308 if ( $expected instanceof Exception ) {
309 try {
310 $wrapper->getParameterFromSettings( $paramName, $paramSettings,
311 $parseLimits );
312 $this->fail( 'No exception thrown' );
313 } catch ( Exception $ex ) {
314 $this->assertEquals( $expected, $ex );
315 }
316 } else {
317 $result = $wrapper->getParameterFromSettings( $paramName,
318 $paramSettings, $parseLimits );
319 if ( isset( $paramSettings[ApiBase::PARAM_TYPE] ) &&
320 $paramSettings[ApiBase::PARAM_TYPE] === 'timestamp' &&
321 $expected === 'now'
322 ) {
323 // Allow one second of fuzziness. Make sure the formats are
324 // correct!
325 $this->assertRegExp( '/^\d{14}$/', $result );
326 $this->assertLessThanOrEqual( 1,
327 abs( wfTimestamp( TS_UNIX, $result ) - time() ),
328 "Result $result differs from expected $expected by " .
329 'more than one second' );
330 } else {
331 $this->assertSame( $expected, $result );
332 }
333 $actualWarnings = array_map( function ( $warn ) {
334 return $warn instanceof Message
335 ? array_merge( [ $warn->getKey() ], $warn->getParams() )
336 : $warn;
337 }, $mock->warnings );
338 $this->assertSame( $warnings, $actualWarnings );
339 }
340
341 if ( !empty( $paramSettings[ApiBase::PARAM_SENSITIVE] ) ||
342 ( isset( $paramSettings[ApiBase::PARAM_TYPE] ) &&
343 $paramSettings[ApiBase::PARAM_TYPE] === 'password' )
344 ) {
345 $mainWrapper = TestingAccessWrapper::newFromObject( $wrapper->getMain() );
346 $this->assertSame( [ 'myParam' ],
347 $mainWrapper->getSensitiveParams() );
348 }
349 }
350
351 /**
352 * @dataProvider provideGetParameterFromSettings
353 * @see self::doGetParameterFromSettings()
354 */
355 public function testGetParameterFromSettings_noprefix(
356 $input, $paramSettings, $expected, $warnings, $options = []
357 ) {
358 $options['prefix'] = false;
359 $this->doGetParameterFromSettings( $input, $paramSettings, $expected, $warnings, $options );
360 }
361
362 /**
363 * @dataProvider provideGetParameterFromSettings
364 * @see self::doGetParameterFromSettings()
365 */
366 public function testGetParameterFromSettings_prefix(
367 $input, $paramSettings, $expected, $warnings, $options = []
368 ) {
369 $options['prefix'] = true;
370 $this->doGetParameterFromSettings( $input, $paramSettings, $expected, $warnings, $options );
371 }
372
373 public static function provideGetParameterFromSettings() {
374 $warnings = [
375 [ 'apiwarn-badutf8', 'myParam' ],
376 ];
377
378 $c0 = '';
379 $enc = '';
380 for ( $i = 0; $i < 32; $i++ ) {
381 $c0 .= chr( $i );
382 $enc .= ( $i === 9 || $i === 10 || $i === 13 )
383 ? chr( $i )
384 : '�';
385 }
386
387 $returnArray = [
388 'Basic param' => [ 'bar', null, 'bar', [] ],
389 'Basic param, C0 controls' => [ $c0, null, $enc, $warnings ],
390 'String param' => [ 'bar', '', 'bar', [] ],
391 'String param, defaulted' => [ null, '', '', [] ],
392 'String param, empty' => [ '', 'default', '', [] ],
393 'String param, required, empty' => [
394 '',
395 [ ApiBase::PARAM_DFLT => 'default', ApiBase::PARAM_REQUIRED => true ],
396 ApiUsageException::newWithMessage( null,
397 [ 'apierror-missingparam', 'myParam' ] ),
398 []
399 ],
400 'Multi-valued parameter' => [
401 'a|b|c',
402 [ ApiBase::PARAM_ISMULTI => true ],
403 [ 'a', 'b', 'c' ],
404 []
405 ],
406 'Multi-valued parameter, alternative separator' => [
407 "\x1fa|b\x1fc|d",
408 [ ApiBase::PARAM_ISMULTI => true ],
409 [ 'a|b', 'c|d' ],
410 []
411 ],
412 'Multi-valued parameter, other C0 controls' => [
413 $c0,
414 [ ApiBase::PARAM_ISMULTI => true ],
415 [ $enc ],
416 $warnings
417 ],
418 'Multi-valued parameter, other C0 controls (2)' => [
419 "\x1f" . $c0,
420 [ ApiBase::PARAM_ISMULTI => true ],
421 [ substr( $enc, 0, -3 ), '' ],
422 $warnings
423 ],
424 'Multi-valued parameter with limits' => [
425 'a|b|c',
426 [
427 ApiBase::PARAM_ISMULTI => true,
428 ApiBase::PARAM_ISMULTI_LIMIT1 => 3,
429 ],
430 [ 'a', 'b', 'c' ],
431 [],
432 ],
433 'Multi-valued parameter with exceeded limits' => [
434 'a|b|c',
435 [
436 ApiBase::PARAM_ISMULTI => true,
437 ApiBase::PARAM_ISMULTI_LIMIT1 => 2,
438 ],
439 ApiUsageException::newWithMessage(
440 null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
441 ),
442 []
443 ],
444 'Multi-valued parameter with exceeded limits for non-bot' => [
445 'a|b|c',
446 [
447 ApiBase::PARAM_ISMULTI => true,
448 ApiBase::PARAM_ISMULTI_LIMIT1 => 2,
449 ApiBase::PARAM_ISMULTI_LIMIT2 => 3,
450 ],
451 ApiUsageException::newWithMessage(
452 null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
453 ),
454 []
455 ],
456 'Multi-valued parameter with non-exceeded limits for bot' => [
457 'a|b|c',
458 [
459 ApiBase::PARAM_ISMULTI => true,
460 ApiBase::PARAM_ISMULTI_LIMIT1 => 2,
461 ApiBase::PARAM_ISMULTI_LIMIT2 => 3,
462 ],
463 [ 'a', 'b', 'c' ],
464 [],
465 [ 'apihighlimits' => true ],
466 ],
467 'Multi-valued parameter with prohibited duplicates' => [
468 'a|b|a|c',
469 [ ApiBase::PARAM_ISMULTI => true ],
470 // Note that the keys are not sequential! This matches
471 // array_unique, but might be unexpected.
472 [ 0 => 'a', 1 => 'b', 3 => 'c' ],
473 [],
474 ],
475 'Multi-valued parameter with allowed duplicates' => [
476 'a|a',
477 [
478 ApiBase::PARAM_ISMULTI => true,
479 ApiBase::PARAM_ALLOW_DUPLICATES => true,
480 ],
481 [ 'a', 'a' ],
482 [],
483 ],
484 'Empty boolean param' => [
485 '',
486 [ ApiBase::PARAM_TYPE => 'boolean' ],
487 true,
488 [],
489 ],
490 'Boolean param 0' => [
491 '0',
492 [ ApiBase::PARAM_TYPE => 'boolean' ],
493 true,
494 [],
495 ],
496 'Boolean param false' => [
497 'false',
498 [ ApiBase::PARAM_TYPE => 'boolean' ],
499 true,
500 [],
501 ],
502 'Boolean multi-param' => [
503 'true|false',
504 [
505 ApiBase::PARAM_TYPE => 'boolean',
506 ApiBase::PARAM_ISMULTI => true,
507 ],
508 new MWException(
509 'Internal error in ApiBase::getParameterFromSettings: ' .
510 'Multi-values not supported for myParam'
511 ),
512 [],
513 ],
514 'Empty boolean param with non-false default' => [
515 '',
516 [
517 ApiBase::PARAM_TYPE => 'boolean',
518 ApiBase::PARAM_DFLT => true,
519 ],
520 new MWException(
521 'Internal error in ApiBase::getParameterFromSettings: ' .
522 "Boolean param myParam's default is set to '1'. " .
523 'Boolean parameters must default to false.' ),
524 [],
525 ],
526 'Deprecated parameter' => [
527 'foo',
528 [ ApiBase::PARAM_DEPRECATED => true ],
529 'foo',
530 [ [ 'apiwarn-deprecation-parameter', 'myParam' ] ],
531 ],
532 'Deprecated parameter value' => [
533 'a',
534 [ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => true ] ],
535 'a',
536 [ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ],
537 ],
538 'Multiple deprecated parameter values' => [
539 'a|b|c|d',
540 [ ApiBase::PARAM_DEPRECATED_VALUES =>
541 [ 'b' => true, 'd' => true ],
542 ApiBase::PARAM_ISMULTI => true ],
543 [ 'a', 'b', 'c', 'd' ],
544 [
545 [ 'apiwarn-deprecation-parameter', 'myParam=b' ],
546 [ 'apiwarn-deprecation-parameter', 'myParam=d' ],
547 ],
548 ],
549 'Deprecated parameter value with custom warning' => [
550 'a',
551 [ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => 'my-msg' ] ],
552 'a',
553 [ 'my-msg' ],
554 ],
555 '"*" when wildcard not allowed' => [
556 '*',
557 [ ApiBase::PARAM_ISMULTI => true,
558 ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ] ],
559 [],
560 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
561 [ 'list' => [ '&#42;' ], 'type' => 'comma' ], 1 ] ],
562 ],
563 'Wildcard "*"' => [
564 '*',
565 [
566 ApiBase::PARAM_ISMULTI => true,
567 ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ],
568 ApiBase::PARAM_ALL => true,
569 ],
570 [ 'a', 'b', 'c' ],
571 [],
572 ],
573 'Wildcard "*" with multiples not allowed' => [
574 '*',
575 [
576 ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ],
577 ApiBase::PARAM_ALL => true,
578 ],
579 ApiUsageException::newWithMessage( null,
580 [ 'apierror-unrecognizedvalue', 'myParam', '&#42;' ],
581 'unknown_myParam' ),
582 [],
583 ],
584 'Wildcard "*" with unrestricted type' => [
585 '*',
586 [
587 ApiBase::PARAM_ISMULTI => true,
588 ApiBase::PARAM_ALL => true,
589 ],
590 [ '*' ],
591 [],
592 ],
593 'Wildcard "x"' => [
594 'x',
595 [
596 ApiBase::PARAM_ISMULTI => true,
597 ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ],
598 ApiBase::PARAM_ALL => 'x',
599 ],
600 [ 'a', 'b', 'c' ],
601 [],
602 ],
603 'Wildcard conflicting with allowed value' => [
604 'a',
605 [
606 ApiBase::PARAM_ISMULTI => true,
607 ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ],
608 ApiBase::PARAM_ALL => 'a',
609 ],
610 new MWException(
611 'Internal error in ApiBase::getParameterFromSettings: ' .
612 'For param myParam, PARAM_ALL collides with a possible ' .
613 'value' ),
614 [],
615 ],
616 'Namespace with wildcard' => [
617 '*',
618 [
619 ApiBase::PARAM_ISMULTI => true,
620 ApiBase::PARAM_TYPE => 'namespace',
621 ],
622 MWNamespace::getValidNamespaces(),
623 [],
624 ],
625 // PARAM_ALL is ignored with namespace types.
626 'Namespace with wildcard suppressed' => [
627 '*',
628 [
629 ApiBase::PARAM_ISMULTI => true,
630 ApiBase::PARAM_TYPE => 'namespace',
631 ApiBase::PARAM_ALL => false,
632 ],
633 MWNamespace::getValidNamespaces(),
634 [],
635 ],
636 'Namespace with wildcard "x"' => [
637 'x',
638 [
639 ApiBase::PARAM_ISMULTI => true,
640 ApiBase::PARAM_TYPE => 'namespace',
641 ApiBase::PARAM_ALL => 'x',
642 ],
643 [],
644 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
645 [ 'list' => [ 'x' ], 'type' => 'comma' ], 1 ] ],
646 ],
647 'Password' => [
648 'dDy+G?e?txnr.1:(@[Ru',
649 [ ApiBase::PARAM_TYPE => 'password' ],
650 'dDy+G?e?txnr.1:(@[Ru',
651 [],
652 ],
653 'Sensitive field' => [
654 'I am fond of pineapples',
655 [ ApiBase::PARAM_SENSITIVE => true ],
656 'I am fond of pineapples',
657 [],
658 ],
659 'Upload with default' => [
660 '',
661 [
662 ApiBase::PARAM_TYPE => 'upload',
663 ApiBase::PARAM_DFLT => '',
664 ],
665 new MWException(
666 'Internal error in ApiBase::getParameterFromSettings: ' .
667 "File upload param myParam's default is set to ''. " .
668 'File upload parameters may not have a default.' ),
669 [],
670 ],
671 'Multiple upload' => [
672 '',
673 [
674 ApiBase::PARAM_TYPE => 'upload',
675 ApiBase::PARAM_ISMULTI => true,
676 ],
677 new MWException(
678 'Internal error in ApiBase::getParameterFromSettings: ' .
679 'Multi-values not supported for myParam' ),
680 [],
681 ],
682 // @todo Test actual upload
683 'Namespace -1' => [
684 '-1',
685 [ ApiBase::PARAM_TYPE => 'namespace' ],
686 ApiUsageException::newWithMessage( null,
687 [ 'apierror-unrecognizedvalue', 'myParam', '-1' ],
688 'unknown_myParam' ),
689 [],
690 ],
691 'Extra namespace -1' => [
692 '-1',
693 [
694 ApiBase::PARAM_TYPE => 'namespace',
695 ApiBase::PARAM_EXTRA_NAMESPACES => [ '-1' ],
696 ],
697 '-1',
698 [],
699 ],
700 // @todo Test with PARAM_SUBMODULE_MAP unset, need
701 // getModuleManager() to return something real
702 'Nonexistent module' => [
703 'not-a-module-name',
704 [
705 ApiBase::PARAM_TYPE => 'submodule',
706 ApiBase::PARAM_SUBMODULE_MAP =>
707 [ 'foo' => 'foo', 'bar' => 'foo+bar' ],
708 ],
709 ApiUsageException::newWithMessage(
710 null,
711 [
712 'apierror-unrecognizedvalue',
713 'myParam',
714 'not-a-module-name',
715 ],
716 'unknown_myParam'
717 ),
718 [],
719 ],
720 '\\x1f with multiples not allowed' => [
721 "\x1f",
722 [],
723 ApiUsageException::newWithMessage( null,
724 'apierror-badvalue-notmultivalue',
725 'badvalue_notmultivalue' ),
726 [],
727 ],
728 'Integer with unenforced min' => [
729 '-2',
730 [
731 ApiBase::PARAM_TYPE => 'integer',
732 ApiBase::PARAM_MIN => -1,
733 ],
734 -1,
735 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
736 -2 ] ],
737 ],
738 'Integer with enforced min' => [
739 '-2',
740 [
741 ApiBase::PARAM_TYPE => 'integer',
742 ApiBase::PARAM_MIN => -1,
743 ApiBase::PARAM_RANGE_ENFORCE => true,
744 ],
745 ApiUsageException::newWithMessage( null,
746 [ 'apierror-integeroutofrange-belowminimum', 'myParam',
747 '-1', '-2' ], 'integeroutofrange',
748 [ 'min' => -1, 'max' => null, 'botMax' => null ] ),
749 [],
750 ],
751 'Integer with unenforced max (internal mode)' => [
752 '8',
753 [
754 ApiBase::PARAM_TYPE => 'integer',
755 ApiBase::PARAM_MAX => 7,
756 ],
757 8,
758 [],
759 ],
760 'Integer with enforced max (internal mode)' => [
761 '8',
762 [
763 ApiBase::PARAM_TYPE => 'integer',
764 ApiBase::PARAM_MAX => 7,
765 ApiBase::PARAM_RANGE_ENFORCE => true,
766 ],
767 8,
768 [],
769 ],
770 'Integer with unenforced max (non-internal mode)' => [
771 '8',
772 [
773 ApiBase::PARAM_TYPE => 'integer',
774 ApiBase::PARAM_MAX => 7,
775 ],
776 7,
777 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 7, 8 ] ],
778 [ 'internalmode' => false ],
779 ],
780 'Integer with enforced max (non-internal mode)' => [
781 '8',
782 [
783 ApiBase::PARAM_TYPE => 'integer',
784 ApiBase::PARAM_MAX => 7,
785 ApiBase::PARAM_RANGE_ENFORCE => true,
786 ],
787 ApiUsageException::newWithMessage(
788 null,
789 [ 'apierror-integeroutofrange-abovemax', 'myParam', '7', '8' ],
790 'integeroutofrange',
791 [ 'min' => null, 'max' => 7, 'botMax' => 7 ]
792 ),
793 [],
794 [ 'internalmode' => false ],
795 ],
796 'Array of integers' => [
797 '3|12|966|-1',
798 [
799 ApiBase::PARAM_ISMULTI => true,
800 ApiBase::PARAM_TYPE => 'integer',
801 ],
802 [ 3, 12, 966, -1 ],
803 [],
804 ],
805 'Array of integers with unenforced min/max (internal mode)' => [
806 '3|12|966|-1',
807 [
808 ApiBase::PARAM_ISMULTI => true,
809 ApiBase::PARAM_TYPE => 'integer',
810 ApiBase::PARAM_MIN => 0,
811 ApiBase::PARAM_MAX => 100,
812 ],
813 [ 3, 12, 966, 0 ],
814 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ] ],
815 ],
816 'Array of integers with enforced min/max (internal mode)' => [
817 '3|12|966|-1',
818 [
819 ApiBase::PARAM_ISMULTI => true,
820 ApiBase::PARAM_TYPE => 'integer',
821 ApiBase::PARAM_MIN => 0,
822 ApiBase::PARAM_MAX => 100,
823 ApiBase::PARAM_RANGE_ENFORCE => true,
824 ],
825 ApiUsageException::newWithMessage(
826 null,
827 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ],
828 'integeroutofrange',
829 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
830 ),
831 [],
832 ],
833 'Array of integers with unenforced min/max (non-internal mode)' => [
834 '3|12|966|-1',
835 [
836 ApiBase::PARAM_ISMULTI => true,
837 ApiBase::PARAM_TYPE => 'integer',
838 ApiBase::PARAM_MIN => 0,
839 ApiBase::PARAM_MAX => 100,
840 ],
841 [ 3, 12, 100, 0 ],
842 [
843 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
844 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ]
845 ],
846 [ 'internalmode' => false ],
847 ],
848 'Array of integers with enforced min/max (non-internal mode)' => [
849 '3|12|966|-1',
850 [
851 ApiBase::PARAM_ISMULTI => true,
852 ApiBase::PARAM_TYPE => 'integer',
853 ApiBase::PARAM_MIN => 0,
854 ApiBase::PARAM_MAX => 100,
855 ApiBase::PARAM_RANGE_ENFORCE => true,
856 ],
857 ApiUsageException::newWithMessage(
858 null,
859 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
860 'integeroutofrange',
861 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
862 ),
863 [],
864 [ 'internalmode' => false ],
865 ],
866 'Limit with parseLimits false' => [
867 '100',
868 [ ApiBase::PARAM_TYPE => 'limit' ],
869 '100',
870 [],
871 [ 'parseLimits' => false ],
872 ],
873 'Limit with no max' => [
874 '100',
875 [
876 ApiBase::PARAM_TYPE => 'limit',
877 ApiBase::PARAM_MAX2 => 10,
878 ApiBase::PARAM_ISMULTI => true,
879 ],
880 new MWException(
881 'Internal error in ApiBase::getParameterFromSettings: ' .
882 'MAX1 or MAX2 are not defined for the limit myParam' ),
883 [],
884 ],
885 'Limit with no max2' => [
886 '100',
887 [
888 ApiBase::PARAM_TYPE => 'limit',
889 ApiBase::PARAM_MAX => 10,
890 ApiBase::PARAM_ISMULTI => true,
891 ],
892 new MWException(
893 'Internal error in ApiBase::getParameterFromSettings: ' .
894 'MAX1 or MAX2 are not defined for the limit myParam' ),
895 [],
896 ],
897 'Limit with multi-value' => [
898 '100',
899 [
900 ApiBase::PARAM_TYPE => 'limit',
901 ApiBase::PARAM_MAX => 10,
902 ApiBase::PARAM_MAX2 => 10,
903 ApiBase::PARAM_ISMULTI => true,
904 ],
905 new MWException(
906 'Internal error in ApiBase::getParameterFromSettings: ' .
907 'Multi-values not supported for myParam' ),
908 [],
909 ],
910 'Valid limit' => [
911 '100',
912 [
913 ApiBase::PARAM_TYPE => 'limit',
914 ApiBase::PARAM_MAX => 100,
915 ApiBase::PARAM_MAX2 => 100,
916 ],
917 100,
918 [],
919 ],
920 'Limit max' => [
921 'max',
922 [
923 ApiBase::PARAM_TYPE => 'limit',
924 ApiBase::PARAM_MAX => 100,
925 ApiBase::PARAM_MAX2 => 101,
926 ],
927 100,
928 [],
929 ],
930 'Limit max for apihighlimits' => [
931 'max',
932 [
933 ApiBase::PARAM_TYPE => 'limit',
934 ApiBase::PARAM_MAX => 100,
935 ApiBase::PARAM_MAX2 => 101,
936 ],
937 101,
938 [],
939 [ 'apihighlimits' => true ],
940 ],
941 'Limit too large (internal mode)' => [
942 '101',
943 [
944 ApiBase::PARAM_TYPE => 'limit',
945 ApiBase::PARAM_MAX => 100,
946 ApiBase::PARAM_MAX2 => 101,
947 ],
948 101,
949 [],
950 ],
951 'Limit okay for apihighlimits (internal mode)' => [
952 '101',
953 [
954 ApiBase::PARAM_TYPE => 'limit',
955 ApiBase::PARAM_MAX => 100,
956 ApiBase::PARAM_MAX2 => 101,
957 ],
958 101,
959 [],
960 [ 'apihighlimits' => true ],
961 ],
962 'Limit too large for apihighlimits (internal mode)' => [
963 '102',
964 [
965 ApiBase::PARAM_TYPE => 'limit',
966 ApiBase::PARAM_MAX => 100,
967 ApiBase::PARAM_MAX2 => 101,
968 ],
969 102,
970 [],
971 [ 'apihighlimits' => true ],
972 ],
973 'Limit too large (non-internal mode)' => [
974 '101',
975 [
976 ApiBase::PARAM_TYPE => 'limit',
977 ApiBase::PARAM_MAX => 100,
978 ApiBase::PARAM_MAX2 => 101,
979 ],
980 100,
981 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 101 ] ],
982 [ 'internalmode' => false ],
983 ],
984 'Limit okay for apihighlimits (non-internal mode)' => [
985 '101',
986 [
987 ApiBase::PARAM_TYPE => 'limit',
988 ApiBase::PARAM_MAX => 100,
989 ApiBase::PARAM_MAX2 => 101,
990 ],
991 101,
992 [],
993 [ 'internalmode' => false, 'apihighlimits' => true ],
994 ],
995 'Limit too large for apihighlimits (non-internal mode)' => [
996 '102',
997 [
998 ApiBase::PARAM_TYPE => 'limit',
999 ApiBase::PARAM_MAX => 100,
1000 ApiBase::PARAM_MAX2 => 101,
1001 ],
1002 101,
1003 [ [ 'apierror-integeroutofrange-abovebotmax', 'myParam', 101, 102 ] ],
1004 [ 'internalmode' => false, 'apihighlimits' => true ],
1005 ],
1006 'Limit too small' => [
1007 '-2',
1008 [
1009 ApiBase::PARAM_TYPE => 'limit',
1010 ApiBase::PARAM_MIN => -1,
1011 ApiBase::PARAM_MAX => 100,
1012 ApiBase::PARAM_MAX2 => 100,
1013 ],
1014 -1,
1015 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
1016 -2 ] ],
1017 ],
1018 'Timestamp' => [
1019 wfTimestamp( TS_UNIX, '20211221122112' ),
1020 [ ApiBase::PARAM_TYPE => 'timestamp' ],
1021 '20211221122112',
1022 [],
1023 ],
1024 'Timestamp 0' => [
1025 '0',
1026 [ ApiBase::PARAM_TYPE => 'timestamp' ],
1027 // Magic keyword
1028 'now',
1029 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '0' ] ],
1030 ],
1031 'Timestamp empty' => [
1032 '',
1033 [ ApiBase::PARAM_TYPE => 'timestamp' ],
1034 'now',
1035 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '' ] ],
1036 ],
1037 // wfTimestamp() interprets this as Unix time
1038 'Timestamp 00' => [
1039 '00',
1040 [ ApiBase::PARAM_TYPE => 'timestamp' ],
1041 '19700101000000',
1042 [],
1043 ],
1044 'Timestamp now' => [
1045 'now',
1046 [ ApiBase::PARAM_TYPE => 'timestamp' ],
1047 'now',
1048 [],
1049 ],
1050 'Invalid timestamp' => [
1051 'a potato',
1052 [ ApiBase::PARAM_TYPE => 'timestamp' ],
1053 ApiUsageException::newWithMessage(
1054 null,
1055 [ 'apierror-badtimestamp', 'myParam', 'a potato' ],
1056 'badtimestamp_myParam'
1057 ),
1058 [],
1059 ],
1060 'Timestamp array' => [
1061 '100|101',
1062 [
1063 ApiBase::PARAM_TYPE => 'timestamp',
1064 ApiBase::PARAM_ISMULTI => 1,
1065 ],
1066 [ wfTimestamp( TS_MW, 100 ), wfTimestamp( TS_MW, 101 ) ],
1067 [],
1068 ],
1069 'User' => [
1070 'foo_bar',
1071 [ ApiBase::PARAM_TYPE => 'user' ],
1072 'Foo bar',
1073 [],
1074 ],
1075 'User prefixed with "User:"' => [
1076 'User:foo_bar',
1077 [ ApiBase::PARAM_TYPE => 'user' ],
1078 'Foo bar',
1079 [],
1080 ],
1081 'Invalid username "|"' => [
1082 '|',
1083 [ ApiBase::PARAM_TYPE => 'user' ],
1084 ApiUsageException::newWithMessage( null,
1085 [ 'apierror-baduser', 'myParam', '&#124;' ],
1086 'baduser_myParam' ),
1087 [],
1088 ],
1089 'Invalid username "300.300.300.300"' => [
1090 '300.300.300.300',
1091 [ ApiBase::PARAM_TYPE => 'user' ],
1092 ApiUsageException::newWithMessage( null,
1093 [ 'apierror-baduser', 'myParam', '300.300.300.300' ],
1094 'baduser_myParam' ),
1095 [],
1096 ],
1097 'IP range as username' => [
1098 '10.0.0.0/8',
1099 [ ApiBase::PARAM_TYPE => 'user' ],
1100 '10.0.0.0/8',
1101 [],
1102 ],
1103 'IPv6 as username' => [
1104 '::1',
1105 [ ApiBase::PARAM_TYPE => 'user' ],
1106 '0:0:0:0:0:0:0:1',
1107 [],
1108 ],
1109 'Obsolete cloaked usemod IP address as username' => [
1110 '1.2.3.xxx',
1111 [ ApiBase::PARAM_TYPE => 'user' ],
1112 '1.2.3.xxx',
1113 [],
1114 ],
1115 'Invalid username containing IP address' => [
1116 'This is [not] valid 1.2.3.xxx, ha!',
1117 [ ApiBase::PARAM_TYPE => 'user' ],
1118 ApiUsageException::newWithMessage(
1119 null,
1120 [ 'apierror-baduser', 'myParam', 'This is &#91;not&#93; valid 1.2.3.xxx, ha!' ],
1121 'baduser_myParam'
1122 ),
1123 [],
1124 ],
1125 'External username' => [
1126 'M>Foo bar',
1127 [ ApiBase::PARAM_TYPE => 'user' ],
1128 'M>Foo bar',
1129 [],
1130 ],
1131 'Array of usernames' => [
1132 'foo|bar',
1133 [
1134 ApiBase::PARAM_TYPE => 'user',
1135 ApiBase::PARAM_ISMULTI => true,
1136 ],
1137 [ 'Foo', 'Bar' ],
1138 [],
1139 ],
1140 'tag' => [
1141 'tag1',
1142 [ ApiBase::PARAM_TYPE => 'tags' ],
1143 [ 'tag1' ],
1144 [],
1145 ],
1146 'Array of one tag' => [
1147 'tag1',
1148 [
1149 ApiBase::PARAM_TYPE => 'tags',
1150 ApiBase::PARAM_ISMULTI => true,
1151 ],
1152 [ 'tag1' ],
1153 [],
1154 ],
1155 'Array of tags' => [
1156 'tag1|tag2',
1157 [
1158 ApiBase::PARAM_TYPE => 'tags',
1159 ApiBase::PARAM_ISMULTI => true,
1160 ],
1161 [ 'tag1', 'tag2' ],
1162 [],
1163 ],
1164 'Invalid tag' => [
1165 'invalid tag',
1166 [ ApiBase::PARAM_TYPE => 'tags' ],
1167 new ApiUsageException( null,
1168 Status::newFatal( 'tags-apply-not-allowed-one',
1169 'invalid tag', 1 ) ),
1170 [],
1171 ],
1172 'Unrecognized type' => [
1173 'foo',
1174 [ ApiBase::PARAM_TYPE => 'nonexistenttype' ],
1175 new MWException(
1176 'Internal error in ApiBase::getParameterFromSettings: ' .
1177 "Param myParam's type is unknown - nonexistenttype" ),
1178 [],
1179 ],
1180 'Too many bytes' => [
1181 '1',
1182 [
1183 ApiBase::PARAM_MAX_BYTES => 0,
1184 ApiBase::PARAM_MAX_CHARS => 0,
1185 ],
1186 ApiUsageException::newWithMessage( null,
1187 [ 'apierror-maxbytes', 'myParam', 0 ] ),
1188 [],
1189 ],
1190 'Too many chars' => [
1191 '§§',
1192 [
1193 ApiBase::PARAM_MAX_BYTES => 4,
1194 ApiBase::PARAM_MAX_CHARS => 1,
1195 ],
1196 ApiUsageException::newWithMessage( null,
1197 [ 'apierror-maxchars', 'myParam', 1 ] ),
1198 [],
1199 ],
1200 'Omitted required param' => [
1201 null,
1202 [ ApiBase::PARAM_REQUIRED => true ],
1203 ApiUsageException::newWithMessage( null,
1204 [ 'apierror-missingparam', 'myParam' ] ),
1205 [],
1206 ],
1207 'Empty multi-value' => [
1208 '',
1209 [ ApiBase::PARAM_ISMULTI => true ],
1210 [],
1211 [],
1212 ],
1213 'Multi-value \x1f' => [
1214 "\x1f",
1215 [ ApiBase::PARAM_ISMULTI => true ],
1216 [],
1217 [],
1218 ],
1219 'Allowed non-multi-value with "|"' => [
1220 'a|b',
1221 [ ApiBase::PARAM_TYPE => [ 'a|b' ] ],
1222 'a|b',
1223 [],
1224 ],
1225 'Prohibited multi-value' => [
1226 'a|b',
1227 [ ApiBase::PARAM_TYPE => [ 'a', 'b' ] ],
1228 ApiUsageException::newWithMessage( null,
1229 [
1230 'apierror-multival-only-one-of',
1231 'myParam',
1232 Message::listParam( [ '<kbd>a</kbd>', '<kbd>b</kbd>' ] ),
1233 2
1234 ],
1235 'multival_myParam'
1236 ),
1237 [],
1238 ],
1239 ];
1240
1241 // The following really just test PHP's string-to-int conversion.
1242 $integerTests = [
1243 [ '+1', 1 ],
1244 [ '-1', -1 ],
1245 [ '1.5', 1 ],
1246 [ '-1.5', -1 ],
1247 [ '1abc', 1 ],
1248 [ ' 1', 1 ],
1249 [ "\t1", 1, '\t1' ],
1250 [ "\r1", 1, '\r1' ],
1251 [ "\f1", 0, '\f1', 'badutf-8' ],
1252 [ "\n1", 1, '\n1' ],
1253 [ "\v1", 0, '\v1', 'badutf-8' ],
1254 [ "\e1", 0, '\e1', 'badutf-8' ],
1255 [ "\x001", 0, '\x001', 'badutf-8' ],
1256 ];
1257
1258 foreach ( $integerTests as $test ) {
1259 $desc = $test[2] ?? $test[0];
1260 $warnings = isset( $test[3] ) ?
1261 [ [ 'apiwarn-badutf8', 'myParam' ] ] : [];
1262 $returnArray["\"$desc\" as integer"] = [
1263 $test[0],
1264 [ ApiBase::PARAM_TYPE => 'integer' ],
1265 $test[1],
1266 $warnings,
1267 ];
1268 }
1269
1270 return $returnArray;
1271 }
1272
1273 public function testErrorArrayToStatus() {
1274 $mock = new MockApi();
1275
1276 // Sanity check empty array
1277 $expect = Status::newGood();
1278 $this->assertEquals( $expect, $mock->errorArrayToStatus( [] ) );
1279
1280 // No blocked $user, so no special block handling
1281 $expect = Status::newGood();
1282 $expect->fatal( 'blockedtext' );
1283 $expect->fatal( 'autoblockedtext' );
1284 $expect->fatal( 'systemblockedtext' );
1285 $expect->fatal( 'mainpage' );
1286 $expect->fatal( 'parentheses', 'foobar' );
1287 $this->assertEquals( $expect, $mock->errorArrayToStatus( [
1288 [ 'blockedtext' ],
1289 [ 'autoblockedtext' ],
1290 [ 'systemblockedtext' ],
1291 'mainpage',
1292 [ 'parentheses', 'foobar' ],
1293 ] ) );
1294
1295 // Has a blocked $user, so special block handling
1296 $user = $this->getMutableTestUser()->getUser();
1297 $block = new \Block( [
1298 'address' => $user->getName(),
1299 'user' => $user->getID(),
1300 'by' => $this->getTestSysop()->getUser()->getId(),
1301 'reason' => __METHOD__,
1302 'expiry' => time() + 100500,
1303 ] );
1304 $block->insert();
1305 $blockinfo = [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ];
1306
1307 $expect = Status::newGood();
1308 $expect->fatal( ApiMessage::create( 'apierror-blocked', 'blocked', $blockinfo ) );
1309 $expect->fatal( ApiMessage::create( 'apierror-autoblocked', 'autoblocked', $blockinfo ) );
1310 $expect->fatal( ApiMessage::create( 'apierror-systemblocked', 'blocked', $blockinfo ) );
1311 $expect->fatal( 'mainpage' );
1312 $expect->fatal( 'parentheses', 'foobar' );
1313 $this->assertEquals( $expect, $mock->errorArrayToStatus( [
1314 [ 'blockedtext' ],
1315 [ 'autoblockedtext' ],
1316 [ 'systemblockedtext' ],
1317 'mainpage',
1318 [ 'parentheses', 'foobar' ],
1319 ], $user ) );
1320 }
1321
1322 public function testDieStatus() {
1323 $mock = new MockApi();
1324
1325 $status = StatusValue::newGood();
1326 $status->error( 'foo' );
1327 $status->warning( 'bar' );
1328 try {
1329 $mock->dieStatus( $status );
1330 $this->fail( 'Expected exception not thrown' );
1331 } catch ( ApiUsageException $ex ) {
1332 $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' );
1333 $this->assertFalse( ApiTestCase::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' );
1334 }
1335
1336 $status = StatusValue::newGood();
1337 $status->warning( 'foo' );
1338 $status->warning( 'bar' );
1339 try {
1340 $mock->dieStatus( $status );
1341 $this->fail( 'Expected exception not thrown' );
1342 } catch ( ApiUsageException $ex ) {
1343 $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' );
1344 $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' );
1345 }
1346
1347 $status = StatusValue::newGood();
1348 $status->setOk( false );
1349 try {
1350 $mock->dieStatus( $status );
1351 $this->fail( 'Expected exception not thrown' );
1352 } catch ( ApiUsageException $ex ) {
1353 $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'unknownerror-nocode' ),
1354 'Exception has "unknownerror-nocode"' );
1355 }
1356 }
1357
1358 /**
1359 * @covers ApiBase::extractRequestParams
1360 */
1361 public function testExtractRequestParams() {
1362 $request = new FauxRequest( [
1363 'xxexists' => 'exists!',
1364 'xxmulti' => 'a|b|c|d|{bad}',
1365 'xxempty' => '',
1366 'xxtemplate-a' => 'A!',
1367 'xxtemplate-b' => 'B1|B2|B3',
1368 'xxtemplate-c' => '',
1369 'xxrecursivetemplate-b-B1' => 'X',
1370 'xxrecursivetemplate-b-B3' => 'Y',
1371 'xxrecursivetemplate-b-B4' => '?',
1372 'xxemptytemplate-' => 'nope',
1373 'foo' => 'a|b|c',
1374 'xxfoo' => 'a|b|c',
1375 'errorformat' => 'raw',
1376 ] );
1377 $context = new DerivativeContext( RequestContext::getMain() );
1378 $context->setRequest( $request );
1379 $main = new ApiMain( $context );
1380
1381 $mock = $this->getMockBuilder( ApiBase::class )
1382 ->setConstructorArgs( [ $main, 'test', 'xx' ] )
1383 ->setMethods( [ 'getAllowedParams' ] )
1384 ->getMockForAbstractClass();
1385 $mock->method( 'getAllowedParams' )->willReturn( [
1386 'notexists' => null,
1387 'exists' => null,
1388 'multi' => [
1389 ApiBase::PARAM_ISMULTI => true,
1390 ],
1391 'empty' => [
1392 ApiBase::PARAM_ISMULTI => true,
1393 ],
1394 'template-{m}' => [
1395 ApiBase::PARAM_ISMULTI => true,
1396 ApiBase::PARAM_TEMPLATE_VARS => [ 'm' => 'multi' ],
1397 ],
1398 'recursivetemplate-{m}-{t}' => [
1399 ApiBase::PARAM_TEMPLATE_VARS => [ 't' => 'template-{m}', 'm' => 'multi' ],
1400 ],
1401 'emptytemplate-{m}' => [
1402 ApiBase::PARAM_ISMULTI => true,
1403 ApiBase::PARAM_TEMPLATE_VARS => [ 'm' => 'empty' ],
1404 ],
1405 'badtemplate-{e}' => [
1406 ApiBase::PARAM_TEMPLATE_VARS => [ 'e' => 'exists' ],
1407 ],
1408 'badtemplate2-{e}' => [
1409 ApiBase::PARAM_TEMPLATE_VARS => [ 'e' => 'badtemplate2-{e}' ],
1410 ],
1411 'badtemplate3-{x}' => [
1412 ApiBase::PARAM_TEMPLATE_VARS => [ 'x' => 'foo' ],
1413 ],
1414 ] );
1415
1416 $this->assertEquals( [
1417 'notexists' => null,
1418 'exists' => 'exists!',
1419 'multi' => [ 'a', 'b', 'c', 'd', '{bad}' ],
1420 'empty' => [],
1421 'template-a' => [ 'A!' ],
1422 'template-b' => [ 'B1', 'B2', 'B3' ],
1423 'template-c' => [],
1424 'template-d' => null,
1425 'recursivetemplate-a-A!' => null,
1426 'recursivetemplate-b-B1' => 'X',
1427 'recursivetemplate-b-B2' => null,
1428 'recursivetemplate-b-B3' => 'Y',
1429 ], $mock->extractRequestParams() );
1430
1431 $used = TestingAccessWrapper::newFromObject( $main )->getParamsUsed();
1432 sort( $used );
1433 $this->assertEquals( [
1434 'xxempty',
1435 'xxexists',
1436 'xxmulti',
1437 'xxnotexists',
1438 'xxrecursivetemplate-a-A!',
1439 'xxrecursivetemplate-b-B1',
1440 'xxrecursivetemplate-b-B2',
1441 'xxrecursivetemplate-b-B3',
1442 'xxtemplate-a',
1443 'xxtemplate-b',
1444 'xxtemplate-c',
1445 'xxtemplate-d',
1446 ], $used );
1447
1448 $warnings = $mock->getResult()->getResultData( 'warnings', [ 'Strip' => 'all' ] );
1449 $this->assertCount( 1, $warnings );
1450 $this->assertSame( 'ignoring-invalid-templated-value', $warnings[0]['code'] );
1451 }
1452
1453 }