wiki.techinc.nl/tests/phpunit/includes/Storage/PageUpdaterTest.php
Brad Jorsch dff469a408 Re-namespace RevisionStore and RevisionRecord classes
During development a lot of classes were placed in MediaWiki\Storage\.
The precedent set would mean that every class relating to something
stored in a database table, plus all related value classes and such,
would go into that namespace.

Let's put them into MediaWiki\Revision\ instead. Then future classes
related to the 'page' table can go into MediaWiki\Page\, future classes
related to the 'user' table can go into MediaWiki\User\, and so on.

Note I didn't move DerivedPageDataUpdater, PageUpdateException,
PageUpdater, or RevisionSlotsUpdate in this patch. If these are kept
long-term, they probably belong in MediaWiki\Page\ or MediaWiki\Edit\
instead.

Bug: T204158
Change-Id: I16bea8927566a3c73c07e4f4afb3537e05aa04a5
2018-10-09 10:22:48 -04:00

581 lines
21 KiB
PHP

<?php
namespace MediaWiki\Tests\Storage;
use CommentStoreComment;
use Content;
use MediaWiki\MediaWikiServices;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\SlotRecord;
use MediaWikiTestCase;
use ParserOptions;
use RecentChange;
use Revision;
use TextContent;
use Title;
use User;
use WikiPage;
/**
* @covers \MediaWiki\Storage\PageUpdater
* @group Database
*/
class PageUpdaterTest extends MediaWikiTestCase {
private function getDummyTitle( $method ) {
return Title::newFromText( $method, $this->getDefaultWikitextNS() );
}
/**
* @param int $revId
*
* @return null|RecentChange
*/
private function getRecentChangeFor( $revId ) {
$qi = RecentChange::getQueryInfo();
$row = $this->db->selectRow(
$qi['tables'],
$qi['fields'],
[ 'rc_this_oldid' => $revId ],
__METHOD__,
[],
$qi['joins']
);
return $row ? RecentChange::newFromRow( $row ) : null;
}
// TODO: test setAjaxEditStash();
/**
* @covers \MediaWiki\Storage\PageUpdater::saveRevision()
* @covers \WikiPage::newPageUpdater()
*/
public function testCreatePage() {
$user = $this->getTestUser()->getUser();
$title = $this->getDummyTitle( __METHOD__ );
$page = WikiPage::factory( $title );
$updater = $page->newPageUpdater( $user );
$oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' );
$this->assertFalse( $updater->wasCommitted(), 'wasCommitted' );
$this->assertFalse( $updater->getOriginalRevisionId(), 'getOriginalRevisionId' );
$this->assertSame( 0, $updater->getUndidRevisionId(), 'getUndidRevisionId' );
$updater->addTag( 'foo' );
$updater->addTags( [ 'bar', 'qux' ] );
$tags = $updater->getExplicitTags();
sort( $tags );
$this->assertSame( [ 'bar', 'foo', 'qux' ], $tags, 'getExplicitTags' );
// TODO: MCR: test additional slots
$content = new TextContent( 'Lorem Ipsum' );
$updater->setContent( SlotRecord::MAIN, $content );
$parent = $updater->grabParentRevision();
$this->assertNull( $parent, 'getParentRevision' );
$this->assertFalse( $updater->wasCommitted(), 'wasCommitted' );
// TODO: test that hasEditConflict() grabs the parent revision
$this->assertFalse( $updater->hasEditConflict( 0 ), 'hasEditConflict' );
$this->assertTrue( $updater->hasEditConflict( 1 ), 'hasEditConflict' );
// TODO: test failure with EDIT_UPDATE
// TODO: test EDIT_MINOR, EDIT_BOT, etc
$summary = CommentStoreComment::newUnsavedComment( 'Just a test' );
$rev = $updater->saveRevision( $summary );
$this->assertNotNull( $rev );
$this->assertSame( 0, $rev->getParentId() );
$this->assertSame( $summary->text, $rev->getComment( RevisionRecord::RAW )->text );
$this->assertSame( $user->getName(), $rev->getUser( RevisionRecord::RAW )->getName() );
$this->assertTrue( $updater->wasCommitted(), 'wasCommitted()' );
$this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
$this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' );
$this->assertTrue( $updater->isNew(), 'isNew()' );
$this->assertFalse( $updater->isUnchanged(), 'isUnchanged()' );
$this->assertNotNull( $updater->getNewRevision(), 'getNewRevision()' );
$this->assertInstanceOf( Revision::class, $updater->getStatus()->value['revision'] );
$rev = $updater->getNewRevision();
$revContent = $rev->getContent( SlotRecord::MAIN );
$this->assertSame( 'Lorem Ipsum', $revContent->serialize(), 'revision content' );
// were the WikiPage and Title objects updated?
$this->assertTrue( $page->exists(), 'WikiPage::exists()' );
$this->assertTrue( $title->exists(), 'Title::exists()' );
$this->assertSame( $rev->getId(), $page->getLatest(), 'WikiPage::getRevision()' );
$this->assertNotNull( $page->getRevision(), 'WikiPage::getRevision()' );
// re-load
$page2 = WikiPage::factory( $title );
$this->assertTrue( $page2->exists(), 'WikiPage::exists()' );
$this->assertSame( $rev->getId(), $page2->getLatest(), 'WikiPage::getRevision()' );
$this->assertNotNull( $page2->getRevision(), 'WikiPage::getRevision()' );
// Check RC entry
$rc = $this->getRecentChangeFor( $rev->getId() );
$this->assertNotNull( $rc, 'RecentChange' );
// check site stats - this asserts that derived data updates where run.
$stats = $this->db->selectRow( 'site_stats', '*', '1=1' );
$this->assertSame( $oldStats->ss_total_pages + 1, (int)$stats->ss_total_pages );
$this->assertSame( $oldStats->ss_total_edits + 1, (int)$stats->ss_total_edits );
// re-edit with same content - should be a "null-edit"
$updater = $page->newPageUpdater( $user );
$updater->setContent( SlotRecord::MAIN, $content );
$summary = CommentStoreComment::newUnsavedComment( 'to to re-edit' );
$rev = $updater->saveRevision( $summary );
$status = $updater->getStatus();
$this->assertNull( $rev, 'getNewRevision()' );
$this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
$this->assertTrue( $updater->isUnchanged(), 'isUnchanged' );
$this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
$this->assertTrue( $status->isOK(), 'getStatus()->isOK()' );
$this->assertTrue( $status->hasMessage( 'edit-no-change' ), 'edit-no-change' );
}
/**
* @covers \MediaWiki\Storage\PageUpdater::saveRevision()
* @covers \WikiPage::newPageUpdater()
*/
public function testUpdatePage() {
$user = $this->getTestUser()->getUser();
$title = $this->getDummyTitle( __METHOD__ );
$this->insertPage( $title );
$page = WikiPage::factory( $title );
$parentId = $page->getLatest();
$updater = $page->newPageUpdater( $user );
$oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' );
$updater->setOriginalRevisionId( 7 );
$this->assertSame( 7, $updater->getOriginalRevisionId(), 'getOriginalRevisionId' );
$this->assertFalse( $updater->hasEditConflict( $parentId ), 'hasEditConflict' );
$this->assertTrue( $updater->hasEditConflict( $parentId - 1 ), 'hasEditConflict' );
$this->assertTrue( $updater->hasEditConflict( 0 ), 'hasEditConflict' );
// TODO: MCR: test additional slots
$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
// TODO: test all flags for saveRevision()!
$summary = CommentStoreComment::newUnsavedComment( 'Just a test' );
$rev = $updater->saveRevision( $summary );
$this->assertNotNull( $rev );
$this->assertSame( $parentId, $rev->getParentId() );
$this->assertSame( $summary->text, $rev->getComment( RevisionRecord::RAW )->text );
$this->assertSame( $user->getName(), $rev->getUser( RevisionRecord::RAW )->getName() );
$this->assertTrue( $updater->wasCommitted(), 'wasCommitted()' );
$this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
$this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' );
$this->assertFalse( $updater->isNew(), 'isNew()' );
$this->assertNotNull( $updater->getNewRevision(), 'getNewRevision()' );
$this->assertInstanceOf( Revision::class, $updater->getStatus()->value['revision'] );
$this->assertFalse( $updater->isUnchanged(), 'isUnchanged()' );
// TODO: Test null revision (with different user): new revision!
$rev = $updater->getNewRevision();
$revContent = $rev->getContent( SlotRecord::MAIN );
$this->assertSame( 'Lorem Ipsum', $revContent->serialize(), 'revision content' );
// were the WikiPage and Title objects updated?
$this->assertTrue( $page->exists(), 'WikiPage::exists()' );
$this->assertTrue( $title->exists(), 'Title::exists()' );
$this->assertSame( $rev->getId(), $page->getLatest(), 'WikiPage::getRevision()' );
$this->assertNotNull( $page->getRevision(), 'WikiPage::getRevision()' );
// re-load
$page2 = WikiPage::factory( $title );
$this->assertTrue( $page2->exists(), 'WikiPage::exists()' );
$this->assertSame( $rev->getId(), $page2->getLatest(), 'WikiPage::getRevision()' );
$this->assertNotNull( $page2->getRevision(), 'WikiPage::getRevision()' );
// Check RC entry
$rc = $this->getRecentChangeFor( $rev->getId() );
$this->assertNotNull( $rc, 'RecentChange' );
// re-edit
$updater = $page->newPageUpdater( $user );
$updater->setContent( SlotRecord::MAIN, new TextContent( 'dolor sit amet' ) );
$summary = CommentStoreComment::newUnsavedComment( 're-edit' );
$updater->saveRevision( $summary );
$this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
$this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' );
// check site stats - this asserts that derived data updates where run.
$stats = $this->db->selectRow( 'site_stats', '*', '1=1' );
$this->assertNotNull( $stats, 'site_stats' );
$this->assertSame( $oldStats->ss_total_pages + 0, (int)$stats->ss_total_pages );
$this->assertSame( $oldStats->ss_total_edits + 2, (int)$stats->ss_total_edits );
}
/**
* Creates a revision in the database.
*
* @param WikiPage $page
* @param $summary
* @param null|string|Content $content
*
* @return RevisionRecord|null
*/
private function createRevision( WikiPage $page, $summary, $content = null ) {
$user = $this->getTestUser()->getUser();
$comment = CommentStoreComment::newUnsavedComment( $summary );
if ( !$content instanceof Content ) {
$content = new TextContent( $content ?? $summary );
}
$updater = $page->newPageUpdater( $user );
$updater->setContent( SlotRecord::MAIN, $content );
$rev = $updater->saveRevision( $comment );
return $rev;
}
/**
* @covers \MediaWiki\Storage\PageUpdater::grabParentRevision()
* @covers \MediaWiki\Storage\PageUpdater::saveRevision()
*/
public function testCompareAndSwapFailure() {
$user = $this->getTestUser()->getUser();
$title = $this->getDummyTitle( __METHOD__ );
// start editing non-existing page
$page = WikiPage::factory( $title );
$updater = $page->newPageUpdater( $user );
$updater->grabParentRevision();
// create page concurrently
$concurrentPage = WikiPage::factory( $title );
$this->createRevision( $concurrentPage, __METHOD__ . '-one' );
// try creating the page - should trigger CAS failure.
$summary = CommentStoreComment::newUnsavedComment( 'create?!' );
$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) );
$updater->saveRevision( $summary );
$status = $updater->getStatus();
$this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
$this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
$this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
$this->assertTrue( $status->hasMessage( 'edit-already-exists' ), 'edit-conflict' );
// start editing existing page
$page = WikiPage::factory( $title );
$updater = $page->newPageUpdater( $user );
$updater->grabParentRevision();
// update page concurrently
$concurrentPage = WikiPage::factory( $title );
$this->createRevision( $concurrentPage, __METHOD__ . '-two' );
// try creating the page - should trigger CAS failure.
$summary = CommentStoreComment::newUnsavedComment( 'edit?!' );
$updater->setContent( SlotRecord::MAIN, new TextContent( 'dolor sit amet' ) );
$updater->saveRevision( $summary );
$status = $updater->getStatus();
$this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
$this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
$this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
$this->assertTrue( $status->hasMessage( 'edit-conflict' ), 'edit-conflict' );
}
/**
* @covers \MediaWiki\Storage\PageUpdater::saveRevision()
*/
public function testFailureOnEditFlags() {
$user = $this->getTestUser()->getUser();
$title = $this->getDummyTitle( __METHOD__ );
// start editing non-existing page
$page = WikiPage::factory( $title );
$updater = $page->newPageUpdater( $user );
// update with EDIT_UPDATE flag should fail
$summary = CommentStoreComment::newUnsavedComment( 'udpate?!' );
$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) );
$updater->saveRevision( $summary, EDIT_UPDATE );
$status = $updater->getStatus();
$this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
$this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
$this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
$this->assertTrue( $status->hasMessage( 'edit-gone-missing' ), 'edit-gone-missing' );
// create the page
$this->createRevision( $page, __METHOD__ );
// update with EDIT_NEW flag should fail
$summary = CommentStoreComment::newUnsavedComment( 'create?!' );
$updater = $page->newPageUpdater( $user );
$updater->setContent( SlotRecord::MAIN, new TextContent( 'dolor sit amet' ) );
$updater->saveRevision( $summary, EDIT_NEW );
$status = $updater->getStatus();
$this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
$this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
$this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
$this->assertTrue( $status->hasMessage( 'edit-already-exists' ), 'edit-already-exists' );
}
public function provideSetRcPatrolStatus( $patrolled ) {
yield [ RecentChange::PRC_UNPATROLLED ];
yield [ RecentChange::PRC_AUTOPATROLLED ];
}
/**
* @dataProvider provideSetRcPatrolStatus
* @covers \MediaWiki\Storage\PageUpdater::setRcPatrolStatus()
*/
public function testSetRcPatrolStatus( $patrolled ) {
$revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
$user = $this->getTestUser()->getUser();
$title = $this->getDummyTitle( __METHOD__ );
$page = WikiPage::factory( $title );
$updater = $page->newPageUpdater( $user );
$summary = CommentStoreComment::newUnsavedComment( 'Lorem ipsum ' . $patrolled );
$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum ' . $patrolled ) );
$updater->setRcPatrolStatus( $patrolled );
$rev = $updater->saveRevision( $summary );
$rc = $revisionStore->getRecentChange( $rev );
$this->assertEquals( $patrolled, $rc->getAttribute( 'rc_patrolled' ) );
}
/**
* @covers \MediaWiki\Storage\PageUpdater::inheritSlot()
* @covers \MediaWiki\Storage\PageUpdater::setContent()
*/
public function testInheritSlot() {
$user = $this->getTestUser()->getUser();
$title = $this->getDummyTitle( __METHOD__ );
$page = WikiPage::factory( $title );
$updater = $page->newPageUpdater( $user );
$summary = CommentStoreComment::newUnsavedComment( 'one' );
$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) );
$rev1 = $updater->saveRevision( $summary, EDIT_NEW );
$updater = $page->newPageUpdater( $user );
$summary = CommentStoreComment::newUnsavedComment( 'two' );
$updater->setContent( SlotRecord::MAIN, new TextContent( 'Foo Bar' ) );
$rev2 = $updater->saveRevision( $summary, EDIT_UPDATE );
$updater = $page->newPageUpdater( $user );
$summary = CommentStoreComment::newUnsavedComment( 'three' );
$updater->inheritSlot( $rev1->getSlot( SlotRecord::MAIN ) );
$rev3 = $updater->saveRevision( $summary, EDIT_UPDATE );
$this->assertNotSame( $rev1->getId(), $rev3->getId() );
$this->assertNotSame( $rev2->getId(), $rev3->getId() );
$main1 = $rev1->getSlot( SlotRecord::MAIN );
$main3 = $rev3->getSlot( SlotRecord::MAIN );
$this->assertNotSame( $main1->getRevision(), $main3->getRevision() );
$this->assertSame( $main1->getAddress(), $main3->getAddress() );
$this->assertTrue( $main1->getContent()->equals( $main3->getContent() ) );
}
// TODO: MCR: test adding multiple slots, inheriting parent slots, and removing slots.
public function testSetUseAutomaticEditSummaries() {
$this->setContentLang( 'qqx' );
$user = $this->getTestUser()->getUser();
$title = $this->getDummyTitle( __METHOD__ );
$page = WikiPage::factory( $title );
$updater = $page->newPageUpdater( $user );
$updater->setUseAutomaticEditSummaries( true );
$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
// empty comment triggers auto-summary
$summary = CommentStoreComment::newUnsavedComment( '' );
$updater->saveRevision( $summary, EDIT_AUTOSUMMARY );
$rev = $updater->getNewRevision();
$comment = $rev->getComment( RevisionRecord::RAW );
$this->assertSame( '(autosumm-new: Lorem Ipsum)', $comment->text, 'comment text' );
// check that this also works when blanking the page
$updater = $page->newPageUpdater( $user );
$updater->setUseAutomaticEditSummaries( true );
$updater->setContent( SlotRecord::MAIN, new TextContent( '' ) );
$summary = CommentStoreComment::newUnsavedComment( '' );
$updater->saveRevision( $summary, EDIT_AUTOSUMMARY );
$rev = $updater->getNewRevision();
$comment = $rev->getComment( RevisionRecord::RAW );
$this->assertSame( '(autosumm-blank)', $comment->text, 'comment text' );
// check that we can also disable edit-summaries
$title2 = $this->getDummyTitle( __METHOD__ . '/2' );
$page2 = WikiPage::factory( $title2 );
$updater = $page2->newPageUpdater( $user );
$updater->setUseAutomaticEditSummaries( false );
$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
$summary = CommentStoreComment::newUnsavedComment( '' );
$updater->saveRevision( $summary, EDIT_AUTOSUMMARY );
$rev = $updater->getNewRevision();
$comment = $rev->getComment( RevisionRecord::RAW );
$this->assertSame( '', $comment->text, 'comment text should still be lank' );
// check that we don't do auto.summaries without the EDIT_AUTOSUMMARY flag
$updater = $page2->newPageUpdater( $user );
$updater->setUseAutomaticEditSummaries( true );
$updater->setContent( SlotRecord::MAIN, new TextContent( '' ) );
$summary = CommentStoreComment::newUnsavedComment( '' );
$updater->saveRevision( $summary, 0 );
$rev = $updater->getNewRevision();
$comment = $rev->getComment( RevisionRecord::RAW );
$this->assertSame( '', $comment->text, 'comment text' );
}
public function provideSetUsePageCreationLog() {
yield [ true, [ [ 'create', 'create' ] ] ];
yield [ false, [] ];
}
/**
* @dataProvider provideSetUsePageCreationLog
* @param bool $use
*/
public function testSetUsePageCreationLog( $use, $expected ) {
$user = $this->getTestUser()->getUser();
$title = $this->getDummyTitle( __METHOD__ . ( $use ? '_logged' : '_unlogged' ) );
$page = WikiPage::factory( $title );
$updater = $page->newPageUpdater( $user );
$updater->setUsePageCreationLog( $use );
$summary = CommentStoreComment::newUnsavedComment( 'cmt' );
$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
$updater->saveRevision( $summary, EDIT_NEW );
$rev = $updater->getNewRevision();
$this->assertSelect(
'logging',
[ 'log_type', 'log_action' ],
[ 'log_page' => $rev->getPageId() ],
$expected
);
}
public function provideMagicWords() {
yield 'PAGEID' => [
'Test {{PAGEID}} Test',
function ( RevisionRecord $rev ) {
return $rev->getPageId();
}
];
yield 'REVISIONID' => [
'Test {{REVISIONID}} Test',
function ( RevisionRecord $rev ) {
return $rev->getId();
}
];
yield 'REVISIONUSER' => [
'Test {{REVISIONUSER}} Test',
function ( RevisionRecord $rev ) {
return $rev->getUser()->getName();
}
];
yield 'REVISIONTIMESTAMP' => [
'Test {{REVISIONTIMESTAMP}} Test',
function ( RevisionRecord $rev ) {
return $rev->getTimestamp();
}
];
yield 'subst:REVISIONUSER' => [
'Test {{subst:REVISIONUSER}} Test',
function ( RevisionRecord $rev ) {
return $rev->getUser()->getName();
},
'subst'
];
yield 'subst:PAGENAME' => [
'Test {{subst:PAGENAME}} Test',
function ( RevisionRecord $rev ) {
return 'PageUpdaterTest::testMagicWords';
},
'subst'
];
}
/**
* @covers \MediaWiki\Storage\PageUpdater::saveRevision()
*
* Integration test for PageUpdater, DerivedPageDataUpdater, RevisionRenderer
* and RenderedRevision, that ensures that magic words depending on revision meta-data
* are handled correctly. Note that each magic word needs to be tested separately,
* to assert correct behavior for each "vary" flag in the ParserOutput.
*
* @dataProvider provideMagicWords
*/
public function testMagicWords( $wikitext, $callback, $subst = false ) {
$user = User::newFromName( 'A user for ' . __METHOD__ );
$user->addToDatabase();
$title = $this->getDummyTitle( __METHOD__ . '-' . $this->getName() );
$this->insertPage( $title );
$page = WikiPage::factory( $title );
$updater = $page->newPageUpdater( $user );
$updater->setContent( SlotRecord::MAIN, new \WikitextContent( $wikitext ) );
$summary = CommentStoreComment::newUnsavedComment( 'Just a test' );
$rev = $updater->saveRevision( $summary, EDIT_UPDATE );
if ( !$rev ) {
$this->fail( $updater->getStatus()->getWikiText() );
}
$expected = strval( $callback( $rev ) );
$output = $page->getParserOutput( ParserOptions::newCanonical( 'canonical' ) );
$html = $output->getText();
$text = $rev->getContent( SlotRecord::MAIN )->serialize();
if ( $subst ) {
$this->assertContains( $expected, $text, 'In Wikitext' );
}
$this->assertContains( $expected, $html, 'In HTML' );
}
}