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