.pipeline/config.yaml: rename dev stage to publish
[lhc/web/wiklou.git] / tests / phpunit / includes / api / ApiQuerySiteinfoTest.php
1 <?php
2
3 use MediaWiki\MediaWikiServices;
4
5 /**
6 * @group API
7 * @group medium
8 * @group Database
9 *
10 * @covers ApiQuerySiteinfo
11 */
12 class ApiQuerySiteinfoTest extends ApiTestCase {
13 // We don't try to test every single thing for every category, just a sample
14
15 protected function doQuery( $siprop = null, $extraParams = [] ) {
16 $params = [ 'action' => 'query', 'meta' => 'siteinfo' ];
17 if ( $siprop !== null ) {
18 $params['siprop'] = $siprop;
19 }
20 $params = array_merge( $params, $extraParams );
21
22 $res = $this->doApiRequest( $params );
23
24 $this->assertArrayNotHasKey( 'warnings', $res[0] );
25 $this->assertCount( 1, $res[0]['query'] );
26
27 return $res[0]['query'][$siprop === null ? 'general' : $siprop];
28 }
29
30 public function testGeneral() {
31 $this->setMwGlobals( [
32 'wgAllowExternalImagesFrom' => '//localhost/',
33 ] );
34
35 $data = $this->doQuery();
36
37 $this->assertSame( Title::newMainPage()->getPrefixedText(), $data['mainpage'] );
38 $this->assertSame( PHP_VERSION, $data['phpversion'] );
39 $this->assertSame( [ '//localhost/' ], $data['externalimages'] );
40 }
41
42 public function testLinkPrefixCharset() {
43 $contLang = Language::factory( 'ar' );
44 $this->setContentLang( $contLang );
45 $this->assertTrue( $contLang->linkPrefixExtension(), 'Sanity check' );
46
47 $data = $this->doQuery();
48
49 $this->assertSame( $contLang->linkPrefixCharset(), $data['linkprefixcharset'] );
50 }
51
52 public function testVariants() {
53 $contLang = Language::factory( 'zh' );
54 $this->setContentLang( $contLang );
55 $this->assertTrue( $contLang->hasVariants(), 'Sanity check' );
56
57 $data = $this->doQuery();
58
59 $expected = array_map(
60 function ( $code ) use ( $contLang ) {
61 return [ 'code' => $code, 'name' => $contLang->getVariantname( $code ) ];
62 },
63 $contLang->getVariants()
64 );
65
66 $this->assertSame( $expected, $data['variants'] );
67 }
68
69 public function testReadOnly() {
70 $svc = MediaWikiServices::getInstance()->getReadOnlyMode();
71 $svc->setReason( 'Need more donations' );
72 try {
73 $data = $this->doQuery();
74 } finally {
75 $svc->setReason( false );
76 }
77
78 $this->assertTrue( $data['readonly'] );
79 $this->assertSame( 'Need more donations', $data['readonlyreason'] );
80 }
81
82 public function testNamespacesBasic() {
83 $this->assertSame(
84 array_keys( MediaWikiServices::getInstance()->getContentLanguage()->getFormattedNamespaces() ),
85 array_keys( $this->doQuery( 'namespaces' ) )
86 );
87 }
88
89 public function testNamespacesExtraNS() {
90 $this->setMwGlobals( 'wgExtraNamespaces', [ '138' => 'Testing' ] );
91 $this->assertSame(
92 array_keys( MediaWikiServices::getInstance()->getContentLanguage()->getFormattedNamespaces() ),
93 array_keys( $this->doQuery( 'namespaces' ) )
94 );
95 }
96
97 public function testNamespacesProtection() {
98 $this->setMwGlobals(
99 'wgNamespaceProtection',
100 [
101 '0' => '',
102 '2' => [ '' ],
103 '4' => 'editsemiprotected',
104 '8' => [
105 'editinterface',
106 'noratelimit'
107 ],
108 '14' => [
109 'move-categorypages',
110 ''
111 ]
112 ]
113 );
114 $data = $this->doQuery( 'namespaces' );
115 $this->assertArrayNotHasKey( 'namespaceprotection', $data['0'] );
116 $this->assertArrayNotHasKey( 'namespaceprotection', $data['2'] );
117 $this->assertSame( 'editsemiprotected', $data['4']['namespaceprotection'] );
118 $this->assertSame( 'editinterface|noratelimit', $data['8']['namespaceprotection'] );
119 $this->assertSame( 'move-categorypages', $data['14']['namespaceprotection'] );
120 }
121
122 public function testNamespaceAliases() {
123 global $wgNamespaceAliases;
124
125 $expected = array_merge(
126 $wgNamespaceAliases,
127 MediaWikiServices::getInstance()->getContentLanguage()->getNamespaceAliases()
128 );
129 $expected = array_map(
130 function ( $key, $val ) {
131 return [ 'id' => $val, 'alias' => strtr( $key, '_', ' ' ) ];
132 },
133 array_keys( $expected ),
134 $expected
135 );
136
137 // Test that we don't list duplicates
138 $this->mergeMwGlobalArrayValue( 'wgNamespaceAliases', [ 'Talk' => NS_TALK ] );
139
140 $this->assertSame( $expected, $this->doQuery( 'namespacealiases' ) );
141 }
142
143 public function testSpecialPageAliases() {
144 $this->assertCount(
145 count( MediaWikiServices::getInstance()->getSpecialPageFactory()->getNames() ),
146 $this->doQuery( 'specialpagealiases' )
147 );
148 }
149
150 public function testMagicWords() {
151 $this->assertCount(
152 count( MediaWikiServices::getInstance()->getContentLanguage()->getMagicWords() ),
153 $this->doQuery( 'magicwords' )
154 );
155 }
156
157 /**
158 * @dataProvider interwikiMapProvider
159 */
160 public function testInterwikiMap( $filter ) {
161 global $wgServer, $wgScriptPath;
162
163 $dbw = wfGetDB( DB_MASTER );
164 $dbw->insert(
165 'interwiki',
166 [
167 [
168 'iw_prefix' => 'self',
169 'iw_url' => "$wgServer$wgScriptPath/index.php?title=$1",
170 'iw_api' => "$wgServer$wgScriptPath/api.php",
171 'iw_wikiid' => 'somedbname',
172 'iw_local' => true,
173 'iw_trans' => true,
174 ],
175 [
176 'iw_prefix' => 'foreign',
177 'iw_url' => '//foreign.example/wiki/$1',
178 'iw_api' => '',
179 'iw_wikiid' => '',
180 'iw_local' => false,
181 'iw_trans' => false,
182 ],
183 ],
184 __METHOD__,
185 'IGNORE'
186 );
187 $this->tablesUsed[] = 'interwiki';
188
189 $this->setMwGlobals( [
190 'wgLocalInterwikis' => [ 'self' ],
191 'wgExtraInterlanguageLinkPrefixes' => [ 'self' ],
192 'wgExtraLanguageNames' => [ 'self' => 'Recursion' ],
193 ] );
194
195 MessageCache::singleton()->enable();
196
197 $this->editPage( 'MediaWiki:Interlanguage-link-self', 'Self!' );
198 $this->editPage( 'MediaWiki:Interlanguage-link-sitename-self', 'Circular logic' );
199
200 $expected = [];
201
202 if ( $filter === null || $filter === '!local' ) {
203 $expected[] = [
204 'prefix' => 'foreign',
205 'url' => wfExpandUrl( '//foreign.example/wiki/$1', PROTO_CURRENT ),
206 'protorel' => true,
207 ];
208 }
209 if ( $filter === null || $filter === 'local' ) {
210 $expected[] = [
211 'prefix' => 'self',
212 'local' => true,
213 'trans' => true,
214 'language' => 'Recursion',
215 'localinterwiki' => true,
216 'extralanglink' => true,
217 'linktext' => 'Self!',
218 'sitename' => 'Circular logic',
219 'url' => "$wgServer$wgScriptPath/index.php?title=$1",
220 'protorel' => false,
221 'wikiid' => 'somedbname',
222 'api' => "$wgServer$wgScriptPath/api.php",
223 ];
224 }
225
226 $data = $this->doQuery( 'interwikimap',
227 $filter === null ? [] : [ 'sifilteriw' => $filter ] );
228
229 $this->assertSame( $expected, $data );
230 }
231
232 public function interwikiMapProvider() {
233 return [ [ 'local' ], [ '!local' ], [ null ] ];
234 }
235
236 /**
237 * @dataProvider dbReplLagProvider
238 */
239 public function testDbReplLagInfo( $showHostnames, $includeAll ) {
240 if ( !$showHostnames && $includeAll ) {
241 $this->setExpectedApiException( 'apierror-siteinfo-includealldenied' );
242 }
243
244 $mockLB = $this->createMock( LoadBalancer::class );
245 $mockLB->method( 'getMaxLag' )->willReturn( [ null, 7, 1 ] );
246 $mockLB->method( 'getLagTimes' )->willReturn( [ 5, 7 ] );
247 $mockLB->method( 'getServerName' )->will( $this->returnValueMap( [
248 [ 0, 'apple' ], [ 1, 'carrot' ]
249 ] ) );
250 $mockLB->method( 'getLocalDomainID' )->willReturn( 'testdomain' );
251 $mockLB->expects( $this->never() )->method( $this->anythingBut(
252 'getMaxLag', 'getLagTimes', 'getServerName', 'getLocalDomainID', '__destruct'
253 ) );
254 $this->setService( 'DBLoadBalancer', $mockLB );
255
256 $this->setMwGlobals( 'wgShowHostnames', $showHostnames );
257
258 $expected = [];
259 if ( $includeAll ) {
260 $expected[] = [ 'host' => $showHostnames ? 'apple' : '', 'lag' => 5 ];
261 }
262 $expected[] = [ 'host' => $showHostnames ? 'carrot' : '', 'lag' => 7 ];
263
264 $data = $this->doQuery( 'dbrepllag', $includeAll ? [ 'sishowalldb' => '' ] : [] );
265
266 $this->assertSame( $expected, $data );
267 }
268
269 public function dbReplLagProvider() {
270 return [
271 'no hostnames, no showalldb' => [ false, false ],
272 'no hostnames, showalldb' => [ false, true ],
273 'hostnames, no showalldb' => [ true, false ],
274 'hostnames, showalldb' => [ true, true ]
275 ];
276 }
277
278 public function testStatistics() {
279 $this->setTemporaryHook( 'APIQuerySiteInfoStatisticsInfo',
280 function ( &$data ) {
281 $data['addedstats'] = 42;
282 }
283 );
284
285 $expected = [
286 'pages' => intval( SiteStats::pages() ),
287 'articles' => intval( SiteStats::articles() ),
288 'edits' => intval( SiteStats::edits() ),
289 'images' => intval( SiteStats::images() ),
290 'users' => intval( SiteStats::users() ),
291 'activeusers' => intval( SiteStats::activeUsers() ),
292 'admins' => intval( SiteStats::numberingroup( 'sysop' ) ),
293 'jobs' => intval( SiteStats::jobs() ),
294 'addedstats' => 42,
295 ];
296
297 $this->assertSame( $expected, $this->doQuery( 'statistics' ) );
298 }
299
300 /**
301 * @dataProvider groupsProvider
302 */
303 public function testUserGroups( $numInGroup ) {
304 global $wgGroupPermissions, $wgAutopromote;
305
306 $this->setGroupPermissions( 'viscount', 'perambulate', 'yes' );
307 $this->setGroupPermissions( 'viscount', 'legislate', '0' );
308 $this->setMwGlobals( [
309 'wgAddGroups' => [ 'viscount' => true, 'bot' => [] ],
310 'wgRemoveGroups' => [ 'viscount' => [ 'sysop' ], 'bot' => [ '*', 'earl' ] ],
311 'wgGroupsAddToSelf' => [ 'bot' => [ 'bureaucrat', 'sysop' ] ],
312 'wgGroupsRemoveFromSelf' => [ 'bot' => [ 'bot' ] ],
313 ] );
314
315 $data = $this->doQuery( 'usergroups', $numInGroup ? [ 'sinumberingroup' => '' ] : [] );
316
317 $names = array_map(
318 function ( $val ) {
319 return $val['name'];
320 },
321 $data
322 );
323
324 $this->assertSame( array_keys( $wgGroupPermissions ), $names );
325
326 foreach ( $data as $val ) {
327 if ( !$numInGroup ) {
328 $expectedSize = null;
329 } elseif ( $val['name'] === 'user' ) {
330 $expectedSize = SiteStats::users();
331 } elseif ( $val['name'] === '*' || isset( $wgAutopromote[$val['name']] ) ) {
332 $expectedSize = null;
333 } else {
334 $expectedSize = SiteStats::numberingroup( $val['name'] );
335 }
336
337 if ( $expectedSize === null ) {
338 $this->assertArrayNotHasKey( 'number', $val );
339 } else {
340 $this->assertSame( $expectedSize, $val['number'] );
341 }
342
343 if ( $val['name'] === 'viscount' ) {
344 $viscountFound = true;
345 $this->assertSame( [ 'perambulate' ], $val['rights'] );
346 $this->assertSame( User::getAllGroups(), $val['add'] );
347 } elseif ( $val['name'] === 'bot' ) {
348 $this->assertArrayNotHasKey( 'add', $val );
349 $this->assertArrayNotHasKey( 'remove', $val );
350 $this->assertSame( [ 'bureaucrat', 'sysop' ], $val['add-self'] );
351 $this->assertSame( [ 'bot' ], $val['remove-self'] );
352 }
353 }
354 }
355
356 public function testFileExtensions() {
357 global $wgFileExtensions;
358
359 // Add duplicate
360 $this->setMwGlobals( 'wgFileExtensions', array_merge( $wgFileExtensions, [ 'png' ] ) );
361
362 $expected = array_map(
363 function ( $val ) {
364 return [ 'ext' => $val ];
365 },
366 array_unique( $wgFileExtensions )
367 );
368
369 $this->assertSame( $expected, $this->doQuery( 'fileextensions' ) );
370 }
371
372 public function groupsProvider() {
373 return [
374 'numingroup' => [ true ],
375 'nonumingroup' => [ false ],
376 ];
377 }
378
379 public function testInstalledLibraries() {
380 // @todo Test no installed.json? Moving installed.json to a different name temporarily
381 // seems a bit scary, but I don't see any other way to do it.
382 //
383 // @todo Install extensions/skins somehow so that we can test they're filtered out
384 global $IP;
385
386 $path = "$IP/vendor/composer/installed.json";
387 if ( !file_exists( $path ) ) {
388 $this->markTestSkipped( 'No installed libraries' );
389 }
390
391 $expected = ( new ComposerInstalled( $path ) )->getInstalledDependencies();
392
393 $expected = array_filter( $expected,
394 function ( $info ) {
395 return strpos( $info['type'], 'mediawiki-' ) !== 0;
396 }
397 );
398
399 $expected = array_map(
400 function ( $name, $info ) {
401 return [ 'name' => $name, 'version' => $info['version'] ];
402 },
403 array_keys( $expected ),
404 array_values( $expected )
405 );
406
407 $this->assertSame( $expected, $this->doQuery( 'libraries' ) );
408 }
409
410 public function testExtensions() {
411 $tmpdir = $this->getNewTempDirectory();
412 touch( "$tmpdir/ErsatzExtension.php" );
413 touch( "$tmpdir/LICENSE" );
414 touch( "$tmpdir/AUTHORS.txt" );
415
416 $val = [
417 'path' => "$tmpdir/ErsatzExtension.php",
418 'name' => 'Ersatz Extension',
419 'namemsg' => 'ersatz-extension-name',
420 'author' => 'John Smith',
421 'version' => '0.0.2',
422 'url' => 'https://www.example.com/software/ersatz-extension',
423 'description' => 'An extension that is not what it seems.',
424 'descriptionmsg' => 'ersatz-extension-desc',
425 'license-name' => 'PD',
426 ];
427
428 $this->setMwGlobals( 'wgExtensionCredits', [ 'api' => [
429 $val,
430 [
431 'author' => [ 'John Smith', 'John Smith Jr.', '...' ],
432 'descriptionmsg' => [ 'another-extension-desc', 'param' ] ],
433 ] ] );
434
435 $data = $this->doQuery( 'extensions' );
436
437 $this->assertCount( 2, $data );
438
439 $this->assertSame( 'api', $data[0]['type'] );
440
441 $sharedKeys = [ 'name', 'namemsg', 'description', 'descriptionmsg', 'author', 'url',
442 'version', 'license-name' ];
443 foreach ( $sharedKeys as $key ) {
444 $this->assertSame( $val[$key], $data[0][$key] );
445 }
446
447 // @todo Test git info
448
449 $this->assertSame(
450 Title::newFromText( 'Special:Version/License/Ersatz Extension' )->getLinkURL(),
451 $data[0]['license']
452 );
453
454 $this->assertSame(
455 Title::newFromText( 'Special:Version/Credits/Ersatz Extension' )->getLinkURL(),
456 $data[0]['credits']
457 );
458
459 $this->assertSame( 'another-extension-desc', $data[1]['descriptionmsg'] );
460 $this->assertSame( [ 'param' ], $data[1]['descriptionmsgparams'] );
461 $this->assertSame( 'John Smith, John Smith Jr., ...', $data[1]['author'] );
462 }
463
464 /**
465 * @dataProvider rightsInfoProvider
466 */
467 public function testRightsInfo( $page, $url, $text, $expectedUrl, $expectedText ) {
468 $this->setMwGlobals( [
469 'wgRightsPage' => $page,
470 'wgRightsUrl' => $url,
471 'wgRightsText' => $text,
472 ] );
473
474 $this->assertSame(
475 [ 'url' => $expectedUrl, 'text' => $expectedText ],
476 $this->doQuery( 'rightsinfo' )
477 );
478 }
479
480 public function rightsInfoProvider() {
481 $textUrl = wfExpandUrl( Title::newFromText( 'License' ), PROTO_CURRENT );
482 $url = 'http://license.example/';
483
484 return [
485 'No rights info' => [ null, null, null, '', '' ],
486 'Only page' => [ 'License', null, null, $textUrl, 'License' ],
487 'Only URL' => [ null, $url, null, $url, '' ],
488 'Only text' => [ null, null, '!!!', '', '!!!' ],
489 // URL is ignored if page is specified
490 'Page and URL' => [ 'License', $url, null, $textUrl, 'License' ],
491 'URL and text' => [ null, $url, '!!!', $url, '!!!' ],
492 'Page and text' => [ 'License', null, '!!!', $textUrl, '!!!' ],
493 'Page and URL and text' => [ 'License', $url, '!!!', $textUrl, '!!!' ],
494 'Pagename "0"' => [ '0', null, null,
495 wfExpandUrl( Title::newFromText( '0' ), PROTO_CURRENT ), '0' ],
496 'URL "0"' => [ null, '0', null, '0', '' ],
497 'Text "0"' => [ null, null, '0', '', '0' ],
498 ];
499 }
500
501 public function testRestrictions() {
502 global $wgRestrictionTypes, $wgRestrictionLevels, $wgCascadingRestrictionLevels,
503 $wgSemiprotectedRestrictionLevels;
504
505 $this->assertSame( [
506 'types' => $wgRestrictionTypes,
507 'levels' => $wgRestrictionLevels,
508 'cascadinglevels' => $wgCascadingRestrictionLevels,
509 'semiprotectedlevels' => $wgSemiprotectedRestrictionLevels,
510 ], $this->doQuery( 'restrictions' ) );
511 }
512
513 /**
514 * @dataProvider languagesProvider
515 */
516 public function testLanguages( $langCode ) {
517 $expected = Language::fetchLanguageNames( (string)$langCode );
518
519 $expected = array_map(
520 function ( $code, $name ) {
521 return [
522 'code' => $code,
523 'bcp47' => LanguageCode::bcp47( $code ),
524 'name' => $name
525 ];
526 },
527 array_keys( $expected ),
528 array_values( $expected )
529 );
530
531 $data = $this->doQuery( 'languages',
532 $langCode !== null ? [ 'siinlanguagecode' => $langCode ] : [] );
533
534 $this->assertSame( $expected, $data );
535 }
536
537 public function languagesProvider() {
538 return [ [ null ], [ 'fr' ] ];
539 }
540
541 public function testLanguageVariants() {
542 $expectedKeys = array_filter( LanguageConverter::$languagesWithVariants,
543 function ( $langCode ) {
544 return !Language::factory( $langCode )->getConverter() instanceof FakeConverter;
545 }
546 );
547 sort( $expectedKeys );
548
549 $this->assertSame( $expectedKeys, array_keys( $this->doQuery( 'languagevariants' ) ) );
550 }
551
552 public function testLanguageVariantsDisabled() {
553 $this->setMwGlobals( 'wgDisableLangConversion', true );
554
555 $this->assertSame( [], $this->doQuery( 'languagevariants' ) );
556 }
557
558 /**
559 * @todo Test a skin with a description that's known to be different in a different language.
560 * Vector will do, but it's not installed by default.
561 *
562 * @todo Test that an invalid language code doesn't actually try reading any messages
563 *
564 * @dataProvider skinsProvider
565 */
566 public function testSkins( $code ) {
567 $data = $this->doQuery( 'skins', $code !== null ? [ 'siinlanguagecode' => $code ] : [] );
568
569 $expectedAllowed = Skin::getAllowedSkins();
570 $expectedDefault = Skin::normalizeKey( 'default' );
571
572 $i = 0;
573 foreach ( Skin::getSkinNames() as $name => $displayName ) {
574 $this->assertSame( $name, $data[$i]['code'] );
575
576 $msg = wfMessage( "skinname-$name" );
577 if ( $code && Language::isValidCode( $code ) ) {
578 $msg->inLanguage( $code );
579 } else {
580 $msg->inContentLanguage();
581 }
582 if ( $msg->exists() ) {
583 $displayName = $msg->text();
584 }
585 $this->assertSame( $displayName, $data[$i]['name'] );
586
587 if ( !isset( $expectedAllowed[$name] ) ) {
588 $this->assertTrue( $data[$i]['unusable'], "$name must be unusable" );
589 }
590 if ( $name === $expectedDefault ) {
591 $this->assertTrue( $data[$i]['default'], "$expectedDefault must be default" );
592 }
593 $i++;
594 }
595 }
596
597 public function skinsProvider() {
598 return [
599 'No language specified' => [ null ],
600 'Czech' => [ 'cs' ],
601 'Invalid language' => [ '/invalid/' ],
602 ];
603 }
604
605 public function testExtensionTags() {
606 $expected = array_map(
607 function ( $tag ) {
608 return "<$tag>";
609 },
610 MediaWikiServices::getInstance()->getParser()->getTags()
611 );
612
613 $this->assertSame( $expected, $this->doQuery( 'extensiontags' ) );
614 }
615
616 public function testFunctionHooks() {
617 $this->assertSame( MediaWikiServices::getInstance()->getParser()->getFunctionHooks(),
618 $this->doQuery( 'functionhooks' ) );
619 }
620
621 public function testVariables() {
622 $this->assertSame(
623 MediaWikiServices::getInstance()->getMagicWordFactory()->getVariableIDs(),
624 $this->doQuery( 'variables' )
625 );
626 }
627
628 public function testProtocols() {
629 global $wgUrlProtocols;
630
631 $this->assertSame( $wgUrlProtocols, $this->doQuery( 'protocols' ) );
632 }
633
634 public function testDefaultOptions() {
635 $this->assertSame( User::getDefaultOptions(), $this->doQuery( 'defaultoptions' ) );
636 }
637
638 public function testUploadDialog() {
639 global $wgUploadDialog;
640
641 $this->assertSame( $wgUploadDialog, $this->doQuery( 'uploaddialog' ) );
642 }
643
644 public function testGetHooks() {
645 global $wgHooks;
646
647 // Make sure there's something to report on
648 $this->setTemporaryHook( 'somehook',
649 function () {
650 return;
651 }
652 );
653
654 $expectedNames = $wgHooks;
655 ksort( $expectedNames );
656
657 $actualNames = array_map(
658 function ( $val ) {
659 return $val['name'];
660 },
661 $this->doQuery( 'showhooks' )
662 );
663
664 $this->assertSame( array_keys( $expectedNames ), $actualNames );
665 }
666
667 public function testContinuation() {
668 // We make lots and lots of URL protocols that are each 100 bytes
669 global $wgAPIMaxResultSize, $wgUrlProtocols;
670
671 $this->setMwGlobals( 'wgUrlProtocols', [] );
672
673 // Just under the limit
674 $chunks = $wgAPIMaxResultSize / 100 - 1;
675
676 for ( $i = 0; $i < $chunks; $i++ ) {
677 $wgUrlProtocols[] = substr( str_repeat( "$i ", 50 ), 0, 100 );
678 }
679
680 $res = $this->doApiRequest( [
681 'action' => 'query',
682 'meta' => 'siteinfo',
683 'siprop' => 'protocols|languages',
684 ] );
685
686 $this->assertSame(
687 wfMessage( 'apiwarn-truncatedresult', Message::numParam( $wgAPIMaxResultSize ) )
688 ->text(),
689 $res[0]['warnings']['result']['warnings']
690 );
691
692 $this->assertSame( $wgUrlProtocols, $res[0]['query']['protocols'] );
693 $this->assertArrayNotHasKey( 'languages', $res[0] );
694 $this->assertTrue( $res[0]['batchcomplete'], 'batchcomplete should be true' );
695 $this->assertSame( [ 'siprop' => 'languages', 'continue' => '-||' ], $res[0]['continue'] );
696 }
697 }