Improve test coverage for ApiBase.php
[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 $result = ( new MockApi() )->getTitleOrPageId(
168 [ 'pageid' => Title::newFromText( 'UTPage' )->getArticleId() ] );
169 $this->assertInstanceOf( WikiPage::class, $result );
170 $this->assertSame( 'UTPage', $result->getTitle()->getPrefixedText() );
171 }
172
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 ] );
178 }
179
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 ] );
185 }
186
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() );
192 }
193
194 public function testGetTitleFromTitleOrPageIdInvalidTitle() {
195 $this->setExpectedException( ApiUsageException::class,
196 'Bad title "|".' );
197 $mock = new MockApi();
198 $mock->getTitleFromTitleOrPageId( [ 'title' => '|' ] );
199 }
200
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() );
206 }
207
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 ] );
213 }
214
215 /**
216 * @dataProvider provideGetParameterFromSettings
217 * @param string|null $input
218 * @param array $paramSettings
219 * @param mixed $expected
220 * @param array $options Key-value pairs:
221 * 'parseLimits': true|false
222 * 'apihighlimits': true|false
223 * 'internalmode': true|false
224 * @param string[] $warnings
225 */
226 public function testGetParameterFromSettings(
227 $input, $paramSettings, $expected, $warnings, $options = []
228 ) {
229 $mock = new MockApi();
230 $wrapper = TestingAccessWrapper::newFromObject( $mock );
231
232 $context = new DerivativeContext( $mock );
233 $context->setRequest( new FauxRequest(
234 $input !== null ? [ 'myParam' => $input ] : [] ) );
235 $wrapper->mMainModule = new ApiMain( $context );
236
237 $parseLimits = isset( $options['parseLimits'] ) ?
238 $options['parseLimits'] : true;
239
240 if ( !empty( $options['apihighlimits'] ) ) {
241 $context->setUser( self::$users['sysop']->getUser() );
242 }
243
244 if ( isset( $options['internalmode'] ) && !$options['internalmode'] ) {
245 $mainWrapper = TestingAccessWrapper::newFromObject( $wrapper->mMainModule );
246 $mainWrapper->mInternalMode = false;
247 }
248
249 // If we're testing tags, set up some tags
250 if ( isset( $paramSettings[ApiBase::PARAM_TYPE] ) &&
251 $paramSettings[ApiBase::PARAM_TYPE] === 'tags'
252 ) {
253 ChangeTags::defineTag( 'tag1' );
254 ChangeTags::defineTag( 'tag2' );
255 }
256
257 if ( $expected instanceof Exception ) {
258 try {
259 $wrapper->getParameterFromSettings( 'myParam', $paramSettings,
260 $parseLimits );
261 $this->fail( 'No exception thrown' );
262 } catch ( Exception $ex ) {
263 $this->assertEquals( $expected, $ex );
264 }
265 } else {
266 $result = $wrapper->getParameterFromSettings( 'myParam',
267 $paramSettings, $parseLimits );
268 if ( isset( $paramSettings[ApiBase::PARAM_TYPE] ) &&
269 $paramSettings[ApiBase::PARAM_TYPE] === 'timestamp' &&
270 $expected === 'now'
271 ) {
272 // Allow one second of fuzziness. Make sure the formats are
273 // correct!
274 $this->assertRegExp( '/^\d{14}$/', $result );
275 $this->assertLessThanOrEqual( 1,
276 abs( wfTimestamp( TS_UNIX, $result ) - time() ),
277 "Result $result differs from expected $expected by " .
278 'more than one second' );
279 } else {
280 $this->assertSame( $expected, $result );
281 }
282 $actualWarnings = array_map( function ( $warn ) {
283 return $warn instanceof Message
284 ? array_merge( [ $warn->getKey() ], $warn->getParams() )
285 : $warn;
286 }, $mock->warnings );
287 $this->assertSame( $warnings, $actualWarnings );
288 }
289
290 if ( !empty( $paramSettings[ApiBase::PARAM_SENSITIVE] ) ||
291 ( isset( $paramSettings[ApiBase::PARAM_TYPE] ) &&
292 $paramSettings[ApiBase::PARAM_TYPE] === 'password' )
293 ) {
294 $mainWrapper = TestingAccessWrapper::newFromObject( $wrapper->getMain() );
295 $this->assertSame( [ 'myParam' ],
296 $mainWrapper->getSensitiveParams() );
297 }
298 }
299
300 public static function provideGetParameterFromSettings() {
301 $warnings = [
302 [ 'apiwarn-badutf8', 'myParam' ],
303 ];
304
305 $c0 = '';
306 $enc = '';
307 for ( $i = 0; $i < 32; $i++ ) {
308 $c0 .= chr( $i );
309 $enc .= ( $i === 9 || $i === 10 || $i === 13 )
310 ? chr( $i )
311 : '�';
312 }
313
314 $returnArray = [
315 'Basic param' => [ 'bar', null, 'bar', [] ],
316 'Basic param, C0 controls' => [ $c0, null, $enc, $warnings ],
317 'String param' => [ 'bar', '', 'bar', [] ],
318 'String param, defaulted' => [ null, '', '', [] ],
319 'String param, empty' => [ '', 'default', '', [] ],
320 'String param, required, empty' => [
321 '',
322 [ ApiBase::PARAM_DFLT => 'default', ApiBase::PARAM_REQUIRED => true ],
323 ApiUsageException::newWithMessage( null,
324 [ 'apierror-missingparam', 'myParam' ] ),
325 []
326 ],
327 'Multi-valued parameter' => [
328 'a|b|c',
329 [ ApiBase::PARAM_ISMULTI => true ],
330 [ 'a', 'b', 'c' ],
331 []
332 ],
333 'Multi-valued parameter, alternative separator' => [
334 "\x1fa|b\x1fc|d",
335 [ ApiBase::PARAM_ISMULTI => true ],
336 [ 'a|b', 'c|d' ],
337 []
338 ],
339 'Multi-valued parameter, other C0 controls' => [
340 $c0,
341 [ ApiBase::PARAM_ISMULTI => true ],
342 [ $enc ],
343 $warnings
344 ],
345 'Multi-valued parameter, other C0 controls (2)' => [
346 "\x1f" . $c0,
347 [ ApiBase::PARAM_ISMULTI => true ],
348 [ substr( $enc, 0, -3 ), '' ],
349 $warnings
350 ],
351 'Multi-valued parameter with limits' => [
352 'a|b|c',
353 [
354 ApiBase::PARAM_ISMULTI => true,
355 ApiBase::PARAM_ISMULTI_LIMIT1 => 3,
356 ],
357 [ 'a', 'b', 'c' ],
358 [],
359 ],
360 'Multi-valued parameter with exceeded limits' => [
361 'a|b|c',
362 [
363 ApiBase::PARAM_ISMULTI => true,
364 ApiBase::PARAM_ISMULTI_LIMIT1 => 2,
365 ],
366 [ 'a', 'b' ],
367 [ [ 'apiwarn-toomanyvalues', 'myParam', 2 ] ],
368 ],
369 'Multi-valued parameter with exceeded limits for non-bot' => [
370 'a|b|c',
371 [
372 ApiBase::PARAM_ISMULTI => true,
373 ApiBase::PARAM_ISMULTI_LIMIT1 => 2,
374 ApiBase::PARAM_ISMULTI_LIMIT2 => 3,
375 ],
376 [ 'a', 'b' ],
377 [ [ 'apiwarn-toomanyvalues', 'myParam', 2 ] ],
378 ],
379 'Multi-valued parameter with non-exceeded limits for bot' => [
380 'a|b|c',
381 [
382 ApiBase::PARAM_ISMULTI => true,
383 ApiBase::PARAM_ISMULTI_LIMIT1 => 2,
384 ApiBase::PARAM_ISMULTI_LIMIT2 => 3,
385 ],
386 [ 'a', 'b', 'c' ],
387 [],
388 [ 'apihighlimits' => true ],
389 ],
390 'Multi-valued parameter with prohibited duplicates' => [
391 'a|b|a|c',
392 [ ApiBase::PARAM_ISMULTI => true ],
393 // Note that the keys are not sequential! This matches
394 // array_unique, but might be unexpected.
395 [ 0 => 'a', 1 => 'b', 3 => 'c' ],
396 [],
397 ],
398 'Multi-valued parameter with allowed duplicates' => [
399 'a|a',
400 [
401 ApiBase::PARAM_ISMULTI => true,
402 ApiBase::PARAM_ALLOW_DUPLICATES => true,
403 ],
404 [ 'a', 'a' ],
405 [],
406 ],
407 'Empty boolean param' => [
408 '',
409 [ ApiBase::PARAM_TYPE => 'boolean' ],
410 true,
411 [],
412 ],
413 'Boolean param 0' => [
414 '0',
415 [ ApiBase::PARAM_TYPE => 'boolean' ],
416 true,
417 [],
418 ],
419 'Boolean param false' => [
420 'false',
421 [ ApiBase::PARAM_TYPE => 'boolean' ],
422 true,
423 [],
424 ],
425 'Boolean multi-param' => [
426 'true|false',
427 [
428 ApiBase::PARAM_TYPE => 'boolean',
429 ApiBase::PARAM_ISMULTI => true,
430 ],
431 new MWException(
432 'Internal error in ApiBase::getParameterFromSettings: ' .
433 'Multi-values not supported for myParam'
434 ),
435 [],
436 ],
437 'Empty boolean param with non-false default' => [
438 '',
439 [
440 ApiBase::PARAM_TYPE => 'boolean',
441 ApiBase::PARAM_DFLT => true,
442 ],
443 new MWException(
444 'Internal error in ApiBase::getParameterFromSettings: ' .
445 "Boolean param myParam's default is set to '1'. " .
446 'Boolean parameters must default to false.' ),
447 [],
448 ],
449 'Deprecated parameter' => [
450 'foo',
451 [ ApiBase::PARAM_DEPRECATED => true ],
452 'foo',
453 [ [ 'apiwarn-deprecation-parameter', 'myParam' ] ],
454 ],
455 'Deprecated parameter value' => [
456 'a',
457 [ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => true ] ],
458 'a',
459 [ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ],
460 ],
461 'Multiple deprecated parameter values' => [
462 'a|b|c|d',
463 [ ApiBase::PARAM_DEPRECATED_VALUES =>
464 [ 'b' => true, 'd' => true ],
465 ApiBase::PARAM_ISMULTI => true ],
466 [ 'a', 'b', 'c', 'd' ],
467 [
468 [ 'apiwarn-deprecation-parameter', 'myParam=b' ],
469 [ 'apiwarn-deprecation-parameter', 'myParam=d' ],
470 ],
471 ],
472 'Deprecated parameter value with custom warning' => [
473 'a',
474 [ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => 'my-msg' ] ],
475 'a',
476 [ 'my-msg' ],
477 ],
478 '"*" when wildcard not allowed' => [
479 '*',
480 [ ApiBase::PARAM_ISMULTI => true,
481 ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ] ],
482 [],
483 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
484 [ 'list' => [ '&#42;' ], 'type' => 'comma' ], 1 ] ],
485 ],
486 'Wildcard "*"' => [
487 '*',
488 [
489 ApiBase::PARAM_ISMULTI => true,
490 ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ],
491 ApiBase::PARAM_ALL => true,
492 ],
493 [ 'a', 'b', 'c' ],
494 [],
495 ],
496 'Wildcard "*" with multiples not allowed' => [
497 '*',
498 [
499 ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ],
500 ApiBase::PARAM_ALL => true,
501 ],
502 ApiUsageException::newWithMessage( null,
503 [ 'apierror-unrecognizedvalue', 'myParam', '&#42;' ],
504 'unknown_myParam' ),
505 [],
506 ],
507 'Wildcard "*" with unrestricted type' => [
508 '*',
509 [
510 ApiBase::PARAM_ISMULTI => true,
511 ApiBase::PARAM_ALL => true,
512 ],
513 [ '*' ],
514 [],
515 ],
516 'Wildcard "x"' => [
517 'x',
518 [
519 ApiBase::PARAM_ISMULTI => true,
520 ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ],
521 ApiBase::PARAM_ALL => 'x',
522 ],
523 [ 'a', 'b', 'c' ],
524 [],
525 ],
526 'Wildcard conflicting with allowed value' => [
527 'a',
528 [
529 ApiBase::PARAM_ISMULTI => true,
530 ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ],
531 ApiBase::PARAM_ALL => 'a',
532 ],
533 new MWException(
534 'Internal error in ApiBase::getParameterFromSettings: ' .
535 'For param myParam, PARAM_ALL collides with a possible ' .
536 'value' ),
537 [],
538 ],
539 'Namespace with wildcard' => [
540 '*',
541 [
542 ApiBase::PARAM_ISMULTI => true,
543 ApiBase::PARAM_TYPE => 'namespace',
544 ],
545 MWNamespace::getValidNamespaces(),
546 [],
547 ],
548 // PARAM_ALL is ignored with namespace types.
549 'Namespace with wildcard suppressed' => [
550 '*',
551 [
552 ApiBase::PARAM_ISMULTI => true,
553 ApiBase::PARAM_TYPE => 'namespace',
554 ApiBase::PARAM_ALL => false,
555 ],
556 MWNamespace::getValidNamespaces(),
557 [],
558 ],
559 'Namespace with wildcard "x"' => [
560 'x',
561 [
562 ApiBase::PARAM_ISMULTI => true,
563 ApiBase::PARAM_TYPE => 'namespace',
564 ApiBase::PARAM_ALL => 'x',
565 ],
566 [],
567 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
568 [ 'list' => [ 'x' ], 'type' => 'comma' ], 1 ] ],
569 ],
570 'Password' => [
571 'dDy+G?e?txnr.1:(@[Ru',
572 [ ApiBase::PARAM_TYPE => 'password' ],
573 'dDy+G?e?txnr.1:(@[Ru',
574 [],
575 ],
576 'Sensitive field' => [
577 'I am fond of pineapples',
578 [ ApiBase::PARAM_SENSITIVE => true ],
579 'I am fond of pineapples',
580 [],
581 ],
582 'Upload with default' => [
583 '',
584 [
585 ApiBase::PARAM_TYPE => 'upload',
586 ApiBase::PARAM_DFLT => '',
587 ],
588 new MWException(
589 'Internal error in ApiBase::getParameterFromSettings: ' .
590 "File upload param myParam's default is set to ''. " .
591 'File upload parameters may not have a default.' ),
592 [],
593 ],
594 'Multiple upload' => [
595 '',
596 [
597 ApiBase::PARAM_TYPE => 'upload',
598 ApiBase::PARAM_ISMULTI => true,
599 ],
600 new MWException(
601 'Internal error in ApiBase::getParameterFromSettings: ' .
602 'Multi-values not supported for myParam' ),
603 [],
604 ],
605 // @todo Test actual upload
606 'Namespace -1' => [
607 '-1',
608 [ ApiBase::PARAM_TYPE => 'namespace' ],
609 ApiUsageException::newWithMessage( null,
610 [ 'apierror-unrecognizedvalue', 'myParam', '-1' ],
611 'unknown_myParam' ),
612 [],
613 ],
614 'Extra namespace -1' => [
615 '-1',
616 [
617 ApiBase::PARAM_TYPE => 'namespace',
618 ApiBase::PARAM_EXTRA_NAMESPACES => [ '-1' ],
619 ],
620 '-1',
621 [],
622 ],
623 // @todo Test with PARAM_SUBMODULE_MAP unset, need
624 // getModuleManager() to return something real
625 'Nonexistent module' => [
626 'not-a-module-name',
627 [
628 ApiBase::PARAM_TYPE => 'submodule',
629 ApiBase::PARAM_SUBMODULE_MAP =>
630 [ 'foo' => 'foo', 'bar' => 'foo+bar' ],
631 ],
632 ApiUsageException::newWithMessage(
633 null,
634 [
635 'apierror-unrecognizedvalue',
636 'myParam',
637 'not-a-module-name',
638 ],
639 'unknown_myParam'
640 ),
641 [],
642 ],
643 '\\x1f with multiples not allowed' => [
644 "\x1f",
645 [],
646 ApiUsageException::newWithMessage( null,
647 'apierror-badvalue-notmultivalue',
648 'badvalue_notmultivalue' ),
649 [],
650 ],
651 'Integer with unenforced min' => [
652 '-2',
653 [
654 ApiBase::PARAM_TYPE => 'integer',
655 ApiBase::PARAM_MIN => -1,
656 ],
657 -1,
658 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
659 -2 ] ],
660 ],
661 'Integer with enforced min' => [
662 '-2',
663 [
664 ApiBase::PARAM_TYPE => 'integer',
665 ApiBase::PARAM_MIN => -1,
666 ApiBase::PARAM_RANGE_ENFORCE => true,
667 ],
668 ApiUsageException::newWithMessage( null,
669 [ 'apierror-integeroutofrange-belowminimum', 'myParam',
670 '-1', '-2' ], 'integeroutofrange',
671 [ 'min' => -1, 'max' => null, 'botMax' => null ] ),
672 [],
673 ],
674 'Integer with unenforced max (internal mode)' => [
675 '8',
676 [
677 ApiBase::PARAM_TYPE => 'integer',
678 ApiBase::PARAM_MAX => 7,
679 ],
680 8,
681 [],
682 ],
683 'Integer with enforced max (internal mode)' => [
684 '8',
685 [
686 ApiBase::PARAM_TYPE => 'integer',
687 ApiBase::PARAM_MAX => 7,
688 ApiBase::PARAM_RANGE_ENFORCE => true,
689 ],
690 8,
691 [],
692 ],
693 'Integer with unenforced max (non-internal mode)' => [
694 '8',
695 [
696 ApiBase::PARAM_TYPE => 'integer',
697 ApiBase::PARAM_MAX => 7,
698 ],
699 7,
700 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 7, 8 ] ],
701 [ 'internalmode' => false ],
702 ],
703 'Integer with enforced max (non-internal mode)' => [
704 '8',
705 [
706 ApiBase::PARAM_TYPE => 'integer',
707 ApiBase::PARAM_MAX => 7,
708 ApiBase::PARAM_RANGE_ENFORCE => true,
709 ],
710 ApiUsageException::newWithMessage(
711 null,
712 [ 'apierror-integeroutofrange-abovemax', 'myParam', '7', '8' ],
713 'integeroutofrange',
714 [ 'min' => null, 'max' => 7, 'botMax' => 7 ]
715 ),
716 [],
717 [ 'internalmode' => false ],
718 ],
719 'Array of integers' => [
720 '3|12|966|-1',
721 [
722 ApiBase::PARAM_ISMULTI => true,
723 ApiBase::PARAM_TYPE => 'integer',
724 ],
725 [ 3, 12, 966, -1 ],
726 [],
727 ],
728 'Array of integers with unenforced min/max (internal mode)' => [
729 '3|12|966|-1',
730 [
731 ApiBase::PARAM_ISMULTI => true,
732 ApiBase::PARAM_TYPE => 'integer',
733 ApiBase::PARAM_MIN => 0,
734 ApiBase::PARAM_MAX => 100,
735 ],
736 [ 3, 12, 966, 0 ],
737 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ] ],
738 ],
739 'Array of integers with enforced min/max (internal mode)' => [
740 '3|12|966|-1',
741 [
742 ApiBase::PARAM_ISMULTI => true,
743 ApiBase::PARAM_TYPE => 'integer',
744 ApiBase::PARAM_MIN => 0,
745 ApiBase::PARAM_MAX => 100,
746 ApiBase::PARAM_RANGE_ENFORCE => true,
747 ],
748 ApiUsageException::newWithMessage(
749 null,
750 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ],
751 'integeroutofrange',
752 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
753 ),
754 [],
755 ],
756 'Array of integers with unenforced min/max (non-internal mode)' => [
757 '3|12|966|-1',
758 [
759 ApiBase::PARAM_ISMULTI => true,
760 ApiBase::PARAM_TYPE => 'integer',
761 ApiBase::PARAM_MIN => 0,
762 ApiBase::PARAM_MAX => 100,
763 ],
764 [ 3, 12, 100, 0 ],
765 [
766 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
767 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ]
768 ],
769 [ 'internalmode' => false ],
770 ],
771 'Array of integers with enforced min/max (non-internal mode)' => [
772 '3|12|966|-1',
773 [
774 ApiBase::PARAM_ISMULTI => true,
775 ApiBase::PARAM_TYPE => 'integer',
776 ApiBase::PARAM_MIN => 0,
777 ApiBase::PARAM_MAX => 100,
778 ApiBase::PARAM_RANGE_ENFORCE => true,
779 ],
780 ApiUsageException::newWithMessage(
781 null,
782 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
783 'integeroutofrange',
784 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
785 ),
786 [],
787 [ 'internalmode' => false ],
788 ],
789 'Limit with parseLimits false' => [
790 '100',
791 [ ApiBase::PARAM_TYPE => 'limit' ],
792 '100',
793 [],
794 [ 'parseLimits' => false ],
795 ],
796 'Limit with no max' => [
797 '100',
798 [
799 ApiBase::PARAM_TYPE => 'limit',
800 ApiBase::PARAM_MAX2 => 10,
801 ApiBase::PARAM_ISMULTI => true,
802 ],
803 new MWException(
804 'Internal error in ApiBase::getParameterFromSettings: ' .
805 'MAX1 or MAX2 are not defined for the limit myParam' ),
806 [],
807 ],
808 'Limit with no max2' => [
809 '100',
810 [
811 ApiBase::PARAM_TYPE => 'limit',
812 ApiBase::PARAM_MAX => 10,
813 ApiBase::PARAM_ISMULTI => true,
814 ],
815 new MWException(
816 'Internal error in ApiBase::getParameterFromSettings: ' .
817 'MAX1 or MAX2 are not defined for the limit myParam' ),
818 [],
819 ],
820 'Limit with multi-value' => [
821 '100',
822 [
823 ApiBase::PARAM_TYPE => 'limit',
824 ApiBase::PARAM_MAX => 10,
825 ApiBase::PARAM_MAX2 => 10,
826 ApiBase::PARAM_ISMULTI => true,
827 ],
828 new MWException(
829 'Internal error in ApiBase::getParameterFromSettings: ' .
830 'Multi-values not supported for myParam' ),
831 [],
832 ],
833 'Valid limit' => [
834 '100',
835 [
836 ApiBase::PARAM_TYPE => 'limit',
837 ApiBase::PARAM_MAX => 100,
838 ApiBase::PARAM_MAX2 => 100,
839 ],
840 100,
841 [],
842 ],
843 'Limit max' => [
844 'max',
845 [
846 ApiBase::PARAM_TYPE => 'limit',
847 ApiBase::PARAM_MAX => 100,
848 ApiBase::PARAM_MAX2 => 101,
849 ],
850 100,
851 [],
852 ],
853 'Limit max for apihighlimits' => [
854 'max',
855 [
856 ApiBase::PARAM_TYPE => 'limit',
857 ApiBase::PARAM_MAX => 100,
858 ApiBase::PARAM_MAX2 => 101,
859 ],
860 101,
861 [],
862 [ 'apihighlimits' => true ],
863 ],
864 'Limit too large (internal mode)' => [
865 '101',
866 [
867 ApiBase::PARAM_TYPE => 'limit',
868 ApiBase::PARAM_MAX => 100,
869 ApiBase::PARAM_MAX2 => 101,
870 ],
871 101,
872 [],
873 ],
874 'Limit okay for apihighlimits (internal mode)' => [
875 '101',
876 [
877 ApiBase::PARAM_TYPE => 'limit',
878 ApiBase::PARAM_MAX => 100,
879 ApiBase::PARAM_MAX2 => 101,
880 ],
881 101,
882 [],
883 [ 'apihighlimits' => true ],
884 ],
885 'Limit too large for apihighlimits (internal mode)' => [
886 '102',
887 [
888 ApiBase::PARAM_TYPE => 'limit',
889 ApiBase::PARAM_MAX => 100,
890 ApiBase::PARAM_MAX2 => 101,
891 ],
892 102,
893 [],
894 [ 'apihighlimits' => true ],
895 ],
896 'Limit too large (non-internal mode)' => [
897 '101',
898 [
899 ApiBase::PARAM_TYPE => 'limit',
900 ApiBase::PARAM_MAX => 100,
901 ApiBase::PARAM_MAX2 => 101,
902 ],
903 100,
904 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 101 ] ],
905 [ 'internalmode' => false ],
906 ],
907 'Limit okay for apihighlimits (non-internal mode)' => [
908 '101',
909 [
910 ApiBase::PARAM_TYPE => 'limit',
911 ApiBase::PARAM_MAX => 100,
912 ApiBase::PARAM_MAX2 => 101,
913 ],
914 101,
915 [],
916 [ 'internalmode' => false, 'apihighlimits' => true ],
917 ],
918 'Limit too large for apihighlimits (non-internal mode)' => [
919 '102',
920 [
921 ApiBase::PARAM_TYPE => 'limit',
922 ApiBase::PARAM_MAX => 100,
923 ApiBase::PARAM_MAX2 => 101,
924 ],
925 101,
926 [ [ 'apierror-integeroutofrange-abovebotmax', 'myParam', 101, 102 ] ],
927 [ 'internalmode' => false, 'apihighlimits' => true ],
928 ],
929 'Limit too small' => [
930 '-2',
931 [
932 ApiBase::PARAM_TYPE => 'limit',
933 ApiBase::PARAM_MIN => -1,
934 ApiBase::PARAM_MAX => 100,
935 ApiBase::PARAM_MAX2 => 100,
936 ],
937 -1,
938 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
939 -2 ] ],
940 ],
941 'Timestamp' => [
942 wfTimestamp( TS_UNIX, '20211221122112' ),
943 [ ApiBase::PARAM_TYPE => 'timestamp' ],
944 '20211221122112',
945 [],
946 ],
947 'Timestamp 0' => [
948 '0',
949 [ ApiBase::PARAM_TYPE => 'timestamp' ],
950 // Magic keyword
951 'now',
952 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '0' ] ],
953 ],
954 'Timestamp empty' => [
955 '',
956 [ ApiBase::PARAM_TYPE => 'timestamp' ],
957 'now',
958 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '' ] ],
959 ],
960 // wfTimestamp() interprets this as Unix time
961 'Timestamp 00' => [
962 '00',
963 [ ApiBase::PARAM_TYPE => 'timestamp' ],
964 '19700101000000',
965 [],
966 ],
967 'Timestamp now' => [
968 'now',
969 [ ApiBase::PARAM_TYPE => 'timestamp' ],
970 'now',
971 [],
972 ],
973 'Invalid timestamp' => [
974 'a potato',
975 [ ApiBase::PARAM_TYPE => 'timestamp' ],
976 ApiUsageException::newWithMessage(
977 null,
978 [ 'apierror-badtimestamp', 'myParam', 'a potato' ],
979 'badtimestamp_myParam'
980 ),
981 [],
982 ],
983 'Timestamp array' => [
984 '100|101',
985 [
986 ApiBase::PARAM_TYPE => 'timestamp',
987 ApiBase::PARAM_ISMULTI => 1,
988 ],
989 [ wfTimestamp( TS_MW, 100 ), wfTimestamp( TS_MW, 101 ) ],
990 [],
991 ],
992 'User' => [
993 'foo_bar',
994 [ ApiBase::PARAM_TYPE => 'user' ],
995 'Foo bar',
996 [],
997 ],
998 'Invalid username "|"' => [
999 '|',
1000 [ ApiBase::PARAM_TYPE => 'user' ],
1001 ApiUsageException::newWithMessage( null,
1002 [ 'apierror-baduser', 'myParam', '&#124;' ],
1003 'baduser_myParam' ),
1004 [],
1005 ],
1006 'Invalid username "300.300.300.300"' => [
1007 '300.300.300.300',
1008 [ ApiBase::PARAM_TYPE => 'user' ],
1009 ApiUsageException::newWithMessage( null,
1010 [ 'apierror-baduser', 'myParam', '300.300.300.300' ],
1011 'baduser_myParam' ),
1012 [],
1013 ],
1014 'IP range as username' => [
1015 '10.0.0.0/8',
1016 [ ApiBase::PARAM_TYPE => 'user' ],
1017 '10.0.0.0/8',
1018 [],
1019 ],
1020 'IPv6 as username' => [
1021 '::1',
1022 [ ApiBase::PARAM_TYPE => 'user' ],
1023 '0:0:0:0:0:0:0:1',
1024 [],
1025 ],
1026 'Obsolete cloaked usemod IP address as username' => [
1027 '1.2.3.xxx',
1028 [ ApiBase::PARAM_TYPE => 'user' ],
1029 '1.2.3.xxx',
1030 [],
1031 ],
1032 'Invalid username containing IP address' => [
1033 'This is [not] valid 1.2.3.xxx, ha!',
1034 [ ApiBase::PARAM_TYPE => 'user' ],
1035 ApiUsageException::newWithMessage(
1036 null,
1037 [ 'apierror-baduser', 'myParam', 'This is &#91;not&#93; valid 1.2.3.xxx, ha!' ],
1038 'baduser_myParam'
1039 ),
1040 [],
1041 ],
1042 'External username' => [
1043 'M>Foo bar',
1044 [ ApiBase::PARAM_TYPE => 'user' ],
1045 'M>Foo bar',
1046 [],
1047 ],
1048 'Array of usernames' => [
1049 'foo|bar',
1050 [
1051 ApiBase::PARAM_TYPE => 'user',
1052 ApiBase::PARAM_ISMULTI => true,
1053 ],
1054 [ 'Foo', 'Bar' ],
1055 [],
1056 ],
1057 'tag' => [
1058 'tag1',
1059 [ ApiBase::PARAM_TYPE => 'tags' ],
1060 [ 'tag1' ],
1061 [],
1062 ],
1063 'Array of one tag' => [
1064 'tag1',
1065 [
1066 ApiBase::PARAM_TYPE => 'tags',
1067 ApiBase::PARAM_ISMULTI => true,
1068 ],
1069 [ 'tag1' ],
1070 [],
1071 ],
1072 'Array of tags' => [
1073 'tag1|tag2',
1074 [
1075 ApiBase::PARAM_TYPE => 'tags',
1076 ApiBase::PARAM_ISMULTI => true,
1077 ],
1078 [ 'tag1', 'tag2' ],
1079 [],
1080 ],
1081 'Invalid tag' => [
1082 'invalid tag',
1083 [ ApiBase::PARAM_TYPE => 'tags' ],
1084 new ApiUsageException( null,
1085 Status::newFatal( 'tags-apply-not-allowed-one',
1086 'invalid tag', 1 ) ),
1087 [],
1088 ],
1089 'Unrecognized type' => [
1090 'foo',
1091 [ ApiBase::PARAM_TYPE => 'nonexistenttype' ],
1092 new MWException(
1093 'Internal error in ApiBase::getParameterFromSettings: ' .
1094 "Param myParam's type is unknown - nonexistenttype" ),
1095 [],
1096 ],
1097 'Too many bytes' => [
1098 '1',
1099 [
1100 ApiBase::PARAM_MAX_BYTES => 0,
1101 ApiBase::PARAM_MAX_CHARS => 0,
1102 ],
1103 ApiUsageException::newWithMessage( null,
1104 [ 'apierror-maxbytes', 'myParam', 0 ] ),
1105 [],
1106 ],
1107 'Too many chars' => [
1108 '§§',
1109 [
1110 ApiBase::PARAM_MAX_BYTES => 4,
1111 ApiBase::PARAM_MAX_CHARS => 1,
1112 ],
1113 ApiUsageException::newWithMessage( null,
1114 [ 'apierror-maxchars', 'myParam', 1 ] ),
1115 [],
1116 ],
1117 'Omitted required param' => [
1118 null,
1119 [ ApiBase::PARAM_REQUIRED => true ],
1120 ApiUsageException::newWithMessage( null,
1121 [ 'apierror-missingparam', 'myParam' ] ),
1122 [],
1123 ],
1124 'Empty multi-value' => [
1125 '',
1126 [ ApiBase::PARAM_ISMULTI => true ],
1127 [],
1128 [],
1129 ],
1130 'Multi-value \x1f' => [
1131 "\x1f",
1132 [ ApiBase::PARAM_ISMULTI => true ],
1133 [],
1134 [],
1135 ],
1136 'Allowed non-multi-value with "|"' => [
1137 'a|b',
1138 [ ApiBase::PARAM_TYPE => [ 'a|b' ] ],
1139 'a|b',
1140 [],
1141 ],
1142 'Prohibited multi-value' => [
1143 'a|b',
1144 [ ApiBase::PARAM_TYPE => [ 'a', 'b' ] ],
1145 ApiUsageException::newWithMessage( null,
1146 [
1147 'apierror-multival-only-one-of',
1148 'myParam',
1149 Message::listParam( [ '<kbd>a</kbd>', '<kbd>b</kbd>' ] ),
1150 2
1151 ],
1152 'multival_myParam'
1153 ),
1154 [],
1155 ],
1156 ];
1157
1158 // The following really just test PHP's string-to-int conversion.
1159 $integerTests = [
1160 [ '+1', 1 ],
1161 [ '-1', -1 ],
1162 [ '1e3', 1 ],
1163 [ '1.5', 1 ],
1164 [ '-1.5', -1 ],
1165 [ '1abc', 1 ],
1166 [ ' 1', 1 ],
1167 [ "\t1", 1, '\t1' ],
1168 [ "\r1", 1, '\r1' ],
1169 [ "\f1", 0, '\f1', 'badutf-8' ],
1170 [ "\n1", 1, '\n1' ],
1171 [ "\v1", 0, '\v1', 'badutf-8' ],
1172 [ "\e1", 0, '\e1', 'badutf-8' ],
1173 [ "\x001", 0, '\x001', 'badutf-8' ],
1174 ];
1175
1176 foreach ( $integerTests as $test ) {
1177 $desc = isset( $test[2] ) ? $test[2] : $test[0];
1178 $warnings = isset( $test[3] ) ?
1179 [ [ 'apiwarn-badutf8', 'myParam' ] ] : [];
1180 $returnArray["\"$desc\" as integer"] = [
1181 $test[0],
1182 [ ApiBase::PARAM_TYPE => 'integer' ],
1183 $test[1],
1184 $warnings,
1185 ];
1186 }
1187
1188 return $returnArray;
1189 }
1190
1191 public function testErrorArrayToStatus() {
1192 $mock = new MockApi();
1193
1194 // Sanity check empty array
1195 $expect = Status::newGood();
1196 $this->assertEquals( $expect, $mock->errorArrayToStatus( [] ) );
1197
1198 // No blocked $user, so no special block handling
1199 $expect = Status::newGood();
1200 $expect->fatal( 'blockedtext' );
1201 $expect->fatal( 'autoblockedtext' );
1202 $expect->fatal( 'systemblockedtext' );
1203 $expect->fatal( 'mainpage' );
1204 $expect->fatal( 'parentheses', 'foobar' );
1205 $this->assertEquals( $expect, $mock->errorArrayToStatus( [
1206 [ 'blockedtext' ],
1207 [ 'autoblockedtext' ],
1208 [ 'systemblockedtext' ],
1209 'mainpage',
1210 [ 'parentheses', 'foobar' ],
1211 ] ) );
1212
1213 // Has a blocked $user, so special block handling
1214 $user = $this->getMutableTestUser()->getUser();
1215 $block = new \Block( [
1216 'address' => $user->getName(),
1217 'user' => $user->getID(),
1218 'by' => $this->getTestSysop()->getUser()->getId(),
1219 'reason' => __METHOD__,
1220 'expiry' => time() + 100500,
1221 ] );
1222 $block->insert();
1223 $blockinfo = [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ];
1224
1225 $expect = Status::newGood();
1226 $expect->fatal( ApiMessage::create( 'apierror-blocked', 'blocked', $blockinfo ) );
1227 $expect->fatal( ApiMessage::create( 'apierror-autoblocked', 'autoblocked', $blockinfo ) );
1228 $expect->fatal( ApiMessage::create( 'apierror-systemblocked', 'blocked', $blockinfo ) );
1229 $expect->fatal( 'mainpage' );
1230 $expect->fatal( 'parentheses', 'foobar' );
1231 $this->assertEquals( $expect, $mock->errorArrayToStatus( [
1232 [ 'blockedtext' ],
1233 [ 'autoblockedtext' ],
1234 [ 'systemblockedtext' ],
1235 'mainpage',
1236 [ 'parentheses', 'foobar' ],
1237 ], $user ) );
1238 }
1239
1240 public function testDieStatus() {
1241 $mock = new MockApi();
1242
1243 $status = StatusValue::newGood();
1244 $status->error( 'foo' );
1245 $status->warning( 'bar' );
1246 try {
1247 $mock->dieStatus( $status );
1248 $this->fail( 'Expected exception not thrown' );
1249 } catch ( ApiUsageException $ex ) {
1250 $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' );
1251 $this->assertFalse( ApiTestCase::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' );
1252 }
1253
1254 $status = StatusValue::newGood();
1255 $status->warning( 'foo' );
1256 $status->warning( 'bar' );
1257 try {
1258 $mock->dieStatus( $status );
1259 $this->fail( 'Expected exception not thrown' );
1260 } catch ( ApiUsageException $ex ) {
1261 $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' );
1262 $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' );
1263 }
1264
1265 $status = StatusValue::newGood();
1266 $status->setOk( false );
1267 try {
1268 $mock->dieStatus( $status );
1269 $this->fail( 'Expected exception not thrown' );
1270 } catch ( ApiUsageException $ex ) {
1271 $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'unknownerror-nocode' ),
1272 'Exception has "unknownerror-nocode"' );
1273 }
1274 }
1275
1276 }