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