3 namespace MediaWiki\Tests\Revision
;
7 use InvalidArgumentException
;
9 use MediaWiki\MediaWikiServices
;
10 use MediaWiki\Revision\RevisionAccessException
;
11 use MediaWiki\Revision\RevisionStore
;
12 use MediaWiki\Revision\SlotRoleRegistry
;
13 use MediaWiki\Revision\SlotRecord
;
14 use MediaWiki\Storage\SqlBlobStore
;
15 use Wikimedia\Rdbms\ILoadBalancer
;
16 use Wikimedia\Rdbms\MaintainableDBConnRef
;
17 use MediaWikiTestCase
;
21 use Wikimedia\Rdbms\IDatabase
;
22 use Wikimedia\Rdbms\LoadBalancer
;
23 use Wikimedia\TestingAccessWrapper
;
29 class RevisionStoreTest
extends MediaWikiTestCase
{
31 private function useTextId() {
32 global $wgMultiContentRevisionSchemaMigrationStage;
34 return (bool)( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_OLD
);
38 * @param LoadBalancer $loadBalancer
39 * @param SqlBlobStore $blobStore
40 * @param WANObjectCache $WANObjectCache
42 * @return RevisionStore
44 private function getRevisionStore(
47 $WANObjectCache = null
49 global $wgMultiContentRevisionSchemaMigrationStage;
50 // the migration stage should be irrelevant, since all the tests that interact with
51 // the database are in RevisionStoreDbTest, not here.
53 return new RevisionStore(
54 $loadBalancer ?
: $this->getMockLoadBalancer(),
55 $blobStore ?
: $this->getMockSqlBlobStore(),
56 $WANObjectCache ?
: $this->getHashWANObjectCache(),
57 MediaWikiServices
::getInstance()->getCommentStore(),
58 MediaWikiServices
::getInstance()->getContentModelStore(),
59 MediaWikiServices
::getInstance()->getSlotRoleStore(),
60 MediaWikiServices
::getInstance()->getSlotRoleRegistry(),
61 $wgMultiContentRevisionSchemaMigrationStage,
62 MediaWikiServices
::getInstance()->getActorMigration()
67 * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
69 private function getMockLoadBalancer() {
70 return $this->getMockBuilder( LoadBalancer
::class )
71 ->disableOriginalConstructor()->getMock();
75 * @return \PHPUnit_Framework_MockObject_MockObject|IDatabase
77 private function getMockDatabase() {
78 return $this->getMockBuilder( IDatabase
::class )
79 ->disableOriginalConstructor()->getMock();
83 * @param ILoadBalancer $mockLoadBalancer
87 private function getMockDBConnRefCallback( ILoadBalancer
$mockLoadBalancer, IDatabase
$db ) {
88 return function ( $i, $g, $domain, $flg ) use ( $mockLoadBalancer, $db ) {
89 return new MaintainableDBConnRef( $mockLoadBalancer, $db, $i );
94 * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
96 private function getMockSqlBlobStore() {
97 return $this->getMockBuilder( SqlBlobStore
::class )
98 ->disableOriginalConstructor()->getMock();
102 * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
104 private function getMockCommentStore() {
105 return $this->getMockBuilder( CommentStore
::class )
106 ->disableOriginalConstructor()->getMock();
110 * @return \PHPUnit_Framework_MockObject_MockObject|SlotRoleRegistry
112 private function getMockSlotRoleRegistry() {
113 return $this->getMockBuilder( SlotRoleRegistry
::class )
114 ->disableOriginalConstructor()->getMock();
117 private function getHashWANObjectCache() {
118 return new WANObjectCache( [ 'cache' => new \
HashBagOStuff() ] );
121 public function provideSetContentHandlerUseDB() {
123 // ContentHandlerUseDB can be true or false pre migration.
124 // During and after migration it can not be false...
125 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, true ],
126 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, true ],
127 [ false, SCHEMA_COMPAT_NEW
, true ],
128 // ...but it can be true.
129 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
130 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
131 [ true, SCHEMA_COMPAT_NEW
, false ],
136 * @dataProvider provideSetContentHandlerUseDB
137 * @covers \MediaWiki\Revision\RevisionStore::getContentHandlerUseDB
138 * @covers \MediaWiki\Revision\RevisionStore::setContentHandlerUseDB
140 public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
141 if ( $expectedFail ) {
142 $this->setExpectedException( MWException
::class );
145 $nameTables = MediaWikiServices
::getInstance()->getNameTableStoreFactory();
147 $store = new RevisionStore(
148 $this->getMockLoadBalancer(),
149 $this->getMockSqlBlobStore(),
150 $this->getHashWANObjectCache(),
151 $this->getMockCommentStore(),
152 $nameTables->getContentModels(),
153 $nameTables->getSlotRoles(),
154 $this->getMockSlotRoleRegistry(),
156 MediaWikiServices
::getInstance()->getActorMigration()
159 $store->setContentHandlerUseDB( $contentHandlerDb );
160 $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
164 * @covers \MediaWiki\Revision\RevisionStore::getTitle
166 public function testGetTitle_successFromPageId() {
167 $mockLoadBalancer = $this->getMockLoadBalancer();
168 // Title calls wfGetDB() so we have to set the main service
169 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
171 $db = $this->getMockDatabase();
172 // RevisionStore uses getConnectionRef
173 $mockLoadBalancer->expects( $this->any() )
174 ->method( 'getConnectionRef' )
175 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
176 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
177 $mockLoadBalancer->expects( $this->atLeastOnce() )
178 ->method( 'getMaintenanceConnectionRef' )
179 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
181 // First call to Title::newFromID, faking no result (db lag?)
182 $db->expects( $this->at( 0 ) )
183 ->method( 'selectRow' )
189 ->willReturn( (object)[
190 'page_namespace' => '1',
191 'page_title' => 'Food',
194 $store = $this->getRevisionStore( $mockLoadBalancer );
195 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
197 $this->assertSame( 1, $title->getNamespace() );
198 $this->assertSame( 'Food', $title->getDBkey() );
202 * @covers \MediaWiki\Revision\RevisionStore::getTitle
204 public function testGetTitle_successFromPageIdOnFallback() {
205 $mockLoadBalancer = $this->getMockLoadBalancer();
206 // Title calls wfGetDB() so we have to set the main service
207 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
209 $db = $this->getMockDatabase();
210 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
211 // Assert that the first call uses a REPLICA and the second falls back to master
212 $mockLoadBalancer->expects( $this->atLeastOnce() )
213 ->method( 'getConnectionRef' )
214 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
215 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
216 $mockLoadBalancer->expects( $this->exactly( 2 ) )
217 ->method( 'getMaintenanceConnectionRef' )
218 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
220 // First call to Title::newFromID, faking no result (db lag?)
221 $db->expects( $this->at( 0 ) )
222 ->method( 'selectRow' )
228 ->willReturn( false );
230 // First select using rev_id, faking no result (db lag?)
231 $db->expects( $this->at( 1 ) )
232 ->method( 'selectRow' )
234 [ 'revision', 'page' ],
238 ->willReturn( false );
240 // Second call to Title::newFromID, no result
241 $db->expects( $this->at( 2 ) )
242 ->method( 'selectRow' )
248 ->willReturn( (object)[
249 'page_namespace' => '2',
250 'page_title' => 'Foodey',
253 $store = $this->getRevisionStore( $mockLoadBalancer );
254 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
256 $this->assertSame( 2, $title->getNamespace() );
257 $this->assertSame( 'Foodey', $title->getDBkey() );
261 * @covers \MediaWiki\Revision\RevisionStore::getTitle
263 public function testGetTitle_successFromRevId() {
264 $mockLoadBalancer = $this->getMockLoadBalancer();
265 // Title calls wfGetDB() so we have to set the main service
266 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
268 $db = $this->getMockDatabase();
269 $mockLoadBalancer->expects( $this->atLeastOnce() )
270 ->method( 'getConnectionRef' )
271 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
272 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
273 // RevisionStore getTitle uses getMaintenanceConnectionRef
274 $mockLoadBalancer->expects( $this->atLeastOnce() )
275 ->method( 'getMaintenanceConnectionRef' )
276 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
278 // First call to Title::newFromID, faking no result (db lag?)
279 $db->expects( $this->at( 0 ) )
280 ->method( 'selectRow' )
286 ->willReturn( false );
288 // First select using rev_id, faking no result (db lag?)
289 $db->expects( $this->at( 1 ) )
290 ->method( 'selectRow' )
292 [ 'revision', 'page' ],
296 ->willReturn( (object)[
297 'page_namespace' => '1',
298 'page_title' => 'Food2',
301 $store = $this->getRevisionStore( $mockLoadBalancer );
302 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
304 $this->assertSame( 1, $title->getNamespace() );
305 $this->assertSame( 'Food2', $title->getDBkey() );
309 * @covers \MediaWiki\Revision\RevisionStore::getTitle
311 public function testGetTitle_successFromRevIdOnFallback() {
312 $mockLoadBalancer = $this->getMockLoadBalancer();
313 // Title calls wfGetDB() so we have to set the main service
314 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
316 $db = $this->getMockDatabase();
317 // Assert that the first call uses a REPLICA and the second falls back to master
318 // RevisionStore uses getMaintenanceConnectionRef
319 $mockLoadBalancer->expects( $this->atLeastOnce() )
320 ->method( 'getConnectionRef' )
321 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
322 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
323 $mockLoadBalancer->expects( $this->exactly( 2 ) )
324 ->method( 'getMaintenanceConnectionRef' )
325 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
327 // First call to Title::newFromID, faking no result (db lag?)
328 $db->expects( $this->at( 0 ) )
329 ->method( 'selectRow' )
335 ->willReturn( false );
337 // First select using rev_id, faking no result (db lag?)
338 $db->expects( $this->at( 1 ) )
339 ->method( 'selectRow' )
341 [ 'revision', 'page' ],
345 ->willReturn( false );
347 // Second call to Title::newFromID, no result
348 $db->expects( $this->at( 2 ) )
349 ->method( 'selectRow' )
355 ->willReturn( false );
357 // Second select using rev_id, result
358 $db->expects( $this->at( 3 ) )
359 ->method( 'selectRow' )
361 [ 'revision', 'page' ],
365 ->willReturn( (object)[
366 'page_namespace' => '2',
367 'page_title' => 'Foodey',
370 $store = $this->getRevisionStore( $mockLoadBalancer );
371 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
373 $this->assertSame( 2, $title->getNamespace() );
374 $this->assertSame( 'Foodey', $title->getDBkey() );
378 * @covers \MediaWiki\Revision\RevisionStore::getTitle
380 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
381 $mockLoadBalancer = $this->getMockLoadBalancer();
382 // Title calls wfGetDB() so we have to set the main service
383 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
385 $db = $this->getMockDatabase();
386 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
387 // Assert that the first call uses a REPLICA and the second falls back to master
389 // RevisionStore getTitle uses getConnectionRef
390 // Title::newFromID uses getMaintenanceConnectionRef
392 'getConnectionRef', 'getMaintenanceConnectionRef'
394 $mockLoadBalancer->expects( $this->exactly( 2 ) )
396 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
397 static $callCounter = 0;
399 // The first call should be to a REPLICA, and the second a MASTER.
400 if ( $callCounter === 1 ) {
401 $this->assertSame( DB_REPLICA
, $masterOrReplica );
402 } elseif ( $callCounter === 2 ) {
403 $this->assertSame( DB_MASTER
, $masterOrReplica );
408 // First and third call to Title::newFromID, faking no result
409 foreach ( [ 0, 2 ] as $counter ) {
410 $db->expects( $this->at( $counter ) )
411 ->method( 'selectRow' )
417 ->willReturn( false );
420 foreach ( [ 1, 3 ] as $counter ) {
421 $db->expects( $this->at( $counter ) )
422 ->method( 'selectRow' )
424 [ 'revision', 'page' ],
428 ->willReturn( false );
431 $store = $this->getRevisionStore( $mockLoadBalancer );
433 $this->setExpectedException( RevisionAccessException
::class );
434 $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
437 public function provideNewRevisionFromRow_legacyEncoding_applied() {
438 yield
'windows-1252, old_flags is empty' => [
443 'old_text' => "S\xF6me Content",
448 yield
'windows-1252, old_flags is null' => [
453 'old_text' => "S\xF6me Content",
460 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
462 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
464 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
465 if ( !$this->useTextId() ) {
466 $this->markTestSkipped( 'No longer applicable with MCR schema' );
469 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
470 $services = MediaWikiServices
::getInstance();
471 $lb = $services->getDBLoadBalancer();
472 $access = $services->getExternalStoreAccess();
474 $blobStore = new SqlBlobStore( $lb, $access, $cache );
476 $blobStore->setLegacyEncoding( $encoding, Language
::factory( $locale ) );
478 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
480 $record = $store->newRevisionFromRow(
481 $this->makeRow( $row ),
483 Title
::newFromText( __METHOD__
. '-UTPage' )
486 $this->assertSame( $text, $record->getContent( SlotRecord
::MAIN
)->serialize() );
490 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
492 public function testNewRevisionFromRow_legacyEncoding_ignored() {
493 if ( !$this->useTextId() ) {
494 $this->markTestSkipped( 'No longer applicable with MCR schema' );
498 'old_flags' => 'utf-8',
499 'old_text' => 'Söme Content',
502 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
503 $services = MediaWikiServices
::getInstance();
504 $lb = $services->getDBLoadBalancer();
505 $access = $services->getExternalStoreAccess();
507 $blobStore = new SqlBlobStore( $lb, $access, $cache );
508 $blobStore->setLegacyEncoding( 'windows-1252', Language
::factory( 'en' ) );
510 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
512 $record = $store->newRevisionFromRow(
513 $this->makeRow( $row ),
515 Title
::newFromText( __METHOD__
. '-UTPage' )
517 $this->assertSame( 'Söme Content', $record->getContent( SlotRecord
::MAIN
)->serialize() );
520 private function makeRow( array $array ) {
524 'rev_timestamp' => '20110101000000',
525 'rev_user_text' => 'Tester',
527 'rev_minor_edit' => 0,
530 'rev_parent_id' => 0,
531 'rev_sha1' => 'deadbeef',
532 'rev_comment_text' => 'Testing',
533 'rev_comment_data' => '{}',
534 'rev_comment_cid' => 111,
535 'page_namespace' => 0,
536 'page_title' => 'TEST',
539 'page_is_redirect' => 0,
541 'user_name' => 'Tester',
544 if ( $this->useTextId() ) {
546 'rev_content_format' => CONTENT_FORMAT_TEXT
,
547 'rev_content_model' => CONTENT_MODEL_TEXT
,
550 'old_text' => 'Hello World',
551 'old_flags' => 'utf-8',
553 } elseif ( !isset( $row['content'] ) && isset( $array['old_text'] ) ) {
555 'main' => new WikitextContent( $array['old_text'] ),
562 public function provideMigrationConstruction() {
564 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
565 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
566 [ SCHEMA_COMPAT_NEW
, false ],
567 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH
, true ],
568 [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH
, true ],
569 [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH
, true ],
574 * @covers \MediaWiki\Revision\RevisionStore::__construct
575 * @dataProvider provideMigrationConstruction
577 public function testMigrationConstruction( $migration, $expectException ) {
578 if ( $expectException ) {
579 $this->setExpectedException( InvalidArgumentException
::class );
581 $loadBalancer = $this->getMockLoadBalancer();
582 $blobStore = $this->getMockSqlBlobStore();
583 $cache = $this->getHashWANObjectCache();
584 $commentStore = $this->getMockCommentStore();
585 $services = MediaWikiServices
::getInstance();
586 $nameTables = $services->getNameTableStoreFactory();
587 $contentModelStore = $nameTables->getContentModels();
588 $slotRoleStore = $nameTables->getSlotRoles();
589 $slotRoleRegistry = $services->getSlotRoleRegistry();
590 $store = new RevisionStore(
595 $nameTables->getContentModels(),
596 $nameTables->getSlotRoles(),
599 $services->getActorMigration()
601 if ( !$expectException ) {
602 $store = TestingAccessWrapper
::newFromObject( $store );
603 $this->assertSame( $loadBalancer, $store->loadBalancer
);
604 $this->assertSame( $blobStore, $store->blobStore
);
605 $this->assertSame( $cache, $store->cache
);
606 $this->assertSame( $commentStore, $store->commentStore
);
607 $this->assertSame( $contentModelStore, $store->contentModelStore
);
608 $this->assertSame( $slotRoleStore, $store->slotRoleStore
);
609 $this->assertSame( $migration, $store->mcrMigrationStage
);