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, SCHEMA_COMPAT_OLD
, false ],
91 [ true, SCHEMA_COMPAT_OLD
, false ],
92 // During and after migration it can not be false...
93 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, true ],
94 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, true ],
95 [ false, SCHEMA_COMPAT_NEW
, true ],
96 // ...but it can be true.
97 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
98 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
99 [ true, SCHEMA_COMPAT_NEW
, false ],
104 * @dataProvider provideSetContentHandlerUseDB
105 * @covers \MediaWiki\Storage\RevisionStore::getContentHandlerUseDB
106 * @covers \MediaWiki\Storage\RevisionStore::setContentHandlerUseDB
108 public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
109 if ( $expectedFail ) {
110 $this->setExpectedException( MWException
::class );
113 $nameTables = MediaWikiServices
::getInstance()->getNameTableStoreFactory();
115 $store = new RevisionStore(
116 $this->getMockLoadBalancer(),
117 $this->getMockSqlBlobStore(),
118 $this->getHashWANObjectCache(),
119 $this->getMockCommentStore(),
120 $nameTables->getContentModels(),
121 $nameTables->getSlotRoles(),
123 MediaWikiServices
::getInstance()->getActorMigration()
126 $store->setContentHandlerUseDB( $contentHandlerDb );
127 $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
130 public function testGetTitle_successFromPageId() {
131 $mockLoadBalancer = $this->getMockLoadBalancer();
132 // Title calls wfGetDB() so we have to set the main service
133 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
135 $db = $this->getMockDatabase();
136 // Title calls wfGetDB() which uses a regular Connection
137 $mockLoadBalancer->expects( $this->atLeastOnce() )
138 ->method( 'getConnection' )
141 // First call to Title::newFromID, faking no result (db lag?)
142 $db->expects( $this->at( 0 ) )
143 ->method( 'selectRow' )
149 ->willReturn( (object)[
150 'page_namespace' => '1',
151 'page_title' => 'Food',
154 $store = $this->getRevisionStore( $mockLoadBalancer );
155 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
157 $this->assertSame( 1, $title->getNamespace() );
158 $this->assertSame( 'Food', $title->getDBkey() );
161 public function testGetTitle_successFromPageIdOnFallback() {
162 $mockLoadBalancer = $this->getMockLoadBalancer();
163 // Title calls wfGetDB() so we have to set the main service
164 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
166 $db = $this->getMockDatabase();
167 // Title calls wfGetDB() which uses a regular Connection
168 // Assert that the first call uses a REPLICA and the second falls back to master
169 $mockLoadBalancer->expects( $this->exactly( 2 ) )
170 ->method( 'getConnection' )
172 // RevisionStore getTitle uses a ConnectionRef
173 $mockLoadBalancer->expects( $this->atLeastOnce() )
174 ->method( 'getConnectionRef' )
177 // First call to Title::newFromID, faking no result (db lag?)
178 $db->expects( $this->at( 0 ) )
179 ->method( 'selectRow' )
185 ->willReturn( false );
187 // First select using rev_id, faking no result (db lag?)
188 $db->expects( $this->at( 1 ) )
189 ->method( 'selectRow' )
191 [ 'revision', 'page' ],
195 ->willReturn( false );
197 // Second call to Title::newFromID, no result
198 $db->expects( $this->at( 2 ) )
199 ->method( 'selectRow' )
205 ->willReturn( (object)[
206 'page_namespace' => '2',
207 'page_title' => 'Foodey',
210 $store = $this->getRevisionStore( $mockLoadBalancer );
211 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
213 $this->assertSame( 2, $title->getNamespace() );
214 $this->assertSame( 'Foodey', $title->getDBkey() );
217 public function testGetTitle_successFromRevId() {
218 $mockLoadBalancer = $this->getMockLoadBalancer();
219 // Title calls wfGetDB() so we have to set the main service
220 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
222 $db = $this->getMockDatabase();
223 // Title calls wfGetDB() which uses a regular Connection
224 $mockLoadBalancer->expects( $this->atLeastOnce() )
225 ->method( 'getConnection' )
227 // RevisionStore getTitle uses a ConnectionRef
228 $mockLoadBalancer->expects( $this->atLeastOnce() )
229 ->method( 'getConnectionRef' )
232 // First call to Title::newFromID, faking no result (db lag?)
233 $db->expects( $this->at( 0 ) )
234 ->method( 'selectRow' )
240 ->willReturn( false );
242 // First select using rev_id, faking no result (db lag?)
243 $db->expects( $this->at( 1 ) )
244 ->method( 'selectRow' )
246 [ 'revision', 'page' ],
250 ->willReturn( (object)[
251 'page_namespace' => '1',
252 'page_title' => 'Food2',
255 $store = $this->getRevisionStore( $mockLoadBalancer );
256 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
258 $this->assertSame( 1, $title->getNamespace() );
259 $this->assertSame( 'Food2', $title->getDBkey() );
262 public function testGetTitle_successFromRevIdOnFallback() {
263 $mockLoadBalancer = $this->getMockLoadBalancer();
264 // Title calls wfGetDB() so we have to set the main service
265 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
267 $db = $this->getMockDatabase();
268 // Title calls wfGetDB() which uses a regular Connection
269 // Assert that the first call uses a REPLICA and the second falls back to master
270 $mockLoadBalancer->expects( $this->exactly( 2 ) )
271 ->method( 'getConnection' )
273 // RevisionStore getTitle uses a ConnectionRef
274 $mockLoadBalancer->expects( $this->atLeastOnce() )
275 ->method( 'getConnectionRef' )
278 // First call to Title::newFromID, faking no result (db lag?)
279 $db->expects( $this->at( 0 ) )
280 ->method( 'selectRow' )
286 ->willReturn( false );
288 // First select using rev_id, faking no result (db lag?)
289 $db->expects( $this->at( 1 ) )
290 ->method( 'selectRow' )
292 [ 'revision', 'page' ],
296 ->willReturn( false );
298 // Second call to Title::newFromID, no result
299 $db->expects( $this->at( 2 ) )
300 ->method( 'selectRow' )
306 ->willReturn( false );
308 // Second select using rev_id, result
309 $db->expects( $this->at( 3 ) )
310 ->method( 'selectRow' )
312 [ 'revision', 'page' ],
316 ->willReturn( (object)[
317 'page_namespace' => '2',
318 'page_title' => 'Foodey',
321 $store = $this->getRevisionStore( $mockLoadBalancer );
322 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
324 $this->assertSame( 2, $title->getNamespace() );
325 $this->assertSame( 'Foodey', $title->getDBkey() );
329 * @covers \MediaWiki\Storage\RevisionStore::getTitle
331 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
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 // Assert that the first call uses a REPLICA and the second falls back to master
340 // RevisionStore getTitle uses getConnectionRef
341 // Title::newFromID uses getConnection
342 foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
343 $mockLoadBalancer->expects( $this->exactly( 2 ) )
345 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
346 static $callCounter = 0;
348 // The first call should be to a REPLICA, and the second a MASTER.
349 if ( $callCounter === 1 ) {
350 $this->assertSame( DB_REPLICA
, $masterOrReplica );
351 } elseif ( $callCounter === 2 ) {
352 $this->assertSame( DB_MASTER
, $masterOrReplica );
357 // First and third call to Title::newFromID, faking no result
358 foreach ( [ 0, 2 ] as $counter ) {
359 $db->expects( $this->at( $counter ) )
360 ->method( 'selectRow' )
366 ->willReturn( false );
369 foreach ( [ 1, 3 ] as $counter ) {
370 $db->expects( $this->at( $counter ) )
371 ->method( 'selectRow' )
373 [ 'revision', 'page' ],
377 ->willReturn( false );
380 $store = $this->getRevisionStore( $mockLoadBalancer );
382 $this->setExpectedException( RevisionAccessException
::class );
383 $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
386 public function provideNewRevisionFromRow_legacyEncoding_applied() {
387 yield
'windows-1252, old_flags is empty' => [
392 'old_text' => "S\xF6me Content",
397 yield
'windows-1252, old_flags is null' => [
402 'old_text' => "S\xF6me Content",
409 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
411 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
413 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
414 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
415 $lb = MediaWikiServices
::getInstance()->getDBLoadBalancer();
417 $blobStore = new SqlBlobStore( $lb, $cache );
418 $blobStore->setLegacyEncoding( $encoding, Language
::factory( $locale ) );
420 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
422 $record = $store->newRevisionFromRow(
423 $this->makeRow( $row ),
425 Title
::newFromText( __METHOD__
. '-UTPage' )
428 $this->assertSame( $text, $record->getContent( 'main' )->serialize() );
432 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
434 public function testNewRevisionFromRow_legacyEncoding_ignored() {
436 'old_flags' => 'utf-8',
437 'old_text' => 'Söme Content',
440 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
441 $lb = MediaWikiServices
::getInstance()->getDBLoadBalancer();
443 $blobStore = new SqlBlobStore( $lb, $cache );
444 $blobStore->setLegacyEncoding( 'windows-1252', Language
::factory( 'en' ) );
446 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
448 $record = $store->newRevisionFromRow(
449 $this->makeRow( $row ),
451 Title
::newFromText( __METHOD__
. '-UTPage' )
453 $this->assertSame( 'Söme Content', $record->getContent( 'main' )->serialize() );
456 private function makeRow( array $array ) {
461 'rev_timestamp' => '20110101000000',
462 'rev_user_text' => 'Tester',
464 'rev_minor_edit' => 0,
467 'rev_parent_id' => 0,
468 'rev_sha1' => 'deadbeef',
469 'rev_comment_text' => 'Testing',
470 'rev_comment_data' => '{}',
471 'rev_comment_cid' => 111,
472 'rev_content_format' => CONTENT_FORMAT_TEXT
,
473 'rev_content_model' => CONTENT_MODEL_TEXT
,
474 'page_namespace' => 0,
475 'page_title' => 'TEST',
478 'page_is_redirect' => 0,
480 'user_name' => 'Tester',
482 'old_text' => 'Hello World',
483 'old_flags' => 'utf-8',
489 public function provideMigrationConstruction() {
491 [ SCHEMA_COMPAT_OLD
, false ],
492 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
493 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
494 [ SCHEMA_COMPAT_NEW
, false ],
495 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH
, true ],
496 [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH
, true ],
497 [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH
, true ],
502 * @covers \MediaWiki\Storage\RevisionStore::__construct
503 * @dataProvider provideMigrationConstruction
505 public function testMigrationConstruction( $migration, $expectException ) {
506 if ( $expectException ) {
507 $this->setExpectedException( InvalidArgumentException
::class );
509 $loadBalancer = $this->getMockLoadBalancer();
510 $blobStore = $this->getMockSqlBlobStore();
511 $cache = $this->getHashWANObjectCache();
512 $commentStore = $this->getMockCommentStore();
513 $services = MediaWikiServices
::getInstance();
514 $nameTables = $services->getNameTableStoreFactory();
515 $contentModelStore = $nameTables->getContentModels();
516 $slotRoleStore = $nameTables->getSlotRoles();
517 $store = new RevisionStore(
522 $nameTables->getContentModels(),
523 $nameTables->getSlotRoles(),
525 $services->getActorMigration()
527 if ( !$expectException ) {
528 $store = TestingAccessWrapper
::newFromObject( $store );
529 $this->assertSame( $loadBalancer, $store->loadBalancer
);
530 $this->assertSame( $blobStore, $store->blobStore
);
531 $this->assertSame( $cache, $store->cache
);
532 $this->assertSame( $commentStore, $store->commentStore
);
533 $this->assertSame( $contentModelStore, $store->contentModelStore
);
534 $this->assertSame( $slotRoleStore, $store->slotRoleStore
);
535 $this->assertSame( $migration, $store->mcrMigrationStage
);