wiki.techinc.nl/tests/phpunit/integration/includes/Storage/EditResultBuilderDbTest.php

353 lines
9.2 KiB
PHP
Raw Normal View History

<?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(
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(
$services->getRevisionStore(),
ChangeTags::listSoftwareDefinedTags(),
$options
);
}
}