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