3 namespace MediaWiki\Tests\Revision
;
6 use InvalidArgumentException
;
7 use MediaWiki\MediaWikiServices
;
8 use MediaWiki\Revision\RevisionAccessException
;
9 use MediaWiki\Revision\RevisionStore
;
10 use MediaWiki\Revision\SlotRoleRegistry
;
11 use MediaWiki\Storage\SqlBlobStore
;
12 use Wikimedia\Rdbms\ILoadBalancer
;
13 use Wikimedia\Rdbms\MaintainableDBConnRef
;
14 use MediaWikiTestCase
;
17 use Wikimedia\Rdbms\IDatabase
;
18 use Wikimedia\Rdbms\LoadBalancer
;
19 use Wikimedia\TestingAccessWrapper
;
24 class RevisionStoreTest
extends MediaWikiTestCase
{
27 * @param LoadBalancer $loadBalancer
28 * @param SqlBlobStore $blobStore
29 * @param WANObjectCache $WANObjectCache
31 * @return RevisionStore
33 private function getRevisionStore(
36 $WANObjectCache = null
38 global $wgMultiContentRevisionSchemaMigrationStage;
39 // the migration stage should be irrelevant, since all the tests that interact with
40 // the database are in RevisionStoreDbTest, not here.
42 return new RevisionStore(
43 $loadBalancer ?
: $this->getMockLoadBalancer(),
44 $blobStore ?
: $this->getMockSqlBlobStore(),
45 $WANObjectCache ?
: $this->getHashWANObjectCache(),
46 MediaWikiServices
::getInstance()->getCommentStore(),
47 MediaWikiServices
::getInstance()->getContentModelStore(),
48 MediaWikiServices
::getInstance()->getSlotRoleStore(),
49 MediaWikiServices
::getInstance()->getSlotRoleRegistry(),
50 $wgMultiContentRevisionSchemaMigrationStage,
51 MediaWikiServices
::getInstance()->getActorMigration()
56 * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
58 private function getMockLoadBalancer() {
59 return $this->getMockBuilder( LoadBalancer
::class )
60 ->disableOriginalConstructor()->getMock();
64 * @return \PHPUnit_Framework_MockObject_MockObject|IDatabase
66 private function getMockDatabase() {
67 return $this->getMockBuilder( IDatabase
::class )
68 ->disableOriginalConstructor()->getMock();
72 * @param ILoadBalancer $mockLoadBalancer
76 private function getMockDBConnRefCallback( ILoadBalancer
$mockLoadBalancer, IDatabase
$db ) {
77 return function ( $i, $g, $domain, $flg ) use ( $mockLoadBalancer, $db ) {
78 return new MaintainableDBConnRef( $mockLoadBalancer, $db, $i );
83 * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
85 private function getMockSqlBlobStore() {
86 return $this->getMockBuilder( SqlBlobStore
::class )
87 ->disableOriginalConstructor()->getMock();
91 * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
93 private function getMockCommentStore() {
94 return $this->getMockBuilder( CommentStore
::class )
95 ->disableOriginalConstructor()->getMock();
99 * @return \PHPUnit_Framework_MockObject_MockObject|SlotRoleRegistry
101 private function getMockSlotRoleRegistry() {
102 return $this->getMockBuilder( SlotRoleRegistry
::class )
103 ->disableOriginalConstructor()->getMock();
106 private function getHashWANObjectCache() {
107 return new WANObjectCache( [ 'cache' => new \
HashBagOStuff() ] );
110 public function provideSetContentHandlerUseDB() {
112 // ContentHandlerUseDB can be true or false pre migration.
113 // During and after migration it can not be false...
114 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, true ],
115 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, true ],
116 [ false, SCHEMA_COMPAT_NEW
, true ],
117 // ...but it can be true.
118 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
119 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
120 [ true, SCHEMA_COMPAT_NEW
, false ],
125 * @dataProvider provideSetContentHandlerUseDB
126 * @covers \MediaWiki\Revision\RevisionStore::getContentHandlerUseDB
127 * @covers \MediaWiki\Revision\RevisionStore::setContentHandlerUseDB
129 public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
130 if ( $expectedFail ) {
131 $this->setExpectedException( MWException
::class );
134 $nameTables = MediaWikiServices
::getInstance()->getNameTableStoreFactory();
136 $store = new RevisionStore(
137 $this->getMockLoadBalancer(),
138 $this->getMockSqlBlobStore(),
139 $this->getHashWANObjectCache(),
140 $this->getMockCommentStore(),
141 $nameTables->getContentModels(),
142 $nameTables->getSlotRoles(),
143 $this->getMockSlotRoleRegistry(),
145 MediaWikiServices
::getInstance()->getActorMigration()
148 $store->setContentHandlerUseDB( $contentHandlerDb );
149 $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
153 * @covers \MediaWiki\Revision\RevisionStore::getTitle
155 public function testGetTitle_successFromPageId() {
156 $mockLoadBalancer = $this->getMockLoadBalancer();
157 // Title calls wfGetDB() so we have to set the main service
158 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
160 $db = $this->getMockDatabase();
161 // RevisionStore uses getConnectionRef
162 $mockLoadBalancer->expects( $this->any() )
163 ->method( 'getConnectionRef' )
164 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
165 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
166 $mockLoadBalancer->expects( $this->atLeastOnce() )
167 ->method( 'getMaintenanceConnectionRef' )
168 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
170 // First call to Title::newFromID, faking no result (db lag?)
171 $db->expects( $this->at( 0 ) )
172 ->method( 'selectRow' )
178 ->willReturn( (object)[
179 'page_namespace' => '1',
180 'page_title' => 'Food',
183 $store = $this->getRevisionStore( $mockLoadBalancer );
184 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
186 $this->assertSame( 1, $title->getNamespace() );
187 $this->assertSame( 'Food', $title->getDBkey() );
191 * @covers \MediaWiki\Revision\RevisionStore::getTitle
193 public function testGetTitle_successFromPageIdOnFallback() {
194 $mockLoadBalancer = $this->getMockLoadBalancer();
195 // Title calls wfGetDB() so we have to set the main service
196 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
198 $db = $this->getMockDatabase();
199 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
200 // Assert that the first call uses a REPLICA and the second falls back to master
201 $mockLoadBalancer->expects( $this->atLeastOnce() )
202 ->method( 'getConnectionRef' )
203 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
204 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
205 $mockLoadBalancer->expects( $this->exactly( 2 ) )
206 ->method( 'getMaintenanceConnectionRef' )
207 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
209 // First call to Title::newFromID, faking no result (db lag?)
210 $db->expects( $this->at( 0 ) )
211 ->method( 'selectRow' )
217 ->willReturn( false );
219 // First select using rev_id, faking no result (db lag?)
220 $db->expects( $this->at( 1 ) )
221 ->method( 'selectRow' )
223 [ 'revision', 'page' ],
227 ->willReturn( false );
229 // Second call to Title::newFromID, no result
230 $db->expects( $this->at( 2 ) )
231 ->method( 'selectRow' )
237 ->willReturn( (object)[
238 'page_namespace' => '2',
239 'page_title' => 'Foodey',
242 $store = $this->getRevisionStore( $mockLoadBalancer );
243 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
245 $this->assertSame( 2, $title->getNamespace() );
246 $this->assertSame( 'Foodey', $title->getDBkey() );
250 * @covers \MediaWiki\Revision\RevisionStore::getTitle
252 public function testGetTitle_successFromRevId() {
253 $mockLoadBalancer = $this->getMockLoadBalancer();
254 // Title calls wfGetDB() so we have to set the main service
255 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
257 $db = $this->getMockDatabase();
258 $mockLoadBalancer->expects( $this->atLeastOnce() )
259 ->method( 'getConnectionRef' )
260 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
261 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
262 // RevisionStore getTitle uses getMaintenanceConnectionRef
263 $mockLoadBalancer->expects( $this->atLeastOnce() )
264 ->method( 'getMaintenanceConnectionRef' )
265 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
267 // First call to Title::newFromID, faking no result (db lag?)
268 $db->expects( $this->at( 0 ) )
269 ->method( 'selectRow' )
275 ->willReturn( false );
277 // First select using rev_id, faking no result (db lag?)
278 $db->expects( $this->at( 1 ) )
279 ->method( 'selectRow' )
281 [ 'revision', 'page' ],
285 ->willReturn( (object)[
286 'page_namespace' => '1',
287 'page_title' => 'Food2',
290 $store = $this->getRevisionStore( $mockLoadBalancer );
291 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
293 $this->assertSame( 1, $title->getNamespace() );
294 $this->assertSame( 'Food2', $title->getDBkey() );
298 * @covers \MediaWiki\Revision\RevisionStore::getTitle
300 public function testGetTitle_successFromRevIdOnFallback() {
301 $mockLoadBalancer = $this->getMockLoadBalancer();
302 // Title calls wfGetDB() so we have to set the main service
303 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
305 $db = $this->getMockDatabase();
306 // Assert that the first call uses a REPLICA and the second falls back to master
307 // RevisionStore uses getMaintenanceConnectionRef
308 $mockLoadBalancer->expects( $this->atLeastOnce() )
309 ->method( 'getConnectionRef' )
310 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
311 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
312 $mockLoadBalancer->expects( $this->exactly( 2 ) )
313 ->method( 'getMaintenanceConnectionRef' )
314 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
316 // First call to Title::newFromID, faking no result (db lag?)
317 $db->expects( $this->at( 0 ) )
318 ->method( 'selectRow' )
324 ->willReturn( false );
326 // First select using rev_id, faking no result (db lag?)
327 $db->expects( $this->at( 1 ) )
328 ->method( 'selectRow' )
330 [ 'revision', 'page' ],
334 ->willReturn( false );
336 // Second call to Title::newFromID, no result
337 $db->expects( $this->at( 2 ) )
338 ->method( 'selectRow' )
344 ->willReturn( false );
346 // Second select using rev_id, result
347 $db->expects( $this->at( 3 ) )
348 ->method( 'selectRow' )
350 [ 'revision', 'page' ],
354 ->willReturn( (object)[
355 'page_namespace' => '2',
356 'page_title' => 'Foodey',
359 $store = $this->getRevisionStore( $mockLoadBalancer );
360 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
362 $this->assertSame( 2, $title->getNamespace() );
363 $this->assertSame( 'Foodey', $title->getDBkey() );
367 * @covers \MediaWiki\Revision\RevisionStore::getTitle
369 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
370 $mockLoadBalancer = $this->getMockLoadBalancer();
371 // Title calls wfGetDB() so we have to set the main service
372 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
374 $db = $this->getMockDatabase();
375 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
376 // Assert that the first call uses a REPLICA and the second falls back to master
378 // RevisionStore getTitle uses getConnectionRef
379 // Title::newFromID uses getMaintenanceConnectionRef
381 'getConnectionRef', 'getMaintenanceConnectionRef'
383 $mockLoadBalancer->expects( $this->exactly( 2 ) )
385 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
386 static $callCounter = 0;
388 // The first call should be to a REPLICA, and the second a MASTER.
389 if ( $callCounter === 1 ) {
390 $this->assertSame( DB_REPLICA
, $masterOrReplica );
391 } elseif ( $callCounter === 2 ) {
392 $this->assertSame( DB_MASTER
, $masterOrReplica );
397 // First and third call to Title::newFromID, faking no result
398 foreach ( [ 0, 2 ] as $counter ) {
399 $db->expects( $this->at( $counter ) )
400 ->method( 'selectRow' )
406 ->willReturn( false );
409 foreach ( [ 1, 3 ] as $counter ) {
410 $db->expects( $this->at( $counter ) )
411 ->method( 'selectRow' )
413 [ 'revision', 'page' ],
417 ->willReturn( false );
420 $store = $this->getRevisionStore( $mockLoadBalancer );
422 $this->setExpectedException( RevisionAccessException
::class );
423 $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
426 public function provideMigrationConstruction() {
428 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
429 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
430 [ SCHEMA_COMPAT_NEW
, false ],
431 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH
, true ],
432 [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH
, true ],
433 [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH
, true ],
438 * @covers \MediaWiki\Revision\RevisionStore::__construct
439 * @dataProvider provideMigrationConstruction
441 public function testMigrationConstruction( $migration, $expectException ) {
442 if ( $expectException ) {
443 $this->setExpectedException( InvalidArgumentException
::class );
445 $loadBalancer = $this->getMockLoadBalancer();
446 $blobStore = $this->getMockSqlBlobStore();
447 $cache = $this->getHashWANObjectCache();
448 $commentStore = $this->getMockCommentStore();
449 $services = MediaWikiServices
::getInstance();
450 $nameTables = $services->getNameTableStoreFactory();
451 $contentModelStore = $nameTables->getContentModels();
452 $slotRoleStore = $nameTables->getSlotRoles();
453 $slotRoleRegistry = $services->getSlotRoleRegistry();
454 $store = new RevisionStore(
459 $nameTables->getContentModels(),
460 $nameTables->getSlotRoles(),
463 $services->getActorMigration()
465 if ( !$expectException ) {
466 $store = TestingAccessWrapper
::newFromObject( $store );
467 $this->assertSame( $loadBalancer, $store->loadBalancer
);
468 $this->assertSame( $blobStore, $store->blobStore
);
469 $this->assertSame( $cache, $store->cache
);
470 $this->assertSame( $commentStore, $store->commentStore
);
471 $this->assertSame( $contentModelStore, $store->contentModelStore
);
472 $this->assertSame( $slotRoleStore, $store->slotRoleStore
);
473 $this->assertSame( $migration, $store->mcrMigrationStage
);