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 of false pre migration.
124 [ false, SCHEMA_COMPAT_OLD
, false ],
125 [ true, SCHEMA_COMPAT_OLD
, false ],
126 // During and after migration it can not be false...
127 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, true ],
128 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, true ],
129 [ false, SCHEMA_COMPAT_NEW
, true ],
130 // ...but it can be true.
131 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
132 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
133 [ true, SCHEMA_COMPAT_NEW
, false ],
138 * @dataProvider provideSetContentHandlerUseDB
139 * @covers \MediaWiki\Revision\RevisionStore::getContentHandlerUseDB
140 * @covers \MediaWiki\Revision\RevisionStore::setContentHandlerUseDB
142 public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
143 if ( $expectedFail ) {
144 $this->setExpectedException( MWException
::class );
147 $nameTables = MediaWikiServices
::getInstance()->getNameTableStoreFactory();
149 $store = new RevisionStore(
150 $this->getMockLoadBalancer(),
151 $this->getMockSqlBlobStore(),
152 $this->getHashWANObjectCache(),
153 $this->getMockCommentStore(),
154 $nameTables->getContentModels(),
155 $nameTables->getSlotRoles(),
156 $this->getMockSlotRoleRegistry(),
158 MediaWikiServices
::getInstance()->getActorMigration()
161 $store->setContentHandlerUseDB( $contentHandlerDb );
162 $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
166 * @covers \MediaWiki\Revision\RevisionStore::getTitle
168 public function testGetTitle_successFromPageId() {
169 $mockLoadBalancer = $this->getMockLoadBalancer();
170 // Title calls wfGetDB() so we have to set the main service
171 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
173 $db = $this->getMockDatabase();
174 // RevisionStore uses getConnectionRef
175 $mockLoadBalancer->expects( $this->any() )
176 ->method( 'getConnectionRef' )
177 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
178 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
179 $mockLoadBalancer->expects( $this->atLeastOnce() )
180 ->method( 'getMaintenanceConnectionRef' )
181 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
183 // First call to Title::newFromID, faking no result (db lag?)
184 $db->expects( $this->at( 0 ) )
185 ->method( 'selectRow' )
191 ->willReturn( (object)[
192 'page_namespace' => '1',
193 'page_title' => 'Food',
196 $store = $this->getRevisionStore( $mockLoadBalancer );
197 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
199 $this->assertSame( 1, $title->getNamespace() );
200 $this->assertSame( 'Food', $title->getDBkey() );
204 * @covers \MediaWiki\Revision\RevisionStore::getTitle
206 public function testGetTitle_successFromPageIdOnFallback() {
207 $mockLoadBalancer = $this->getMockLoadBalancer();
208 // Title calls wfGetDB() so we have to set the main service
209 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
211 $db = $this->getMockDatabase();
212 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
213 // Assert that the first call uses a REPLICA and the second falls back to master
214 $mockLoadBalancer->expects( $this->atLeastOnce() )
215 ->method( 'getConnectionRef' )
216 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
217 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
218 $mockLoadBalancer->expects( $this->exactly( 2 ) )
219 ->method( 'getMaintenanceConnectionRef' )
220 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
222 // First call to Title::newFromID, faking no result (db lag?)
223 $db->expects( $this->at( 0 ) )
224 ->method( 'selectRow' )
230 ->willReturn( false );
232 // First select using rev_id, faking no result (db lag?)
233 $db->expects( $this->at( 1 ) )
234 ->method( 'selectRow' )
236 [ 'revision', 'page' ],
240 ->willReturn( false );
242 // Second call to Title::newFromID, no result
243 $db->expects( $this->at( 2 ) )
244 ->method( 'selectRow' )
250 ->willReturn( (object)[
251 'page_namespace' => '2',
252 'page_title' => 'Foodey',
255 $store = $this->getRevisionStore( $mockLoadBalancer );
256 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
258 $this->assertSame( 2, $title->getNamespace() );
259 $this->assertSame( 'Foodey', $title->getDBkey() );
263 * @covers \MediaWiki\Revision\RevisionStore::getTitle
265 public function testGetTitle_successFromRevId() {
266 $mockLoadBalancer = $this->getMockLoadBalancer();
267 // Title calls wfGetDB() so we have to set the main service
268 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
270 $db = $this->getMockDatabase();
271 $mockLoadBalancer->expects( $this->atLeastOnce() )
272 ->method( 'getConnectionRef' )
273 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
274 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
275 // RevisionStore getTitle uses getMaintenanceConnectionRef
276 $mockLoadBalancer->expects( $this->atLeastOnce() )
277 ->method( 'getMaintenanceConnectionRef' )
278 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
280 // First call to Title::newFromID, faking no result (db lag?)
281 $db->expects( $this->at( 0 ) )
282 ->method( 'selectRow' )
288 ->willReturn( false );
290 // First select using rev_id, faking no result (db lag?)
291 $db->expects( $this->at( 1 ) )
292 ->method( 'selectRow' )
294 [ 'revision', 'page' ],
298 ->willReturn( (object)[
299 'page_namespace' => '1',
300 'page_title' => 'Food2',
303 $store = $this->getRevisionStore( $mockLoadBalancer );
304 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
306 $this->assertSame( 1, $title->getNamespace() );
307 $this->assertSame( 'Food2', $title->getDBkey() );
311 * @covers \MediaWiki\Revision\RevisionStore::getTitle
313 public function testGetTitle_successFromRevIdOnFallback() {
314 $mockLoadBalancer = $this->getMockLoadBalancer();
315 // Title calls wfGetDB() so we have to set the main service
316 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
318 $db = $this->getMockDatabase();
319 // Assert that the first call uses a REPLICA and the second falls back to master
320 // RevisionStore uses getMaintenanceConnectionRef
321 $mockLoadBalancer->expects( $this->atLeastOnce() )
322 ->method( 'getConnectionRef' )
323 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
324 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
325 $mockLoadBalancer->expects( $this->exactly( 2 ) )
326 ->method( 'getMaintenanceConnectionRef' )
327 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
329 // First call to Title::newFromID, faking no result (db lag?)
330 $db->expects( $this->at( 0 ) )
331 ->method( 'selectRow' )
337 ->willReturn( false );
339 // First select using rev_id, faking no result (db lag?)
340 $db->expects( $this->at( 1 ) )
341 ->method( 'selectRow' )
343 [ 'revision', 'page' ],
347 ->willReturn( false );
349 // Second call to Title::newFromID, no result
350 $db->expects( $this->at( 2 ) )
351 ->method( 'selectRow' )
357 ->willReturn( false );
359 // Second select using rev_id, result
360 $db->expects( $this->at( 3 ) )
361 ->method( 'selectRow' )
363 [ 'revision', 'page' ],
367 ->willReturn( (object)[
368 'page_namespace' => '2',
369 'page_title' => 'Foodey',
372 $store = $this->getRevisionStore( $mockLoadBalancer );
373 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
375 $this->assertSame( 2, $title->getNamespace() );
376 $this->assertSame( 'Foodey', $title->getDBkey() );
380 * @covers \MediaWiki\Revision\RevisionStore::getTitle
382 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
383 $mockLoadBalancer = $this->getMockLoadBalancer();
384 // Title calls wfGetDB() so we have to set the main service
385 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
387 $db = $this->getMockDatabase();
388 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
389 // Assert that the first call uses a REPLICA and the second falls back to master
391 // RevisionStore getTitle uses getConnectionRef
392 // Title::newFromID uses getMaintenanceConnectionRef
394 'getConnectionRef', 'getMaintenanceConnectionRef'
396 $mockLoadBalancer->expects( $this->exactly( 2 ) )
398 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
399 static $callCounter = 0;
401 // The first call should be to a REPLICA, and the second a MASTER.
402 if ( $callCounter === 1 ) {
403 $this->assertSame( DB_REPLICA
, $masterOrReplica );
404 } elseif ( $callCounter === 2 ) {
405 $this->assertSame( DB_MASTER
, $masterOrReplica );
410 // First and third call to Title::newFromID, faking no result
411 foreach ( [ 0, 2 ] as $counter ) {
412 $db->expects( $this->at( $counter ) )
413 ->method( 'selectRow' )
419 ->willReturn( false );
422 foreach ( [ 1, 3 ] as $counter ) {
423 $db->expects( $this->at( $counter ) )
424 ->method( 'selectRow' )
426 [ 'revision', 'page' ],
430 ->willReturn( false );
433 $store = $this->getRevisionStore( $mockLoadBalancer );
435 $this->setExpectedException( RevisionAccessException
::class );
436 $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
439 public function provideNewRevisionFromRow_legacyEncoding_applied() {
440 yield
'windows-1252, old_flags is empty' => [
445 'old_text' => "S\xF6me Content",
450 yield
'windows-1252, old_flags is null' => [
455 'old_text' => "S\xF6me Content",
462 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
464 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
466 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
467 if ( !$this->useTextId() ) {
468 $this->markTestSkipped( 'No longer applicable with MCR schema' );
471 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
472 $services = MediaWikiServices
::getInstance();
473 $lb = $services->getDBLoadBalancer();
474 $access = $services->getExternalStoreAccess();
476 $blobStore = new SqlBlobStore( $lb, $access, $cache );
478 $blobStore->setLegacyEncoding( $encoding, Language
::factory( $locale ) );
480 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
482 $record = $store->newRevisionFromRow(
483 $this->makeRow( $row ),
485 Title
::newFromText( __METHOD__
. '-UTPage' )
488 $this->assertSame( $text, $record->getContent( SlotRecord
::MAIN
)->serialize() );
492 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
494 public function testNewRevisionFromRow_legacyEncoding_ignored() {
495 if ( !$this->useTextId() ) {
496 $this->markTestSkipped( 'No longer applicable with MCR schema' );
500 'old_flags' => 'utf-8',
501 'old_text' => 'Söme Content',
504 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
505 $services = MediaWikiServices
::getInstance();
506 $lb = $services->getDBLoadBalancer();
507 $access = $services->getExternalStoreAccess();
509 $blobStore = new SqlBlobStore( $lb, $access, $cache );
510 $blobStore->setLegacyEncoding( 'windows-1252', Language
::factory( 'en' ) );
512 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
514 $record = $store->newRevisionFromRow(
515 $this->makeRow( $row ),
517 Title
::newFromText( __METHOD__
. '-UTPage' )
519 $this->assertSame( 'Söme Content', $record->getContent( SlotRecord
::MAIN
)->serialize() );
522 private function makeRow( array $array ) {
526 'rev_timestamp' => '20110101000000',
527 'rev_user_text' => 'Tester',
529 'rev_minor_edit' => 0,
532 'rev_parent_id' => 0,
533 'rev_sha1' => 'deadbeef',
534 'rev_comment_text' => 'Testing',
535 'rev_comment_data' => '{}',
536 'rev_comment_cid' => 111,
537 'page_namespace' => 0,
538 'page_title' => 'TEST',
541 'page_is_redirect' => 0,
543 'user_name' => 'Tester',
546 if ( $this->useTextId() ) {
548 'rev_content_format' => CONTENT_FORMAT_TEXT
,
549 'rev_content_model' => CONTENT_MODEL_TEXT
,
552 'old_text' => 'Hello World',
553 'old_flags' => 'utf-8',
555 } elseif ( !isset( $row['content'] ) && isset( $array['old_text'] ) ) {
557 'main' => new WikitextContent( $array['old_text'] ),
564 public function provideMigrationConstruction() {
566 [ SCHEMA_COMPAT_OLD
, false ],
567 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
568 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
569 [ SCHEMA_COMPAT_NEW
, false ],
570 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH
, true ],
571 [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH
, true ],
572 [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH
, true ],
577 * @covers \MediaWiki\Revision\RevisionStore::__construct
578 * @dataProvider provideMigrationConstruction
580 public function testMigrationConstruction( $migration, $expectException ) {
581 if ( $expectException ) {
582 $this->setExpectedException( InvalidArgumentException
::class );
584 $loadBalancer = $this->getMockLoadBalancer();
585 $blobStore = $this->getMockSqlBlobStore();
586 $cache = $this->getHashWANObjectCache();
587 $commentStore = $this->getMockCommentStore();
588 $services = MediaWikiServices
::getInstance();
589 $nameTables = $services->getNameTableStoreFactory();
590 $contentModelStore = $nameTables->getContentModels();
591 $slotRoleStore = $nameTables->getSlotRoles();
592 $slotRoleRegistry = $services->getSlotRoleRegistry();
593 $store = new RevisionStore(
598 $nameTables->getContentModels(),
599 $nameTables->getSlotRoles(),
602 $services->getActorMigration()
604 if ( !$expectException ) {
605 $store = TestingAccessWrapper
::newFromObject( $store );
606 $this->assertSame( $loadBalancer, $store->loadBalancer
);
607 $this->assertSame( $blobStore, $store->blobStore
);
608 $this->assertSame( $cache, $store->cache
);
609 $this->assertSame( $commentStore, $store->commentStore
);
610 $this->assertSame( $contentModelStore, $store->contentModelStore
);
611 $this->assertSame( $slotRoleStore, $store->slotRoleStore
);
612 $this->assertSame( $migration, $store->mcrMigrationStage
);