wiki.techinc.nl/tests/phpunit/integration/includes/Storage/EditResultBuilderDbTest.php
Petr Pchelko 3158ba5dfb Move EditResultBuilder::findIdenticalRevision to RevStore
Additionally it switches the query from DB_PRIMARY to DB_REPLICA.
I understand the idea with a quick revert, but I do not think
it can be that quick - to revert a newest revision of a page,
an editor or a bot needs to actually read it first, and reads
come from a replica. So we know at least some replicas already
had the latest revision showing to the user. Very likely by the
time revert is made, we'd have it in all replicas. If not - oh well,
we can't be perfect. But we shouldn't really do such a query on
primary - it's too heavy.

Change-Id: I2fae8dbe5f19635f4d99e26242e3b08ddad8f8af
2021-07-25 07:36:31 -07:00

352 lines
9.2 KiB
PHP

<?php
namespace MediaWiki\Tests\Storage;
use ChangeTags;
use CommentStoreComment;
use IDatabase;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\MediaWikiServices;
use MediaWiki\Revision\MutableRevisionRecord;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionStore;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Storage\EditResult;
use MediaWiki\Storage\EditResultBuilder;
use MediaWikiIntegrationTestCase;
use MockTitleTrait;
use WikiPage;
use WikitextContent;
/**
* @covers \MediaWiki\Storage\EditResultBuilder
* @group Database
* @see EditResultBuilderTest for non-DB tests
*/
class EditResultBuilderDbTest extends MediaWikiIntegrationTestCase {
use MockTitleTrait;
private const PAGE_NAME = 'ManualRevertTestPage';
private const CONTENT_A = 'Aaa.';
private const CONTENT_B = 'Bbb.';
private const CONTENT_C = 'Ccc.';
/** @var WikiPage */
private $wikiPage;
/** @var RevisionRecord[] */
private $revisions;
/**
* We track the top revision of the test page on our own to avoid having to update the
* page table in the DB.
*
* @var RevisionRecord
*/
private $latestTestRevision = null;
/** @var RevisionStore */
private $revisionStore;
/** @var IDatabase */
private $dbw;
protected function setUp(): void {
parent::setUp();
$services = MediaWikiServices::getInstance();
$this->revisionStore = $services->getRevisionStore();
$this->dbw = $services->getDBLoadBalancer()->getConnection( DB_PRIMARY );
$this->wikiPage = $this->getExistingTestPage( self::PAGE_NAME );
$this->revisions = [];
$this->revisions['C1'] = $this->insertRevisionToTestPage(
self::CONTENT_C,
'20050101210030'
);
$this->revisions['A1'] = $this->insertRevisionToTestPage(
self::CONTENT_A,
'20050101210037'
);
$this->revisions['B1'] = $this->insertRevisionToTestPage(
self::CONTENT_B,
'20050101210038'
);
$this->revisions['C2'] = $this->insertRevisionToTestPage(
self::CONTENT_C,
'20050101210039'
);
$this->revisions['A2'] = $this->insertRevisionToTestPage(
self::CONTENT_A,
'20050101210040'
);
$this->revisions['A3'] = $this->insertRevisionToTestPage(
self::CONTENT_A,
'20050101210040' // same timestamp to try to confuse the query
);
$this->revisions['A4'] = $this->insertRevisionToTestPage(
self::CONTENT_A,
'20050101210040'
);
$this->revisions['B2'] = $this->insertRevisionToTestPage(
self::CONTENT_B,
'20050101210041'
);
$this->tablesUsed = [
'page',
'revision',
'comment',
'text',
'content'
];
}
private function getLatestTestRevision(): RevisionRecord {
if ( $this->latestTestRevision !== null ) {
return $this->latestTestRevision;
}
return $this->revisionStore->getRevisionByPageId(
$this->wikiPage->getId()
);
}
/**
* Inserts a new revision of the test page to the DB with specified content.
*
* We do not use MediaWikiIntegrationTestCase::editPage() on purpose, it can lead to all
* kinds of issues, the most significant being that it ultimately calls the code we wish
* to test here.
*
* @param string $content
*
* @param string $timestamp
*
* @return RevisionRecord
*/
private function insertRevisionToTestPage(
string $content,
string $timestamp
): RevisionRecord {
$revisionRecord = $this->getNewRevisionForTestPage( $content );
$revisionRecord->setUser( $this->getTestUser()->getUser() );
$revisionRecord->setTimestamp( $timestamp );
$revisionRecord->setComment( CommentStoreComment::newUnsavedComment( '' ) );
$this->latestTestRevision = $this->revisionStore->insertRevisionOn(
$revisionRecord,
$this->dbw
);
return $this->latestTestRevision;
}
/**
* Returns a next in sequence revision of the test page with specified content.
*
* @param string $content
*
* @return MutableRevisionRecord
*/
private function getNewRevisionForTestPage(
string $content
): MutableRevisionRecord {
$parentRevision = $this->getLatestTestRevision();
$revision = new MutableRevisionRecord( $this->wikiPage->getTitle() );
$revision->setParentId( $parentRevision->getId() );
$revision->setPageId( $this->wikiPage->getId() );
$revision->setContent(
SlotRecord::MAIN,
new WikitextContent( $content )
);
return $revision;
}
public function provideManualReverts(): array {
return [
'reverting a single edit' => [
self::CONTENT_A,
'A4',
'B2',
'B2'
],
'reverting multiple edits' => [
self::CONTENT_C,
'C2',
'A2',
'B2'
]
];
}
/**
* @dataProvider provideManualReverts
* @covers \MediaWiki\Storage\EditResultBuilder::detectManualRevert
*
* @param string $content
* @param string $expectedOriginalRevKey
* @param string $expectedOldestRevertedRevKey
* @param string $expectedNewestRevertedRevKey
*/
public function testManualRevert(
string $content,
string $expectedOriginalRevKey,
string $expectedOldestRevertedRevKey,
string $expectedNewestRevertedRevKey
) {
$erb = $this->getEditResultBuilder();
$newRevision = $this->getNewRevisionForTestPage( $content );
// we will fool the EditResultBuilder into thinking this is a saved revision
$newRevision->setId( 12345 );
$erb->setRevisionRecord( $newRevision );
$er = $erb->buildEditResult();
// first some basic tests we can do without revision magic
$this->assertTrue(
$er->isRevert(),
'EditResult::isRevert()'
);
$this->assertTrue(
$er->isExactRevert(),
'EditResult::isExactRevert()'
);
$this->assertSame(
EditResult::REVERT_MANUAL,
$er->getRevertMethod(),
'EditResult::getRevertMethod()'
);
$this->assertNotFalse(
$er->getOriginalRevisionId(),
'EditResult::getOriginalRevisionId()'
);
$this->assertNotNull(
$er->getOldestRevertedRevisionId(),
'EditResult::getOldestRevertedRevisionId()'
);
$this->assertNotNull(
$er->getNewestRevertedRevisionId(),
'EditResult::getNewestRevertedRevisionId()'
);
$this->assertArrayEquals(
[ 'mw-manual-revert' ],
$er->getRevertTags(),
'EditResult::getRevertTags()'
);
// test the original revision referenced by this EditResult
$originalRev = $this->revisionStore->getRevisionById(
$er->getOriginalRevisionId()
);
$this->assertSame(
$newRevision->getSha1(),
$originalRev->getSha1(),
"original revision's SHA1 matches new revision's SHA1"
);
$expectedOriginalRev = $this->revisions[$expectedOriginalRevKey];
$this->assertSame(
$expectedOriginalRev->getId(),
$originalRev->getId(),
"original revision's ID"
);
// test the oldest reverted revision
$oldestRevertedRev = $this->revisionStore->getRevisionById(
$er->getOldestRevertedRevisionId()
);
$expectedOldestRevertedRev = $this->revisions[$expectedOldestRevertedRevKey];
$this->assertSame(
$expectedOldestRevertedRev->getId(),
$oldestRevertedRev->getId(),
"oldest reverted revision's ID"
);
// test the newest reverted revision
$newestRevertedRev = $this->revisionStore->getRevisionById(
$er->getnewestRevertedRevisionId()
);
$expectedNewestRevertedRev = $this->revisions[$expectedNewestRevertedRevKey];
$this->assertSame(
$expectedNewestRevertedRev->getId(),
$newestRevertedRev->getId(),
"newest reverted revision's ID"
);
}
public function provideNotManualReverts(): array {
return [
'edit not changing anything' => [
self::CONTENT_B,
15
],
'revert outside search radius' => [
self::CONTENT_C,
3
],
'normal edit' => [
'Some text.',
15
]
];
}
/**
* @dataProvider provideNotManualReverts
* @covers \MediaWiki\Storage\EditResultBuilder::detectManualRevert
*
* @param string $content
* @param int $searchRadius
*/
public function testNotManualRevert(
string $content,
int $searchRadius
) {
$erb = $this->getEditResultBuilder( $searchRadius );
$parentRevision = $this->getLatestTestRevision();
$newRevision = $this->getNewRevisionForTestPage( $content );
// we will fool the EditResultBuilder into thinking this is a saved revision
$newRevision->setId( 12345 );
$erb->setRevisionRecord( $newRevision );
// emulate WikiPage's behaviour for null edits
if ( $newRevision->getSha1() === $parentRevision->getSha1() ) {
$erb->setOriginalRevisionId( $parentRevision->getId() );
}
$er = $erb->buildEditResult();
$this->assertFalse( $er->isRevert(), 'EditResult::isRevert()' );
$this->assertFalse( $er->isExactRevert(), 'EditResult::isExactRevert()' );
$this->assertNull( $er->getRevertMethod(), 'EditResult::getRevertMethod()' );
$this->assertNull(
$er->getOldestRevertedRevisionId(),
'EditResult::getOldestRevertedRevisionId()'
);
$this->assertNull(
$er->getNewestRevertedRevisionId(),
'EditResult::getNewestRevertedRevisionId()'
);
$this->assertArrayEquals( [], $er->getRevertTags(), 'EditResult::getRevertTags()' );
}
/**
* Convenience function for creating a new EditResultBuilder object.
*
* @param int $manualRevertSearchRadius
*
* @return EditResultBuilder
*/
private function getEditResultBuilder( int $manualRevertSearchRadius = 15 ) {
$services = MediaWikiServices::getInstance();
$options = new ServiceOptions(
EditResultBuilder::CONSTRUCTOR_OPTIONS,
[ 'ManualRevertSearchRadius' => $manualRevertSearchRadius ]
);
return new EditResultBuilder(
$services->getRevisionStore(),
ChangeTags::listSoftwareDefinedTags(),
$options
);
}
}