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