From: daniel Date: Mon, 14 May 2018 11:57:43 +0000 (+0200) Subject: Introduce per-schema unit tests for revision storage. X-Git-Tag: 1.34.0-rc.0~5164^2 X-Git-Url: http://git.cyclocoop.org/%22%20.%20generer_url_ecrire%28%22auteur_infos%22%2C%22id_auteur=%24connect_id_auteur%22%29%20.%20%22?a=commitdiff_plain;h=32e9266d8dccbb5e6f4ff8f208658eae1e1b4d7a;p=lhc%2Fweb%2Fwiklou.git Introduce per-schema unit tests for revision storage. This introduces traits for testing different schema variations. These are not very useful in this patch, but make it much easier to add tests for MCR schema migration in subsequent patches. The code in this patch was previously part of If259b1e1c49ceaa4. Change-Id: I239572f75bebbc9c731a3e3860c4eff179dc15e4 --- diff --git a/autoload.php b/autoload.php index 9981cabe01..867430b4af 100644 --- a/autoload.php +++ b/autoload.php @@ -889,6 +889,7 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Auth\\Throttler' => __DIR__ . '/includes/auth/Throttler.php', 'MediaWiki\\Auth\\UserDataAuthenticationRequest' => __DIR__ . '/includes/auth/UserDataAuthenticationRequest.php', 'MediaWiki\\Auth\\UsernameAuthenticationRequest' => __DIR__ . '/includes/auth/UsernameAuthenticationRequest.php', + 'MediaWiki\\DB\\PatchFileLocation' => __DIR__ . '/includes/db/PatchFileLocation.php', 'MediaWiki\\Diff\\ComplexityException' => __DIR__ . '/includes/diff/ComplexityException.php', 'MediaWiki\\Diff\\WordAccumulator' => __DIR__ . '/includes/diff/WordAccumulator.php', 'MediaWiki\\HeaderCallback' => __DIR__ . '/includes/HeaderCallback.php', diff --git a/includes/db/PatchFileLocation.php b/includes/db/PatchFileLocation.php new file mode 100644 index 0000000000..013724c343 --- /dev/null +++ b/includes/db/PatchFileLocation.php @@ -0,0 +1,90 @@ +getType(); + + if ( $patchDir === null ) { + $patchDir = $GLOBALS['IP'] . '/maintenance'; + } + + $paths = [ + + // For a small number of patch files, closely associated with code, + // e.g. for unit tests: + "$patchDir/$name.$dbType.sql", + + // For a large number of patch files, e.g. for schema updates of extensions: + "$patchDir/$dbType/$name.sql", + + // For MediaWiki core schema update patches: + "$patchDir/$dbType/archives/$name.sql", + + // Database-agnostic fallback: + "$patchDir/$name.sql", + + // Database-agnostic fallback for MediaWiki core schema update patches: + "$patchDir/archives/$name.sql" + ]; + + foreach ( $paths as $p ) { + if ( file_exists( $p ) ) { + return $p; + } + } + + throw new RuntimeException( "No SQL script matching $name could be found in $patchDir" ); + } + +} diff --git a/tests/common/TestsAutoLoader.php b/tests/common/TestsAutoLoader.php index abf718d07e..a79867913e 100644 --- a/tests/common/TestsAutoLoader.php +++ b/tests/common/TestsAutoLoader.php @@ -149,8 +149,11 @@ $wgAutoloadClasses += [ 'SpecialPageExecutor' => "$testDir/phpunit/includes/specials/SpecialPageExecutor.php", # tests/phpunit/includes/Storage + 'MediaWiki\Tests\Storage\McrSchemaDetection' => "$testDir/phpunit/includes/Storage/McrSchemaDetection.php", 'MediaWiki\Tests\Storage\RevisionSlotsTest' => "$testDir/phpunit/includes/Storage/RevisionSlotsTest.php", 'MediaWiki\Tests\Storage\RevisionRecordTests' => "$testDir/phpunit/includes/Storage/RevisionRecordTests.php", + 'MediaWiki\Tests\Storage\RevisionStoreDbTestBase' => "$testDir/phpunit/includes/Storage/RevisionStoreDbTestBase.php", + 'MediaWiki\Tests\Storage\PreMcrSchemaOverride' => "$testDir/phpunit/includes/Storage/PreMcrSchemaOverride.php", # tests/phpunit/languages 'LanguageClassesTestCase' => "$testDir/phpunit/languages/LanguageClassesTestCase.php", diff --git a/tests/phpunit/includes/RevisionContentHandlerDbTest.php b/tests/phpunit/includes/RevisionContentHandlerDbTest.php deleted file mode 100644 index fa0153d3e0..0000000000 --- a/tests/phpunit/includes/RevisionContentHandlerDbTest.php +++ /dev/null @@ -1,14 +0,0 @@ -tablesUsed += $this->getMcrTablesToReset(); + parent::setUp(); $this->mergeMwGlobalArrayValue( @@ -72,11 +84,17 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase { ); $this->setMwGlobals( 'wgContentHandlerUseDB', $this->getContentHandlerUseDB() ); + $this->setMwGlobals( + 'wgMultiContentRevisionSchemaMigrationStage', + $this->getMcrMigrationStage() + ); MWNamespace::clearCaches(); // Reset namespace cache $wgContLang->resetNamespaces(); + $this->overrideMwServices(); + if ( !$this->testPage ) { /** * We have to create a new page for each subclass as the page creation may result @@ -1346,6 +1364,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase { */ public function testNewKnownCurrent() { // Setup the services + $this->resetGlobalServices(); $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ); $this->setService( 'MainWANObjectCache', $cache ); $db = wfGetDB( DB_MASTER ); diff --git a/tests/phpunit/includes/RevisionNoContentHandlerDbTest.php b/tests/phpunit/includes/RevisionNoContentHandlerDbTest.php deleted file mode 100644 index c980a487f7..0000000000 --- a/tests/phpunit/includes/RevisionNoContentHandlerDbTest.php +++ /dev/null @@ -1,14 +0,0 @@ -tableExists( 'slots', __METHOD__ ); + } + + /** + * Returns true if pre-MCR fields still exist in the database. + * If yes, the database is compatible with with MIGRATION_OLD mode. + * If hasMcrTables() also returns true, the database supports MIGRATION_WRITE_BOTH mode. + * + * Note that if the database has been updated in MIGRATION_NEW mode, + * the rev_text_id field will be 0 for new revisions. This means that + * in MIGRATION_OLD mode, reading such revisions will fail, even though + * all the necessary fields exist. + * This is not relevant for unit tests, since unit tests reset the database content anyway. + * + * @param IDatabase $db + * @return bool + */ + protected function hasPreMcrFields( IDatabase $db ) { + return $db->fieldExists( 'revision', 'rev_content_model', __METHOD__ ); + } + +} diff --git a/tests/phpunit/includes/Storage/NoContentModelRevisionStoreDbTest.php b/tests/phpunit/includes/Storage/NoContentModelRevisionStoreDbTest.php new file mode 100644 index 0000000000..c77a94a2c9 --- /dev/null +++ b/tests/phpunit/includes/Storage/NoContentModelRevisionStoreDbTest.php @@ -0,0 +1,114 @@ + [ 'archive' ], + 'fields' => array_merge( + $this->getDefaultArchiveFields(), + [ + 'ar_comment_text' => 'ar_comment', + 'ar_comment_data' => 'NULL', + 'ar_comment_cid' => 'NULL', + 'ar_user_text' => 'ar_user_text', + 'ar_user' => 'ar_user', + 'ar_actor' => 'NULL', + ] + ), + 'joins' => [], + ] + ]; + } + + public function provideGetQueryInfo() { + yield [ + [], + [ + 'tables' => [ 'revision' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields() + ), + 'joins' => [], + ] + ]; + yield [ + [ 'page' ], + [ + 'tables' => [ 'revision', 'page' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields(), + [ + 'page_namespace', + 'page_title', + 'page_id', + 'page_latest', + 'page_is_redirect', + 'page_len', + ] + ), + 'joins' => [ + 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ], + ], + ] + ]; + yield [ + [ 'user' ], + [ + 'tables' => [ 'revision', 'user' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields(), + [ + 'user_name', + ] + ), + 'joins' => [ + 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ], + ], + ] + ]; + yield [ + [ 'text' ], + [ + 'tables' => [ 'revision', 'text' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields(), + [ + 'old_text', + 'old_flags', + ] + ), + 'joins' => [ + 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ], + ], + ] + ]; + } + +} diff --git a/tests/phpunit/includes/Storage/PreMcrRevisionStoreDbTest.php b/tests/phpunit/includes/Storage/PreMcrRevisionStoreDbTest.php new file mode 100644 index 0000000000..4336691185 --- /dev/null +++ b/tests/phpunit/includes/Storage/PreMcrRevisionStoreDbTest.php @@ -0,0 +1,84 @@ + [ 'archive' ], + 'fields' => array_merge( + $this->getDefaultArchiveFields(), + [ + 'ar_comment_text' => 'ar_comment', + 'ar_comment_data' => 'NULL', + 'ar_comment_cid' => 'NULL', + 'ar_user_text' => 'ar_user_text', + 'ar_user' => 'ar_user', + 'ar_actor' => 'NULL', + 'ar_content_format', + 'ar_content_model', + ] + ), + 'joins' => [], + ] + ]; + } + + public function provideGetQueryInfo() { + yield [ + [], + [ + 'tables' => [ 'revision' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields(), + $this->getContentHandlerQueryFields() + ), + 'joins' => [], + ] + ]; + yield [ + [ 'page', 'user', 'text' ], + [ + 'tables' => [ 'revision', 'page', 'user', 'text' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields(), + $this->getContentHandlerQueryFields(), + [ + 'page_namespace', + 'page_title', + 'page_id', + 'page_latest', + 'page_is_redirect', + 'page_len', + 'user_name', + 'old_text', + 'old_flags' + ] + ), + 'joins' => [ + 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ], + 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ], + 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ], + ], + ] + ]; + } + +} diff --git a/tests/phpunit/includes/Storage/PreMcrSchemaOverride.php b/tests/phpunit/includes/Storage/PreMcrSchemaOverride.php new file mode 100644 index 0000000000..5d516e8258 --- /dev/null +++ b/tests/phpunit/includes/Storage/PreMcrSchemaOverride.php @@ -0,0 +1,54 @@ + [], + 'drop' => [], + 'create' => [], + 'alter' => [], + ]; + + if ( $this->hasMcrTables( $db ) ) { + $overrides['drop'] = [ 'slots', 'content', 'slot_roles', 'content_models', ]; + $overrides['scripts'][] = $this->getSqlPatchPath( $db, '/drop-mcr-tables', __DIR__ ); + } + + if ( !$this->hasPreMcrFields( $db ) ) { + $overrides['alter'][] = 'revision'; + $overrides['scripts'][] = $this->getSqlPatchPath( $db, '/create-pre-mcr-fields', __DIR__ ); + } + + return $overrides; + } + +} diff --git a/tests/phpunit/includes/Storage/RevisionStoreDbTest.php b/tests/phpunit/includes/Storage/RevisionStoreDbTest.php deleted file mode 100644 index 2a9295628e..0000000000 --- a/tests/phpunit/includes/Storage/RevisionStoreDbTest.php +++ /dev/null @@ -1,1282 +0,0 @@ -tablesUsed[] = 'archive'; - $this->tablesUsed[] = 'page'; - $this->tablesUsed[] = 'revision'; - $this->tablesUsed[] = 'comment'; - } - - /** - * @return LoadBalancer - */ - private function getLoadBalancerMock( array $server ) { - $lb = $this->getMockBuilder( LoadBalancer::class ) - ->setMethods( [ 'reallyOpenConnection' ] ) - ->setConstructorArgs( [ [ 'servers' => [ $server ] ] ] ) - ->getMock(); - - $lb->method( 'reallyOpenConnection' )->willReturnCallback( - function ( array $server, $dbNameOverride ) { - return $this->getDatabaseMock( $server ); - } - ); - - return $lb; - } - - /** - * @return Database - */ - private function getDatabaseMock( array $params ) { - $db = $this->getMockBuilder( DatabaseSqlite::class ) - ->setMethods( [ 'select', 'doQuery', 'open', 'closeConnection', 'isOpen' ] ) - ->setConstructorArgs( [ $params ] ) - ->getMock(); - - $db->method( 'select' )->willReturn( new FakeResultWrapper( [] ) ); - $db->method( 'isOpen' )->willReturn( true ); - - return $db; - } - - public function provideDomainCheck() { - yield [ false, 'test', '' ]; - yield [ 'test', 'test', '' ]; - - yield [ false, 'test', 'foo_' ]; - yield [ 'test-foo_', 'test', 'foo_' ]; - - yield [ false, 'dash-test', '' ]; - yield [ 'dash-test', 'dash-test', '' ]; - - yield [ false, 'underscore_test', 'foo_' ]; - yield [ 'underscore_test-foo_', 'underscore_test', 'foo_' ]; - } - - /** - * @dataProvider provideDomainCheck - * @covers \MediaWiki\Storage\RevisionStore::checkDatabaseWikiId - */ - public function testDomainCheck( $wikiId, $dbName, $dbPrefix ) { - $this->setMwGlobals( - [ - 'wgDBname' => $dbName, - 'wgDBprefix' => $dbPrefix, - ] - ); - - $loadBalancer = $this->getLoadBalancerMock( - [ - 'host' => '*dummy*', - 'dbDirectory' => '*dummy*', - 'user' => 'test', - 'password' => 'test', - 'flags' => 0, - 'variables' => [], - 'schema' => '', - 'cliMode' => true, - 'agent' => '', - 'load' => 100, - 'profiler' => null, - 'trxProfiler' => new TransactionProfiler(), - 'connLogger' => new \Psr\Log\NullLogger(), - 'queryLogger' => new \Psr\Log\NullLogger(), - 'errorLogger' => function () { - }, - 'deprecationLogger' => function () { - }, - 'type' => 'test', - 'dbname' => $dbName, - 'tablePrefix' => $dbPrefix, - ] - ); - $db = $loadBalancer->getConnection( DB_REPLICA ); - - $blobStore = $this->getMockBuilder( SqlBlobStore::class ) - ->disableOriginalConstructor() - ->getMock(); - - $store = new RevisionStore( - $loadBalancer, - $blobStore, - new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ), - MediaWikiServices::getInstance()->getCommentStore(), - MediaWikiServices::getInstance()->getActorMigration(), - $wikiId - ); - - $count = $store->countRevisionsByPageId( $db, 0 ); - - // Dummy check to make PhpUnit happy. We are really only interested in - // countRevisionsByPageId not failing due to the DB domain check. - $this->assertSame( 0, $count ); - } - - private function assertLinkTargetsEqual( LinkTarget $l1, LinkTarget $l2 ) { - $this->assertEquals( $l1->getDBkey(), $l2->getDBkey() ); - $this->assertEquals( $l1->getNamespace(), $l2->getNamespace() ); - $this->assertEquals( $l1->getFragment(), $l2->getFragment() ); - $this->assertEquals( $l1->getInterwiki(), $l2->getInterwiki() ); - } - - private function assertRevisionRecordsEqual( RevisionRecord $r1, RevisionRecord $r2 ) { - $this->assertEquals( $r1->getUser()->getName(), $r2->getUser()->getName() ); - $this->assertEquals( $r1->getUser()->getId(), $r2->getUser()->getId() ); - $this->assertEquals( $r1->getComment(), $r2->getComment() ); - $this->assertEquals( $r1->getPageAsLinkTarget(), $r2->getPageAsLinkTarget() ); - $this->assertEquals( $r1->getTimestamp(), $r2->getTimestamp() ); - $this->assertEquals( $r1->getVisibility(), $r2->getVisibility() ); - $this->assertEquals( $r1->getSha1(), $r2->getSha1() ); - $this->assertEquals( $r1->getParentId(), $r2->getParentId() ); - $this->assertEquals( $r1->getSize(), $r2->getSize() ); - $this->assertEquals( $r1->getPageId(), $r2->getPageId() ); - $this->assertEquals( $r1->getSlotRoles(), $r2->getSlotRoles() ); - $this->assertEquals( $r1->getWikiId(), $r2->getWikiId() ); - $this->assertEquals( $r1->isMinor(), $r2->isMinor() ); - foreach ( $r1->getSlotRoles() as $role ) { - $this->assertSlotRecordsEqual( $r1->getSlot( $role ), $r2->getSlot( $role ) ); - $this->assertTrue( $r1->getContent( $role )->equals( $r2->getContent( $role ) ) ); - } - foreach ( [ - RevisionRecord::DELETED_TEXT, - RevisionRecord::DELETED_COMMENT, - RevisionRecord::DELETED_USER, - RevisionRecord::DELETED_RESTRICTED, - ] as $field ) { - $this->assertEquals( $r1->isDeleted( $field ), $r2->isDeleted( $field ) ); - } - } - - private function assertSlotRecordsEqual( SlotRecord $s1, SlotRecord $s2 ) { - $this->assertSame( $s1->getRole(), $s2->getRole() ); - $this->assertSame( $s1->getModel(), $s2->getModel() ); - $this->assertSame( $s1->getFormat(), $s2->getFormat() ); - $this->assertSame( $s1->getSha1(), $s2->getSha1() ); - $this->assertSame( $s1->getSize(), $s2->getSize() ); - $this->assertTrue( $s1->getContent()->equals( $s2->getContent() ) ); - - $s1->hasRevision() ? $this->assertSame( $s1->getRevision(), $s2->getRevision() ) : null; - $s1->hasAddress() ? $this->assertSame( $s1->hasAddress(), $s2->hasAddress() ) : null; - } - - private function assertRevisionCompleteness( RevisionRecord $r ) { - foreach ( $r->getSlotRoles() as $role ) { - $this->assertSlotCompleteness( $r, $r->getSlot( $role ) ); - } - } - - private function assertSlotCompleteness( RevisionRecord $r, SlotRecord $slot ) { - $this->assertTrue( $slot->hasAddress() ); - $this->assertSame( $r->getId(), $slot->getRevision() ); - } - - /** - * @param mixed[] $details - * - * @return RevisionRecord - */ - private function getRevisionRecordFromDetailsArray( $title, $details = [] ) { - // Convert some values that can't be provided by dataProviders - $page = WikiPage::factory( $title ); - if ( isset( $details['user'] ) && $details['user'] === true ) { - $details['user'] = $this->getTestUser()->getUser(); - } - if ( isset( $details['page'] ) && $details['page'] === true ) { - $details['page'] = $page->getId(); - } - if ( isset( $details['parent'] ) && $details['parent'] === true ) { - $details['parent'] = $page->getLatest(); - } - - // Create the RevisionRecord with any available data - $rev = new MutableRevisionRecord( $title ); - isset( $details['slot'] ) ? $rev->setSlot( $details['slot'] ) : null; - isset( $details['parent'] ) ? $rev->setParentId( $details['parent'] ) : null; - isset( $details['page'] ) ? $rev->setPageId( $details['page'] ) : null; - isset( $details['size'] ) ? $rev->setSize( $details['size'] ) : null; - isset( $details['sha1'] ) ? $rev->setSha1( $details['sha1'] ) : null; - isset( $details['comment'] ) ? $rev->setComment( $details['comment'] ) : null; - isset( $details['timestamp'] ) ? $rev->setTimestamp( $details['timestamp'] ) : null; - isset( $details['minor'] ) ? $rev->setMinorEdit( $details['minor'] ) : null; - isset( $details['user'] ) ? $rev->setUser( $details['user'] ) : null; - isset( $details['visibility'] ) ? $rev->setVisibility( $details['visibility'] ) : null; - isset( $details['id'] ) ? $rev->setId( $details['id'] ) : null; - - return $rev; - } - - private function getRandomCommentStoreComment() { - return CommentStoreComment::newUnsavedComment( __METHOD__ . '.' . rand( 0, 1000 ) ); - } - - public function provideInsertRevisionOn_successes() { - yield 'Bare minimum revision insertion' => [ - Title::newFromText( 'UTPage' ), - [ - 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), - 'parent' => true, - 'comment' => $this->getRandomCommentStoreComment(), - 'timestamp' => '20171117010101', - 'user' => true, - ], - ]; - yield 'Detailed revision insertion' => [ - Title::newFromText( 'UTPage' ), - [ - 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), - 'parent' => true, - 'page' => true, - 'comment' => $this->getRandomCommentStoreComment(), - 'timestamp' => '20171117010101', - 'user' => true, - 'minor' => true, - 'visibility' => RevisionRecord::DELETED_RESTRICTED, - ], - ]; - } - - /** - * @dataProvider provideInsertRevisionOn_successes - * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn - */ - public function testInsertRevisionOn_successes( Title $title, array $revDetails = [] ) { - $rev = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $return = $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) ); - - $this->assertLinkTargetsEqual( $title, $return->getPageAsLinkTarget() ); - $this->assertRevisionRecordsEqual( $rev, $return ); - $this->assertRevisionCompleteness( $return ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn - */ - public function testInsertRevisionOn_blobAddressExists() { - $title = Title::newFromText( 'UTPage' ); - $revDetails = [ - 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), - 'parent' => true, - 'comment' => $this->getRandomCommentStoreComment(), - 'timestamp' => '20171117010101', - 'user' => true, - ]; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - - // Insert the first revision - $revOne = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); - $firstReturn = $store->insertRevisionOn( $revOne, wfGetDB( DB_MASTER ) ); - $this->assertLinkTargetsEqual( $title, $firstReturn->getPageAsLinkTarget() ); - $this->assertRevisionRecordsEqual( $revOne, $firstReturn ); - - // Insert a second revision inheriting the same blob address - $revDetails['slot'] = SlotRecord::newInherited( $firstReturn->getSlot( 'main' ) ); - $revTwo = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); - $secondReturn = $store->insertRevisionOn( $revTwo, wfGetDB( DB_MASTER ) ); - $this->assertLinkTargetsEqual( $title, $secondReturn->getPageAsLinkTarget() ); - $this->assertRevisionRecordsEqual( $revTwo, $secondReturn ); - - // Assert that the same blob address has been used. - $this->assertEquals( - $firstReturn->getSlot( 'main' )->getAddress(), - $secondReturn->getSlot( 'main' )->getAddress() - ); - // And that different revisions have been created. - $this->assertNotSame( - $firstReturn->getId(), - $secondReturn->getId() - ); - } - - public function provideInsertRevisionOn_failures() { - yield 'no slot' => [ - Title::newFromText( 'UTPage' ), - [ - 'comment' => $this->getRandomCommentStoreComment(), - 'timestamp' => '20171117010101', - 'user' => true, - ], - new InvalidArgumentException( 'At least one slot needs to be defined!' ) - ]; - yield 'slot that is not main slot' => [ - Title::newFromText( 'UTPage' ), - [ - 'slot' => SlotRecord::newUnsaved( 'lalala', new WikitextContent( 'Chicken' ) ), - 'comment' => $this->getRandomCommentStoreComment(), - 'timestamp' => '20171117010101', - 'user' => true, - ], - new InvalidArgumentException( 'Only the main slot is supported for now!' ) - ]; - yield 'no timestamp' => [ - Title::newFromText( 'UTPage' ), - [ - 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), - 'comment' => $this->getRandomCommentStoreComment(), - 'user' => true, - ], - new IncompleteRevisionException( 'timestamp field must not be NULL!' ) - ]; - yield 'no comment' => [ - Title::newFromText( 'UTPage' ), - [ - 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), - 'timestamp' => '20171117010101', - 'user' => true, - ], - new IncompleteRevisionException( 'comment must not be NULL!' ) - ]; - yield 'no user' => [ - Title::newFromText( 'UTPage' ), - [ - 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), - 'comment' => $this->getRandomCommentStoreComment(), - 'timestamp' => '20171117010101', - ], - new IncompleteRevisionException( 'user must not be NULL!' ) - ]; - } - - /** - * @dataProvider provideInsertRevisionOn_failures - * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn - */ - public function testInsertRevisionOn_failures( - Title $title, - array $revDetails = [], - Exception $exception ) { - $rev = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - - $this->setExpectedException( - get_class( $exception ), - $exception->getMessage(), - $exception->getCode() - ); - $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) ); - } - - public function provideNewNullRevision() { - yield [ - Title::newFromText( 'UTPage' ), - CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment1' ), - true, - ]; - yield [ - Title::newFromText( 'UTPage' ), - CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment2', [ 'a' => 1 ] ), - false, - ]; - } - - /** - * @dataProvider provideNewNullRevision - * @covers \MediaWiki\Storage\RevisionStore::newNullRevision - */ - public function testNewNullRevision( Title $title, $comment, $minor ) { - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $user = TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser(); - - $parent = $store->getRevisionByTitle( $title ); - $record = $store->newNullRevision( - wfGetDB( DB_MASTER ), - $title, - $comment, - $minor, - $user - ); - - $this->assertEquals( $title->getNamespace(), $record->getPageAsLinkTarget()->getNamespace() ); - $this->assertEquals( $title->getDBkey(), $record->getPageAsLinkTarget()->getDBkey() ); - $this->assertEquals( $comment, $record->getComment() ); - $this->assertEquals( $minor, $record->isMinor() ); - $this->assertEquals( $user->getName(), $record->getUser()->getName() ); - $this->assertEquals( $parent->getId(), $record->getParentId() ); - - $parentSlot = $parent->getSlot( 'main' ); - $slot = $record->getSlot( 'main' ); - - $this->assertTrue( $slot->isInherited(), 'isInherited' ); - $this->assertSame( $parentSlot->getOrigin(), $slot->getOrigin(), 'getOrigin' ); - $this->assertSame( $parentSlot->getAddress(), $slot->getAddress(), 'getAddress' ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::newNullRevision - */ - public function testNewNullRevision_nonExistingTitle() { - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $record = $store->newNullRevision( - wfGetDB( DB_MASTER ), - Title::newFromText( __METHOD__ . '.iDontExist!' ), - CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment' ), - false, - TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser() - ); - $this->assertNull( $record ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled - */ - public function testGetRcIdIfUnpatrolled_returnsRecentChangesId() { - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $status = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); - /** @var Revision $rev */ - $rev = $status->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $revisionRecord = $store->getRevisionById( $rev->getId() ); - $result = $store->getRcIdIfUnpatrolled( $revisionRecord ); - - $this->assertGreaterThan( 0, $result ); - $this->assertSame( - $page->getRevision()->getRecentChange()->getAttribute( 'rc_id' ), - $result - ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled - */ - public function testGetRcIdIfUnpatrolled_returnsZeroIfPatrolled() { - // This assumes that sysops are auto patrolled - $sysop = $this->getTestSysop()->getUser(); - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $status = $page->doEditContent( - new WikitextContent( __METHOD__ ), - __METHOD__, - 0, - false, - $sysop - ); - /** @var Revision $rev */ - $rev = $status->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $revisionRecord = $store->getRevisionById( $rev->getId() ); - $result = $store->getRcIdIfUnpatrolled( $revisionRecord ); - - $this->assertSame( 0, $result ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getRecentChange - */ - public function testGetRecentChange() { - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $content = new WikitextContent( __METHOD__ ); - $status = $page->doEditContent( $content, __METHOD__ ); - /** @var Revision $rev */ - $rev = $status->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $revRecord = $store->getRevisionById( $rev->getId() ); - $recentChange = $store->getRecentChange( $revRecord ); - - $this->assertEquals( $rev->getId(), $recentChange->getAttribute( 'rc_this_oldid' ) ); - $this->assertEquals( $rev->getRecentChange(), $recentChange ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getRevisionById - */ - public function testGetRevisionById() { - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $content = new WikitextContent( __METHOD__ ); - $status = $page->doEditContent( $content, __METHOD__ ); - /** @var Revision $rev */ - $rev = $status->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $revRecord = $store->getRevisionById( $rev->getId() ); - - $this->assertSame( $rev->getId(), $revRecord->getId() ); - $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); - $this->assertSame( __METHOD__, $revRecord->getComment()->text ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getRevisionByTitle - */ - public function testGetRevisionByTitle() { - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $content = new WikitextContent( __METHOD__ ); - $status = $page->doEditContent( $content, __METHOD__ ); - /** @var Revision $rev */ - $rev = $status->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $revRecord = $store->getRevisionByTitle( $page->getTitle() ); - - $this->assertSame( $rev->getId(), $revRecord->getId() ); - $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); - $this->assertSame( __METHOD__, $revRecord->getComment()->text ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getRevisionByPageId - */ - public function testGetRevisionByPageId() { - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $content = new WikitextContent( __METHOD__ ); - $status = $page->doEditContent( $content, __METHOD__ ); - /** @var Revision $rev */ - $rev = $status->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $revRecord = $store->getRevisionByPageId( $page->getId() ); - - $this->assertSame( $rev->getId(), $revRecord->getId() ); - $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); - $this->assertSame( __METHOD__, $revRecord->getComment()->text ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getRevisionByTimestamp - */ - public function testGetRevisionByTimestamp() { - // Make sure there is 1 second between the last revision and the rev we create... - // Otherwise we might not get the correct revision and the test may fail... - // :( - sleep( 1 ); - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $content = new WikitextContent( __METHOD__ ); - $status = $page->doEditContent( $content, __METHOD__ ); - /** @var Revision $rev */ - $rev = $status->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $revRecord = $store->getRevisionByTimestamp( - $page->getTitle(), - $rev->getTimestamp() - ); - - $this->assertSame( $rev->getId(), $revRecord->getId() ); - $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); - $this->assertSame( __METHOD__, $revRecord->getComment()->text ); - } - - private function revisionToRow( Revision $rev ) { - $page = WikiPage::factory( $rev->getTitle() ); - - return (object)[ - 'rev_id' => (string)$rev->getId(), - 'rev_page' => (string)$rev->getPage(), - 'rev_text_id' => (string)$rev->getTextId(), - 'rev_timestamp' => (string)$rev->getTimestamp(), - 'rev_user_text' => (string)$rev->getUserText(), - 'rev_user' => (string)$rev->getUser(), - 'rev_minor_edit' => $rev->isMinor() ? '1' : '0', - 'rev_deleted' => (string)$rev->getVisibility(), - 'rev_len' => (string)$rev->getSize(), - 'rev_parent_id' => (string)$rev->getParentId(), - 'rev_sha1' => (string)$rev->getSha1(), - 'rev_comment_text' => $rev->getComment(), - 'rev_comment_data' => null, - 'rev_comment_cid' => null, - 'rev_content_format' => $rev->getContentFormat(), - 'rev_content_model' => $rev->getContentModel(), - 'page_namespace' => (string)$page->getTitle()->getNamespace(), - 'page_title' => $page->getTitle()->getDBkey(), - 'page_id' => (string)$page->getId(), - 'page_latest' => (string)$page->getLatest(), - 'page_is_redirect' => $page->isRedirect() ? '1' : '0', - 'page_len' => (string)$page->getContent()->getSize(), - 'user_name' => (string)$rev->getUserText(), - ]; - } - - private function assertRevisionRecordMatchesRevision( - Revision $rev, - RevisionRecord $record - ) { - $this->assertSame( $rev->getId(), $record->getId() ); - $this->assertSame( $rev->getPage(), $record->getPageId() ); - $this->assertSame( $rev->getTimestamp(), $record->getTimestamp() ); - $this->assertSame( $rev->getUserText(), $record->getUser()->getName() ); - $this->assertSame( $rev->getUser(), $record->getUser()->getId() ); - $this->assertSame( $rev->isMinor(), $record->isMinor() ); - $this->assertSame( $rev->getVisibility(), $record->getVisibility() ); - $this->assertSame( $rev->getSize(), $record->getSize() ); - /** - * @note As of MW 1.31, the database schema allows the parent ID to be - * NULL to indicate that it is unknown. - */ - $expectedParent = $rev->getParentId(); - if ( $expectedParent === null ) { - $expectedParent = 0; - } - $this->assertSame( $expectedParent, $record->getParentId() ); - $this->assertSame( $rev->getSha1(), $record->getSha1() ); - $this->assertSame( $rev->getComment(), $record->getComment()->text ); - $this->assertSame( $rev->getContentFormat(), $record->getContent( 'main' )->getDefaultFormat() ); - $this->assertSame( $rev->getContentModel(), $record->getContent( 'main' )->getModel() ); - $this->assertLinkTargetsEqual( $rev->getTitle(), $record->getPageAsLinkTarget() ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow - * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29 - */ - public function testNewRevisionFromRow_anonEdit() { - $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); - $this->overrideMwServices(); - - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $text = __METHOD__ . 'a-ä'; - /** @var Revision $rev */ - $rev = $page->doEditContent( - new WikitextContent( $text ), - __METHOD__ . 'a' - )->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $record = $store->newRevisionFromRow( - $this->revisionToRow( $rev ), - [], - $page->getTitle() - ); - $this->assertRevisionRecordMatchesRevision( $rev, $record ); - $this->assertSame( $text, $rev->getContent()->serialize() ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow - * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29 - */ - public function testNewRevisionFromRow_anonEdit_legacyEncoding() { - $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' ); - $this->overrideMwServices(); - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $text = __METHOD__ . 'a-ä'; - /** @var Revision $rev */ - $rev = $page->doEditContent( - new WikitextContent( $text ), - __METHOD__. 'a' - )->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $record = $store->newRevisionFromRow( - $this->revisionToRow( $rev ), - [], - $page->getTitle() - ); - $this->assertRevisionRecordMatchesRevision( $rev, $record ); - $this->assertSame( $text, $rev->getContent()->serialize() ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow - * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29 - */ - public function testNewRevisionFromRow_userEdit() { - $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); - $this->overrideMwServices(); - - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $text = __METHOD__ . 'b-ä'; - /** @var Revision $rev */ - $rev = $page->doEditContent( - new WikitextContent( $text ), - __METHOD__ . 'b', - 0, - false, - $this->getTestUser()->getUser() - )->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $record = $store->newRevisionFromRow( - $this->revisionToRow( $rev ), - [], - $page->getTitle() - ); - $this->assertRevisionRecordMatchesRevision( $rev, $record ); - $this->assertSame( $text, $rev->getContent()->serialize() ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow - */ - public function testNewRevisionFromArchiveRow() { - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $title = Title::newFromText( __METHOD__ ); - $text = __METHOD__ . '-bä'; - $page = WikiPage::factory( $title ); - /** @var Revision $orig */ - $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ ) - ->value['revision']; - $page->doDeleteArticle( __METHOD__ ); - - $db = wfGetDB( DB_MASTER ); - $arQuery = $store->getArchiveQueryInfo(); - $res = $db->select( - $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ], - __METHOD__, [], $arQuery['joins'] - ); - $this->assertTrue( is_object( $res ), 'query failed' ); - - $row = $res->fetchObject(); - $res->free(); - $record = $store->newRevisionFromArchiveRow( $row ); - - $this->assertRevisionRecordMatchesRevision( $orig, $record ); - $this->assertSame( $text, $record->getContent( 'main' )->serialize() ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow - */ - public function testNewRevisionFromArchiveRow_legacyEncoding() { - $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' ); - $this->overrideMwServices(); - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $title = Title::newFromText( __METHOD__ ); - $text = __METHOD__ . '-bä'; - $page = WikiPage::factory( $title ); - /** @var Revision $orig */ - $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ ) - ->value['revision']; - $page->doDeleteArticle( __METHOD__ ); - - $db = wfGetDB( DB_MASTER ); - $arQuery = $store->getArchiveQueryInfo(); - $res = $db->select( - $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ], - __METHOD__, [], $arQuery['joins'] - ); - $this->assertTrue( is_object( $res ), 'query failed' ); - - $row = $res->fetchObject(); - $res->free(); - $record = $store->newRevisionFromArchiveRow( $row ); - - $this->assertRevisionRecordMatchesRevision( $orig, $record ); - $this->assertSame( $text, $record->getContent( 'main' )->serialize() ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromId - */ - public function testLoadRevisionFromId() { - $title = Title::newFromText( __METHOD__ ); - $page = WikiPage::factory( $title ); - /** @var Revision $rev */ - $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) - ->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $result = $store->loadRevisionFromId( wfGetDB( DB_MASTER ), $rev->getId() ); - $this->assertRevisionRecordMatchesRevision( $rev, $result ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromPageId - */ - public function testLoadRevisionFromPageId() { - $title = Title::newFromText( __METHOD__ ); - $page = WikiPage::factory( $title ); - /** @var Revision $rev */ - $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) - ->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $result = $store->loadRevisionFromPageId( wfGetDB( DB_MASTER ), $page->getId() ); - $this->assertRevisionRecordMatchesRevision( $rev, $result ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromTitle - */ - public function testLoadRevisionFromTitle() { - $title = Title::newFromText( __METHOD__ ); - $page = WikiPage::factory( $title ); - /** @var Revision $rev */ - $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) - ->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $result = $store->loadRevisionFromTitle( wfGetDB( DB_MASTER ), $title ); - $this->assertRevisionRecordMatchesRevision( $rev, $result ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromTimestamp - */ - public function testLoadRevisionFromTimestamp() { - $title = Title::newFromText( __METHOD__ ); - $page = WikiPage::factory( $title ); - /** @var Revision $revOne */ - $revOne = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) - ->value['revision']; - // Sleep to ensure different timestamps... )(evil) - sleep( 1 ); - /** @var Revision $revTwo */ - $revTwo = $page->doEditContent( new WikitextContent( __METHOD__ . 'a' ), '' ) - ->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $this->assertNull( - $store->loadRevisionFromTimestamp( wfGetDB( DB_MASTER ), $title, '20150101010101' ) - ); - $this->assertSame( - $revOne->getId(), - $store->loadRevisionFromTimestamp( - wfGetDB( DB_MASTER ), - $title, - $revOne->getTimestamp() - )->getId() - ); - $this->assertSame( - $revTwo->getId(), - $store->loadRevisionFromTimestamp( - wfGetDB( DB_MASTER ), - $title, - $revTwo->getTimestamp() - )->getId() - ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::listRevisionSizes - */ - public function testGetParentLengths() { - $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); - /** @var Revision $revOne */ - $revOne = $page->doEditContent( - new WikitextContent( __METHOD__ ), __METHOD__ - )->value['revision']; - /** @var Revision $revTwo */ - $revTwo = $page->doEditContent( - new WikitextContent( __METHOD__ . '2' ), __METHOD__ - )->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $this->assertSame( - [ - $revOne->getId() => strlen( __METHOD__ ), - ], - $store->listRevisionSizes( - wfGetDB( DB_MASTER ), - [ $revOne->getId() ] - ) - ); - $this->assertSame( - [ - $revOne->getId() => strlen( __METHOD__ ), - $revTwo->getId() => strlen( __METHOD__ ) + 1, - ], - $store->listRevisionSizes( - wfGetDB( DB_MASTER ), - [ $revOne->getId(), $revTwo->getId() ] - ) - ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getPreviousRevision - */ - public function testGetPreviousRevision() { - $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); - /** @var Revision $revOne */ - $revOne = $page->doEditContent( - new WikitextContent( __METHOD__ ), __METHOD__ - )->value['revision']; - /** @var Revision $revTwo */ - $revTwo = $page->doEditContent( - new WikitextContent( __METHOD__ . '2' ), __METHOD__ - )->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $this->assertNull( - $store->getPreviousRevision( $store->getRevisionById( $revOne->getId() ) ) - ); - $this->assertSame( - $revOne->getId(), - $store->getPreviousRevision( $store->getRevisionById( $revTwo->getId() ) )->getId() - ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getNextRevision - */ - public function testGetNextRevision() { - $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); - /** @var Revision $revOne */ - $revOne = $page->doEditContent( - new WikitextContent( __METHOD__ ), __METHOD__ - )->value['revision']; - /** @var Revision $revTwo */ - $revTwo = $page->doEditContent( - new WikitextContent( __METHOD__ . '2' ), __METHOD__ - )->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $this->assertSame( - $revTwo->getId(), - $store->getNextRevision( $store->getRevisionById( $revOne->getId() ) )->getId() - ); - $this->assertNull( - $store->getNextRevision( $store->getRevisionById( $revTwo->getId() ) ) - ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId - */ - public function testGetTimestampFromId_found() { - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - /** @var Revision $rev */ - $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) - ->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $result = $store->getTimestampFromId( - $page->getTitle(), - $rev->getId() - ); - - $this->assertSame( $rev->getTimestamp(), $result ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId - */ - public function testGetTimestampFromId_notFound() { - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - /** @var Revision $rev */ - $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) - ->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $result = $store->getTimestampFromId( - $page->getTitle(), - $rev->getId() + 1 - ); - - $this->assertFalse( $result ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::countRevisionsByPageId - */ - public function testCountRevisionsByPageId() { - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); - - $this->assertSame( - 0, - $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() ) - ); - $page->doEditContent( new WikitextContent( 'a' ), 'a' ); - $this->assertSame( - 1, - $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() ) - ); - $page->doEditContent( new WikitextContent( 'b' ), 'b' ); - $this->assertSame( - 2, - $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() ) - ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::countRevisionsByTitle - */ - public function testCountRevisionsByTitle() { - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); - - $this->assertSame( - 0, - $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() ) - ); - $page->doEditContent( new WikitextContent( 'a' ), 'a' ); - $this->assertSame( - 1, - $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() ) - ); - $page->doEditContent( new WikitextContent( 'b' ), 'b' ); - $this->assertSame( - 2, - $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() ) - ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::userWasLastToEdit - */ - public function testUserWasLastToEdit_false() { - $sysop = $this->getTestSysop()->getUser(); - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $result = $store->userWasLastToEdit( - wfGetDB( DB_MASTER ), - $page->getId(), - $sysop->getId(), - '20160101010101' - ); - $this->assertFalse( $result ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::userWasLastToEdit - */ - public function testUserWasLastToEdit_true() { - $startTime = wfTimestampNow(); - $sysop = $this->getTestSysop()->getUser(); - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $page->doEditContent( - new WikitextContent( __METHOD__ ), - __METHOD__, - 0, - false, - $sysop - ); - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $result = $store->userWasLastToEdit( - wfGetDB( DB_MASTER ), - $page->getId(), - $sysop->getId(), - $startTime - ); - $this->assertTrue( $result ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getKnownCurrentRevision - */ - public function testGetKnownCurrentRevision() { - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - /** @var Revision $rev */ - $rev = $page->doEditContent( - new WikitextContent( __METHOD__ . 'b' ), - __METHOD__ . 'b', - 0, - false, - $this->getTestUser()->getUser() - )->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $record = $store->getKnownCurrentRevision( - $page->getTitle(), - $rev->getId() - ); - - $this->assertRevisionRecordMatchesRevision( $rev, $record ); - } - - public function provideNewMutableRevisionFromArray() { - yield 'Basic array, with page & id' => [ - [ - 'id' => 2, - 'page' => 1, - 'text_id' => 2, - 'timestamp' => '20171017114835', - 'user_text' => '111.0.1.2', - 'user' => 0, - 'minor_edit' => false, - 'deleted' => 0, - 'len' => 46, - 'parent_id' => 1, - 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', - 'comment' => 'Goat Comment!', - 'content_format' => 'text/x-wiki', - 'content_model' => 'wikitext', - ] - ]; - yield 'Basic array, content object' => [ - [ - 'id' => 2, - 'page' => 1, - 'timestamp' => '20171017114835', - 'user_text' => '111.0.1.2', - 'user' => 0, - 'minor_edit' => false, - 'deleted' => 0, - 'len' => 46, - 'parent_id' => 1, - 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', - 'comment' => 'Goat Comment!', - 'content' => new WikitextContent( 'Some Content' ), - ] - ]; - yield 'Basic array, serialized text' => [ - [ - 'id' => 2, - 'page' => 1, - 'timestamp' => '20171017114835', - 'user_text' => '111.0.1.2', - 'user' => 0, - 'minor_edit' => false, - 'deleted' => 0, - 'len' => 46, - 'parent_id' => 1, - 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', - 'comment' => 'Goat Comment!', - 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(), - ] - ]; - yield 'Basic array, serialized text, utf-8 flags' => [ - [ - 'id' => 2, - 'page' => 1, - 'timestamp' => '20171017114835', - 'user_text' => '111.0.1.2', - 'user' => 0, - 'minor_edit' => false, - 'deleted' => 0, - 'len' => 46, - 'parent_id' => 1, - 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', - 'comment' => 'Goat Comment!', - 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(), - 'flags' => 'utf-8', - ] - ]; - yield 'Basic array, with title' => [ - [ - 'title' => Title::newFromText( 'SomeText' ), - 'text_id' => 2, - 'timestamp' => '20171017114835', - 'user_text' => '111.0.1.2', - 'user' => 0, - 'minor_edit' => false, - 'deleted' => 0, - 'len' => 46, - 'parent_id' => 1, - 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', - 'comment' => 'Goat Comment!', - 'content_format' => 'text/x-wiki', - 'content_model' => 'wikitext', - ] - ]; - yield 'Basic array, no user field' => [ - [ - 'id' => 2, - 'page' => 1, - 'text_id' => 2, - 'timestamp' => '20171017114835', - 'user_text' => '111.0.1.3', - 'minor_edit' => false, - 'deleted' => 0, - 'len' => 46, - 'parent_id' => 1, - 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', - 'comment' => 'Goat Comment!', - 'content_format' => 'text/x-wiki', - 'content_model' => 'wikitext', - ] - ]; - } - - /** - * @dataProvider provideNewMutableRevisionFromArray - * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray - */ - public function testNewMutableRevisionFromArray( array $array ) { - $store = MediaWikiServices::getInstance()->getRevisionStore(); - - $result = $store->newMutableRevisionFromArray( $array ); - - if ( isset( $array['id'] ) ) { - $this->assertSame( $array['id'], $result->getId() ); - } - if ( isset( $array['page'] ) ) { - $this->assertSame( $array['page'], $result->getPageId() ); - } - $this->assertSame( $array['timestamp'], $result->getTimestamp() ); - $this->assertSame( $array['user_text'], $result->getUser()->getName() ); - if ( isset( $array['user'] ) ) { - $this->assertSame( $array['user'], $result->getUser()->getId() ); - } - $this->assertSame( (bool)$array['minor_edit'], $result->isMinor() ); - $this->assertSame( $array['deleted'], $result->getVisibility() ); - $this->assertSame( $array['len'], $result->getSize() ); - $this->assertSame( $array['parent_id'], $result->getParentId() ); - $this->assertSame( $array['sha1'], $result->getSha1() ); - $this->assertSame( $array['comment'], $result->getComment()->text ); - if ( isset( $array['content'] ) ) { - $this->assertTrue( - $result->getSlot( 'main' )->getContent()->equals( $array['content'] ) - ); - } elseif ( isset( $array['text'] ) ) { - $this->assertSame( $array['text'], $result->getSlot( 'main' )->getContent()->serialize() ); - } else { - $this->assertSame( - $array['content_format'], - $result->getSlot( 'main' )->getContent()->getDefaultFormat() - ); - $this->assertSame( $array['content_model'], $result->getSlot( 'main' )->getModel() ); - } - } - - /** - * @dataProvider provideNewMutableRevisionFromArray - * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray - */ - public function testNewMutableRevisionFromArray_legacyEncoding( array $array ) { - $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ); - $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); - $blobStore = new SqlBlobStore( $lb, $cache ); - $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) ); - - $factory = $this->getMockBuilder( BlobStoreFactory::class ) - ->setMethods( [ 'newBlobStore', 'newSqlBlobStore' ] ) - ->disableOriginalConstructor() - ->getMock(); - $factory->expects( $this->any() ) - ->method( 'newBlobStore' ) - ->willReturn( $blobStore ); - $factory->expects( $this->any() ) - ->method( 'newSqlBlobStore' ) - ->willReturn( $blobStore ); - - $this->setService( 'BlobStoreFactory', $factory ); - - $this->testNewMutableRevisionFromArray( $array ); - } - -} diff --git a/tests/phpunit/includes/Storage/RevisionStoreDbTestBase.php b/tests/phpunit/includes/Storage/RevisionStoreDbTestBase.php new file mode 100644 index 0000000000..bdff4cd7d4 --- /dev/null +++ b/tests/phpunit/includes/Storage/RevisionStoreDbTestBase.php @@ -0,0 +1,1500 @@ +tablesUsed[] = 'archive'; + $this->tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $this->tablesUsed[] = 'comment'; + + $this->tablesUsed += $this->getMcrTablesToReset(); + + $this->setMwGlobals( + 'wgMultiContentRevisionSchemaMigrationStage', + $this->getMcrMigrationStage() + ); + + $this->setMwGlobals( + 'wgContentHandlerUseDB', + $this->getContentHandlerUseDB() + ); + + $this->overrideMwServices(); + } + + /** + * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject + */ + private function getLoadBalancerMock( array $server ) { + $lb = $this->getMockBuilder( LoadBalancer::class ) + ->setMethods( [ 'reallyOpenConnection' ] ) + ->setConstructorArgs( [ [ 'servers' => [ $server ] ] ] ) + ->getMock(); + + $lb->method( 'reallyOpenConnection' )->willReturnCallback( + function ( array $server, $dbNameOverride ) { + return $this->getDatabaseMock( $server ); + } + ); + + return $lb; + } + + /** + * @return Database|PHPUnit_Framework_MockObject_MockObject + */ + private function getDatabaseMock( array $params ) { + $db = $this->getMockBuilder( DatabaseSqlite::class ) + ->setMethods( [ 'select', 'doQuery', 'open', 'closeConnection', 'isOpen' ] ) + ->setConstructorArgs( [ $params ] ) + ->getMock(); + + $db->method( 'select' )->willReturn( new FakeResultWrapper( [] ) ); + $db->method( 'isOpen' )->willReturn( true ); + + return $db; + } + + public function provideDomainCheck() { + yield [ false, 'test', '' ]; + yield [ 'test', 'test', '' ]; + + yield [ false, 'test', 'foo_' ]; + yield [ 'test-foo_', 'test', 'foo_' ]; + + yield [ false, 'dash-test', '' ]; + yield [ 'dash-test', 'dash-test', '' ]; + + yield [ false, 'underscore_test', 'foo_' ]; + yield [ 'underscore_test-foo_', 'underscore_test', 'foo_' ]; + } + + /** + * @dataProvider provideDomainCheck + * @covers \MediaWiki\Storage\RevisionStore::checkDatabaseWikiId + */ + public function testDomainCheck( $wikiId, $dbName, $dbPrefix ) { + $this->setMwGlobals( + [ + 'wgDBname' => $dbName, + 'wgDBprefix' => $dbPrefix, + ] + ); + + $loadBalancer = $this->getLoadBalancerMock( + [ + 'host' => '*dummy*', + 'dbDirectory' => '*dummy*', + 'user' => 'test', + 'password' => 'test', + 'flags' => 0, + 'variables' => [], + 'schema' => '', + 'cliMode' => true, + 'agent' => '', + 'load' => 100, + 'profiler' => null, + 'trxProfiler' => new TransactionProfiler(), + 'connLogger' => new \Psr\Log\NullLogger(), + 'queryLogger' => new \Psr\Log\NullLogger(), + 'errorLogger' => function () { + }, + 'deprecationLogger' => function () { + }, + 'type' => 'test', + 'dbname' => $dbName, + 'tablePrefix' => $dbPrefix, + ] + ); + $db = $loadBalancer->getConnection( DB_REPLICA ); + + /** @var SqlBlobStore $blobStore */ + $blobStore = $this->getMockBuilder( SqlBlobStore::class ) + ->disableOriginalConstructor() + ->getMock(); + + $store = new RevisionStore( + $loadBalancer, + $blobStore, + new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ), + MediaWikiServices::getInstance()->getCommentStore(), + MediaWikiServices::getInstance()->getActorMigration(), + $wikiId + ); + + $count = $store->countRevisionsByPageId( $db, 0 ); + + // Dummy check to make PhpUnit happy. We are really only interested in + // countRevisionsByPageId not failing due to the DB domain check. + $this->assertSame( 0, $count ); + } + + private function assertLinkTargetsEqual( LinkTarget $l1, LinkTarget $l2 ) { + $this->assertEquals( $l1->getDBkey(), $l2->getDBkey() ); + $this->assertEquals( $l1->getNamespace(), $l2->getNamespace() ); + $this->assertEquals( $l1->getFragment(), $l2->getFragment() ); + $this->assertEquals( $l1->getInterwiki(), $l2->getInterwiki() ); + } + + private function assertRevisionRecordsEqual( RevisionRecord $r1, RevisionRecord $r2 ) { + $this->assertEquals( $r1->getUser()->getName(), $r2->getUser()->getName() ); + $this->assertEquals( $r1->getUser()->getId(), $r2->getUser()->getId() ); + $this->assertEquals( $r1->getComment(), $r2->getComment() ); + $this->assertEquals( $r1->getPageAsLinkTarget(), $r2->getPageAsLinkTarget() ); + $this->assertEquals( $r1->getTimestamp(), $r2->getTimestamp() ); + $this->assertEquals( $r1->getVisibility(), $r2->getVisibility() ); + $this->assertEquals( $r1->getSha1(), $r2->getSha1() ); + $this->assertEquals( $r1->getParentId(), $r2->getParentId() ); + $this->assertEquals( $r1->getSize(), $r2->getSize() ); + $this->assertEquals( $r1->getPageId(), $r2->getPageId() ); + $this->assertEquals( $r1->getSlotRoles(), $r2->getSlotRoles() ); + $this->assertEquals( $r1->getWikiId(), $r2->getWikiId() ); + $this->assertEquals( $r1->isMinor(), $r2->isMinor() ); + foreach ( $r1->getSlotRoles() as $role ) { + $this->assertSlotRecordsEqual( $r1->getSlot( $role ), $r2->getSlot( $role ) ); + $this->assertTrue( $r1->getContent( $role )->equals( $r2->getContent( $role ) ) ); + } + foreach ( [ + RevisionRecord::DELETED_TEXT, + RevisionRecord::DELETED_COMMENT, + RevisionRecord::DELETED_USER, + RevisionRecord::DELETED_RESTRICTED, + ] as $field ) { + $this->assertEquals( $r1->isDeleted( $field ), $r2->isDeleted( $field ) ); + } + } + + private function assertSlotRecordsEqual( SlotRecord $s1, SlotRecord $s2 ) { + $this->assertSame( $s1->getRole(), $s2->getRole() ); + $this->assertSame( $s1->getModel(), $s2->getModel() ); + $this->assertSame( $s1->getFormat(), $s2->getFormat() ); + $this->assertSame( $s1->getSha1(), $s2->getSha1() ); + $this->assertSame( $s1->getSize(), $s2->getSize() ); + $this->assertTrue( $s1->getContent()->equals( $s2->getContent() ) ); + + $s1->hasRevision() ? $this->assertSame( $s1->getRevision(), $s2->getRevision() ) : null; + $s1->hasAddress() ? $this->assertSame( $s1->hasAddress(), $s2->hasAddress() ) : null; + } + + private function assertRevisionCompleteness( RevisionRecord $r ) { + foreach ( $r->getSlotRoles() as $role ) { + $this->assertSlotCompleteness( $r, $r->getSlot( $role ) ); + } + } + + private function assertSlotCompleteness( RevisionRecord $r, SlotRecord $slot ) { + $this->assertTrue( $slot->hasAddress() ); + $this->assertSame( $r->getId(), $slot->getRevision() ); + } + + /** + * @param mixed[] $details + * + * @return RevisionRecord + */ + private function getRevisionRecordFromDetailsArray( $title, $details = [] ) { + // Convert some values that can't be provided by dataProviders + $page = WikiPage::factory( $title ); + if ( isset( $details['user'] ) && $details['user'] === true ) { + $details['user'] = $this->getTestUser()->getUser(); + } + if ( isset( $details['page'] ) && $details['page'] === true ) { + $details['page'] = $page->getId(); + } + if ( isset( $details['parent'] ) && $details['parent'] === true ) { + $details['parent'] = $page->getLatest(); + } + + // Create the RevisionRecord with any available data + $rev = new MutableRevisionRecord( $title ); + isset( $details['slot'] ) ? $rev->setSlot( $details['slot'] ) : null; + isset( $details['parent'] ) ? $rev->setParentId( $details['parent'] ) : null; + isset( $details['page'] ) ? $rev->setPageId( $details['page'] ) : null; + isset( $details['size'] ) ? $rev->setSize( $details['size'] ) : null; + isset( $details['sha1'] ) ? $rev->setSha1( $details['sha1'] ) : null; + isset( $details['comment'] ) ? $rev->setComment( $details['comment'] ) : null; + isset( $details['timestamp'] ) ? $rev->setTimestamp( $details['timestamp'] ) : null; + isset( $details['minor'] ) ? $rev->setMinorEdit( $details['minor'] ) : null; + isset( $details['user'] ) ? $rev->setUser( $details['user'] ) : null; + isset( $details['visibility'] ) ? $rev->setVisibility( $details['visibility'] ) : null; + isset( $details['id'] ) ? $rev->setId( $details['id'] ) : null; + + return $rev; + } + + public function provideInsertRevisionOn_successes() { + yield 'Bare minimum revision insertion' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'parent' => true, + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + 'user' => true, + ], + ]; + yield 'Detailed revision insertion' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'parent' => true, + 'page' => true, + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + 'user' => true, + 'minor' => true, + 'visibility' => RevisionRecord::DELETED_RESTRICTED, + ], + ]; + } + + private function getRandomCommentStoreComment() { + return CommentStoreComment::newUnsavedComment( __METHOD__ . '.' . rand( 0, 1000 ) ); + } + + /** + * @dataProvider provideInsertRevisionOn_successes + * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn + */ + public function testInsertRevisionOn_successes( + Title $title, + array $revDetails = [] + ) { + $rev = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); + + $this->overrideMwServices(); + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $return = $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) ); + + $this->assertLinkTargetsEqual( $title, $return->getPageAsLinkTarget() ); + $this->assertRevisionRecordsEqual( $rev, $return ); + $this->assertRevisionCompleteness( $return ); + $this->assertRevisionExistsInDatabase( $return ); + } + + protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) { + $this->assertSelect( + 'revision', [ 'count(*)' ], [ 'rev_id' => $rev->getId() ], [ [ '1' ] ] + ); + } + + /** + * @param SlotRecord $a + * @param SlotRecord $b + */ + protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) { + // Assert that the same blob address has been used. + $this->assertSame( $a->getAddress(), $b->getAddress() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn + */ + public function testInsertRevisionOn_blobAddressExists() { + $title = Title::newFromText( 'UTPage' ); + $revDetails = [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'parent' => true, + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + 'user' => true, + ]; + + $this->overrideMwServices(); + $store = MediaWikiServices::getInstance()->getRevisionStore(); + + // Insert the first revision + $revOne = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); + $firstReturn = $store->insertRevisionOn( $revOne, wfGetDB( DB_MASTER ) ); + $this->assertLinkTargetsEqual( $title, $firstReturn->getPageAsLinkTarget() ); + $this->assertRevisionRecordsEqual( $revOne, $firstReturn ); + + // Insert a second revision inheriting the same blob address + $revDetails['slot'] = SlotRecord::newInherited( $firstReturn->getSlot( 'main' ) ); + $revTwo = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); + $secondReturn = $store->insertRevisionOn( $revTwo, wfGetDB( DB_MASTER ) ); + $this->assertLinkTargetsEqual( $title, $secondReturn->getPageAsLinkTarget() ); + $this->assertRevisionRecordsEqual( $revTwo, $secondReturn ); + + $firstMainSlot = $firstReturn->getSlot( 'main' ); + $secondMainSlot = $secondReturn->getSlot( 'main' ); + + $this->assertSameSlotContent( $firstMainSlot, $secondMainSlot ); + + // And that different revisions have been created. + $this->assertNotSame( $firstReturn->getId(), $secondReturn->getId() ); + + // Make sure the slot rows reference the correct revision + $this->assertSame( $firstReturn->getId(), $firstMainSlot->getRevision() ); + $this->assertSame( $secondReturn->getId(), $secondMainSlot->getRevision() ); + } + + public function provideInsertRevisionOn_failures() { + yield 'no slot' => [ + Title::newFromText( 'UTPage' ), + [ + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + 'user' => true, + ], + new InvalidArgumentException( 'At least one slot needs to be defined!' ) + ]; + yield 'slot that is not main slot' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'lalala', new WikitextContent( 'Chicken' ) ), + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + 'user' => true, + ], + new InvalidArgumentException( 'Only the main slot is supported for now!' ) + ]; + yield 'no timestamp' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'comment' => $this->getRandomCommentStoreComment(), + 'user' => true, + ], + new IncompleteRevisionException( 'timestamp field must not be NULL!' ) + ]; + yield 'no comment' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'timestamp' => '20171117010101', + 'user' => true, + ], + new IncompleteRevisionException( 'comment must not be NULL!' ) + ]; + yield 'no user' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + ], + new IncompleteRevisionException( 'user must not be NULL!' ) + ]; + } + + /** + * @dataProvider provideInsertRevisionOn_failures + * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn + */ + public function testInsertRevisionOn_failures( + Title $title, + array $revDetails = [], + Exception $exception + ) { + $rev = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + + $this->setExpectedException( + get_class( $exception ), + $exception->getMessage(), + $exception->getCode() + ); + $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) ); + } + + public function provideNewNullRevision() { + yield [ + Title::newFromText( 'UTPage_notAutoCreated' ), + CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment1' ), + true, + ]; + yield [ + Title::newFromText( 'UTPage_notAutoCreated' ), + CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment2', [ 'a' => 1 ] ), + false, + ]; + } + + /** + * @dataProvider provideNewNullRevision + * @covers \MediaWiki\Storage\RevisionStore::newNullRevision + */ + public function testNewNullRevision( Title $title, $comment, $minor ) { + $this->overrideMwServices(); + + $page = WikiPage::factory( $title ); + $status = $page->doEditContent( + new WikitextContent( __METHOD__ ), + __METHOD__, + 0, + false + ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $user = TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser(); + + $parent = $store->getRevisionById( $rev->getId() ); + $record = $store->newNullRevision( + wfGetDB( DB_MASTER ), + $title, + $comment, + $minor, + $user + ); + + $this->assertEquals( $title->getNamespace(), $record->getPageAsLinkTarget()->getNamespace() ); + $this->assertEquals( $title->getDBkey(), $record->getPageAsLinkTarget()->getDBkey() ); + $this->assertEquals( $comment, $record->getComment() ); + $this->assertEquals( $minor, $record->isMinor() ); + $this->assertEquals( $user->getName(), $record->getUser()->getName() ); + $this->assertEquals( $parent->getId(), $record->getParentId() ); + + $parentSlot = $parent->getSlot( 'main' ); + $slot = $record->getSlot( 'main' ); + + $this->assertTrue( $slot->isInherited(), 'isInherited' ); + $this->assertSame( $parentSlot->getOrigin(), $slot->getOrigin(), 'getOrigin' ); + $this->assertSameSlotContent( $parentSlot, $slot ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newNullRevision + */ + public function testNewNullRevision_nonExistingTitle() { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $record = $store->newNullRevision( + wfGetDB( DB_MASTER ), + Title::newFromText( __METHOD__ . '.iDontExist!' ), + CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment' ), + false, + TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser() + ); + $this->assertNull( $record ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled + */ + public function testGetRcIdIfUnpatrolled_returnsRecentChangesId() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $status = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revisionRecord = $store->getRevisionById( $rev->getId() ); + $result = $store->getRcIdIfUnpatrolled( $revisionRecord ); + + $this->assertGreaterThan( 0, $result ); + $this->assertSame( + $page->getRevision()->getRecentChange()->getAttribute( 'rc_id' ), + $result + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled + */ + public function testGetRcIdIfUnpatrolled_returnsZeroIfPatrolled() { + // This assumes that sysops are auto patrolled + $sysop = $this->getTestSysop()->getUser(); + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $status = $page->doEditContent( + new WikitextContent( __METHOD__ ), + __METHOD__, + 0, + false, + $sysop + ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revisionRecord = $store->getRevisionById( $rev->getId() ); + $result = $store->getRcIdIfUnpatrolled( $revisionRecord ); + + $this->assertSame( 0, $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRecentChange + */ + public function testGetRecentChange() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $content = new WikitextContent( __METHOD__ ); + $status = $page->doEditContent( $content, __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revRecord = $store->getRevisionById( $rev->getId() ); + $recentChange = $store->getRecentChange( $revRecord ); + + $this->assertEquals( $rev->getId(), $recentChange->getAttribute( 'rc_this_oldid' ) ); + $this->assertEquals( $rev->getRecentChange(), $recentChange ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRevisionById + */ + public function testGetRevisionById() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $content = new WikitextContent( __METHOD__ ); + $status = $page->doEditContent( $content, __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revRecord = $store->getRevisionById( $rev->getId() ); + + $this->assertSame( $rev->getId(), $revRecord->getId() ); + $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); + $this->assertSame( __METHOD__, $revRecord->getComment()->text ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRevisionByTitle + */ + public function testGetRevisionByTitle() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $content = new WikitextContent( __METHOD__ ); + $status = $page->doEditContent( $content, __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revRecord = $store->getRevisionByTitle( $page->getTitle() ); + + $this->assertSame( $rev->getId(), $revRecord->getId() ); + $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); + $this->assertSame( __METHOD__, $revRecord->getComment()->text ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRevisionByPageId + */ + public function testGetRevisionByPageId() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $content = new WikitextContent( __METHOD__ ); + $status = $page->doEditContent( $content, __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revRecord = $store->getRevisionByPageId( $page->getId() ); + + $this->assertSame( $rev->getId(), $revRecord->getId() ); + $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); + $this->assertSame( __METHOD__, $revRecord->getComment()->text ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRevisionByTimestamp + */ + public function testGetRevisionByTimestamp() { + // Make sure there is 1 second between the last revision and the rev we create... + // Otherwise we might not get the correct revision and the test may fail... + // :( + sleep( 1 ); + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $content = new WikitextContent( __METHOD__ ); + $status = $page->doEditContent( $content, __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revRecord = $store->getRevisionByTimestamp( + $page->getTitle(), + $rev->getTimestamp() + ); + + $this->assertSame( $rev->getId(), $revRecord->getId() ); + $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); + $this->assertSame( __METHOD__, $revRecord->getComment()->text ); + } + + protected function revisionToRow( Revision $rev ) { + $page = WikiPage::factory( $rev->getTitle() ); + + return (object)[ + 'rev_id' => (string)$rev->getId(), + 'rev_page' => (string)$rev->getPage(), + 'rev_text_id' => (string)$rev->getTextId(), + 'rev_timestamp' => $this->db->timestamp( $rev->getTimestamp() ), + 'rev_user_text' => (string)$rev->getUserText(), + 'rev_user' => (string)$rev->getUser(), + 'rev_minor_edit' => $rev->isMinor() ? '1' : '0', + 'rev_deleted' => (string)$rev->getVisibility(), + 'rev_len' => (string)$rev->getSize(), + 'rev_parent_id' => (string)$rev->getParentId(), + 'rev_sha1' => (string)$rev->getSha1(), + 'rev_comment_text' => $rev->getComment(), + 'rev_comment_data' => null, + 'rev_comment_cid' => null, + 'rev_content_format' => $rev->getContentFormat(), + 'rev_content_model' => $rev->getContentModel(), + 'page_namespace' => (string)$page->getTitle()->getNamespace(), + 'page_title' => $page->getTitle()->getDBkey(), + 'page_id' => (string)$page->getId(), + 'page_latest' => (string)$page->getLatest(), + 'page_is_redirect' => $page->isRedirect() ? '1' : '0', + 'page_len' => (string)$page->getContent()->getSize(), + 'user_name' => (string)$rev->getUserText(), + ]; + } + + private function assertRevisionRecordMatchesRevision( + Revision $rev, + RevisionRecord $record + ) { + $this->assertSame( $rev->getId(), $record->getId() ); + $this->assertSame( $rev->getPage(), $record->getPageId() ); + $this->assertSame( $rev->getTimestamp(), $record->getTimestamp() ); + $this->assertSame( $rev->getUserText(), $record->getUser()->getName() ); + $this->assertSame( $rev->getUser(), $record->getUser()->getId() ); + $this->assertSame( $rev->isMinor(), $record->isMinor() ); + $this->assertSame( $rev->getVisibility(), $record->getVisibility() ); + $this->assertSame( $rev->getSize(), $record->getSize() ); + /** + * @note As of MW 1.31, the database schema allows the parent ID to be + * NULL to indicate that it is unknown. + */ + $expectedParent = $rev->getParentId(); + if ( $expectedParent === null ) { + $expectedParent = 0; + } + $this->assertSame( $expectedParent, $record->getParentId() ); + $this->assertSame( $rev->getSha1(), $record->getSha1() ); + $this->assertSame( $rev->getComment(), $record->getComment()->text ); + $this->assertSame( $rev->getContentFormat(), $record->getContent( 'main' )->getDefaultFormat() ); + $this->assertSame( $rev->getContentModel(), $record->getContent( 'main' )->getModel() ); + $this->assertLinkTargetsEqual( $rev->getTitle(), $record->getPageAsLinkTarget() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29 + */ + public function testNewRevisionFromRow_anonEdit() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $text = __METHOD__ . 'a-ä'; + /** @var Revision $rev */ + $rev = $page->doEditContent( + new WikitextContent( $text ), + __METHOD__ . 'a' + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $record = $store->newRevisionFromRow( + $this->revisionToRow( $rev ), + [], + $page->getTitle() + ); + $this->assertRevisionRecordMatchesRevision( $rev, $record ); + $this->assertSame( $text, $rev->getContent()->serialize() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29 + */ + public function testNewRevisionFromRow_anonEdit_legacyEncoding() { + $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' ); + $this->overrideMwServices(); + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $text = __METHOD__ . 'a-ä'; + /** @var Revision $rev */ + $rev = $page->doEditContent( + new WikitextContent( $text ), + __METHOD__. 'a' + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $record = $store->newRevisionFromRow( + $this->revisionToRow( $rev ), + [], + $page->getTitle() + ); + $this->assertRevisionRecordMatchesRevision( $rev, $record ); + $this->assertSame( $text, $rev->getContent()->serialize() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29 + */ + public function testNewRevisionFromRow_userEdit() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $text = __METHOD__ . 'b-ä'; + /** @var Revision $rev */ + $rev = $page->doEditContent( + new WikitextContent( $text ), + __METHOD__ . 'b', + 0, + false, + $this->getTestUser()->getUser() + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $record = $store->newRevisionFromRow( + $this->revisionToRow( $rev ), + [], + $page->getTitle() + ); + $this->assertRevisionRecordMatchesRevision( $rev, $record ); + $this->assertSame( $text, $rev->getContent()->serialize() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow + */ + public function testNewRevisionFromArchiveRow() { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $title = Title::newFromText( __METHOD__ ); + $text = __METHOD__ . '-bä'; + $page = WikiPage::factory( $title ); + /** @var Revision $orig */ + $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ ) + ->value['revision']; + $page->doDeleteArticle( __METHOD__ ); + + $db = wfGetDB( DB_MASTER ); + $arQuery = $store->getArchiveQueryInfo(); + $res = $db->select( + $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ], + __METHOD__, [], $arQuery['joins'] + ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + $record = $store->newRevisionFromArchiveRow( $row ); + + $this->assertRevisionRecordMatchesRevision( $orig, $record ); + $this->assertSame( $text, $record->getContent( 'main' )->serialize() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow + */ + public function testNewRevisionFromArchiveRow_legacyEncoding() { + $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' ); + $this->overrideMwServices(); + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $title = Title::newFromText( __METHOD__ ); + $text = __METHOD__ . '-bä'; + $page = WikiPage::factory( $title ); + /** @var Revision $orig */ + $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ ) + ->value['revision']; + $page->doDeleteArticle( __METHOD__ ); + + $db = wfGetDB( DB_MASTER ); + $arQuery = $store->getArchiveQueryInfo(); + $res = $db->select( + $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ], + __METHOD__, [], $arQuery['joins'] + ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + $record = $store->newRevisionFromArchiveRow( $row ); + + $this->assertRevisionRecordMatchesRevision( $orig, $record ); + $this->assertSame( $text, $record->getContent( 'main' )->serialize() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromId + */ + public function testLoadRevisionFromId() { + $title = Title::newFromText( __METHOD__ ); + $page = WikiPage::factory( $title ); + /** @var Revision $rev */ + $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->loadRevisionFromId( wfGetDB( DB_MASTER ), $rev->getId() ); + $this->assertRevisionRecordMatchesRevision( $rev, $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromPageId + */ + public function testLoadRevisionFromPageId() { + $title = Title::newFromText( __METHOD__ ); + $page = WikiPage::factory( $title ); + /** @var Revision $rev */ + $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->loadRevisionFromPageId( wfGetDB( DB_MASTER ), $page->getId() ); + $this->assertRevisionRecordMatchesRevision( $rev, $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromTitle + */ + public function testLoadRevisionFromTitle() { + $title = Title::newFromText( __METHOD__ ); + $page = WikiPage::factory( $title ); + /** @var Revision $rev */ + $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->loadRevisionFromTitle( wfGetDB( DB_MASTER ), $title ); + $this->assertRevisionRecordMatchesRevision( $rev, $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromTimestamp + */ + public function testLoadRevisionFromTimestamp() { + $title = Title::newFromText( __METHOD__ ); + $page = WikiPage::factory( $title ); + /** @var Revision $revOne */ + $revOne = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + // Sleep to ensure different timestamps... )(evil) + sleep( 1 ); + /** @var Revision $revTwo */ + $revTwo = $page->doEditContent( new WikitextContent( __METHOD__ . 'a' ), '' ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $this->assertNull( + $store->loadRevisionFromTimestamp( wfGetDB( DB_MASTER ), $title, '20150101010101' ) + ); + $this->assertSame( + $revOne->getId(), + $store->loadRevisionFromTimestamp( + wfGetDB( DB_MASTER ), + $title, + $revOne->getTimestamp() + )->getId() + ); + $this->assertSame( + $revTwo->getId(), + $store->loadRevisionFromTimestamp( + wfGetDB( DB_MASTER ), + $title, + $revTwo->getTimestamp() + )->getId() + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::listRevisionSizes + */ + public function testGetParentLengths() { + $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); + /** @var Revision $revOne */ + $revOne = $page->doEditContent( + new WikitextContent( __METHOD__ ), __METHOD__ + )->value['revision']; + /** @var Revision $revTwo */ + $revTwo = $page->doEditContent( + new WikitextContent( __METHOD__ . '2' ), __METHOD__ + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $this->assertSame( + [ + $revOne->getId() => strlen( __METHOD__ ), + ], + $store->listRevisionSizes( + wfGetDB( DB_MASTER ), + [ $revOne->getId() ] + ) + ); + $this->assertSame( + [ + $revOne->getId() => strlen( __METHOD__ ), + $revTwo->getId() => strlen( __METHOD__ ) + 1, + ], + $store->listRevisionSizes( + wfGetDB( DB_MASTER ), + [ $revOne->getId(), $revTwo->getId() ] + ) + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getPreviousRevision + */ + public function testGetPreviousRevision() { + $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); + /** @var Revision $revOne */ + $revOne = $page->doEditContent( + new WikitextContent( __METHOD__ ), __METHOD__ + )->value['revision']; + /** @var Revision $revTwo */ + $revTwo = $page->doEditContent( + new WikitextContent( __METHOD__ . '2' ), __METHOD__ + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $this->assertNull( + $store->getPreviousRevision( $store->getRevisionById( $revOne->getId() ) ) + ); + $this->assertSame( + $revOne->getId(), + $store->getPreviousRevision( $store->getRevisionById( $revTwo->getId() ) )->getId() + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getNextRevision + */ + public function testGetNextRevision() { + $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); + /** @var Revision $revOne */ + $revOne = $page->doEditContent( + new WikitextContent( __METHOD__ ), __METHOD__ + )->value['revision']; + /** @var Revision $revTwo */ + $revTwo = $page->doEditContent( + new WikitextContent( __METHOD__ . '2' ), __METHOD__ + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $this->assertSame( + $revTwo->getId(), + $store->getNextRevision( $store->getRevisionById( $revOne->getId() ) )->getId() + ); + $this->assertNull( + $store->getNextRevision( $store->getRevisionById( $revTwo->getId() ) ) + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId + */ + public function testGetTimestampFromId_found() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + /** @var Revision $rev */ + $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->getTimestampFromId( + $page->getTitle(), + $rev->getId() + ); + + $this->assertSame( $rev->getTimestamp(), $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId + */ + public function testGetTimestampFromId_notFound() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + /** @var Revision $rev */ + $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->getTimestampFromId( + $page->getTitle(), + $rev->getId() + 1 + ); + + $this->assertFalse( $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::countRevisionsByPageId + */ + public function testCountRevisionsByPageId() { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); + + $this->assertSame( + 0, + $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() ) + ); + $page->doEditContent( new WikitextContent( 'a' ), 'a' ); + $this->assertSame( + 1, + $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() ) + ); + $page->doEditContent( new WikitextContent( 'b' ), 'b' ); + $this->assertSame( + 2, + $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() ) + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::countRevisionsByTitle + */ + public function testCountRevisionsByTitle() { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); + + $this->assertSame( + 0, + $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() ) + ); + $page->doEditContent( new WikitextContent( 'a' ), 'a' ); + $this->assertSame( + 1, + $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() ) + ); + $page->doEditContent( new WikitextContent( 'b' ), 'b' ); + $this->assertSame( + 2, + $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() ) + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::userWasLastToEdit + */ + public function testUserWasLastToEdit_false() { + $sysop = $this->getTestSysop()->getUser(); + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->userWasLastToEdit( + wfGetDB( DB_MASTER ), + $page->getId(), + $sysop->getId(), + '20160101010101' + ); + $this->assertFalse( $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::userWasLastToEdit + */ + public function testUserWasLastToEdit_true() { + $startTime = wfTimestampNow(); + $sysop = $this->getTestSysop()->getUser(); + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $page->doEditContent( + new WikitextContent( __METHOD__ ), + __METHOD__, + 0, + false, + $sysop + ); + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->userWasLastToEdit( + wfGetDB( DB_MASTER ), + $page->getId(), + $sysop->getId(), + $startTime + ); + $this->assertTrue( $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getKnownCurrentRevision + */ + public function testGetKnownCurrentRevision() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + /** @var Revision $rev */ + $rev = $page->doEditContent( + new WikitextContent( __METHOD__ . 'b' ), + __METHOD__ . 'b', + 0, + false, + $this->getTestUser()->getUser() + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $record = $store->getKnownCurrentRevision( + $page->getTitle(), + $rev->getId() + ); + + $this->assertRevisionRecordMatchesRevision( $rev, $record ); + } + + public function provideNewMutableRevisionFromArray() { + yield 'Basic array, with page & id' => [ + [ + 'id' => 2, + 'page' => 1, + 'text_id' => 2, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.2', + 'user' => 0, + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'content_format' => 'text/x-wiki', + 'content_model' => 'wikitext', + ] + ]; + yield 'Basic array, content object' => [ + [ + 'id' => 2, + 'page' => 1, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.2', + 'user' => 0, + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'content' => new WikitextContent( 'Some Content' ), + ] + ]; + yield 'Basic array, serialized text' => [ + [ + 'id' => 2, + 'page' => 1, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.2', + 'user' => 0, + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(), + ] + ]; + yield 'Basic array, serialized text, utf-8 flags' => [ + [ + 'id' => 2, + 'page' => 1, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.2', + 'user' => 0, + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(), + 'flags' => 'utf-8', + ] + ]; + yield 'Basic array, with title' => [ + [ + 'title' => Title::newFromText( 'SomeText' ), + 'text_id' => 2, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.2', + 'user' => 0, + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'content_format' => 'text/x-wiki', + 'content_model' => 'wikitext', + ] + ]; + yield 'Basic array, no user field' => [ + [ + 'id' => 2, + 'page' => 1, + 'text_id' => 2, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.3', + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'content_format' => 'text/x-wiki', + 'content_model' => 'wikitext', + ] + ]; + } + + /** + * @dataProvider provideNewMutableRevisionFromArray + * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray + */ + public function testNewMutableRevisionFromArray( array $array ) { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + + $result = $store->newMutableRevisionFromArray( $array ); + + if ( isset( $array['id'] ) ) { + $this->assertSame( $array['id'], $result->getId() ); + } + if ( isset( $array['page'] ) ) { + $this->assertSame( $array['page'], $result->getPageId() ); + } + $this->assertSame( $array['timestamp'], $result->getTimestamp() ); + $this->assertSame( $array['user_text'], $result->getUser()->getName() ); + if ( isset( $array['user'] ) ) { + $this->assertSame( $array['user'], $result->getUser()->getId() ); + } + $this->assertSame( (bool)$array['minor_edit'], $result->isMinor() ); + $this->assertSame( $array['deleted'], $result->getVisibility() ); + $this->assertSame( $array['len'], $result->getSize() ); + $this->assertSame( $array['parent_id'], $result->getParentId() ); + $this->assertSame( $array['sha1'], $result->getSha1() ); + $this->assertSame( $array['comment'], $result->getComment()->text ); + if ( isset( $array['content'] ) ) { + $this->assertTrue( + $result->getSlot( 'main' )->getContent()->equals( $array['content'] ) + ); + } elseif ( isset( $array['text'] ) ) { + $this->assertSame( $array['text'], $result->getSlot( 'main' )->getContent()->serialize() ); + } else { + $this->assertSame( + $array['content_format'], + $result->getSlot( 'main' )->getContent()->getDefaultFormat() + ); + $this->assertSame( $array['content_model'], $result->getSlot( 'main' )->getModel() ); + } + } + + /** + * @dataProvider provideNewMutableRevisionFromArray + * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray + */ + public function testNewMutableRevisionFromArray_legacyEncoding( array $array ) { + $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ); + $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); + $blobStore = new SqlBlobStore( $lb, $cache ); + $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) ); + + $factory = $this->getMockBuilder( BlobStoreFactory::class ) + ->setMethods( [ 'newBlobStore', 'newSqlBlobStore' ] ) + ->disableOriginalConstructor() + ->getMock(); + $factory->expects( $this->any() ) + ->method( 'newBlobStore' ) + ->willReturn( $blobStore ); + $factory->expects( $this->any() ) + ->method( 'newSqlBlobStore' ) + ->willReturn( $blobStore ); + + $this->setService( 'BlobStoreFactory', $factory ); + + $this->testNewMutableRevisionFromArray( $array ); + } + + protected function getDefaultQueryFields( $returnTextIdField = true ) { + $fields = [ + 'rev_id', + 'rev_page', + 'rev_timestamp', + 'rev_minor_edit', + 'rev_deleted', + 'rev_len', + 'rev_parent_id', + 'rev_sha1', + ]; + if ( $returnTextIdField ) { + $fields[] = 'rev_text_id'; + } + return $fields; + } + + protected function getCommentQueryFields() { + return [ + 'rev_comment_text' => 'rev_comment', + 'rev_comment_data' => 'NULL', + 'rev_comment_cid' => 'NULL', + ]; + } + + protected function getActorQueryFields() { + return [ + 'rev_user' => 'rev_user', + 'rev_user_text' => 'rev_user_text', + 'rev_actor' => 'NULL', + ]; + } + + protected function getContentHandlerQueryFields() { + return [ + 'rev_content_format', + 'rev_content_model', + ]; + } + + abstract public function provideGetQueryInfo(); + + /** + * @dataProvider provideGetQueryInfo + * @covers \MediaWiki\Storage\RevisionStore::getQueryInfo + */ + public function testGetQueryInfo( $options, $expected ) { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + + $queryInfo = $store->getQueryInfo( $options ); + + $this->assertArrayEqualsIgnoringIntKeyOrder( + $expected['tables'], + $queryInfo['tables'] + ); + $this->assertArrayEqualsIgnoringIntKeyOrder( + $expected['fields'], + $queryInfo['fields'] + ); + $this->assertArrayEqualsIgnoringIntKeyOrder( + $expected['joins'], + $queryInfo['joins'] + ); + } + + protected function getDefaultArchiveFields( $returnTextFields = true ) { + $fields = [ + 'ar_id', + 'ar_page_id', + 'ar_namespace', + 'ar_title', + 'ar_rev_id', + 'ar_timestamp', + 'ar_minor_edit', + 'ar_deleted', + 'ar_len', + 'ar_parent_id', + 'ar_sha1', + ]; + if ( $returnTextFields ) { + $fields[] = 'ar_text_id'; + } + return $fields; + } + + abstract public function provideGetArchiveQueryInfo(); + + /** + * @dataProvider provideGetArchiveQueryInfo + * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo + */ + public function testGetArchiveQueryInfo( $expected ) { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + + $archiveQueryInfo = $store->getArchiveQueryInfo(); + + $this->assertArrayEqualsIgnoringIntKeyOrder( + $expected['tables'], + $archiveQueryInfo['tables'] + ); + + $this->assertArrayEqualsIgnoringIntKeyOrder( + $expected['fields'], + $archiveQueryInfo['fields'] + ); + + $this->assertArrayEqualsIgnoringIntKeyOrder( + $expected['joins'], + $archiveQueryInfo['joins'] + ); + } + + /** + * Assert that the two arrays passed are equal, ignoring the order of the values that integer + * keys. + * + * Note: Failures of this assertion can be slightly confusing as the arrays are actually + * split into a string key array and an int key array before assertions occur. + * + * @param array $expected + * @param array $actual + */ + private function assertArrayEqualsIgnoringIntKeyOrder( array $expected, array $actual ) { + $this->objectAssociativeSort( $expected ); + $this->objectAssociativeSort( $actual ); + + // Separate the int key values from the string key values so that assertion failures are + // easier to understand. + $expectedIntKeyValues = []; + $actualIntKeyValues = []; + + // Remove all int keys and re add them at the end after sorting by value + // This will result in all int keys being in the same order with same ints at the end of + // the array + foreach ( $expected as $key => $value ) { + if ( is_int( $key ) ) { + unset( $expected[$key] ); + $expectedIntKeyValues[] = $value; + } + } + foreach ( $actual as $key => $value ) { + if ( is_int( $key ) ) { + unset( $actual[$key] ); + $actualIntKeyValues[] = $value; + } + } + + $this->assertArrayEquals( $expected, $actual, false, true ); + $this->assertArrayEquals( $expectedIntKeyValues, $actualIntKeyValues, false, true ); + } + +} diff --git a/tests/phpunit/includes/Storage/RevisionStoreTest.php b/tests/phpunit/includes/Storage/RevisionStoreTest.php index fed9a0c3d5..3749f294bf 100644 --- a/tests/phpunit/includes/Storage/RevisionStoreTest.php +++ b/tests/phpunit/includes/Storage/RevisionStoreTest.php @@ -247,87 +247,6 @@ class RevisionStoreTest extends MediaWikiTestCase { $this->assertEquals( $expected, $store->getQueryInfo( $options ) ); } - private function getDefaultArchiveFields() { - return [ - 'ar_id', - 'ar_page_id', - 'ar_namespace', - 'ar_title', - 'ar_rev_id', - 'ar_text_id', - 'ar_timestamp', - 'ar_minor_edit', - 'ar_deleted', - 'ar_len', - 'ar_parent_id', - 'ar_sha1', - ]; - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo - */ - public function testGetArchiveQueryInfo_contentHandlerDb() { - $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD ); - $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD ); - $this->overrideMwServices(); - $store = $this->getRevisionStore(); - $store->setContentHandlerUseDB( true ); - $this->assertEquals( - [ - 'tables' => [ - 'archive' - ], - 'fields' => array_merge( - $this->getDefaultArchiveFields(), - [ - 'ar_comment_text' => 'ar_comment', - 'ar_comment_data' => 'NULL', - 'ar_comment_cid' => 'NULL', - 'ar_user_text' => 'ar_user_text', - 'ar_user' => 'ar_user', - 'ar_actor' => 'NULL', - 'ar_content_format', - 'ar_content_model', - ] - ), - 'joins' => [], - ], - $store->getArchiveQueryInfo() - ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo - */ - public function testGetArchiveQueryInfo_noContentHandlerDb() { - $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD ); - $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD ); - $this->overrideMwServices(); - $store = $this->getRevisionStore(); - $store->setContentHandlerUseDB( false ); - $this->assertEquals( - [ - 'tables' => [ - 'archive' - ], - 'fields' => array_merge( - $this->getDefaultArchiveFields(), - [ - 'ar_comment_text' => 'ar_comment', - 'ar_comment_data' => 'NULL', - 'ar_comment_cid' => 'NULL', - 'ar_user_text' => 'ar_user_text', - 'ar_user' => 'ar_user', - 'ar_actor' => 'NULL', - ] - ), - 'joins' => [], - ], - $store->getArchiveQueryInfo() - ); - } - public function testGetTitle_successFromPageId() { $mockLoadBalancer = $this->getMockLoadBalancer(); // Title calls wfGetDB() so we have to set the main service diff --git a/tests/phpunit/includes/Storage/create-pre-mcr-fields.sql b/tests/phpunit/includes/Storage/create-pre-mcr-fields.sql new file mode 100644 index 0000000000..09deb4f2cd --- /dev/null +++ b/tests/phpunit/includes/Storage/create-pre-mcr-fields.sql @@ -0,0 +1,3 @@ +ALTER TABLE /*_*/revision ADD rev_text_id INTEGER DEFAULT 0; +ALTER TABLE /*_*/revision ADD rev_content_model VARBINARY(32) DEFAULT NULL; +ALTER TABLE /*_*/revision ADD rev_content_format VARBINARY(64) DEFAULT NULL; diff --git a/tests/phpunit/includes/Storage/drop-mcr-tables.sql b/tests/phpunit/includes/Storage/drop-mcr-tables.sql new file mode 100644 index 0000000000..bc89edc95e --- /dev/null +++ b/tests/phpunit/includes/Storage/drop-mcr-tables.sql @@ -0,0 +1,4 @@ +DROP TABLE /*_*/slots; +DROP TABLE /*_*/content; +DROP TABLE /*_*/content_models; +DROP TABLE /*_*/slot_roles; diff --git a/tests/phpunit/includes/page/WikiPageContentHandlerDbTest.php b/tests/phpunit/includes/page/WikiPageContentHandlerDbTest.php deleted file mode 100644 index 2d7d6cc3aa..0000000000 --- a/tests/phpunit/includes/page/WikiPageContentHandlerDbTest.php +++ /dev/null @@ -1,42 +0,0 @@ -createPage( - __METHOD__, - "some text", - CONTENT_MODEL_JAVASCRIPT - ); - - $page = new WikiPage( $page->getTitle() ); - $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $page->getContentModel() ); - } - - /** - * @covers WikiPage::getContentHandler - */ - public function testGetContentHandler() { - $page = $this->createPage( - __METHOD__, - "some text", - CONTENT_MODEL_JAVASCRIPT - ); - - $page = new WikiPage( $page->getTitle() ); - $this->assertEquals( JavaScriptContentHandler::class, get_class( $page->getContentHandler() ) ); - } - -} diff --git a/tests/phpunit/includes/page/WikiPageDbTestBase.php b/tests/phpunit/includes/page/WikiPageDbTestBase.php index 40c4e1ecd3..b150da18af 100644 --- a/tests/phpunit/includes/page/WikiPageDbTestBase.php +++ b/tests/phpunit/includes/page/WikiPageDbTestBase.php @@ -30,10 +30,29 @@ abstract class WikiPageDbTestBase extends MediaWikiLangTestCase { 'iwlinks' ] ); } + /** + * @return int + */ + abstract protected function getMcrMigrationStage(); + + /** + * @return string[] + */ + abstract protected function getMcrTablesToReset(); + protected function setUp() { parent::setUp(); + + $this->tablesUsed += $this->getMcrTablesToReset(); + $this->setMwGlobals( 'wgContentHandlerUseDB', $this->getContentHandlerUseDB() ); + $this->setMwGlobals( + 'wgMultiContentRevisionSchemaMigrationStage', + $this->getMcrMigrationStage() + ); $this->pagesToDelete = []; + + $this->overrideMwServices(); } protected function tearDown() { diff --git a/tests/phpunit/includes/page/WikiPageNoContentHandlerDbTest.php b/tests/phpunit/includes/page/WikiPageNoContentHandlerDbTest.php deleted file mode 100644 index a6ce185a12..0000000000 --- a/tests/phpunit/includes/page/WikiPageNoContentHandlerDbTest.php +++ /dev/null @@ -1,14 +0,0 @@ -createPage( + __METHOD__, + "some text", + CONTENT_MODEL_JAVASCRIPT + ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $page->getContentModel() ); + } + + /** + * @covers WikiPage::getContentHandler + */ + public function testGetContentHandler() { + $page = $this->createPage( + __METHOD__, + "some text", + CONTENT_MODEL_JAVASCRIPT + ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( JavaScriptContentHandler::class, get_class( $page->getContentHandler() ) ); + } + +}