3 namespace MediaWiki\Tests\Storage
;
7 use InvalidArgumentException
;
9 use MediaWiki\MediaWikiServices
;
10 use MediaWiki\Storage\RevisionAccessException
;
11 use MediaWiki\Storage\RevisionStore
;
12 use MediaWiki\Storage\SqlBlobStore
;
13 use MediaWikiTestCase
;
17 use Wikimedia\Rdbms\Database
;
18 use Wikimedia\Rdbms\LoadBalancer
;
19 use Wikimedia\TestingAccessWrapper
;
21 class RevisionStoreTest
extends MediaWikiTestCase
{
24 * @param LoadBalancer $loadBalancer
25 * @param SqlBlobStore $blobStore
26 * @param WANObjectCache $WANObjectCache
28 * @return RevisionStore
30 private function getRevisionStore(
33 $WANObjectCache = null
35 global $wgMultiContentRevisionSchemaMigrationStage;
36 // the migration stage should be irrelevant, since all the tests that interact with
37 // the database are in RevisionStoreDbTest, not here.
39 return new RevisionStore(
40 $loadBalancer ?
: $this->getMockLoadBalancer(),
41 $blobStore ?
: $this->getMockSqlBlobStore(),
42 $WANObjectCache ?
: $this->getHashWANObjectCache(),
43 MediaWikiServices
::getInstance()->getCommentStore(),
44 MediaWikiServices
::getInstance()->getContentModelStore(),
45 MediaWikiServices
::getInstance()->getSlotRoleStore(),
46 $wgMultiContentRevisionSchemaMigrationStage,
47 MediaWikiServices
::getInstance()->getActorMigration()
52 * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
54 private function getMockLoadBalancer() {
55 return $this->getMockBuilder( LoadBalancer
::class )
56 ->disableOriginalConstructor()->getMock();
60 * @return \PHPUnit_Framework_MockObject_MockObject|Database
62 private function getMockDatabase() {
63 return $this->getMockBuilder( Database
::class )
64 ->disableOriginalConstructor()->getMock();
68 * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
70 private function getMockSqlBlobStore() {
71 return $this->getMockBuilder( SqlBlobStore
::class )
72 ->disableOriginalConstructor()->getMock();
76 * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
78 private function getMockCommentStore() {
79 return $this->getMockBuilder( CommentStore
::class )
80 ->disableOriginalConstructor()->getMock();
83 private function getHashWANObjectCache() {
84 return new WANObjectCache( [ 'cache' => new \
HashBagOStuff() ] );
87 public function provideSetContentHandlerUseDB() {
89 // ContentHandlerUseDB can be true of false pre migration
90 [ false, MIGRATION_OLD
, false ],
91 [ true, MIGRATION_OLD
, false ],
92 // During migration it can not be false
93 [ false, MIGRATION_WRITE_BOTH
, true ],
95 [ true, MIGRATION_WRITE_BOTH
, false ],
100 * @dataProvider provideSetContentHandlerUseDB
101 * @covers \MediaWiki\Storage\RevisionStore::getContentHandlerUseDB
102 * @covers \MediaWiki\Storage\RevisionStore::setContentHandlerUseDB
104 public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
105 if ( $expectedFail ) {
106 $this->setExpectedException( MWException
::class );
109 $store = new RevisionStore(
110 $this->getMockLoadBalancer(),
111 $this->getMockSqlBlobStore(),
112 $this->getHashWANObjectCache(),
113 $this->getMockCommentStore(),
114 MediaWikiServices
::getInstance()->getContentModelStore(),
115 MediaWikiServices
::getInstance()->getSlotRoleStore(),
117 MediaWikiServices
::getInstance()->getActorMigration()
120 $store->setContentHandlerUseDB( $contentHandlerDb );
121 $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
124 public function testGetTitle_successFromPageId() {
125 $mockLoadBalancer = $this->getMockLoadBalancer();
126 // Title calls wfGetDB() so we have to set the main service
127 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
129 $db = $this->getMockDatabase();
130 // Title calls wfGetDB() which uses a regular Connection
131 $mockLoadBalancer->expects( $this->atLeastOnce() )
132 ->method( 'getConnection' )
135 // First call to Title::newFromID, faking no result (db lag?)
136 $db->expects( $this->at( 0 ) )
137 ->method( 'selectRow' )
143 ->willReturn( (object)[
144 'page_namespace' => '1',
145 'page_title' => 'Food',
148 $store = $this->getRevisionStore( $mockLoadBalancer );
149 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
151 $this->assertSame( 1, $title->getNamespace() );
152 $this->assertSame( 'Food', $title->getDBkey() );
155 public function testGetTitle_successFromPageIdOnFallback() {
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 // Title calls wfGetDB() which uses a regular Connection
162 // Assert that the first call uses a REPLICA and the second falls back to master
163 $mockLoadBalancer->expects( $this->exactly( 2 ) )
164 ->method( 'getConnection' )
166 // RevisionStore getTitle uses a ConnectionRef
167 $mockLoadBalancer->expects( $this->atLeastOnce() )
168 ->method( 'getConnectionRef' )
171 // First call to Title::newFromID, faking no result (db lag?)
172 $db->expects( $this->at( 0 ) )
173 ->method( 'selectRow' )
179 ->willReturn( false );
181 // First select using rev_id, faking no result (db lag?)
182 $db->expects( $this->at( 1 ) )
183 ->method( 'selectRow' )
185 [ 'revision', 'page' ],
189 ->willReturn( false );
191 // Second call to Title::newFromID, no result
192 $db->expects( $this->at( 2 ) )
193 ->method( 'selectRow' )
199 ->willReturn( (object)[
200 'page_namespace' => '2',
201 'page_title' => 'Foodey',
204 $store = $this->getRevisionStore( $mockLoadBalancer );
205 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
207 $this->assertSame( 2, $title->getNamespace() );
208 $this->assertSame( 'Foodey', $title->getDBkey() );
211 public function testGetTitle_successFromRevId() {
212 $mockLoadBalancer = $this->getMockLoadBalancer();
213 // Title calls wfGetDB() so we have to set the main service
214 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
216 $db = $this->getMockDatabase();
217 // Title calls wfGetDB() which uses a regular Connection
218 $mockLoadBalancer->expects( $this->atLeastOnce() )
219 ->method( 'getConnection' )
221 // RevisionStore getTitle uses a ConnectionRef
222 $mockLoadBalancer->expects( $this->atLeastOnce() )
223 ->method( 'getConnectionRef' )
226 // First call to Title::newFromID, faking no result (db lag?)
227 $db->expects( $this->at( 0 ) )
228 ->method( 'selectRow' )
234 ->willReturn( false );
236 // First select using rev_id, faking no result (db lag?)
237 $db->expects( $this->at( 1 ) )
238 ->method( 'selectRow' )
240 [ 'revision', 'page' ],
244 ->willReturn( (object)[
245 'page_namespace' => '1',
246 'page_title' => 'Food2',
249 $store = $this->getRevisionStore( $mockLoadBalancer );
250 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
252 $this->assertSame( 1, $title->getNamespace() );
253 $this->assertSame( 'Food2', $title->getDBkey() );
256 public function testGetTitle_successFromRevIdOnFallback() {
257 $mockLoadBalancer = $this->getMockLoadBalancer();
258 // Title calls wfGetDB() so we have to set the main service
259 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
261 $db = $this->getMockDatabase();
262 // Title calls wfGetDB() which uses a regular Connection
263 // Assert that the first call uses a REPLICA and the second falls back to master
264 $mockLoadBalancer->expects( $this->exactly( 2 ) )
265 ->method( 'getConnection' )
267 // RevisionStore getTitle uses a ConnectionRef
268 $mockLoadBalancer->expects( $this->atLeastOnce() )
269 ->method( 'getConnectionRef' )
272 // First call to Title::newFromID, faking no result (db lag?)
273 $db->expects( $this->at( 0 ) )
274 ->method( 'selectRow' )
280 ->willReturn( false );
282 // First select using rev_id, faking no result (db lag?)
283 $db->expects( $this->at( 1 ) )
284 ->method( 'selectRow' )
286 [ 'revision', 'page' ],
290 ->willReturn( false );
292 // Second call to Title::newFromID, no result
293 $db->expects( $this->at( 2 ) )
294 ->method( 'selectRow' )
300 ->willReturn( false );
302 // Second select using rev_id, result
303 $db->expects( $this->at( 3 ) )
304 ->method( 'selectRow' )
306 [ 'revision', 'page' ],
310 ->willReturn( (object)[
311 'page_namespace' => '2',
312 'page_title' => 'Foodey',
315 $store = $this->getRevisionStore( $mockLoadBalancer );
316 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
318 $this->assertSame( 2, $title->getNamespace() );
319 $this->assertSame( 'Foodey', $title->getDBkey() );
323 * @covers \MediaWiki\Storage\RevisionStore::getTitle
325 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
326 $mockLoadBalancer = $this->getMockLoadBalancer();
327 // Title calls wfGetDB() so we have to set the main service
328 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
330 $db = $this->getMockDatabase();
331 // Title calls wfGetDB() which uses a regular Connection
332 // Assert that the first call uses a REPLICA and the second falls back to master
334 // RevisionStore getTitle uses getConnectionRef
335 // Title::newFromID uses getConnection
336 foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
337 $mockLoadBalancer->expects( $this->exactly( 2 ) )
339 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
340 static $callCounter = 0;
342 // The first call should be to a REPLICA, and the second a MASTER.
343 if ( $callCounter === 1 ) {
344 $this->assertSame( DB_REPLICA
, $masterOrReplica );
345 } elseif ( $callCounter === 2 ) {
346 $this->assertSame( DB_MASTER
, $masterOrReplica );
351 // First and third call to Title::newFromID, faking no result
352 foreach ( [ 0, 2 ] as $counter ) {
353 $db->expects( $this->at( $counter ) )
354 ->method( 'selectRow' )
360 ->willReturn( false );
363 foreach ( [ 1, 3 ] as $counter ) {
364 $db->expects( $this->at( $counter ) )
365 ->method( 'selectRow' )
367 [ 'revision', 'page' ],
371 ->willReturn( false );
374 $store = $this->getRevisionStore( $mockLoadBalancer );
376 $this->setExpectedException( RevisionAccessException
::class );
377 $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
380 public function provideNewRevisionFromRow_legacyEncoding_applied() {
381 yield
'windows-1252, old_flags is empty' => [
386 'old_text' => "S\xF6me Content",
391 yield
'windows-1252, old_flags is null' => [
396 'old_text' => "S\xF6me Content",
403 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
405 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
407 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
408 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
409 $lb = MediaWikiServices
::getInstance()->getDBLoadBalancer();
411 $blobStore = new SqlBlobStore( $lb, $cache );
412 $blobStore->setLegacyEncoding( $encoding, Language
::factory( $locale ) );
414 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
416 $record = $store->newRevisionFromRow(
417 $this->makeRow( $row ),
419 Title
::newFromText( __METHOD__
. '-UTPage' )
422 $this->assertSame( $text, $record->getContent( 'main' )->serialize() );
426 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
428 public function testNewRevisionFromRow_legacyEncoding_ignored() {
430 'old_flags' => 'utf-8',
431 'old_text' => 'Söme Content',
434 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
435 $lb = MediaWikiServices
::getInstance()->getDBLoadBalancer();
437 $blobStore = new SqlBlobStore( $lb, $cache );
438 $blobStore->setLegacyEncoding( 'windows-1252', Language
::factory( 'en' ) );
440 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
442 $record = $store->newRevisionFromRow(
443 $this->makeRow( $row ),
445 Title
::newFromText( __METHOD__
. '-UTPage' )
447 $this->assertSame( 'Söme Content', $record->getContent( 'main' )->serialize() );
450 private function makeRow( array $array ) {
455 'rev_timestamp' => '20110101000000',
456 'rev_user_text' => 'Tester',
458 'rev_minor_edit' => 0,
461 'rev_parent_id' => 0,
462 'rev_sha1' => 'deadbeef',
463 'rev_comment_text' => 'Testing',
464 'rev_comment_data' => '{}',
465 'rev_comment_cid' => 111,
466 'rev_content_format' => CONTENT_FORMAT_TEXT
,
467 'rev_content_model' => CONTENT_MODEL_TEXT
,
468 'page_namespace' => 0,
469 'page_title' => 'TEST',
472 'page_is_redirect' => 0,
474 'user_name' => 'Tester',
476 'old_text' => 'Hello World',
477 'old_flags' => 'utf-8',
483 public function provideMigrationConstruction() {
485 [ MIGRATION_OLD
, false ],
486 [ MIGRATION_WRITE_BOTH
, false ],
491 * @covers \MediaWiki\Storage\RevisionStore::__construct
492 * @dataProvider provideMigrationConstruction
494 public function testMigrationConstruction( $migration, $expectException ) {
495 if ( $expectException ) {
496 $this->setExpectedException( InvalidArgumentException
::class );
498 $loadBalancer = $this->getMockLoadBalancer();
499 $blobStore = $this->getMockSqlBlobStore();
500 $cache = $this->getHashWANObjectCache();
501 $commentStore = $this->getMockCommentStore();
502 $contentModelStore = MediaWikiServices
::getInstance()->getContentModelStore();
503 $slotRoleStore = MediaWikiServices
::getInstance()->getSlotRoleStore();
504 $store = new RevisionStore(
509 MediaWikiServices
::getInstance()->getContentModelStore(),
510 MediaWikiServices
::getInstance()->getSlotRoleStore(),
512 MediaWikiServices
::getInstance()->getActorMigration()
514 if ( !$expectException ) {
515 $store = TestingAccessWrapper
::newFromObject( $store );
516 $this->assertSame( $loadBalancer, $store->loadBalancer
);
517 $this->assertSame( $blobStore, $store->blobStore
);
518 $this->assertSame( $cache, $store->cache
);
519 $this->assertSame( $commentStore, $store->commentStore
);
520 $this->assertSame( $contentModelStore, $store->contentModelStore
);
521 $this->assertSame( $slotRoleStore, $store->slotRoleStore
);
522 $this->assertSame( $migration, $store->mcrMigrationStage
);