wiki.techinc.nl/tests/phpunit/includes/Revision/ContributionsLookupTest.php
2022-12-01 10:45:32 +00:00

412 lines
13 KiB
PHP

<?php
namespace MediaWiki\Tests\Revision;
use ChangeTags;
use MediaWiki\Permissions\Authority;
use MediaWiki\Permissions\SimpleAuthority;
use MediaWiki\Permissions\UltimateAuthority;
use MediaWiki\Revision\ContributionsLookup;
use MediaWiki\Revision\ContributionsSegment;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\User\UserIdentityValue;
use MediaWikiIntegrationTestCase;
use Message;
use Wikimedia\Timestamp\ConvertibleTimestamp;
/**
* @covers \MediaWiki\Revision\ContributionsLookup
*
* @group Database
*/
class ContributionsLookupTest extends MediaWikiIntegrationTestCase {
/**
* @var RevisionRecord[] Associative array mapping revision number (e.g. 1 for first
* revision made) to revision record
*/
private static $storedRevisions;
/**
* @var \User
*/
private static $testUser;
/**
* @var Authority
*/
private static $performer;
/**
* @var int[][]
*/
private static $storedDeltas;
/**
* @var string[][]
*/
private static $storedTags;
private const TAG1 = 'test-ContributionsLookup-1';
private const TAG2 = 'test-ContributionsLookup-2';
private const TAG3 = 'test-ContributionsLookup-3';
private const TAG_DISPLAY = 'ContributionsLookup Tag Display Text';
protected function setUp(): void {
parent::setUp();
// Work around T256006
ChangeTags::$avoidReopeningTablesForTesting = true;
// MessageCache needs to be explicitly enabled to load changetag display text.
$this->getServiceContainer()->getMessageCache()->enable();
}
protected 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 )
->getNewRevision();
self::$storedRevisions[2] = $this->editPage( __METHOD__ . '_2', $revisionText[2], 'test', NS_TALK, $user )
->getNewRevision();
self::$storedRevisions[3] = $this->editPage( __METHOD__ . '_1', $revisionText[3], 'test', NS_MAIN, $user )
->getNewRevision();
self::$storedRevisions[4] = $this->editPage( __METHOD__ . '_2', $revisionText[4], 'test', NS_TALK, $user )
->getNewRevision();
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 )
->getNewRevision();
$rev2 = $this->editPage( __METHOD__ . '_2', 'bar', 'test', NS_TALK, $user )
->getNewRevision();
// 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->assertSame( [], $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( "<i>" . self::TAG_DISPLAY . "</i>", $actualTag->parse() );
$this->assertEquals( "''" . self::TAG_DISPLAY . "''", $actualTag->text() );
}
}
}