mediawiki.api: Adopt async-await and assert.rejects() in various tests
[mediawiki.git] / tests / phpunit / includes / deferred / LinksUpdateTest.php
blob56d8127e69364d9f2b482dc56d053a66ab3ce5e8
1 <?php
3 use MediaWiki\Content\WikitextContent;
4 use MediaWiki\Debug\MWDebug;
5 use MediaWiki\Deferred\LinksUpdate\LinksTable;
6 use MediaWiki\Deferred\LinksUpdate\LinksTableGroup;
7 use MediaWiki\Deferred\LinksUpdate\LinksUpdate;
8 use MediaWiki\Interwiki\ClassicInterwikiLookup;
9 use MediaWiki\MainConfigNames;
10 use MediaWiki\MediaWikiServices;
11 use MediaWiki\Page\PageIdentityValue;
12 use MediaWiki\Page\PageReference;
13 use MediaWiki\Parser\ParserOutput;
14 use MediaWiki\Title\Title;
15 use MediaWiki\Title\TitleValue;
16 use PHPUnit\Framework\MockObject\MockObject;
17 use Wikimedia\TestingAccessWrapper;
19 /**
20 * @covers \MediaWiki\Deferred\LinksUpdate\LinksUpdate
21 * @covers \MediaWiki\Deferred\LinksUpdate\CategoryLinksTable
22 * @covers \MediaWiki\Deferred\LinksUpdate\ExternalLinksTable
23 * @covers \MediaWiki\Deferred\LinksUpdate\GenericPageLinksTable
24 * @covers \MediaWiki\Deferred\LinksUpdate\ImageLinksTable
25 * @covers \MediaWiki\Deferred\LinksUpdate\InterwikiLinksTable
26 * @covers \MediaWiki\Deferred\LinksUpdate\LangLinksTable
27 * @covers \MediaWiki\Deferred\LinksUpdate\LinksTable
28 * @covers \MediaWiki\Deferred\LinksUpdate\LinksTableGroup
29 * @covers \MediaWiki\Deferred\LinksUpdate\PageLinksTable
30 * @covers \MediaWiki\Deferred\LinksUpdate\PagePropsTable
31 * @covers \MediaWiki\Deferred\LinksUpdate\TemplateLinksTable
32 * @covers \MediaWiki\Deferred\LinksUpdate\TitleLinksTable
34 * @group LinksUpdate
35 * @group Database
37 class LinksUpdateTest extends MediaWikiLangTestCase {
38 /** @var int */
39 protected static $testingPageId;
41 protected function setUp(): void {
42 parent::setUp();
44 // Set up 'linksupdatetest' as a interwiki prefix for testing
45 // See ParserTestRunner:appendInterwikiSetup for similar test code
46 static $testInterwikis = [
48 'iw_prefix' => 'linksupdatetest',
49 'iw_url' => 'http://testing.com/wiki/$1',
50 // 'iw_api' => 'http://testing.com/w/api.php',
51 'iw_local' => 0,
54 $GLOBAL_SCOPE = 2; // See ParserTestRunner::appendInterwikiSetup
55 $this->overrideConfigValues( [
56 MainConfigNames::InterwikiScopes => $GLOBAL_SCOPE,
57 MainConfigNames::InterwikiCache =>
58 ClassicInterwikiLookup::buildCdbHash( $testInterwikis, $GLOBAL_SCOPE ),
59 MainConfigNames::RCWatchCategoryMembership => true,
60 ] );
63 public function addDBDataOnce() {
64 $res = $this->insertPage( 'Testing' );
65 self::$testingPageId = $res['id'];
66 $this->insertPage( 'Some_other_page' );
67 $this->insertPage( 'Template:TestingTemplate' );
70 protected function makeTitleAndParserOutput( $name, $id ) {
71 // Force the value returned by getArticleID, even is
72 // READ_LATEST is passed.
74 /** @var Title|MockObject $t */
75 $t = $this->getMockBuilder( Title::class )
76 ->disableOriginalConstructor()
77 ->onlyMethods( [ 'getArticleID' ] )
78 ->getMock();
79 $t->method( 'getArticleID' )->willReturn( $id );
81 $tAccess = TestingAccessWrapper::newFromObject( $t );
82 $tAccess->secureAndSplit( $name );
84 $po = new ParserOutput();
85 $po->setTitleText( $name );
87 return [ $t, $po ];
90 /**
91 * @covers \MediaWiki\Parser\ParserOutput::addLink
93 public function testUpdate_pagelinks() {
94 /** @var Title $t */
95 /** @var ParserOutput $po */
96 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
98 $po->addLink( Title::newFromText( "Foo" ) );
99 $po->addLink( Title::newFromText( "Bar" ) );
100 $po->addLink( Title::newFromText( "Special:Foo" ) ); // special namespace should be ignored
101 $po->addLink( Title::newFromText( "linksupdatetest:Foo" ) ); // interwiki link should be ignored
102 $po->addLink( Title::newFromText( "#Foo" ) ); // hash link should be ignored
104 $update = $this->assertLinksUpdate(
106 $po,
107 'pagelinks',
108 [ 'lt_namespace', 'lt_title' ],
109 [ 'pl_from' => self::$testingPageId ],
111 [ NS_MAIN, 'Bar' ],
112 [ NS_MAIN, 'Foo' ],
115 $this->assertArrayEquals( [
116 [ NS_MAIN, 'Foo' ],
117 [ NS_MAIN, 'Bar' ],
118 ], array_map(
119 static function ( PageReference $pageReference ) {
120 return [ $pageReference->getNamespace(), $pageReference->getDbKey() ];
122 $update->getPageReferenceArray( 'pagelinks', LinksTable::INSERTED )
123 ) );
125 $po = new ParserOutput();
126 $po->setTitleText( $t->getPrefixedText() );
128 $po->addLink( Title::newFromText( "Bar" ) );
129 $po->addLink( Title::newFromText( "Baz" ) );
130 $po->addLink( Title::newFromText( "Talk:Baz" ) );
132 $update = $this->assertLinksUpdate(
134 $po,
135 'pagelinks',
136 [ 'lt_namespace', 'lt_title' ],
137 [ 'pl_from' => self::$testingPageId ],
139 [ NS_MAIN, 'Bar' ],
140 [ NS_MAIN, 'Baz' ],
141 [ NS_TALK, 'Baz' ],
144 $this->assertArrayEquals( [
145 [ NS_MAIN, 'Baz' ],
146 [ NS_TALK, 'Baz' ],
147 ], array_map(
148 static function ( PageReference $pageReference ) {
149 return [ $pageReference->getNamespace(), $pageReference->getDbKey() ];
151 $update->getPageReferenceArray( 'pagelinks', LinksTable::INSERTED )
152 ) );
153 $this->assertArrayEquals( [
154 [ NS_MAIN, 'Foo' ],
155 ], array_map(
156 static function ( PageReference $pageReference ) {
157 return [ $pageReference->getNamespace(), $pageReference->getDbKey() ];
159 $update->getPageReferenceArray( 'pagelinks', LinksTable::DELETED )
160 ) );
163 public function testUpdate_pagelinks_move() {
164 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
166 $po->addLink( Title::newFromText( "Foo" ) );
167 $this->assertLinksUpdate(
169 $po,
170 'pagelinks',
171 [ 'lt_namespace', 'lt_title', 'pl_from_namespace' ],
172 [ 'pl_from' => self::$testingPageId ],
174 [ NS_MAIN, 'Foo', NS_MAIN ],
178 [ $t, $po ] = $this->makeTitleAndParserOutput( "User:Testing", self::$testingPageId );
179 $po->addLink( Title::newFromText( "Foo" ) );
180 $this->assertMoveLinksUpdate(
182 new PageIdentityValue( 2, 0, "Foo", false ),
183 $po,
184 'pagelinks',
185 [ 'lt_namespace', 'lt_title', 'pl_from_namespace' ],
186 [ 'pl_from' => self::$testingPageId ],
188 [ NS_MAIN, 'Foo', NS_USER ],
194 * @covers \MediaWiki\Parser\ParserOutput::addExternalLink
196 public function testUpdate_externallinks() {
197 /** @var ParserOutput $po */
198 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
200 $po->addExternalLink( "http://testing.com/wiki/Foo" );
201 $po->addExternalLink( "http://testing.com/wiki/Bar" );
203 $update = $this->assertLinksUpdate(
205 $po,
206 'externallinks',
207 [ 'el_to_domain_index', 'el_to_path' ],
208 [ 'el_from' => self::$testingPageId ],
210 [ 'http://com.testing.', '/wiki/Bar' ],
211 [ 'http://com.testing.', '/wiki/Foo' ],
215 $this->assertArrayEquals( [
216 "http://testing.com/wiki/Bar",
217 "http://testing.com/wiki/Foo"
218 ], $update->getAddedExternalLinks() );
220 $po = new ParserOutput();
221 $po->setTitleText( $t->getPrefixedText() );
222 $po->addExternalLink( 'http://testing.com/wiki/Bar' );
223 $po->addExternalLink( 'http://testing.com/wiki/Baz' );
224 $update = $this->assertLinksUpdate(
226 $po,
227 'externallinks',
228 [ 'el_to_domain_index', 'el_to_path' ],
229 [ 'el_from' => self::$testingPageId ],
231 [ 'http://com.testing.', '/wiki/Bar' ],
232 [ 'http://com.testing.', '/wiki/Baz' ],
236 $this->assertArrayEquals( [
237 "http://testing.com/wiki/Baz"
238 ], $update->getAddedExternalLinks() );
239 $this->assertArrayEquals( [
240 "http://testing.com/wiki/Foo"
241 ], $update->getRemovedExternalLinks() );
244 public function testUpdate_externallinksWrongOldEntry() {
245 /** @var ParserOutput $po */
246 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
248 // Insert invalid entry from T350476
249 $this->getDb()->newInsertQueryBuilder()
250 ->insertInto( 'externallinks' )
251 ->row( [
252 'el_from' => self::$testingPageId,
253 'el_to_domain_index' => 'http://.com.testing.',
254 'el_to_path' => '/',
256 ->row( [
257 'el_from' => self::$testingPageId,
258 'el_to_domain_index' => 'http://.',
259 'el_to_path' => '/',
261 ->row( [
262 'el_from' => self::$testingPageId,
263 'el_to_domain_index' => '',
264 'el_to_path' => null,
266 ->execute();
268 // Test that the invalid entries are removed on LinksUpdate
269 $po = new ParserOutput();
270 $po->setTitleText( $t->getPrefixedText() );
271 $po->addExternalLink( 'http://testing.com/wiki/Bar' );
272 $po->addExternalLink( 'http://testing.com/wiki/Baz' );
273 $update = $this->assertLinksUpdate(
275 $po,
276 'externallinks',
277 [ 'el_to_domain_index', 'el_to_path' ],
278 [ 'el_from' => self::$testingPageId ],
280 [ 'http://com.testing.', '/wiki/Bar' ],
281 [ 'http://com.testing.', '/wiki/Baz' ],
285 $this->assertArrayEquals( [
286 'http://testing.com/wiki/Bar',
287 'http://testing.com/wiki/Baz',
288 ], $update->getAddedExternalLinks() );
289 $this->assertArrayEquals( [
290 'http://testing.com/',
291 'http:///',
293 ], $update->getRemovedExternalLinks() );
297 * @covers \MediaWiki\Parser\ParserOutput::addCategory
299 public function testUpdate_categorylinks() {
300 /** @var ParserOutput $po */
301 $this->overrideConfigValue( MainConfigNames::CategoryCollation, 'uppercase' );
303 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
305 $po->addCategory( "Foo", "FOO" );
306 $po->addCategory( "Bar", "BAR" );
308 $this->assertLinksUpdate(
310 $po,
311 'categorylinks',
312 [ 'cl_to', 'cl_sortkey' ],
313 [ 'cl_from' => self::$testingPageId ],
315 [ 'Bar', "BAR\nTESTING" ],
316 [ 'Foo', "FOO\nTESTING" ]
320 // Check category count
321 $this->newSelectQueryBuilder()
322 ->select( [ 'cat_title', 'cat_pages' ] )
323 ->from( 'category' )
324 ->where( [ 'cat_title' => [ 'Foo', 'Bar', 'Baz' ] ] )
325 ->assertResultSet( [
326 [ 'Bar', 1 ],
327 [ 'Foo', 1 ]
328 ] );
330 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
331 $po->addCategory( "Bar", "Bar" );
332 $po->addCategory( "Baz", "Baz" );
334 $this->assertLinksUpdate(
336 $po,
337 'categorylinks',
338 [ 'cl_to', 'cl_sortkey' ],
339 [ 'cl_from' => self::$testingPageId ],
341 [ 'Bar', "BAR\nTESTING" ],
342 [ 'Baz', "BAZ\nTESTING" ]
346 // Check category count decrement
347 $this->newSelectQueryBuilder()
348 ->select( [ 'cat_title', 'cat_pages' ] )
349 ->from( 'category' )
350 ->where( [ 'cat_title' => [ 'Foo', 'Bar', 'Baz' ] ] )
351 ->assertResultSet( [
352 [ 'Bar', 1 ],
353 [ 'Baz', 1 ],
354 ] );
357 public function testOnAddingAndRemovingCategory_recentChangesRowIsAdded() {
358 $this->overrideConfigValue( MainConfigNames::CategoryCollation, 'uppercase' );
360 $title = Title::newFromText( 'Testing' );
361 $wikiPage = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title );
362 $wikiPage->doUserEditContent(
363 new WikitextContent( '[[Category:Foo]]' ),
364 $this->getTestSysop()->getUser(),
365 'added category'
367 $this->runAllRelatedJobs();
369 $this->assertRecentChangeByCategorization(
370 Title::newFromText( 'Category:Foo' ),
371 [ [ 'Foo', '[[:Testing]] added to category' ] ]
374 $wikiPage->doUserEditContent(
375 new WikitextContent( '[[Category:Bar]]' ),
376 $this->getTestSysop()->getUser(),
377 'replaced category'
379 $this->runAllRelatedJobs();
381 $this->assertRecentChangeByCategorization(
382 Title::newFromText( 'Category:Foo' ),
384 [ 'Foo', '[[:Testing]] added to category' ],
385 [ 'Foo', '[[:Testing]] removed from category' ],
389 $this->assertRecentChangeByCategorization(
390 Title::newFromText( 'Category:Bar' ),
392 [ 'Bar', '[[:Testing]] added to category' ],
397 public function testOnAddingAndRemovingCategoryToTemplates_embeddingPagesAreIgnored() {
398 $this->overrideConfigValue( MainConfigNames::CategoryCollation, 'uppercase' );
400 $templateTitle = Title::newFromText( 'Template:TestingTemplate' );
401 $templatePage = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $templateTitle );
403 $wikiPage = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( Title::newFromText( 'Testing' ) );
404 $wikiPage->doUserEditContent(
405 new WikitextContent( '{{TestingTemplate}}' ),
406 $this->getTestSysop()->getUser(),
407 'added template'
409 $this->runAllRelatedJobs();
411 $otherWikiPage = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( Title::newFromText( 'Some_other_page' ) );
412 $otherWikiPage->doUserEditContent(
413 new WikitextContent( '{{TestingTemplate}}' ),
414 $this->getTestSysop()->getUser(),
415 'added template'
417 $this->runAllRelatedJobs();
419 $this->assertRecentChangeByCategorization(
420 Title::newFromText( 'Baz' ),
424 $templatePage->doUserEditContent(
425 new WikitextContent( '[[Category:Baz]]' ),
426 $this->getTestSysop()->getUser(),
427 'added category'
429 $this->runAllRelatedJobs();
431 $this->assertRecentChangeByCategorization(
432 Title::newFromText( 'Baz' ),
434 'Baz',
435 '[[:Template:TestingTemplate]] added to category, ' .
436 '[[Special:WhatLinksHere/Template:TestingTemplate|this page is included within other pages]]'
441 public function testUpdate_categorylinks_move() {
442 $this->overrideConfigValue( MainConfigNames::CategoryCollation, 'uppercase' );
444 /** @var ParserOutput $po */
445 [ $t, $po ] = $this->makeTitleAndParserOutput( "Old", self::$testingPageId );
447 $po->addCategory( "Bar", "BAR" );
448 $po->addCategory( "Foo", "FOO" );
450 $this->assertLinksUpdate(
452 $po,
453 'categorylinks',
454 [ 'cl_to', 'cl_sortkey' ],
455 [ 'cl_from' => self::$testingPageId ],
457 [ 'Bar', "BAR\nOLD" ],
458 [ 'Foo', "FOO\nOLD" ],
462 // Check category count
463 $this->newSelectQueryBuilder()
464 ->select( [ 'cat_title', 'cat_pages' ] )
465 ->from( 'category' )
466 ->where( [ 'cat_title' => [ 'Foo', 'Bar', 'Baz' ] ] )
467 ->assertResultSet( [
468 [ 'Bar', '1' ],
469 [ 'Foo', '1' ],
470 ] );
472 /** @var ParserOutput $po */
473 [ $t, $po ] = $this->makeTitleAndParserOutput( "New", self::$testingPageId );
475 $po->addCategory( "Bar", "BAR" );
476 $po->addCategory( "Foo", "FOO" );
478 // An update to cl_sortkey is not expected if there was no move
479 $this->assertLinksUpdate(
481 $po,
482 'categorylinks',
483 [ 'cl_to', 'cl_sortkey' ],
484 [ 'cl_from' => self::$testingPageId ],
486 [ 'Bar', "BAR\nOLD" ],
487 [ 'Foo', "FOO\nOLD" ],
491 // Check category count
492 $this->newSelectQueryBuilder()
493 ->select( [ 'cat_title', 'cat_pages' ] )
494 ->from( 'category' )
495 ->where( [ 'cat_title' => [ 'Foo', 'Bar', 'Baz' ] ] )
496 ->assertResultSet( [
497 [ 'Bar', '1' ],
498 [ 'Foo', '1' ],
499 ] );
501 // A category changed on move
502 $po->setCategories( [
503 "Baz" => "BAZ",
504 "Foo" => "FOO",
505 ] );
507 // With move notification, update to cl_sortkey is expected
508 $this->assertMoveLinksUpdate(
510 new PageIdentityValue( 2, 0, "new", false ),
511 $po,
512 'categorylinks',
513 [ 'cl_to', 'cl_sortkey' ],
514 [ 'cl_from' => self::$testingPageId ],
516 [ 'Baz', "BAZ\nNEW" ],
517 [ 'Foo', "FOO\nNEW" ],
521 // Check category count
522 $this->newSelectQueryBuilder()
523 ->select( [ 'cat_title', 'cat_pages' ] )
524 ->from( 'category' )
525 ->where( [ 'cat_title' => [ 'Foo', 'Bar', 'Baz' ] ] )
526 ->assertResultSet( [
527 [ 'Baz', '1' ],
528 [ 'Foo', '1' ],
529 ] );
533 * @covers \MediaWiki\Parser\ParserOutput::addInterwikiLink
535 public function testUpdate_iwlinks() {
536 /** @var ParserOutput $po */
537 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
539 $target1 = Title::makeTitleSafe( NS_MAIN, "T1", '', 'linksupdatetest' );
540 $target2 = Title::makeTitleSafe( NS_MAIN, "T2", '', 'linksupdatetest' );
541 $target3 = Title::makeTitleSafe( NS_MAIN, "T3", '', 'linksupdatetest' );
542 $po->addInterwikiLink( $target1 );
543 $po->addInterwikiLink( $target2 );
545 $this->assertLinksUpdate(
547 $po,
548 'iwlinks',
549 [ 'iwl_prefix', 'iwl_title' ],
550 [ 'iwl_from' => self::$testingPageId ],
552 [ 'linksupdatetest', 'T1' ],
553 [ 'linksupdatetest', 'T2' ],
557 /** @var ParserOutput $po */
558 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
560 $po->addInterwikiLink( $target2 );
561 $po->addInterwikiLink( $target3 );
563 $this->assertLinksUpdate(
565 $po,
566 'iwlinks',
567 [ 'iwl_prefix', 'iwl_title' ],
568 [ 'iwl_from' => self::$testingPageId ],
570 [ 'linksupdatetest', 'T2' ],
571 [ 'linksupdatetest', 'T3' ]
577 * @covers \MediaWiki\Parser\ParserOutput::addTemplate
579 public function testUpdate_templatelinks() {
580 /** @var ParserOutput $po */
581 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
582 $linkTargetLookup = MediaWikiServices::getInstance()->getLinkTargetLookup();
584 $target1 = Title::newFromText( "Template:T1" );
585 $target2 = Title::newFromText( "Template:T2" );
586 $target3 = Title::newFromText( "Template:T3" );
588 $po->addTemplate( $target1, 23, 42 );
589 $po->addTemplate( $target2, 23, 42 );
591 $this->assertLinksUpdate(
593 $po,
594 'templatelinks',
595 [ 'tl_target_id' ],
596 [ 'tl_from' => self::$testingPageId ],
598 [ $linkTargetLookup->acquireLinkTargetId( $target1, $this->getDb() ) ],
599 [ $linkTargetLookup->acquireLinkTargetId( $target2, $this->getDb() ) ],
603 /** @var ParserOutput $po */
604 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
606 $po->addTemplate( $target2, 23, 42 );
607 $po->addTemplate( $target3, 23, 42 );
609 $this->assertLinksUpdate(
611 $po,
612 'templatelinks',
613 [ 'tl_target_id' ],
614 [ 'tl_from' => self::$testingPageId ],
616 [ $linkTargetLookup->acquireLinkTargetId( $target2, $this->getDb() ) ],
617 [ $linkTargetLookup->acquireLinkTargetId( $target3, $this->getDb() ) ],
623 * @covers \MediaWiki\Parser\ParserOutput::addImage
625 public function testUpdate_imagelinks() {
626 /** @var ParserOutput $po */
627 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
629 $po->addImage( new TitleValue( NS_FILE, "1.png" ) );
630 $po->addImage( new TitleValue( NS_FILE, "2.png" ) );
632 $this->assertLinksUpdate(
634 $po,
635 'imagelinks',
636 'il_to',
637 [ 'il_from' => self::$testingPageId ],
638 [ [ '1.png' ], [ '2.png' ] ]
641 /** @var ParserOutput $po */
642 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
644 $po->addImage( new TitleValue( NS_FILE, "2.png" ) );
645 $po->addImage( new TitleValue( NS_FILE, "3.png" ) );
647 $this->assertLinksUpdate(
649 $po,
650 'imagelinks',
651 'il_to',
652 [ 'il_from' => self::$testingPageId ],
653 [ [ '2.png' ], [ '3.png' ] ]
657 public function testUpdate_imagelinks_move() {
658 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
660 $po->addImage( new TitleValue( NS_FILE, "1.png" ) );
661 $po->addImage( new TitleValue( NS_FILE, "2.png" ) );
663 $fromNamespace = $t->getNamespace();
664 $this->assertLinksUpdate(
666 $po,
667 'imagelinks',
668 [ 'il_to', 'il_from_namespace' ],
669 [ 'il_from' => self::$testingPageId ],
670 [ [ '1.png', $fromNamespace ], [ '2.png', $fromNamespace ] ]
673 $oldT = $t;
674 [ $t, $po ] = $this->makeTitleAndParserOutput( "User:Testing", self::$testingPageId );
675 $po->addImage( new TitleValue( NS_FILE, "1.png" ) );
676 $po->addImage( new TitleValue( NS_FILE, "2.png" ) );
678 $fromNamespace = $t->getNamespace();
679 $this->assertMoveLinksUpdate(
681 $oldT->toPageIdentity(),
682 $po,
683 'imagelinks',
684 [ 'il_to', 'il_from_namespace' ],
685 [ 'il_from' => self::$testingPageId ],
686 [ [ '1.png', $fromNamespace ], [ '2.png', $fromNamespace ] ]
691 * @covers \MediaWiki\Parser\ParserOutput::addLanguageLink
693 public function testUpdate_langlinks() {
694 $this->overrideConfigValue( MainConfigNames::CapitalLinks, true );
696 /** @var ParserOutput $po */
697 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
699 $po->addLanguageLink( new TitleValue( 0, '1', '', 'De' ) );
700 $po->addLanguageLink( new TitleValue( 0, '1', '', 'En' ) );
701 $po->addLanguageLink( new TitleValue( 0, '1', '', 'Fr' ) );
703 $this->assertLinksUpdate(
705 $po,
706 'langlinks',
707 [ 'll_lang', 'll_title' ],
708 [ 'll_from' => self::$testingPageId ],
710 [ 'De', '1' ],
711 [ 'En', '1' ],
712 [ 'Fr', '1' ]
716 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
717 $po->addLanguageLink( new TitleValue( 0, '2', '', 'En' ) );
718 $po->addLanguageLink( new TitleValue( 0, '1', '', 'Fr' ) );
720 $this->assertLinksUpdate(
722 $po,
723 'langlinks',
724 [ 'll_lang', 'll_title' ],
725 [ 'll_from' => self::$testingPageId ],
727 [ 'En', '2' ],
728 [ 'Fr', '1' ]
734 * @param bool $useDeprecatedApi
735 * @covers \MediaWiki\Parser\ParserOutput::setPageProperty
736 * @covers \MediaWiki\Parser\ParserOutput::setNumericPageProperty
737 * @covers \MediaWiki\Parser\ParserOutput::setUnsortedPageProperty
738 * @dataProvider provideUseDeprecatedApi
740 public function testUpdate_page_props( $useDeprecatedApi ) {
741 /** @var ParserOutput $po */
742 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
744 $fields = [ 'pp_propname', 'pp_value', 'pp_sortkey' ];
745 $cond = [ 'pp_page' => self::$testingPageId ];
747 $setNumericPageProperty = 'setNumericPageProperty';
748 $setUnsortedPageProperty = 'setUnsortedPageProperty';
749 if ( $useDeprecatedApi ) {
750 // ::setPageProperty is deprecated when used for non-string values;
751 // and when used for string values it is identical to
752 // ::setUnsortedPageProperty
753 $indexedPageProperty = 'setPageProperty';
754 $setUnsortedPageProperty = 'setPageProperty';
755 MWDebug::filterDeprecationForTest( '/::setPageProperty with non-string value/' );
758 $po->$setNumericPageProperty( 'deleted', 1 );
759 $po->$setNumericPageProperty( 'changed', 1 );
760 $this->assertLinksUpdate(
761 $t, $po, 'page_props', $fields, $cond,
763 [ 'changed', '1', 1 ],
764 [ 'deleted', '1', 1 ]
768 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
770 // Elements of the $expected array are 3-element arrays:
771 // First element is the page property name
772 // Second element is the page property value
773 // (These are stringified when encoded into the database.)
774 // Third element is the sort key (as a float, or null)
775 $expected = [];
777 if ( $useDeprecatedApi ) {
778 // Using legacy API this is only coerced during LinksUpdate
779 $po->setPageProperty( 'bool', true );
780 $expected[] = [ "bool", true, 1.0 ];
783 $po->$setNumericPageProperty( 'changed', 2 );
784 $expected[] = [ 'changed', 2, 2.0 ];
786 $f = 4.0 + 1.0 / 4.0;
787 $po->$setNumericPageProperty( "float", $f );
788 $expected[] = [ "float", $f, $f ];
790 $po->$setNumericPageProperty( "int", -7 );
791 $expected[] = [ "int", -7, -7.0 ];
793 $po->$setUnsortedPageProperty( "string", "33 bar" );
794 $expected[] = [ "string", "33 bar", null ];
796 if ( !$useDeprecatedApi ) {
797 // A numeric string *does* get indexed if you use
798 // ::setNumericPageProperty
799 $po->setNumericPageProperty( "numeric-string", "33" );
800 $expected[] = [ "numeric-string", 33, 33.0 ];
801 // And similarly a numeric argument won't get indexed if you
802 // use ::setUnsortedPageProperty
803 $po->setUnsortedPageProperty( "unsorted", 33 );
804 $expected[] = [ "unsorted", "33", null ];
807 // Note that the ::assertSelect machinery will sort by the columns
808 // provided in $fields; in our case we should sort by property name
809 usort( $expected, static fn ( $a, $b ): int => $a[0] <=> $b[0] );
811 $update = $this->assertLinksUpdate(
812 $t, $po, 'page_props', $fields, [ 'pp_page' => self::$testingPageId ], $expected );
814 $expectedAssoc = [];
815 foreach ( $expected as [ $name, $value ] ) {
816 $expectedAssoc[$name] = $value;
818 $this->assertArrayEquals( $expectedAssoc, $update->getAddedProperties() );
819 $this->assertArrayEquals(
821 'changed' => '1',
822 'deleted' => '1'
824 $update->getRemovedProperties()
828 public static function provideUseDeprecatedApi() {
829 yield "Non-deprecated API" => [ false ];
830 yield "Deprecated API" => [ true ];
833 // @todo test recursive, too!
835 protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput,
836 $table, $fields, $condition, array $expectedRows
838 return $this->assertMoveLinksUpdate( $title, null, $parserOutput,
839 $table, $fields, $condition, $expectedRows );
842 protected function assertMoveLinksUpdate(
843 Title $title, ?PageIdentityValue $oldTitle, ParserOutput $parserOutput,
844 $table, $fields, $condition, array $expectedRows
846 $update = new LinksUpdate( $title, $parserOutput );
847 $update->setStrictTestMode();
848 if ( $oldTitle ) {
849 $update->setMoveDetails( $oldTitle );
851 $this->setTransactionTicket( $update );
853 $update->doUpdate();
855 $qb = $this->newSelectQueryBuilder()
856 ->select( $fields )
857 ->from( $table )
858 ->where( $condition );
859 if ( $table === 'pagelinks' ) {
860 $qb->join( 'linktarget', null, 'pl_target_id=lt_id' );
862 $qb->assertResultSet( $expectedRows );
863 return $update;
866 protected function assertRecentChangeByCategorization(
867 Title $categoryTitle, $expectedRows
869 $this->newSelectQueryBuilder()
870 ->select( [ 'rc_title', 'comment_text' ] )
871 ->from( 'recentchanges' )
872 ->join( 'comment', null, 'comment_id = rc_comment_id' )
873 ->where( [
874 'rc_type' => RC_CATEGORIZE,
875 'rc_namespace' => NS_CATEGORY,
876 'rc_title' => $categoryTitle->getDBkey(),
878 ->assertResultSet( $expectedRows );
881 private function runAllRelatedJobs() {
882 $queueGroup = $this->getServiceContainer()->getJobQueueGroup();
883 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
884 while ( $job = $queueGroup->pop( 'refreshLinksPrioritized' ) ) {
885 $job->run();
886 $queueGroup->ack( $job );
888 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
889 while ( $job = $queueGroup->pop( 'categoryMembershipChange' ) ) {
890 $job->run();
891 $queueGroup->ack( $job );
895 public function testIsRecursive() {
896 [ $title, $po ] = $this->makeTitleAndParserOutput( 'Test', 1 );
897 $linksUpdate = new LinksUpdate( $title, $po );
898 $this->assertTrue( $linksUpdate->isRecursive(), 'LinksUpdate is recursive by default' );
900 $linksUpdate = new LinksUpdate( $title, $po, true );
901 $this->assertTrue( $linksUpdate->isRecursive(),
902 'LinksUpdate is recursive when asked to be recursive' );
904 $linksUpdate = new LinksUpdate( $title, $po, false );
905 $this->assertFalse( $linksUpdate->isRecursive(),
906 'LinksUpdate is not recursive when asked to be not recursive' );
910 * Confirm that repeatedly saving the same ParserOutput does not lead to
911 * DELETE/INSERT queries (T299662)
912 * @dataProvider provideUseDeprecatedApi
914 public function testNullEdit( bool $useDeprecatedApi ) {
915 $setNumericPageProperty = 'setNumericPageProperty';
916 $setUnsortedPageProperty = 'setUnsortedPageProperty';
917 if ( $useDeprecatedApi ) {
918 $setNumericPageProperty = 'setPageProperty';
919 $setUnsortedPageProperty = 'setPageProperty';
920 MWDebug::filterDeprecationForTest( '/::setPageProperty with non-string value/' );
923 /** @var ParserOutput $po */
924 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
925 $po->addCategory( 'Test', 'Test' );
926 $po->addExternalLink( 'http://www.example.com/' );
927 $po->addImage( new TitleValue( NS_FILE, 'Test' ) );
928 $po->addInterwikiLink( new TitleValue( 0, 'test', '', 'test' ) );
929 $po->addLanguageLink( new TitleValue( 0, 'Test', '', 'en' ) );
930 $po->addLink( new TitleValue( 0, 'Test' ) );
931 $po->$setUnsortedPageProperty( 'string', 'x' );
932 $po->$setUnsortedPageProperty( 'numeric-string', '1' );
933 $po->$setNumericPageProperty( 'int', 10 );
934 $po->$setNumericPageProperty( 'float', 2 / 3 );
935 if ( $useDeprecatedApi ) {
936 $po->setPageProperty( 'true', true );
937 $po->setPageProperty( 'false', false );
938 $this->expectDeprecationAndContinue( '/::setPageProperty with null value/' );
939 $po->setPageProperty( 'null', null );
940 } else {
941 $po->$setUnsortedPageProperty( 'null', '' );
944 $update = new LinksUpdate( $t, $po );
945 $update->setStrictTestMode();
946 $this->setTransactionTicket( $update );
947 $update->doUpdate();
949 $time1 = $this->getDb()->lastDoneWrites();
950 $this->assertGreaterThan( 0, $time1 );
952 $update = new class( $t, $po ) extends LinksUpdate {
953 protected function updateLinksTimestamp() {
954 // Updating the timestamp is allowed, ignore
957 $update->setStrictTestMode();
958 $update->doUpdate();
959 $time2 = $this->getDb()->lastDoneWrites();
960 $this->assertSame( $time1, $time2 );
963 public static function provideNumericKeys() {
964 $tables = TestingAccessWrapper::constant( LinksTableGroup::class, 'CORE_LIST' );
965 foreach ( $tables as $tableName => $spec ) {
966 yield [ $tableName ];
971 * Unit test for numeric strings in ParserOutput array keys (T301433)
973 * @dataProvider provideNumericKeys
975 public function testNumericKeys( $tableName ) {
976 $s = '123';
977 $i = 123;
979 /** @var ParserOutput $po */
980 [ $t, $po ] = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
981 $po->addCategory( $s, $s );
982 $po->addExternalLink( 'https://foo.com' );
983 $po->addImage( new TitleValue( NS_FILE, $s ) );
984 $po->addInterwikiLink( new TitleValue( 0, $s, '', $s ) );
985 $po->addLanguageLink( new TitleValue( 0, $s, '', $s ) );
986 $po->addLink( new TitleValue( 0, $s ) );
987 $po->setUnsortedPageProperty( $s, $s );
988 $po->addTemplate( new TitleValue( 0, $s ), 1, 1 );
990 $update = new LinksUpdate( $t, $po );
991 /** @var LinksTableGroup $tg */
992 $tg = TestingAccessWrapper::newFromObject( $update )->tableFactory;
993 $table = $tg->get( $tableName );
994 /** @var LinksTable $tt */
995 $tt = TestingAccessWrapper::newFromObject( $table );
996 $tableName = $tt->getTableName();
997 foreach ( $tt->getNewLinkIDs() as $linkID ) {
998 foreach ( (array)$linkID as $component ) {
999 $this->assertNotSame( $i, $component,
1000 "Link ID of table $tableName should not be an integer " );
1006 * Integration test for numeric category names (T301433)
1008 public function testNumericCategory() {
1009 [ $t, $po ] = $this->makeTitleAndParserOutput( "Test 1", self::$testingPageId + 1 );
1010 $po->addCategory( '123a', '123a' );
1011 $update = new LinksUpdate( $t, $po );
1012 $this->setTransactionTicket( $update );
1013 $update->setStrictTestMode();
1014 $update->doUpdate();
1016 [ $t, $po ] = $this->makeTitleAndParserOutput( "Test 2", self::$testingPageId + 2 );
1017 $po->addCategory( '123', '123' );
1018 $update = new LinksUpdate( $t, $po );
1019 $this->setTransactionTicket( $update );
1020 $update->setStrictTestMode();
1021 $update->doUpdate();
1023 $this->newSelectQueryBuilder()
1024 ->select( 'cat_pages' )
1025 ->from( 'category' )
1026 ->where( [ 'cat_title' => '123a' ] )
1027 ->assertFieldValue( '1' );
1030 private function setTransactionTicket( LinksUpdate $update ) {
1031 $update->setTransactionTicket(
1032 $this->getServiceContainer()->getConnectionProvider()->getEmptyTransactionTicket( __METHOD__ )