wiki.techinc.nl/tests/phpunit/unit/includes/Storage/EditResultBuilderTest.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

395 lines
14 KiB
PHP

<?php
namespace MediaWiki\Tests\Storage;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Revision\MutableRevisionRecord;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionStore;
use MediaWiki\Storage\EditResult;
use MediaWiki\Storage\EditResultBuilder;
use MediaWiki\Storage\PageUpdateException;
use MediaWikiUnitTestCase;
use MockTitleTrait;
/**
* @covers \MediaWiki\Storage\EditResultBuilder
* @covers \MediaWiki\Storage\EditResult
* @see EditResultBuilderDbTest for integration tests with the database
*/
class EditResultBuilderTest extends MediaWikiUnitTestCase {
use MockTitleTrait;
/**
* @covers \MediaWiki\Storage\EditResultBuilder::buildEditResult
*/
public function testBuilderThrowsExceptionOnMissingRevision() {
$erb = $this->getNewEditResultBuilder();
$this->expectException( PageUpdateException::class );
$erb->buildEditResult();
}
/**
* @covers \MediaWiki\Storage\EditResultBuilder
*/
public function testIsNewUnset() {
$erb = $this->getNewEditResultBuilder();
$erb->setRevisionRecord( $this->getDummyRevision() );
$er = $erb->buildEditResult();
$this->assertFalse( $er->isNew(), 'EditResult::isNew()' );
}
public function provideSetIsNew() {
return [
'not a new page' => [ false ],
'a new page' => [ true ]
];
}
/**
* @dataProvider provideSetIsNew
* @covers \MediaWiki\Storage\EditResultBuilder
* @param bool $isNew
*/
public function testSetIsNew( bool $isNew ) {
$erb = $this->getNewEditResultBuilder();
$erb->setIsNew( $isNew );
$erb->setRevisionRecord( $this->getDummyRevision() );
$er = $erb->buildEditResult();
$this->assertSame( $isNew, $er->isNew(), 'EditResult::isNew()' );
}
/**
* Tests a normal edit to the page
* @covers \MediaWiki\Storage\EditResult
* @covers \MediaWiki\Storage\EditResultBuilder
*/
public function testEditNotARevert() {
$erb = $this->getNewEditResultBuilder();
$erb->setRevisionRecord( $this->getDummyRevision() );
$er = $erb->buildEditResult();
$this->assertFalse( $er->isNew(), 'EditResult::isNew()' );
$this->assertFalse( $er->isNullEdit(), 'EditResult::isNullEdit()' );
$this->assertFalse( $er->getOriginalRevisionId(),
'EditResult::getOriginalRevisionId()' );
$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->assertSame( 0, $er->getUndidRevId(), 'EditResult::getUndidRevId' );
$this->assertArrayEquals( [], $er->getRevertTags(), 'EditResult::getRevertTags' );
}
/**
* Tests the case when the new edit doesn't actually change anything on the page,
* i.e. is a null edit.
* @covers \MediaWiki\Storage\EditResult
* @covers \MediaWiki\Storage\EditResultBuilder
*/
public function testNullEdit() {
$originalRevision = $this->getExistingRevision();
$erb = $this->getNewEditResultBuilder( $originalRevision );
$newRevision = MutableRevisionRecord::newFromParentRevision( $originalRevision );
$erb->setOriginalRevisionId( $originalRevision->getId() );
$erb->setRevisionRecord( $newRevision );
$er = $erb->buildEditResult();
$this->assertFalse( $er->isNew(), 'EditResult::isNew()' );
$this->assertTrue( $er->isNullEdit(), 'EditResult::isNullEdit()' );
$this->assertSame( $originalRevision->getId(), $er->getOriginalRevisionId(),
'EditResult::getOriginalRevisionId()' );
$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->assertSame( 0, $er->getUndidRevId(), 'EditResult::getUndidRevId' );
$this->assertArrayEquals( [], $er->getRevertTags(), 'EditResult::getRevertTags' );
}
public function provideEnabledSoftwareTagsForRollback(): array {
return [
"all change tags enabled" => [
$this->getSoftwareTags(),
[ "mw-rollback" ]
],
"no change tags enabled" => [
[],
[]
]
];
}
/**
* Test the case where the edit restored the page exactly to a previous state.
*
* @covers \MediaWiki\Storage\EditResult
* @covers \MediaWiki\Storage\EditResultBuilder
* @dataProvider provideEnabledSoftwareTagsForRollback
*
* @param string[] $changeTags
* @param string[] $expectedRevertTags
*/
public function testRollback( array $changeTags, array $expectedRevertTags ) {
$originalRevision = $this->getExistingRevision();
$erb = $this->getNewEditResultBuilder( $originalRevision, $changeTags );
$newRevision = MutableRevisionRecord::newFromParentRevision( $originalRevision );
// We change the parent id to something different, so it's not treated as a null edit
$newRevision->setParentId( 125 );
$erb->setOriginalRevisionId( $originalRevision->getId() );
$erb->setRevisionRecord( $newRevision );
// We are bluffing here, those revision ids don't exist.
// EditResult is as dumb as possible, it doesn't check that.
$erb->markAsRevert( EditResult::REVERT_ROLLBACK, 123, 125 );
$er = $erb->buildEditResult();
$this->assertFalse( $er->isNew(), 'EditResult::isNew()' );
$this->assertFalse( $er->isNullEdit(), 'EditResult::isNullEdit()' );
$this->assertSame( $originalRevision->getId(), $er->getOriginalRevisionId(),
'EditResult::getOriginalRevisionId()' );
$this->assertTrue( $er->isRevert(), 'EditResult::isRevert()' );
$this->assertTrue( $er->isExactRevert(), 'EditResult::isExactRevert()' );
$this->assertSame( EditResult::REVERT_ROLLBACK, $er->getRevertMethod(),
'EditResult::getRevertMethod()' );
$this->assertSame( 123, $er->getOldestRevertedRevisionId(),
'EditResult::getOldestRevertedRevisionId()' );
$this->assertSame( 125, $er->getNewestRevertedRevisionId(),
'EditResult::getNewestRevertedRevisionId()' );
$this->assertSame( 0, $er->getUndidRevId(), 'EditResult::getUndidRevId' );
$this->assertArrayEquals( $expectedRevertTags, $er->getRevertTags(),
'EditResult::getRevertTags' );
}
public function provideEnabledSoftwareTagsForUndo(): array {
return [
"all change tags enabled" => [
$this->getSoftwareTags(),
[ "mw-undo" ]
],
"no change tags enabled" => [
[],
[]
]
];
}
/**
* Test the case where the edit was an undo
*
* @covers \MediaWiki\Storage\EditResult
* @covers \MediaWiki\Storage\EditResultBuilder
* @dataProvider provideEnabledSoftwareTagsForUndo
*
* @param string[] $changeTags
* @param string[] $expectedRevertTags
*/
public function testUndo( array $changeTags, array $expectedRevertTags ) {
$originalRevision = $this->getExistingRevision();
$erb = $this->getNewEditResultBuilder( $originalRevision, $changeTags );
$newRevision = MutableRevisionRecord::newFromParentRevision( $originalRevision );
// We change the parent id to something different, so it's not treated as a null edit
$newRevision->setParentId( 124 );
$erb->setOriginalRevisionId( $originalRevision->getId() );
$erb->setRevisionRecord( $newRevision );
$erb->markAsRevert( EditResult::REVERT_UNDO, 124 );
$er = $erb->buildEditResult();
$this->assertFalse( $er->isNew(), 'EditResult::isNew()' );
$this->assertFalse( $er->isNullEdit(), 'EditResult::isNullEdit()' );
$this->assertSame( $originalRevision->getId(), $er->getOriginalRevisionId(),
'EditResult::getOriginalRevisionId()' );
$this->assertTrue( $er->isRevert(), 'EditResult::isRevert()' );
$this->assertTrue( $er->isExactRevert(), 'EditResult::isExactRevert()' );
$this->assertSame( EditResult::REVERT_UNDO, $er->getRevertMethod(),
'EditResult::getRevertMethod()' );
$this->assertSame( 124, $er->getOldestRevertedRevisionId(),
'EditResult::getOldestRevertedRevisionId()' );
$this->assertSame( 124, $er->getNewestRevertedRevisionId(),
'EditResult::getNewestRevertedRevisionId()' );
$this->assertSame( 124, $er->getUndidRevId(), 'EditResult::getUndidRevId' );
$this->assertArrayEquals( $expectedRevertTags, $er->getRevertTags(),
'EditResult::getRevertTags' );
}
/**
* Test the case where setRevert() is called, but nothing was really reverted
*
* @covers \MediaWiki\Storage\EditResult
* @covers \MediaWiki\Storage\EditResultBuilder
*/
public function testIgnoreEmptyRevert() {
$erb = $this->getNewEditResultBuilder();
$newRevision = $this->getDummyRevision();
$erb->setRevisionRecord( $newRevision );
$erb->markAsRevert( EditResult::REVERT_UNDO, 0 );
$er = $erb->buildEditResult();
$this->assertFalse( $er->isNew(), 'EditResult::isNew()' );
$this->assertFalse( $er->isNullEdit(), 'EditResult::isNullEdit()' );
$this->assertFalse( $er->getOriginalRevisionId(), 'EditResult::getOriginalRevisionId()' );
$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->assertSame( 0, $er->getUndidRevId(), 'EditResult::getUndidRevId' );
$this->assertArrayEquals( [], $er->getRevertTags(), 'EditResult::getRevertTags' );
}
/**
* Test the case where setRevert() is properly called, but the original revision was not set
*
* @covers \MediaWiki\Storage\EditResult
* @covers \MediaWiki\Storage\EditResultBuilder
*/
public function testRevertWithoutOriginalRevision() {
$erb = $this->getNewEditResultBuilder(
null,
$this->getSoftwareTags()
);
$newRevision = $this->getDummyRevision();
$erb->setRevisionRecord( $newRevision );
$erb->markAsRevert( EditResult::REVERT_UNDO, 123 );
$er = $erb->buildEditResult();
$this->assertFalse( $er->isNew(), 'EditResult::isNew()' );
$this->assertFalse( $er->isNullEdit(), 'EditResult::isNullEdit()' );
$this->assertFalse( $er->getOriginalRevisionId(), 'EditResult::getOriginalRevisionId()' );
$this->assertTrue( $er->isRevert(), 'EditResult::isRevert()' );
$this->assertFalse( $er->isExactRevert(), 'EditResult::isExactRevert()' );
$this->assertSame( EditResult::REVERT_UNDO, $er->getRevertMethod(),
'EditResult::getRevertMethod()' );
$this->assertSame( 123, $er->getOldestRevertedRevisionId(),
'EditResult::getOldestRevertedRevisionId()' );
$this->assertSame( 123, $er->getNewestRevertedRevisionId(),
'EditResult::getNewestRevertedRevisionId()' );
$this->assertSame( 123, $er->getUndidRevId(), 'EditResult::getUndidRevId' );
$this->assertArrayEquals( [ 'mw-undo' ], $er->getRevertTags(),
'EditResult::getRevertTags' );
}
/**
* This case satisfies all criteria to be eligible for manual revert detection, but the
* feature is disabled. Any attempt to call the LoadBalancer will fail this test.
*
* @covers \MediaWiki\Storage\EditResultBuilder::buildEditResult
*/
public function testManualRevertDetectionDisabled() {
$erb = $this->getNewEditResultBuilder(
null,
$this->getSoftwareTags(),
0 // set the search radius to 0 to disable the feature entirely
);
$newRevision = $this->getDummyRevision();
$newRevision->setParentId( 125 );
$erb->setRevisionRecord( $newRevision );
$er = $erb->buildEditResult();
$this->assertFalse( $er->getOriginalRevisionId(), 'EditResult::getOriginalRevisionId()' );
$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->assertSame( 0, $er->getUndidRevId(), 'EditResult::getUndidRevId' );
$this->assertArrayEquals( [], $er->getRevertTags(), 'EditResult::getRevertTags' );
}
/**
* Returns an empty RevisionRecord
*
* @return MutableRevisionRecord
*/
private function getDummyRevision(): MutableRevisionRecord {
return new MutableRevisionRecord(
$this->makeMockTitle( 'Dummy' )
);
}
/**
* Returns a RevisionRecord that pretends to have an ID and a page ID.
*
* @return MutableRevisionRecord
*/
private function getExistingRevision(): MutableRevisionRecord {
$revisionRecord = $this->getDummyRevision();
$revisionRecord->setId( 5 );
$revisionRecord->setPageId( 5 );
return $revisionRecord;
}
/**
* Convenience function for creating a new EditResultBuilder object.
*
* @param RevisionRecord|null $originalRevisionRecord RevisionRecord that should be returned
* by RevisionStore::getRevisionById.
* @param string[] $changeTags
* @param int $manualRevertSearchRadius
*
* @return EditResultBuilder
*/
private function getNewEditResultBuilder(
?RevisionRecord $originalRevisionRecord = null,
array $changeTags = [],
int $manualRevertSearchRadius = 15
) {
$store = $this->createMock( RevisionStore::class );
$store->method( 'getRevisionById' )
->willReturn( $originalRevisionRecord );
$options = new ServiceOptions(
EditResultBuilder::CONSTRUCTOR_OPTIONS,
[ 'ManualRevertSearchRadius' => $manualRevertSearchRadius ]
);
return new EditResultBuilder(
$store,
$changeTags,
$options
);
}
/**
* Meant to reproduce the values provided by ChangeTags::getSoftwareTags.
*
* @return string[]
*/
private function getSoftwareTags(): array {
return [
"mw-contentmodelchange",
"mw-new-redirect",
"mw-removed-redirect",
"mw-changed-redirect-target",
"mw-blank",
"mw-replace",
"mw-rollback",
"mw-undo",
"mw-manual-revert",
"mw-add-media",
"mw-remove-media",
];
}
}