3 use Wikimedia\TestingAccessWrapper;
19 new FauxRequest( [
'action' =>
'query',
'meta' =>
'siteinfo' ] )
22 $data = $api->getResult()->getResultData();
23 $this->assertInternalType(
'array', $data );
24 $this->assertArrayHasKey(
'query', $data );
30 $data = $api->getResult()->getResultData();
31 $this->assertInternalType(
'array', $data );
49 ->setMethods( [
'response',
'getRawIP' ] )
53 $req->method(
'getRawIP' )->willReturn(
'127.0.0.1' );
55 $wrapper = TestingAccessWrapper::newFromObject(
$req );
56 $wrapper->data = $requestData;
58 $wrapper->headers = $headers;
77 $this->assertSame(
'fr',
$wgLang->getCode() );
93 ], [
'ORIGIN' =>
'https://www.example.com https://www.com.example' ] );
96 [ [ Psr\Log\LogLevel::WARNING,
'Non-whitelisted CORS request with session cookies' ] ],
107 'meta' =>
'siteinfo',
115 $this->assertNotSame( $origUser,
$wgUser );
116 $this->assertSame(
'true', $api->getContext()->getRequest()->response()
117 ->getHeader(
'MediaWiki-Login-Suppressed' ) );
123 $api->setContinuationManager( $manager );
124 $this->assertTrue(
true,
'No exception' );
125 return [ $api, $manager ];
133 'ApiMain::setContinuationManager: tried to set manager from ' .
134 'when a manager is already set from ' );
137 $api->setContinuationManager( $manager );
142 $api->setCacheMode(
'unrecognized' );
145 TestingAccessWrapper::newFromObject( $api )->mCacheMode,
146 'Unrecognized params must be silently ignored'
153 $wrappedApi = TestingAccessWrapper::newFromObject(
new ApiMain() );
154 $wrappedApi->setCacheMode(
'public' );
155 $this->assertSame(
'private', $wrappedApi->mCacheMode );
156 $wrappedApi->setCacheMode(
'anon-public-user-private' );
157 $this->assertSame(
'private', $wrappedApi->mCacheMode );
163 'meta' =>
'siteinfo',
164 'requestid' =>
'123456',
168 $this->assertSame(
'123456', $api->getResult()->getResultData()[
'requestid'] );
174 'meta' =>
'siteinfo',
175 'curtimestamp' =>
'',
179 $timestamp = $api->getResult()->getResultData()[
'curtimestamp'];
180 $this->assertLessThanOrEqual( 1, abs( strtotime( $timestamp ) - time() ) );
186 'meta' =>
'siteinfo',
188 'errorformat' =>
'plaintext',
191 'responselanginfo' =>
'',
195 $data = $api->getResult()->getResultData();
196 $this->assertSame(
'fr', $data[
'uselang'] );
197 $this->assertSame(
'ja', $data[
'errorlang'] );
202 'Unrecognized value for parameter "action": unknownaction.' );
211 'The "token" parameter must be set.' );
215 'title' =>
'New page',
216 'text' =>
'Some text',
227 'title' =>
'New page',
228 'text' =>
'Some text',
229 'token' =>
"This isn't a real token!",
237 "Module 'testmodule' must be updated for the new token handling. " .
238 "See documentation for ApiBase::needsToken for details." );
241 $mock->method(
'getModuleName' )->willReturn(
'testmodule' );
242 $mock->method(
'needsToken' )->willReturn(
true );
245 $api->getModuleManager()->addModule(
'testmodule',
'action', get_class( $mock ),
246 function ()
use ( $mock ) {
255 "Module 'testmodule' must require POST to use tokens." );
258 $mock->method(
'getModuleName' )->willReturn(
'testmodule' );
259 $mock->method(
'needsToken' )->willReturn(
'csrf' );
260 $mock->method(
'mustBePosted' )->willReturn(
false );
263 $api->getModuleManager()->addModule(
'testmodule',
'action', get_class( $mock ),
264 function ()
use ( $mock ) {
276 'meta' =>
'siteinfo',
280 ->setConstructorArgs( [
$req ] )
281 ->setMethods( [
'checkMaxLag' ] )
283 $mock->method(
'checkMaxLag' )->willReturn(
false );
287 $this->assertArrayNotHasKey(
'query', $mock->getResult()->getResultData() );
297 $this->
setMwGlobals(
'wgCacheEpoch',
'20030516000000' );
300 $mock->method(
'getModuleName' )->willReturn(
'testmodule' );
301 $mock->method(
'getConditionalRequestData' )
303 $mock->expects( $this->exactly( 0 ) )->method(
'execute' );
306 'action' =>
'testmodule',
308 $req->setHeader(
'If-Modified-Since',
wfTimestamp( TS_RFC2822, $now - 3600 ) );
309 $req->setRequestURL(
"http://localhost" );
312 $api->getModuleManager()->addModule(
'testmodule',
'action', get_class( $mock ),
313 function ()
use ( $mock ) {
318 $wrapper = TestingAccessWrapper::newFromObject( $api );
319 $wrapper->mInternalMode =
false;
328 ->disableOriginalConstructor()
329 ->setMethods( [
'getMaxLag',
'__destruct' ] )
331 $mockLB->method(
'getMaxLag' )->willReturn( [
'somehost', $lag ] );
332 $this->
setService(
'DBLoadBalancer', $mockLB );
337 $wrapper = TestingAccessWrapper::newFromObject( $api );
340 $mockModule->method(
'shouldCheckMaxLag' )->willReturn(
true );
343 $wrapper->checkMaxLag( $mockModule, [
'maxlag' => 3 ] );
346 $this->assertSame(
'5',
$req->response()->getHeader(
'Retry-After' ) );
347 $this->assertSame( (
string)$lag,
$req->response()->getHeader(
'X-Database-Lag' ) );
356 $this->assertTrue(
true );
361 'Waiting for a database server: 4 seconds lagged.' );
370 'Waiting for somehost: 4 seconds lagged.' );
379 [
false, [],
'user',
'assertuserfailed' ],
380 [
true, [],
'user',
false ],
381 [
true, [],
'bot',
'assertbotfailed' ],
382 [
true, [
'bot' ],
'user',
false ],
383 [
true, [
'bot' ],
'bot',
false ],
396 public function testAssert( $registered, $rights, $assert, $error ) {
403 $user->mRights = $rights;
408 ],
null,
null,
$user );
409 $this->assertFalse( $error );
411 $this->assertTrue( self::apiExceptionHasCode(
$e, $error ),
412 "Error '{$e->getMessage()}' matched expected '$error'" );
423 'assertuser' =>
$user->getName(),
424 ],
null,
null,
$user );
429 'assertuser' =>
$user->getName() .
'X',
430 ],
null,
null,
$user );
431 $this->fail(
'Expected exception not thrown' );
433 $this->assertTrue( self::apiExceptionHasCode(
$e,
'assertnameduserfailed' ) );
442 new FauxRequest( [
'action' =>
'query',
'meta' =>
'siteinfo' ] )
444 $modules = $api->getModuleManager()->getNamesWithClasses();
448 class_exists( $class ),
449 'Class ' . $class .
' for api module ' .
$name .
' does not exist (with exact case)'
469 [
'action' =>
'query',
'meta' =>
'siteinfo' ],
473 $request->response()->statusHeader( 200 );
477 $priv = TestingAccessWrapper::newFromObject( $api );
478 $priv->mInternalMode =
false;
485 $this->
setMwGlobals(
'wgCacheEpoch',
'20030516000000' );
488 ->setConstructorArgs( [ $api,
'mock' ] )
489 ->setMethods( [
'getConditionalRequestData' ] )
490 ->getMockForAbstractClass();
491 $module->expects( $this->
any() )
492 ->method(
'getConditionalRequestData' )
493 ->will( $this->returnCallback(
function ( $condition )
use ( $conditions ) {
494 return isset( $conditions[$condition] ) ? $conditions[$condition] :
null;
497 $ret = $priv->checkConditionalRequestHeaders( $module );
509 'If-None-Match' => [ [
'If-None-Match' =>
'"foo", "bar"' ], [], 200 ],
510 'If-Modified-Since' =>
511 [ [
'If-Modified-Since' =>
'Tue, 18 Aug 2015 00:00:00 GMT' ], [], 200 ],
514 'No headers' => [ [], [
'etag' =>
'""',
'last-modified' =>
'20150815000000', ], 200 ],
517 'If-None-Match with matching etag' =>
518 [ [
'If-None-Match' =>
'"foo", "bar"' ], [
'etag' =>
'"bar"' ], 304 ],
519 'If-None-Match with non-matching etag' =>
520 [ [
'If-None-Match' =>
'"foo", "bar"' ], [
'etag' =>
'"baz"' ], 200 ],
521 'Strong If-None-Match with weak matching etag' =>
522 [ [
'If-None-Match' =>
'"foo"' ], [
'etag' =>
'W/"foo"' ], 304 ],
523 'Weak If-None-Match with strong matching etag' =>
524 [ [
'If-None-Match' =>
'W/"foo"' ], [
'etag' =>
'"foo"' ], 304 ],
525 'Weak If-None-Match with weak matching etag' =>
526 [ [
'If-None-Match' =>
'W/"foo"' ], [
'etag' =>
'W/"foo"' ], 304 ],
529 'If-None-Match: *' => [ [
'If-None-Match' =>
'*' ], [], 304 ],
532 'If-Modified-Since, modified one second earlier' =>
533 [ [
'If-Modified-Since' =>
wfTimestamp( TS_RFC2822, $now ) ],
534 [
'last-modified' =>
wfTimestamp( TS_MW, $now - 1 ) ], 304 ],
535 'If-Modified-Since, modified now' =>
536 [ [
'If-Modified-Since' =>
wfTimestamp( TS_RFC2822, $now ) ],
537 [
'last-modified' =>
wfTimestamp( TS_MW, $now ) ], 304 ],
538 'If-Modified-Since, modified one second later' =>
539 [ [
'If-Modified-Since' =>
wfTimestamp( TS_RFC2822, $now ) ],
540 [
'last-modified' =>
wfTimestamp( TS_MW, $now + 1 ) ], 200 ],
543 'Non-matching If-None-Match and matching If-Modified-Since' =>
544 [ [
'If-None-Match' =>
'""',
545 'If-Modified-Since' =>
wfTimestamp( TS_RFC2822, $now ) ],
546 [
'etag' =>
'"x"',
'last-modified' =>
wfTimestamp( TS_MW, $now - 1 ) ], 200 ],
547 'Non-matching If-None-Match and matching If-Modified-Since with no ETag' =>
550 'If-None-Match' =>
'""',
551 'If-Modified-Since' =>
wfTimestamp( TS_RFC2822, $now )
553 [
'last-modified' =>
wfTimestamp( TS_MW, $now - 1 ) ],
558 'Matching If-None-Match with POST' =>
559 [ [
'If-None-Match' =>
'"foo", "bar"' ], [
'etag' =>
'"bar"' ], 200,
560 [
'post' =>
true ] ],
561 'Matching If-Modified-Since with POST' =>
562 [ [
'If-Modified-Since' =>
wfTimestamp( TS_RFC2822, $now ) ],
563 [
'last-modified' =>
wfTimestamp( TS_MW, $now - 1 ) ], 200,
564 [
'post' =>
true ] ],
567 'If-Modified-Since with alternate date format 1' =>
568 [ [
'If-Modified-Since' => gmdate(
'l, d-M-y H:i:s', $now ) .
' GMT' ],
569 [
'last-modified' =>
wfTimestamp( TS_MW, $now - 1 ) ], 304 ],
570 'If-Modified-Since with alternate date format 2' =>
571 [ [
'If-Modified-Since' => gmdate(
'D M j H:i:s Y', $now ) ],
572 [
'last-modified' =>
wfTimestamp( TS_MW, $now - 1 ) ], 304 ],
575 'If-Modified-Since with length' =>
576 [ [
'If-Modified-Since' =>
wfTimestamp( TS_RFC2822, $now ) .
'; length=123' ],
577 [
'last-modified' =>
wfTimestamp( TS_MW, $now - 1 ) ], 304 ],
580 'If-Modified-Since with invalid date format' =>
581 [ [
'If-Modified-Since' => gmdate(
'Y-m-d H:i:s', $now ) .
' GMT' ],
582 [
'last-modified' =>
wfTimestamp( TS_MW, $now - 1 ) ], 200 ],
583 'If-Modified-Since with entirely unparseable date' =>
584 [ [
'If-Modified-Since' =>
'a potato' ],
585 [
'last-modified' =>
wfTimestamp( TS_MW, $now - 1 ) ], 200 ],
589 'If-Modified-Since with CDN post-expiry' =>
592 200, [
'cdn' =>
true ] ],
593 'If-Modified-Since with CDN pre-expiry' =>
596 304, [
'cdn' =>
true ] ],
609 $conditions, $headers, $isError =
false, $post =
false
615 $priv = TestingAccessWrapper::newFromObject( $api );
616 $priv->mInternalMode =
false;
619 ->setConstructorArgs( [ $api,
'mock' ] )
620 ->setMethods( [
'getConditionalRequestData' ] )
621 ->getMockForAbstractClass();
622 $module->expects( $this->
any() )
623 ->method(
'getConditionalRequestData' )
624 ->will( $this->returnCallback(
function ( $condition )
use ( $conditions ) {
625 return isset( $conditions[$condition] ) ? $conditions[$condition] :
null;
627 $priv->mModule = $module;
629 $priv->sendCacheHeaders( $isError );
631 foreach ( [
'Last-Modified',
'ETag' ]
as $header ) {
647 [
'etag' =>
'"foo"' ],
648 [
'ETag' =>
'"foo"' ]
651 [
'last-modified' =>
'20150818000102' ],
652 [
'Last-Modified' =>
'Tue, 18 Aug 2015 00:01:02 GMT' ]
655 [
'etag' =>
'"foo"',
'last-modified' =>
'20150818000102' ],
656 [
'ETag' =>
'"foo"',
'Last-Modified' =>
'Tue, 18 Aug 2015 00:01:02 GMT' ]
659 [
'etag' =>
'"foo"',
'last-modified' =>
'20150818000102' ],
664 [
'etag' =>
'"foo"',
'last-modified' =>
'20150818000102' ],
674 'You need read permission to use this module.' );
678 $main =
new ApiMain(
new FauxRequest( [
'action' =>
'query',
'meta' =>
'siteinfo' ] ) );
684 'Editing of this wiki through the API is disabled. Make sure the ' .
685 '"$wgEnableWriteAPI=true;" statement is included in the wiki\'s ' .
686 '"LocalSettings.php" file.' );
689 'title' =>
'Some page',
690 'text' =>
'Some text',
698 "You're not allowed to edit this wiki through the API." );
703 'title' =>
'Some page',
704 'text' =>
'Some text',
712 'The "Promise-Non-Write-API-Action" HTTP header cannot be sent ' .
713 'to write-mode API modules.' );
717 'title' =>
'Some page',
718 'text' =>
'Some text',
721 $req->setHeaders( [
'Promise-Non-Write-API-Action' =>
'1' ] );
729 $this->
setTemporaryHook(
'ApiCheckCanExecute',
function ( $unused1, $unused2, &$message ) {
730 $message =
'mainpage';
736 'title' =>
'Some page',
737 'text' =>
'Some text',
746 'meta' =>
'siteinfo',
747 'siprop' => [
'general',
'namespaces' ],
749 $this->assertSame(
'myDefault', $main->getVal(
'siprop',
'myDefault' ) );
751 $this->assertSame(
'Parameter "siprop" uses unsupported PHP array syntax.',
752 $main->getResult()->getResultData()[
'warnings'][
'main'][
'warnings'] );
758 'meta' =>
'siteinfo',
759 'unusedparam' =>
'unusedval',
760 'anotherunusedparam' =>
'anotherval',
763 $this->assertSame(
'Unrecognized parameters: unusedparam, anotherunusedparam.',
764 $main->getResult()->getResultData()[
'warnings'][
'main'][
'warnings'] );
769 $main =
new ApiMain(
new FauxRequest( [
'action' =>
'query',
'meta' =>
'siteinfo' ] ) );
770 $this->assertFalse( $main->lacksSameOriginSecurity(),
'Basic test, should have security' );
774 new FauxRequest( [
'action' =>
'query',
'format' =>
'xml',
'callback' =>
'foo' ] )
776 $this->assertTrue( $main->lacksSameOriginSecurity(),
'JSONp, should lack security' );
780 $request->setHeader(
'TrEaT-As-UnTrUsTeD',
'' );
782 $this->assertTrue( $main->lacksSameOriginSecurity(),
'Header supplied, should lack security' );
786 'RequestHasSameOriginSecurity' => [
function () {
790 $main =
new ApiMain(
new FauxRequest( [
'action' =>
'query',
'meta' =>
'siteinfo' ] ) );
791 $this->assertTrue( $main->lacksSameOriginSecurity(),
'Hook, should lack security' );
812 $formatter = $main->getErrorFormatter();
813 $wrappedFormatter = TestingAccessWrapper::newFromObject( $formatter );
815 $this->assertSame( $expect[
'uselang'], $main->getLanguage()->getCode() );
816 $this->assertInstanceOf( $expect[
'class'], $formatter );
817 $this->assertSame( $expect[
'lang'], $formatter->getLanguage()->getCode() );
818 $this->assertSame( $expect[
'format'], $wrappedFormatter->format );
819 $this->assertSame( $expect[
'usedb'], $wrappedFormatter->useDB );
824 'Default (BC)' => [ [], [
831 'BC ignores fields' => [ [
'errorlang' =>
'de',
'errorsuselocal' => 1 ], [
838 'Explicit BC' => [ [
'errorformat' =>
'bc' ], [
845 'Basic' => [ [
'errorformat' =>
'wikitext' ], [
849 'format' =>
'wikitext',
852 'Follows uselang' => [ [
'uselang' =>
'fr',
'errorformat' =>
'plaintext' ], [
856 'format' =>
'plaintext',
859 'Explicitly follows uselang' => [
860 [
'uselang' =>
'fr',
'errorlang' =>
'uselang',
'errorformat' =>
'plaintext' ],
865 'format' =>
'plaintext',
869 'uselang=content' => [
870 [
'uselang' =>
'content',
'errorformat' =>
'plaintext' ],
875 'format' =>
'plaintext',
879 'errorlang=content' => [
880 [
'errorlang' =>
'content',
'errorformat' =>
'plaintext' ],
885 'format' =>
'plaintext',
889 'Explicit parameters' => [
890 [
'errorlang' =>
'de',
'errorformat' =>
'html',
'errorsuselocal' => 1 ],
899 'Explicit parameters override uselang' => [
900 [
'errorlang' =>
'de',
'uselang' =>
'fr',
'errorformat' =>
'raw' ],
909 'Bogus language doesn\'t explode' => [
910 [
'errorlang' =>
'<bogus1>',
'uselang' =>
'<bogus2>',
'errorformat' =>
'none' ],
919 'Bogus format doesn\'t explode' => [ [
'errorformat' =>
'bogus' ], [
941 'ShowHostnames' =>
true,
'ShowSQLErrors' =>
false,
942 'ShowExceptionDetails' =>
true,
'ShowDBErrorBacktrace' =>
true,
948 $main->addWarning(
new RawMessage(
'existing warning' ),
'existing-warning' );
949 $main->addError(
new RawMessage(
'existing error' ),
'existing-error' );
951 $ret = TestingAccessWrapper::newFromObject( $main )->substituteResultWithError( $error );
952 $this->assertSame( $expectReturn,
$ret );
958 $main->getResult()->getResultData( [], [
'Strip' =>
'all' ] )
967 $ex =
new InvalidArgumentException(
'Random exception' );
968 $trace =
wfMessage(
'api-exception-trace',
973 )->inLanguage(
'en' )->useDatabase(
false )->text();
975 $dbex =
new DBQueryError(
977 'error', 1234,
'SELECT 1', __METHOD__ );
978 $dbtrace =
wfMessage(
'api-exception-trace',
983 )->inLanguage(
'en' )->useDatabase(
false )->text();
985 Wikimedia\suppressWarnings();
986 $usageEx =
new UsageException(
'Usage exception!',
'ue', 0, [
'foo' =>
'bar' ] );
987 Wikimedia\restoreWarnings();
991 TestingAccessWrapper::newFromObject( $apiEx1 )->modulePath =
'foo+bar';
992 $apiEx1->getStatusValue()->warning(
new ApiRawMessage(
'A warning',
'sv-warn1' ) );
993 $apiEx1->getStatusValue()->warning(
new ApiRawMessage(
'Another warning',
'sv-warn2' ) );
994 $apiEx1->getStatusValue()->fatal(
new ApiRawMessage(
'Another error',
'sv-error2' ) );
999 [
'existing-error',
'internal_api_error_InvalidArgumentException' ],
1002 [
'code' =>
'existing-warning',
'text' =>
'existing warning',
'module' =>
'main' ],
1005 [
'code' =>
'existing-error',
'text' =>
'existing error',
'module' =>
'main' ],
1007 'code' =>
'internal_api_error_InvalidArgumentException',
1008 'text' =>
"[$reqId] Exception caught: Random exception",
1017 [
'existing-error',
'internal_api_error_DBQueryError' ],
1020 [
'code' =>
'existing-warning',
'text' =>
'existing warning',
'module' =>
'main' ],
1023 [
'code' =>
'existing-error',
'text' =>
'existing error',
'module' =>
'main' ],
1025 'code' =>
'internal_api_error_DBQueryError',
1026 'text' =>
"[$reqId] Database query error.",
1029 'trace' => $dbtrace,
1035 [
'existing-error',
'ue' ],
1038 [
'code' =>
'existing-warning',
'text' =>
'existing warning',
'module' =>
'main' ],
1041 [
'code' =>
'existing-error',
'text' =>
'existing error',
'module' =>
'main' ],
1042 [
'code' =>
'ue',
'text' =>
"Usage exception!",
'data' => [
'foo' =>
'bar' ] ]
1044 'docref' =>
"See $doclink for API usage. Subscribe to the mediawiki-api-announce mailing " .
1045 "list at <https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce> " .
1046 "for notice of API deprecations and breaking changes.",
1052 [
'existing-error',
'sv-error1',
'sv-error2' ],
1055 [
'code' =>
'existing-warning',
'text' =>
'existing warning',
'module' =>
'main' ],
1056 [
'code' =>
'sv-warn1',
'text' =>
'A warning',
'module' =>
'foo+bar' ],
1057 [
'code' =>
'sv-warn2',
'text' =>
'Another warning',
'module' =>
'foo+bar' ],
1060 [
'code' =>
'existing-error',
'text' =>
'existing error',
'module' =>
'main' ],
1061 [
'code' =>
'sv-error1',
'text' =>
'An error',
'module' =>
'foo+bar' ],
1062 [
'code' =>
'sv-error2',
'text' =>
'Another error',
'module' =>
'foo+bar' ],
1064 'docref' =>
"See $doclink for API usage. Subscribe to the mediawiki-api-announce mailing " .
1065 "list at <https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce> " .
1066 "for notice of API deprecations and breaking changes.",