3 namespace MediaWiki\Tests\Storage
;
7 use MediaWiki\MediaWikiServices
;
8 use MediaWiki\Storage\RevisionAccessException
;
9 use MediaWiki\Storage\RevisionStore
;
10 use MediaWiki\Storage\SqlBlobStore
;
11 use MediaWikiTestCase
;
14 use Wikimedia\Rdbms\Database
;
15 use Wikimedia\Rdbms\LoadBalancer
;
17 class RevisionStoreTest
extends MediaWikiTestCase
{
20 * @param LoadBalancer $loadBalancer
21 * @param SqlBlobStore $blobStore
22 * @param WANObjectCache $WANObjectCache
24 * @return RevisionStore
26 private function getRevisionStore(
29 $WANObjectCache = null
31 return new RevisionStore(
32 $loadBalancer ?
$loadBalancer : $this->getMockLoadBalancer(),
33 $blobStore ?
$blobStore : $this->getMockSqlBlobStore(),
34 $WANObjectCache ?
$WANObjectCache : $this->getHashWANObjectCache(),
35 MediaWikiServices
::getInstance()->getCommentStore(),
36 MediaWikiServices
::getInstance()->getActorMigration()
41 * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
43 private function getMockLoadBalancer() {
44 return $this->getMockBuilder( LoadBalancer
::class )
45 ->disableOriginalConstructor()->getMock();
49 * @return \PHPUnit_Framework_MockObject_MockObject|Database
51 private function getMockDatabase() {
52 return $this->getMockBuilder( Database
::class )
53 ->disableOriginalConstructor()->getMock();
57 * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
59 private function getMockSqlBlobStore() {
60 return $this->getMockBuilder( SqlBlobStore
::class )
61 ->disableOriginalConstructor()->getMock();
64 private function getHashWANObjectCache() {
65 return new WANObjectCache( [ 'cache' => new \
HashBagOStuff() ] );
69 * @covers \MediaWiki\Storage\RevisionStore::getContentHandlerUseDB
70 * @covers \MediaWiki\Storage\RevisionStore::setContentHandlerUseDB
72 public function testGetSetContentHandlerDb() {
73 $store = $this->getRevisionStore();
74 $this->assertTrue( $store->getContentHandlerUseDB() );
75 $store->setContentHandlerUseDB( false );
76 $this->assertFalse( $store->getContentHandlerUseDB() );
77 $store->setContentHandlerUseDB( true );
78 $this->assertTrue( $store->getContentHandlerUseDB() );
81 private function getDefaultQueryFields() {
95 private function getCommentQueryFields() {
97 'rev_comment_text' => 'rev_comment',
98 'rev_comment_data' => 'NULL',
99 'rev_comment_cid' => 'NULL',
103 private function getActorQueryFields() {
105 'rev_user' => 'rev_user',
106 'rev_user_text' => 'rev_user_text',
107 'rev_actor' => 'NULL',
111 private function getContentHandlerQueryFields() {
113 'rev_content_format',
118 public function provideGetQueryInfo() {
123 'tables' => [ 'revision' ],
124 'fields' => array_merge(
125 $this->getDefaultQueryFields(),
126 $this->getCommentQueryFields(),
127 $this->getActorQueryFields(),
128 $this->getContentHandlerQueryFields()
137 'tables' => [ 'revision' ],
138 'fields' => array_merge(
139 $this->getDefaultQueryFields(),
140 $this->getCommentQueryFields(),
141 $this->getActorQueryFields()
150 'tables' => [ 'revision', 'page' ],
151 'fields' => array_merge(
152 $this->getDefaultQueryFields(),
153 $this->getCommentQueryFields(),
154 $this->getActorQueryFields(),
165 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
173 'tables' => [ 'revision', 'user' ],
174 'fields' => array_merge(
175 $this->getDefaultQueryFields(),
176 $this->getCommentQueryFields(),
177 $this->getActorQueryFields(),
183 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
191 'tables' => [ 'revision', 'text' ],
192 'fields' => array_merge(
193 $this->getDefaultQueryFields(),
194 $this->getCommentQueryFields(),
195 $this->getActorQueryFields(),
202 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
208 [ 'page', 'user', 'text' ],
210 'tables' => [ 'revision', 'page', 'user', 'text' ],
211 'fields' => array_merge(
212 $this->getDefaultQueryFields(),
213 $this->getCommentQueryFields(),
214 $this->getActorQueryFields(),
215 $this->getContentHandlerQueryFields(),
229 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
230 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
231 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
238 * @dataProvider provideGetQueryInfo
239 * @covers \MediaWiki\Storage\RevisionStore::getQueryInfo
241 public function testGetQueryInfo( $contentHandlerUseDb, $options, $expected ) {
242 $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD
);
243 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD
);
244 $this->overrideMwServices();
245 $store = $this->getRevisionStore();
246 $store->setContentHandlerUseDB( $contentHandlerUseDb );
247 $this->assertEquals( $expected, $store->getQueryInfo( $options ) );
250 private function getDefaultArchiveFields() {
268 * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo
270 public function testGetArchiveQueryInfo_contentHandlerDb() {
271 $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD
);
272 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD
);
273 $this->overrideMwServices();
274 $store = $this->getRevisionStore();
275 $store->setContentHandlerUseDB( true );
281 'fields' => array_merge(
282 $this->getDefaultArchiveFields(),
284 'ar_comment_text' => 'ar_comment',
285 'ar_comment_data' => 'NULL',
286 'ar_comment_cid' => 'NULL',
287 'ar_user_text' => 'ar_user_text',
288 'ar_user' => 'ar_user',
289 'ar_actor' => 'NULL',
296 $store->getArchiveQueryInfo()
301 * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo
303 public function testGetArchiveQueryInfo_noContentHandlerDb() {
304 $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD
);
305 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD
);
306 $this->overrideMwServices();
307 $store = $this->getRevisionStore();
308 $store->setContentHandlerUseDB( false );
314 'fields' => array_merge(
315 $this->getDefaultArchiveFields(),
317 'ar_comment_text' => 'ar_comment',
318 'ar_comment_data' => 'NULL',
319 'ar_comment_cid' => 'NULL',
320 'ar_user_text' => 'ar_user_text',
321 'ar_user' => 'ar_user',
322 'ar_actor' => 'NULL',
327 $store->getArchiveQueryInfo()
331 public function testGetTitle_successFromPageId() {
332 $mockLoadBalancer = $this->getMockLoadBalancer();
333 // Title calls wfGetDB() so we have to set the main service
334 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
336 $db = $this->getMockDatabase();
337 // Title calls wfGetDB() which uses a regular Connection
338 $mockLoadBalancer->expects( $this->atLeastOnce() )
339 ->method( 'getConnection' )
342 // First call to Title::newFromID, faking no result (db lag?)
343 $db->expects( $this->at( 0 ) )
344 ->method( 'selectRow' )
350 ->willReturn( (object)[
351 'page_namespace' => '1',
352 'page_title' => 'Food',
355 $store = $this->getRevisionStore( $mockLoadBalancer );
356 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
358 $this->assertSame( 1, $title->getNamespace() );
359 $this->assertSame( 'Food', $title->getDBkey() );
362 public function testGetTitle_successFromPageIdOnFallback() {
363 $mockLoadBalancer = $this->getMockLoadBalancer();
364 // Title calls wfGetDB() so we have to set the main service
365 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
367 $db = $this->getMockDatabase();
368 // Title calls wfGetDB() which uses a regular Connection
369 // Assert that the first call uses a REPLICA and the second falls back to master
370 $mockLoadBalancer->expects( $this->exactly( 2 ) )
371 ->method( 'getConnection' )
373 // RevisionStore getTitle uses a ConnectionRef
374 $mockLoadBalancer->expects( $this->atLeastOnce() )
375 ->method( 'getConnectionRef' )
378 // First call to Title::newFromID, faking no result (db lag?)
379 $db->expects( $this->at( 0 ) )
380 ->method( 'selectRow' )
386 ->willReturn( false );
388 // First select using rev_id, faking no result (db lag?)
389 $db->expects( $this->at( 1 ) )
390 ->method( 'selectRow' )
392 [ 'revision', 'page' ],
396 ->willReturn( false );
398 // Second call to Title::newFromID, no result
399 $db->expects( $this->at( 2 ) )
400 ->method( 'selectRow' )
406 ->willReturn( (object)[
407 'page_namespace' => '2',
408 'page_title' => 'Foodey',
411 $store = $this->getRevisionStore( $mockLoadBalancer );
412 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
414 $this->assertSame( 2, $title->getNamespace() );
415 $this->assertSame( 'Foodey', $title->getDBkey() );
418 public function testGetTitle_successFromRevId() {
419 $mockLoadBalancer = $this->getMockLoadBalancer();
420 // Title calls wfGetDB() so we have to set the main service
421 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
423 $db = $this->getMockDatabase();
424 // Title calls wfGetDB() which uses a regular Connection
425 $mockLoadBalancer->expects( $this->atLeastOnce() )
426 ->method( 'getConnection' )
428 // RevisionStore getTitle uses a ConnectionRef
429 $mockLoadBalancer->expects( $this->atLeastOnce() )
430 ->method( 'getConnectionRef' )
433 // First call to Title::newFromID, faking no result (db lag?)
434 $db->expects( $this->at( 0 ) )
435 ->method( 'selectRow' )
441 ->willReturn( false );
443 // First select using rev_id, faking no result (db lag?)
444 $db->expects( $this->at( 1 ) )
445 ->method( 'selectRow' )
447 [ 'revision', 'page' ],
451 ->willReturn( (object)[
452 'page_namespace' => '1',
453 'page_title' => 'Food2',
456 $store = $this->getRevisionStore( $mockLoadBalancer );
457 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
459 $this->assertSame( 1, $title->getNamespace() );
460 $this->assertSame( 'Food2', $title->getDBkey() );
463 public function testGetTitle_successFromRevIdOnFallback() {
464 $mockLoadBalancer = $this->getMockLoadBalancer();
465 // Title calls wfGetDB() so we have to set the main service
466 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
468 $db = $this->getMockDatabase();
469 // Title calls wfGetDB() which uses a regular Connection
470 // Assert that the first call uses a REPLICA and the second falls back to master
471 $mockLoadBalancer->expects( $this->exactly( 2 ) )
472 ->method( 'getConnection' )
474 // RevisionStore getTitle uses a ConnectionRef
475 $mockLoadBalancer->expects( $this->atLeastOnce() )
476 ->method( 'getConnectionRef' )
479 // First call to Title::newFromID, faking no result (db lag?)
480 $db->expects( $this->at( 0 ) )
481 ->method( 'selectRow' )
487 ->willReturn( false );
489 // First select using rev_id, faking no result (db lag?)
490 $db->expects( $this->at( 1 ) )
491 ->method( 'selectRow' )
493 [ 'revision', 'page' ],
497 ->willReturn( false );
499 // Second call to Title::newFromID, no result
500 $db->expects( $this->at( 2 ) )
501 ->method( 'selectRow' )
507 ->willReturn( false );
509 // Second select using rev_id, result
510 $db->expects( $this->at( 3 ) )
511 ->method( 'selectRow' )
513 [ 'revision', 'page' ],
517 ->willReturn( (object)[
518 'page_namespace' => '2',
519 'page_title' => 'Foodey',
522 $store = $this->getRevisionStore( $mockLoadBalancer );
523 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
525 $this->assertSame( 2, $title->getNamespace() );
526 $this->assertSame( 'Foodey', $title->getDBkey() );
530 * @covers \MediaWiki\Storage\RevisionStore::getTitle
532 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
533 $mockLoadBalancer = $this->getMockLoadBalancer();
534 // Title calls wfGetDB() so we have to set the main service
535 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
537 $db = $this->getMockDatabase();
538 // Title calls wfGetDB() which uses a regular Connection
539 // Assert that the first call uses a REPLICA and the second falls back to master
541 // RevisionStore getTitle uses getConnectionRef
542 // Title::newFromID uses getConnection
543 foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
544 $mockLoadBalancer->expects( $this->exactly( 2 ) )
546 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
547 static $callCounter = 0;
549 // The first call should be to a REPLICA, and the second a MASTER.
550 if ( $callCounter === 1 ) {
551 $this->assertSame( DB_REPLICA
, $masterOrReplica );
552 } elseif ( $callCounter === 2 ) {
553 $this->assertSame( DB_MASTER
, $masterOrReplica );
558 // First and third call to Title::newFromID, faking no result
559 foreach ( [ 0, 2 ] as $counter ) {
560 $db->expects( $this->at( $counter ) )
561 ->method( 'selectRow' )
567 ->willReturn( false );
570 foreach ( [ 1, 3 ] as $counter ) {
571 $db->expects( $this->at( $counter ) )
572 ->method( 'selectRow' )
574 [ 'revision', 'page' ],
578 ->willReturn( false );
581 $store = $this->getRevisionStore( $mockLoadBalancer );
583 $this->setExpectedException( RevisionAccessException
::class );
584 $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
587 public function provideNewRevisionFromRow_legacyEncoding_applied() {
588 yield
'windows-1252, old_flags is empty' => [
593 'old_text' => "S\xF6me Content",
598 yield
'windows-1252, old_flags is null' => [
603 'old_text' => "S\xF6me Content",
610 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
612 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
613 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
615 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
616 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
618 $blobStore = new SqlBlobStore( wfGetLB(), $cache );
619 $blobStore->setLegacyEncoding( $encoding, Language
::factory( $locale ) );
621 $store = $this->getRevisionStore( wfGetLB(), $blobStore, $cache );
623 $record = $store->newRevisionFromRow(
624 $this->makeRow( $row ),
626 Title
::newFromText( __METHOD__
. '-UTPage' )
629 $this->assertSame( $text, $record->getContent( 'main' )->serialize() );
633 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
634 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
636 public function testNewRevisionFromRow_legacyEncoding_ignored() {
638 'old_flags' => 'utf-8',
639 'old_text' => 'Söme Content',
642 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
644 $blobStore = new SqlBlobStore( wfGetLB(), $cache );
645 $blobStore->setLegacyEncoding( 'windows-1252', Language
::factory( 'en' ) );
647 $store = $this->getRevisionStore( wfGetLB(), $blobStore, $cache );
649 $record = $store->newRevisionFromRow(
650 $this->makeRow( $row ),
652 Title
::newFromText( __METHOD__
. '-UTPage' )
654 $this->assertSame( 'Söme Content', $record->getContent( 'main' )->serialize() );
657 private function makeRow( array $array ) {
662 'rev_timestamp' => '20110101000000',
663 'rev_user_text' => 'Tester',
665 'rev_minor_edit' => 0,
668 'rev_parent_id' => 0,
669 'rev_sha1' => 'deadbeef',
670 'rev_comment_text' => 'Testing',
671 'rev_comment_data' => '{}',
672 'rev_comment_cid' => 111,
673 'rev_content_format' => CONTENT_FORMAT_TEXT
,
674 'rev_content_model' => CONTENT_MODEL_TEXT
,
675 'page_namespace' => 0,
676 'page_title' => 'TEST',
679 'page_is_redirect' => 0,
681 'user_name' => 'Tester',
683 'old_text' => 'Hello World',
684 'old_flags' => 'utf-8',