wiki.techinc.nl/tests/phpunit/unit/includes/Storage/EditResultBuilderTest.php

396 lines
14 KiB
PHP
Raw Normal View History

<?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(
Add mw-reverted change tag The tag is added to reverted edits as described in T254074. Functionality: * Adding the mw-reverted tag to reverted edits (duh) * Limiting the maximum depth of the update through a config variable (mitigation #2 from T259014). * Only applying the reverted tag after the edit has been somehow approved. Only the patrol subsystem currently implements this, but there's a hook that extensions can use (mitigation #4 from T259014, more explanation in T259103). * When performing the delayed update, it is checked whether the reverted edit was reverted itself. If so, the update is ignored. This is probably the only way to make the feature work due to the lack of an explicit "disapproval" mechanism other than reverting. * The update is also ignored if the revert is marked as deleted. Technical design: * The update code is in RevertedTagUpdate.php, which is a deferrable update, but is not used as such. It's separated to allow for better DI, testing and better code reusability in the future. * The update is queued / ran using the Job subsystem. The relevant job is in RevertedTagUpdateJob.php * PageUpdater determines whether the edit is approved or not and passes that to the DerivedPageDataUpdater. * The BeforeRevertedTagUpdate hook lets extensions decide whether the update should be ran right away or await approval. * DerivedPageDataUpdater checks whether the edit is a revert and if so either enqueues the job (if it's auto-approved) or caches the EditResult for later use (if it needs approval). * RevertedTagUpdateManager allows for easy re-enqueueing of the update for extensions. Thus, it has a very minimal interface. Other notes: * The unit testing setup for RevertedTagUpdate is a bit complicated, but it was the only way I could make this class testable while using the static ChangeTags class. Bug: T254074 Depends-On: I86d0e660f0acd51a7351396c5c82a400d3963b94 Change-Id: I70d5b29fec6b6058613f7ac2fb49f9fad9dc8da4
2020-07-06 11:47:22 +00:00
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",
];
}
}