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()
40 * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
42 private function getMockLoadBalancer() {
43 return $this->getMockBuilder( LoadBalancer
::class )
44 ->disableOriginalConstructor()->getMock();
48 * @return \PHPUnit_Framework_MockObject_MockObject|Database
50 private function getMockDatabase() {
51 return $this->getMockBuilder( Database
::class )
52 ->disableOriginalConstructor()->getMock();
56 * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
58 private function getMockSqlBlobStore() {
59 return $this->getMockBuilder( SqlBlobStore
::class )
60 ->disableOriginalConstructor()->getMock();
63 private function getHashWANObjectCache() {
64 return new WANObjectCache( [ 'cache' => new \
HashBagOStuff() ] );
68 * @covers \MediaWiki\Storage\RevisionStore::getContentHandlerUseDB
69 * @covers \MediaWiki\Storage\RevisionStore::setContentHandlerUseDB
71 public function testGetSetContentHandlerDb() {
72 $store = $this->getRevisionStore();
73 $this->assertTrue( $store->getContentHandlerUseDB() );
74 $store->setContentHandlerUseDB( false );
75 $this->assertFalse( $store->getContentHandlerUseDB() );
76 $store->setContentHandlerUseDB( true );
77 $this->assertTrue( $store->getContentHandlerUseDB() );
80 private function getDefaultQueryFields() {
96 private function getCommentQueryFields() {
98 'rev_comment_text' => 'rev_comment',
99 'rev_comment_data' => 'NULL',
100 'rev_comment_cid' => 'NULL',
104 private function getContentHandlerQueryFields() {
106 'rev_content_format',
111 public function provideGetQueryInfo() {
116 'tables' => [ 'revision' ],
117 'fields' => array_merge(
118 $this->getDefaultQueryFields(),
119 $this->getCommentQueryFields(),
120 $this->getContentHandlerQueryFields()
129 'tables' => [ 'revision' ],
130 'fields' => array_merge(
131 $this->getDefaultQueryFields(),
132 $this->getCommentQueryFields()
141 'tables' => [ 'revision', 'page' ],
142 'fields' => array_merge(
143 $this->getDefaultQueryFields(),
144 $this->getCommentQueryFields(),
155 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
163 'tables' => [ 'revision', 'user' ],
164 'fields' => array_merge(
165 $this->getDefaultQueryFields(),
166 $this->getCommentQueryFields(),
172 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
180 'tables' => [ 'revision', 'text' ],
181 'fields' => array_merge(
182 $this->getDefaultQueryFields(),
183 $this->getCommentQueryFields(),
190 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
196 [ 'page', 'user', 'text' ],
198 'tables' => [ 'revision', 'page', 'user', 'text' ],
199 'fields' => array_merge(
200 $this->getDefaultQueryFields(),
201 $this->getCommentQueryFields(),
202 $this->getContentHandlerQueryFields(),
216 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
217 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
218 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
225 * @dataProvider provideGetQueryInfo
226 * @covers \MediaWiki\Storage\RevisionStore::getQueryInfo
228 public function testGetQueryInfo( $contentHandlerUseDb, $options, $expected ) {
229 $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD
);
230 $this->overrideMwServices();
231 $store = $this->getRevisionStore();
232 $store->setContentHandlerUseDB( $contentHandlerUseDb );
233 $this->assertEquals( $expected, $store->getQueryInfo( $options ) );
236 private function getDefaultArchiveFields() {
257 * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo
259 public function testGetArchiveQueryInfo_contentHandlerDb() {
260 $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD
);
261 $this->overrideMwServices();
262 $store = $this->getRevisionStore();
263 $store->setContentHandlerUseDB( true );
269 'fields' => array_merge(
270 $this->getDefaultArchiveFields(),
272 'ar_comment_text' => 'ar_comment',
273 'ar_comment_data' => 'NULL',
274 'ar_comment_cid' => 'NULL',
281 $store->getArchiveQueryInfo()
286 * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo
288 public function testGetArchiveQueryInfo_noContentHandlerDb() {
289 $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD
);
290 $this->overrideMwServices();
291 $store = $this->getRevisionStore();
292 $store->setContentHandlerUseDB( false );
298 'fields' => array_merge(
299 $this->getDefaultArchiveFields(),
301 'ar_comment_text' => 'ar_comment',
302 'ar_comment_data' => 'NULL',
303 'ar_comment_cid' => 'NULL',
308 $store->getArchiveQueryInfo()
312 public function testGetTitle_successFromPageId() {
313 $mockLoadBalancer = $this->getMockLoadBalancer();
314 // Title calls wfGetDB() so we have to set the main service
315 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
317 $db = $this->getMockDatabase();
318 // Title calls wfGetDB() which uses a regular Connection
319 $mockLoadBalancer->expects( $this->atLeastOnce() )
320 ->method( 'getConnection' )
323 // First call to Title::newFromID, faking no result (db lag?)
324 $db->expects( $this->at( 0 ) )
325 ->method( 'selectRow' )
331 ->willReturn( (object)[
332 'page_namespace' => '1',
333 'page_title' => 'Food',
336 $store = $this->getRevisionStore( $mockLoadBalancer );
337 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
339 $this->assertSame( 1, $title->getNamespace() );
340 $this->assertSame( 'Food', $title->getDBkey() );
343 public function testGetTitle_successFromPageIdOnFallback() {
344 $mockLoadBalancer = $this->getMockLoadBalancer();
345 // Title calls wfGetDB() so we have to set the main service
346 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
348 $db = $this->getMockDatabase();
349 // Title calls wfGetDB() which uses a regular Connection
350 // Assert that the first call uses a REPLICA and the second falls back to master
351 $mockLoadBalancer->expects( $this->exactly( 2 ) )
352 ->method( 'getConnection' )
354 // RevisionStore getTitle uses a ConnectionRef
355 $mockLoadBalancer->expects( $this->atLeastOnce() )
356 ->method( 'getConnectionRef' )
359 // First call to Title::newFromID, faking no result (db lag?)
360 $db->expects( $this->at( 0 ) )
361 ->method( 'selectRow' )
367 ->willReturn( false );
369 // First select using rev_id, faking no result (db lag?)
370 $db->expects( $this->at( 1 ) )
371 ->method( 'selectRow' )
373 [ 'revision', 'page' ],
377 ->willReturn( false );
379 // Second call to Title::newFromID, no result
380 $db->expects( $this->at( 2 ) )
381 ->method( 'selectRow' )
387 ->willReturn( (object)[
388 'page_namespace' => '2',
389 'page_title' => 'Foodey',
392 $store = $this->getRevisionStore( $mockLoadBalancer );
393 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
395 $this->assertSame( 2, $title->getNamespace() );
396 $this->assertSame( 'Foodey', $title->getDBkey() );
399 public function testGetTitle_successFromRevId() {
400 $mockLoadBalancer = $this->getMockLoadBalancer();
401 // Title calls wfGetDB() so we have to set the main service
402 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
404 $db = $this->getMockDatabase();
405 // Title calls wfGetDB() which uses a regular Connection
406 $mockLoadBalancer->expects( $this->atLeastOnce() )
407 ->method( 'getConnection' )
409 // RevisionStore getTitle uses a ConnectionRef
410 $mockLoadBalancer->expects( $this->atLeastOnce() )
411 ->method( 'getConnectionRef' )
414 // First call to Title::newFromID, faking no result (db lag?)
415 $db->expects( $this->at( 0 ) )
416 ->method( 'selectRow' )
422 ->willReturn( false );
424 // First select using rev_id, faking no result (db lag?)
425 $db->expects( $this->at( 1 ) )
426 ->method( 'selectRow' )
428 [ 'revision', 'page' ],
432 ->willReturn( (object)[
433 'page_namespace' => '1',
434 'page_title' => 'Food2',
437 $store = $this->getRevisionStore( $mockLoadBalancer );
438 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
440 $this->assertSame( 1, $title->getNamespace() );
441 $this->assertSame( 'Food2', $title->getDBkey() );
444 public function testGetTitle_successFromRevIdOnFallback() {
445 $mockLoadBalancer = $this->getMockLoadBalancer();
446 // Title calls wfGetDB() so we have to set the main service
447 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
449 $db = $this->getMockDatabase();
450 // Title calls wfGetDB() which uses a regular Connection
451 // Assert that the first call uses a REPLICA and the second falls back to master
452 $mockLoadBalancer->expects( $this->exactly( 2 ) )
453 ->method( 'getConnection' )
455 // RevisionStore getTitle uses a ConnectionRef
456 $mockLoadBalancer->expects( $this->atLeastOnce() )
457 ->method( 'getConnectionRef' )
460 // First call to Title::newFromID, faking no result (db lag?)
461 $db->expects( $this->at( 0 ) )
462 ->method( 'selectRow' )
468 ->willReturn( false );
470 // First select using rev_id, faking no result (db lag?)
471 $db->expects( $this->at( 1 ) )
472 ->method( 'selectRow' )
474 [ 'revision', 'page' ],
478 ->willReturn( false );
480 // Second call to Title::newFromID, no result
481 $db->expects( $this->at( 2 ) )
482 ->method( 'selectRow' )
488 ->willReturn( false );
490 // Second select using rev_id, result
491 $db->expects( $this->at( 3 ) )
492 ->method( 'selectRow' )
494 [ 'revision', 'page' ],
498 ->willReturn( (object)[
499 'page_namespace' => '2',
500 'page_title' => 'Foodey',
503 $store = $this->getRevisionStore( $mockLoadBalancer );
504 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
506 $this->assertSame( 2, $title->getNamespace() );
507 $this->assertSame( 'Foodey', $title->getDBkey() );
511 * @covers \MediaWiki\Storage\RevisionStore::getTitle
513 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
514 $mockLoadBalancer = $this->getMockLoadBalancer();
515 // Title calls wfGetDB() so we have to set the main service
516 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
518 $db = $this->getMockDatabase();
519 // Title calls wfGetDB() which uses a regular Connection
520 // Assert that the first call uses a REPLICA and the second falls back to master
522 // RevisionStore getTitle uses getConnectionRef
523 // Title::newFromID uses getConnection
524 foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
525 $mockLoadBalancer->expects( $this->exactly( 2 ) )
527 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
528 static $callCounter = 0;
530 // The first call should be to a REPLICA, and the second a MASTER.
531 if ( $callCounter === 1 ) {
532 $this->assertSame( DB_REPLICA
, $masterOrReplica );
533 } elseif ( $callCounter === 2 ) {
534 $this->assertSame( DB_MASTER
, $masterOrReplica );
539 // First and third call to Title::newFromID, faking no result
540 foreach ( [ 0, 2 ] as $counter ) {
541 $db->expects( $this->at( $counter ) )
542 ->method( 'selectRow' )
548 ->willReturn( false );
551 foreach ( [ 1, 3 ] as $counter ) {
552 $db->expects( $this->at( $counter ) )
553 ->method( 'selectRow' )
555 [ 'revision', 'page' ],
559 ->willReturn( false );
562 $store = $this->getRevisionStore( $mockLoadBalancer );
564 $this->setExpectedException( RevisionAccessException
::class );
565 $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
568 public function provideNewRevisionFromRow_legacyEncoding_applied() {
569 yield
'windows-1252, old_flags is empty' => [
574 'old_text' => "S\xF6me Content",
579 yield
'windows-1252, old_flags is null' => [
584 'old_text' => "S\xF6me Content",
591 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
593 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
594 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
596 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
597 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
599 $blobStore = new SqlBlobStore( wfGetLB(), $cache );
600 $blobStore->setLegacyEncoding( $encoding, Language
::factory( $locale ) );
602 $store = $this->getRevisionStore( wfGetLB(), $blobStore, $cache );
604 $record = $store->newRevisionFromRow(
605 $this->makeRow( $row ),
607 Title
::newFromText( __METHOD__
. '-UTPage' )
610 $this->assertSame( $text, $record->getContent( 'main' )->serialize() );
614 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
615 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
617 public function testNewRevisionFromRow_legacyEncoding_ignored() {
619 'old_flags' => 'utf-8',
620 'old_text' => 'Söme Content',
623 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
625 $blobStore = new SqlBlobStore( wfGetLB(), $cache );
626 $blobStore->setLegacyEncoding( 'windows-1252', Language
::factory( 'en' ) );
628 $store = $this->getRevisionStore( wfGetLB(), $blobStore, $cache );
630 $record = $store->newRevisionFromRow(
631 $this->makeRow( $row ),
633 Title
::newFromText( __METHOD__
. '-UTPage' )
635 $this->assertSame( 'Söme Content', $record->getContent( 'main' )->serialize() );
638 private function makeRow( array $array ) {
643 'rev_timestamp' => '20110101000000',
644 'rev_user_text' => 'Tester',
646 'rev_minor_edit' => 0,
649 'rev_parent_id' => 0,
650 'rev_sha1' => 'deadbeef',
651 'rev_comment_text' => 'Testing',
652 'rev_comment_data' => '{}',
653 'rev_comment_cid' => 111,
654 'rev_content_format' => CONTENT_FORMAT_TEXT
,
655 'rev_content_model' => CONTENT_MODEL_TEXT
,
656 'page_namespace' => 0,
657 'page_title' => 'TEST',
660 'page_is_redirect' => 0,
662 'user_name' => 'Tester',
664 'old_text' => 'Hello World',
665 'old_flags' => 'utf-8',