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
;
22 class RevisionStoreTest
extends MediaWikiTestCase
{
24 private function useTextId() {
25 global $wgMultiContentRevisionSchemaMigrationStage;
27 return (bool)( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_OLD
);
31 * @param LoadBalancer $loadBalancer
32 * @param SqlBlobStore $blobStore
33 * @param WANObjectCache $WANObjectCache
35 * @return RevisionStore
37 private function getRevisionStore(
40 $WANObjectCache = null
42 global $wgMultiContentRevisionSchemaMigrationStage;
43 // the migration stage should be irrelevant, since all the tests that interact with
44 // the database are in RevisionStoreDbTest, not here.
46 return new RevisionStore(
47 $loadBalancer ?
: $this->getMockLoadBalancer(),
48 $blobStore ?
: $this->getMockSqlBlobStore(),
49 $WANObjectCache ?
: $this->getHashWANObjectCache(),
50 MediaWikiServices
::getInstance()->getCommentStore(),
51 MediaWikiServices
::getInstance()->getContentModelStore(),
52 MediaWikiServices
::getInstance()->getSlotRoleStore(),
53 $wgMultiContentRevisionSchemaMigrationStage,
54 MediaWikiServices
::getInstance()->getActorMigration()
59 * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
61 private function getMockLoadBalancer() {
62 return $this->getMockBuilder( LoadBalancer
::class )
63 ->disableOriginalConstructor()->getMock();
67 * @return \PHPUnit_Framework_MockObject_MockObject|Database
69 private function getMockDatabase() {
70 return $this->getMockBuilder( Database
::class )
71 ->disableOriginalConstructor()->getMock();
75 * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
77 private function getMockSqlBlobStore() {
78 return $this->getMockBuilder( SqlBlobStore
::class )
79 ->disableOriginalConstructor()->getMock();
83 * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
85 private function getMockCommentStore() {
86 return $this->getMockBuilder( CommentStore
::class )
87 ->disableOriginalConstructor()->getMock();
90 private function getHashWANObjectCache() {
91 return new WANObjectCache( [ 'cache' => new \
HashBagOStuff() ] );
94 public function provideSetContentHandlerUseDB() {
96 // ContentHandlerUseDB can be true of false pre migration.
97 [ false, SCHEMA_COMPAT_OLD
, false ],
98 [ true, SCHEMA_COMPAT_OLD
, false ],
99 // During and after migration it can not be false...
100 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, true ],
101 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, true ],
102 [ false, SCHEMA_COMPAT_NEW
, true ],
103 // ...but it can be true.
104 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
105 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
106 [ true, SCHEMA_COMPAT_NEW
, false ],
111 * @dataProvider provideSetContentHandlerUseDB
112 * @covers \MediaWiki\Storage\RevisionStore::getContentHandlerUseDB
113 * @covers \MediaWiki\Storage\RevisionStore::setContentHandlerUseDB
115 public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
116 if ( $expectedFail ) {
117 $this->setExpectedException( MWException
::class );
120 $nameTables = MediaWikiServices
::getInstance()->getNameTableStoreFactory();
122 $store = new RevisionStore(
123 $this->getMockLoadBalancer(),
124 $this->getMockSqlBlobStore(),
125 $this->getHashWANObjectCache(),
126 $this->getMockCommentStore(),
127 $nameTables->getContentModels(),
128 $nameTables->getSlotRoles(),
130 MediaWikiServices
::getInstance()->getActorMigration()
133 $store->setContentHandlerUseDB( $contentHandlerDb );
134 $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
137 public function testGetTitle_successFromPageId() {
138 $mockLoadBalancer = $this->getMockLoadBalancer();
139 // Title calls wfGetDB() so we have to set the main service
140 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
142 $db = $this->getMockDatabase();
143 // Title calls wfGetDB() which uses a regular Connection
144 $mockLoadBalancer->expects( $this->atLeastOnce() )
145 ->method( 'getConnection' )
148 // First call to Title::newFromID, faking no result (db lag?)
149 $db->expects( $this->at( 0 ) )
150 ->method( 'selectRow' )
156 ->willReturn( (object)[
157 'page_namespace' => '1',
158 'page_title' => 'Food',
161 $store = $this->getRevisionStore( $mockLoadBalancer );
162 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
164 $this->assertSame( 1, $title->getNamespace() );
165 $this->assertSame( 'Food', $title->getDBkey() );
168 public function testGetTitle_successFromPageIdOnFallback() {
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 // Title calls wfGetDB() which uses a regular Connection
175 // Assert that the first call uses a REPLICA and the second falls back to master
176 $mockLoadBalancer->expects( $this->exactly( 2 ) )
177 ->method( 'getConnection' )
179 // RevisionStore getTitle uses a ConnectionRef
180 $mockLoadBalancer->expects( $this->atLeastOnce() )
181 ->method( 'getConnectionRef' )
184 // First call to Title::newFromID, faking no result (db lag?)
185 $db->expects( $this->at( 0 ) )
186 ->method( 'selectRow' )
192 ->willReturn( false );
194 // First select using rev_id, faking no result (db lag?)
195 $db->expects( $this->at( 1 ) )
196 ->method( 'selectRow' )
198 [ 'revision', 'page' ],
202 ->willReturn( false );
204 // Second call to Title::newFromID, no result
205 $db->expects( $this->at( 2 ) )
206 ->method( 'selectRow' )
212 ->willReturn( (object)[
213 'page_namespace' => '2',
214 'page_title' => 'Foodey',
217 $store = $this->getRevisionStore( $mockLoadBalancer );
218 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
220 $this->assertSame( 2, $title->getNamespace() );
221 $this->assertSame( 'Foodey', $title->getDBkey() );
224 public function testGetTitle_successFromRevId() {
225 $mockLoadBalancer = $this->getMockLoadBalancer();
226 // Title calls wfGetDB() so we have to set the main service
227 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
229 $db = $this->getMockDatabase();
230 // Title calls wfGetDB() which uses a regular Connection
231 $mockLoadBalancer->expects( $this->atLeastOnce() )
232 ->method( 'getConnection' )
234 // RevisionStore getTitle uses a ConnectionRef
235 $mockLoadBalancer->expects( $this->atLeastOnce() )
236 ->method( 'getConnectionRef' )
239 // First call to Title::newFromID, faking no result (db lag?)
240 $db->expects( $this->at( 0 ) )
241 ->method( 'selectRow' )
247 ->willReturn( false );
249 // First select using rev_id, faking no result (db lag?)
250 $db->expects( $this->at( 1 ) )
251 ->method( 'selectRow' )
253 [ 'revision', 'page' ],
257 ->willReturn( (object)[
258 'page_namespace' => '1',
259 'page_title' => 'Food2',
262 $store = $this->getRevisionStore( $mockLoadBalancer );
263 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
265 $this->assertSame( 1, $title->getNamespace() );
266 $this->assertSame( 'Food2', $title->getDBkey() );
269 public function testGetTitle_successFromRevIdOnFallback() {
270 $mockLoadBalancer = $this->getMockLoadBalancer();
271 // Title calls wfGetDB() so we have to set the main service
272 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
274 $db = $this->getMockDatabase();
275 // Title calls wfGetDB() which uses a regular Connection
276 // Assert that the first call uses a REPLICA and the second falls back to master
277 $mockLoadBalancer->expects( $this->exactly( 2 ) )
278 ->method( 'getConnection' )
280 // RevisionStore getTitle uses a ConnectionRef
281 $mockLoadBalancer->expects( $this->atLeastOnce() )
282 ->method( 'getConnectionRef' )
285 // First call to Title::newFromID, faking no result (db lag?)
286 $db->expects( $this->at( 0 ) )
287 ->method( 'selectRow' )
293 ->willReturn( false );
295 // First select using rev_id, faking no result (db lag?)
296 $db->expects( $this->at( 1 ) )
297 ->method( 'selectRow' )
299 [ 'revision', 'page' ],
303 ->willReturn( false );
305 // Second call to Title::newFromID, no result
306 $db->expects( $this->at( 2 ) )
307 ->method( 'selectRow' )
313 ->willReturn( false );
315 // Second select using rev_id, result
316 $db->expects( $this->at( 3 ) )
317 ->method( 'selectRow' )
319 [ 'revision', 'page' ],
323 ->willReturn( (object)[
324 'page_namespace' => '2',
325 'page_title' => 'Foodey',
328 $store = $this->getRevisionStore( $mockLoadBalancer );
329 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
331 $this->assertSame( 2, $title->getNamespace() );
332 $this->assertSame( 'Foodey', $title->getDBkey() );
336 * @covers \MediaWiki\Storage\RevisionStore::getTitle
338 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
339 $mockLoadBalancer = $this->getMockLoadBalancer();
340 // Title calls wfGetDB() so we have to set the main service
341 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
343 $db = $this->getMockDatabase();
344 // Title calls wfGetDB() which uses a regular Connection
345 // Assert that the first call uses a REPLICA and the second falls back to master
347 // RevisionStore getTitle uses getConnectionRef
348 // Title::newFromID uses getConnection
349 foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
350 $mockLoadBalancer->expects( $this->exactly( 2 ) )
352 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
353 static $callCounter = 0;
355 // The first call should be to a REPLICA, and the second a MASTER.
356 if ( $callCounter === 1 ) {
357 $this->assertSame( DB_REPLICA
, $masterOrReplica );
358 } elseif ( $callCounter === 2 ) {
359 $this->assertSame( DB_MASTER
, $masterOrReplica );
364 // First and third call to Title::newFromID, faking no result
365 foreach ( [ 0, 2 ] as $counter ) {
366 $db->expects( $this->at( $counter ) )
367 ->method( 'selectRow' )
373 ->willReturn( false );
376 foreach ( [ 1, 3 ] as $counter ) {
377 $db->expects( $this->at( $counter ) )
378 ->method( 'selectRow' )
380 [ 'revision', 'page' ],
384 ->willReturn( false );
387 $store = $this->getRevisionStore( $mockLoadBalancer );
389 $this->setExpectedException( RevisionAccessException
::class );
390 $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
393 public function provideNewRevisionFromRow_legacyEncoding_applied() {
394 yield
'windows-1252, old_flags is empty' => [
399 'old_text' => "S\xF6me Content",
404 yield
'windows-1252, old_flags is null' => [
409 'old_text' => "S\xF6me Content",
416 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
418 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
420 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
421 if ( !$this->useTextId() ) {
422 $this->markTestSkipped( 'No longer applicable with MCR schema' );
425 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
426 $lb = MediaWikiServices
::getInstance()->getDBLoadBalancer();
428 $blobStore = new SqlBlobStore( $lb, $cache );
429 $blobStore->setLegacyEncoding( $encoding, Language
::factory( $locale ) );
431 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
433 $record = $store->newRevisionFromRow(
434 $this->makeRow( $row ),
436 Title
::newFromText( __METHOD__
. '-UTPage' )
439 $this->assertSame( $text, $record->getContent( 'main' )->serialize() );
443 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
445 public function testNewRevisionFromRow_legacyEncoding_ignored() {
446 if ( !$this->useTextId() ) {
447 $this->markTestSkipped( 'No longer applicable with MCR schema' );
451 'old_flags' => 'utf-8',
452 'old_text' => 'Söme Content',
455 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
456 $lb = MediaWikiServices
::getInstance()->getDBLoadBalancer();
458 $blobStore = new SqlBlobStore( $lb, $cache );
459 $blobStore->setLegacyEncoding( 'windows-1252', Language
::factory( 'en' ) );
461 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
463 $record = $store->newRevisionFromRow(
464 $this->makeRow( $row ),
466 Title
::newFromText( __METHOD__
. '-UTPage' )
468 $this->assertSame( 'Söme Content', $record->getContent( 'main' )->serialize() );
471 private function makeRow( array $array ) {
475 'rev_timestamp' => '20110101000000',
476 'rev_user_text' => 'Tester',
478 'rev_minor_edit' => 0,
481 'rev_parent_id' => 0,
482 'rev_sha1' => 'deadbeef',
483 'rev_comment_text' => 'Testing',
484 'rev_comment_data' => '{}',
485 'rev_comment_cid' => 111,
486 'page_namespace' => 0,
487 'page_title' => 'TEST',
490 'page_is_redirect' => 0,
492 'user_name' => 'Tester',
495 if ( $this->useTextId() ) {
497 'rev_content_format' => CONTENT_FORMAT_TEXT
,
498 'rev_content_model' => CONTENT_MODEL_TEXT
,
501 'old_text' => 'Hello World',
502 'old_flags' => 'utf-8',
505 if ( !isset( $row['content'] ) && isset( $array['old_text'] ) ) {
507 'main' => new WikitextContent( $array['old_text'] ),
515 public function provideMigrationConstruction() {
517 [ SCHEMA_COMPAT_OLD
, false ],
518 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
519 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
520 [ SCHEMA_COMPAT_NEW
, false ],
521 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH
, true ],
522 [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH
, true ],
523 [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH
, true ],
528 * @covers \MediaWiki\Storage\RevisionStore::__construct
529 * @dataProvider provideMigrationConstruction
531 public function testMigrationConstruction( $migration, $expectException ) {
532 if ( $expectException ) {
533 $this->setExpectedException( InvalidArgumentException
::class );
535 $loadBalancer = $this->getMockLoadBalancer();
536 $blobStore = $this->getMockSqlBlobStore();
537 $cache = $this->getHashWANObjectCache();
538 $commentStore = $this->getMockCommentStore();
539 $services = MediaWikiServices
::getInstance();
540 $nameTables = $services->getNameTableStoreFactory();
541 $contentModelStore = $nameTables->getContentModels();
542 $slotRoleStore = $nameTables->getSlotRoles();
543 $store = new RevisionStore(
548 $nameTables->getContentModels(),
549 $nameTables->getSlotRoles(),
551 $services->getActorMigration()
553 if ( !$expectException ) {
554 $store = TestingAccessWrapper
::newFromObject( $store );
555 $this->assertSame( $loadBalancer, $store->loadBalancer
);
556 $this->assertSame( $blobStore, $store->blobStore
);
557 $this->assertSame( $cache, $store->cache
);
558 $this->assertSame( $commentStore, $store->commentStore
);
559 $this->assertSame( $contentModelStore, $store->contentModelStore
);
560 $this->assertSame( $slotRoleStore, $store->slotRoleStore
);
561 $this->assertSame( $migration, $store->mcrMigrationStage
);