getServiceContainer()->getMessageCache()->enable(); } public function tearDown(): void { ChangeTags::$avoidReopeningTablesForTesting = false; $this->getServiceContainer()->getMessageCache()->disable(); parent::tearDown(); } public function addDBDataOnce() { $user = $this->getTestUser()->getUser(); $clock = (int)ConvertibleTimestamp::now( TS_UNIX ); ConvertibleTimestamp::setFakeTime( static function () use ( &$clock ) { return ++$clock; } ); self::$testUser = $user; self::$performer = new UltimateAuthority( $user ); $revisionText = [ 1 => 'Lorem', 2 => 'Lorem Ipsum', 3 => 'Lor', 4 => 'Lorem' ]; // maps $storedRevision revision number to delta of revision length from its parent self::$storedDeltas = [ 1 => 5, 2 => 11, 3 => -2, 4 => -6 ]; self::$storedRevisions[1] = $this->editPage( __METHOD__ . '_1', $revisionText[1], 'test', NS_MAIN, $user ) ->getValue()['revision-record']; self::$storedRevisions[2] = $this->editPage( __METHOD__ . '_2', $revisionText[2], 'test', NS_TALK, $user ) ->getValue()['revision-record']; self::$storedRevisions[3] = $this->editPage( __METHOD__ . '_1', $revisionText[3], 'test', NS_MAIN, $user ) ->getValue()['revision-record']; self::$storedRevisions[4] = $this->editPage( __METHOD__ . '_2', $revisionText[4], 'test', NS_TALK, $user ) ->getValue()['revision-record']; ChangeTags::defineTag( self::TAG1 ); ChangeTags::defineTag( self::TAG2 ); self::$storedTags = [ 1 => [], 2 => [ self::TAG1, self::TAG2 ], 3 => [ self::TAG2 ], 4 => [], ]; foreach ( self::$storedTags as $idx => $tags ) { if ( !$tags ) { continue; } $revId = self::$storedRevisions[$idx]->getId(); ChangeTags::addTags( $tags, null, $revId ); } // Pages at MediaWiki:tag-{tag_name} override any tag display text $this->insertPage( "tag-" . self::TAG1, "''" . self::TAG_DISPLAY . "''", NS_MEDIAWIKI ); $this->insertPage( "tag-" . self::TAG2, "''" . self::TAG_DISPLAY . "''", NS_MEDIAWIKI ); // Add a 3rd disabled tag (empty string as display name equates to disabled tag) ChangeTags::addTags( [ self::TAG3 ], null, $revId ); $this->insertPage( "tag-" . self::TAG3, '', NS_MEDIAWIKI ); ConvertibleTimestamp::setFakeTime( false ); } /** * @return ContributionsLookup */ private function getContributionsLookup() { // Helper to simplify potential switch to unit tests return $this->getServiceContainer()->getContributionsLookup(); } /** * @covers \MediaWiki\Revision\ContributionsLookup::getContributions() */ public function testGetDeltas() { $contributionsLookup = $this->getContributionsLookup(); $segment = $contributionsLookup->getContributions( self::$testUser, 10, self::$performer ); // Contributions are returned in descending order. $revIds = array_map( static function ( RevisionRecord $rev ) { return $rev->getId(); }, $segment->getRevisions() ); $this->assertEquals( self::$storedDeltas[ 4 ], $segment->getDeltaForRevision( $revIds[0] ) ); $this->assertEquals( self::$storedDeltas[ 3 ], $segment->getDeltaForRevision( $revIds[1] ) ); $this->assertEquals( self::$storedDeltas[ 2 ], $segment->getDeltaForRevision( $revIds[2] ) ); $this->assertEquals( self::$storedDeltas[ 1 ], $segment->getDeltaForRevision( $revIds[3] ) ); } /** * @covers \MediaWiki\Revision\ContributionsLookup::getContributions() */ public function testGetUnknownDeltas() { $user = $this->getTestUser()->getUser(); $rev1 = $this->editPage( __METHOD__ . '_1', 'foo', 'test', NS_MAIN, $user ) ->getValue()['revision-record']; $rev2 = $this->editPage( __METHOD__ . '_2', 'bar', 'test', NS_TALK, $user ) ->getValue()['revision-record']; // Parent of revision 1 is not in revision table (deleted) $this->db->update( 'revision', [ 'rev_parent_id' => $rev2->getId() + 100 ], [ 'rev_id' => $rev1->getId() ], __METHOD__ ); // Parent of revision 2 is unknown $this->db->update( 'revision', [ 'rev_parent_id' => null ], [ 'rev_id' => $rev2->getId() ], __METHOD__ ); $contributionsLookup = $this->getContributionsLookup(); $segment = $contributionsLookup->getContributions( $user, 10, self::$performer ); $this->assertNull( $segment->getDeltaForRevision( $rev1->getId() ) ); $this->assertNull( $segment->getDeltaForRevision( $rev2->getId() ) ); } /** * @covers \MediaWiki\Revision\ContributionsLookup::getContributions() */ public function testGetContributionsByUserIdentity() { $contributionsLookup = $this->getContributionsLookup(); $segment = $contributionsLookup->getContributions( self::$testUser, 2, self::$performer ); // Desc order comes back from db query $this->assertSegmentRevisions( [ 4, 3 ], $segment ); } /** * @covers \MediaWiki\Revision\ContributionsLookup::getContributions() */ public function testGetListOfContributions_revisionOnly() { $this->setTemporaryHook( 'ContribsPager::reallyDoQuery', function ( &$data ) { $this->fail( 'ContribsPager::reallyDoQuery was not expected to be called' ); } ); $contributionsLookup = $this->getContributionsLookup(); $segment = $contributionsLookup->getContributions( self::$testUser, 2, self::$performer ); // Desc order comes back from db query $this->assertSegmentRevisions( [ 4, 3 ], $segment ); } /** * @covers \MediaWiki\Revision\ContributionsLookup::getContributions() */ public function testGetContributionsFilteredByTag() { $contributionsLookup = $this->getContributionsLookup(); $target = self::$testUser; $segment1 = $contributionsLookup->getContributions( $target, 10, self::$performer, '', self::TAG1 ); $this->assertSegmentRevisions( [ 2 ], $segment1 ); $this->assertTrue( $segment1->isOldest() ); $segment2 = $contributionsLookup->getContributions( $target, 10, self::$performer, '', self::TAG2 ); $this->assertSegmentRevisions( [ 3, 2 ], $segment2 ); $this->assertTrue( $segment1->isOldest() ); $segment0 = $contributionsLookup->getContributions( $target, 10, self::$performer, '', 'not-a-tag' ); $this->assertEmpty( $segment0->getRevisions() ); } /** * @covers \MediaWiki\Revision\ContributionsLookup::getContributions() */ public function testGetSegmentChain() { $contributionsLookup = $this->getContributionsLookup(); $segment1 = $contributionsLookup->getContributions( self::$testUser, 2, self::$performer ); $this->assertInstanceOf( ContributionsSegment::class, $segment1 ); $this->assertCount( 2, $segment1->getRevisions() ); $this->assertNotNull( $segment1->getAfter() ); $this->assertNotNull( $segment1->getBefore() ); $this->assertFalse( $segment1->isOldest() ); $segment2 = $contributionsLookup->getContributions( self::$testUser, 2, self::$performer, $segment1->getBefore() ); $this->assertCount( 2, $segment2->getRevisions() ); $this->assertNotNull( $segment2->getAfter() ); $this->assertNull( $segment2->getBefore() ); $this->assertTrue( $segment2->isOldest() ); $segment3 = $contributionsLookup->getContributions( self::$testUser, 2, self::$performer, $segment2->getAfter() ); $this->assertInstanceOf( ContributionsSegment::class, $segment3 ); $this->assertCount( 2, $segment3->getRevisions() ); $this->assertNotNull( $segment3->getAfter() ); $this->assertNotNull( $segment3->getBefore() ); $this->assertFalse( $segment3->isOldest() ); $this->assertSegmentRevisions( [ 4, 3 ], $segment1 ); $this->assertSegmentRevisions( [ 2, 1 ], $segment2 ); $this->assertSegmentRevisions( [ 4, 3 ], $segment3 ); } /** * @param int[] $expectedRevisions A list of indexes into self::$storedRevisions * @param ContributionsSegment $segmentObject */ private function assertSegmentRevisions( $expectedRevisions, $segmentObject ) { $revisions = $segmentObject->getRevisions(); $this->assertSameSize( $expectedRevisions, $revisions ); foreach ( $expectedRevisions as $idx => $editNumber ) { $expected = self::$storedRevisions[$editNumber]; $actual = $revisions[$idx]; $this->assertSame( $expected->getId(), $actual->getId(), 'rev_id' ); $this->assertSame( $expected->getPageId(), $actual->getPageId(), 'rev_page_id' ); $this->assertTrue( $expected->getPageAsLinkTarget()->isSameLinkAs( $actual->getPageAsLinkTarget() ), 'getPageAsLinkTarget' ); $expectedUser = $expected->getUser( RevisionRecord::RAW )->getName(); $actualUser = $actual->getUser( RevisionRecord::RAW )->getName(); $this->assertSame( $expectedUser, $actualUser ); $expectedTags = self::$storedTags[ $editNumber ]; $this->assertRevisionTags( $expectedTags, $segmentObject, $actual ); $expectedDelta = self::$storedDeltas[$editNumber]; $actualDelta = $segmentObject->getDeltaForRevision( $actual->getId() ); $this->assertSame( $expectedDelta, $actualDelta, 'delta' ); } } public function provideBadSegmentMarker() { yield [ '' ]; yield [ '|' ]; yield [ '0' ]; yield [ '9' ]; yield [ 'x|0' ]; yield [ 'x|9' ]; yield [ 'x|x' ]; } /** * @dataProvider provideBadSegmentMarker */ public function testBadSegmentMarkerReturnsLatestSegment( $segment ) { $contributionsLookup = $this->getContributionsLookup(); $segment = $contributionsLookup->getContributions( self::$testUser, 2, self::$performer, $segment ); $this->assertSegmentRevisions( [ 4, 3 ], $segment ); } /** * @covers \MediaWiki\Revision\ContributionsLookup::getContributionCount() */ public function testGetCountOfContributionsByUserIdentity() { $contributionsLookup = $this->getContributionsLookup(); $count = $contributionsLookup->getContributionCount( self::$testUser, self::$performer ); $this->assertSame( count( self::$storedRevisions ), $count ); } /** * @covers \MediaWiki\Revision\ContributionsLookup::getContributionCount() */ public function testGetCountOfContributionsByUserAndTag() { $contributionsLookup = $this->getContributionsLookup(); $count = $contributionsLookup->getContributionCount( self::$testUser, self::$performer, self::TAG2 ); $this->assertSame( 2, $count ); } public function testPermissionChecksAreApplied() { $editingUser = self::$testUser; $dummyUser = new UserIdentityValue( 0, 'test' ); $contributionsLookup = $this->getContributionsLookup(); $revIds = [ self::$storedRevisions[1]->getId(), self::$storedRevisions[2]->getId() ]; $this->db->update( 'revision', [ 'rev_deleted' => RevisionRecord::DELETED_USER ], [ 'rev_id' => $revIds ], __METHOD__ ); $this->assertSame( 2, $this->db->affectedRows() ); // anons should not see suppressed contribs $segment = $contributionsLookup->getContributions( $editingUser, 10, new SimpleAuthority( $dummyUser, [] ) ); $this->assertSegmentRevisions( [ 4, 3 ], $segment ); $count = $contributionsLookup->getContributionCount( $editingUser, new SimpleAuthority( $dummyUser, [] ) ); $this->assertSame( 2, $count ); // Actor with suppressrevision right should $segment = $contributionsLookup->getContributions( $editingUser, 10, new SimpleAuthority( $dummyUser, [ 'deletedhistory', 'suppressrevision' ] ) ); $this->assertSegmentRevisions( [ 4, 3, 2, 1 ], $segment ); $count = $contributionsLookup->getContributionCount( $editingUser, new SimpleAuthority( $dummyUser, [ 'deletedhistory', 'suppressrevision' ] ) ); $this->assertSame( 4, $count ); } /** * @param array $expectedTags * @param ContributionsSegment $segmentObject * @param RevisionRecord $actual */ private function assertRevisionTags( array $expectedTags, ContributionsSegment $segmentObject, RevisionRecord $actual ): void { $actualTags = $segmentObject->getTagsForRevision( $actual->getId() ); // Tag 3 was disabled and should not be included in results $this->assertArrayNotHasKey( self::TAG3, $actualTags ); foreach ( $actualTags as $tagName => $actualTag ) { $this->assertContains( $tagName, $expectedTags ); $this->assertInstanceOf( Message::class, $actualTag ); $this->assertEquals( "" . self::TAG_DISPLAY . "", $actualTag->parse() ); $this->assertEquals( "''" . self::TAG_DISPLAY . "''", $actualTag->text() ); } } }