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 $store = new RevisionStore(
114 $this->getMockLoadBalancer(),
115 $this->getMockSqlBlobStore(),
116 $this->getHashWANObjectCache(),
117 $this->getMockCommentStore(),
118 MediaWikiServices
::getInstance()->getContentModelStore(),
119 MediaWikiServices
::getInstance()->getSlotRoleStore(),
121 MediaWikiServices
::getInstance()->getActorMigration()
124 $store->setContentHandlerUseDB( $contentHandlerDb );
125 $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
128 public function testGetTitle_successFromPageId() {
129 $mockLoadBalancer = $this->getMockLoadBalancer();
130 // Title calls wfGetDB() so we have to set the main service
131 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
133 $db = $this->getMockDatabase();
134 // Title calls wfGetDB() which uses a regular Connection
135 $mockLoadBalancer->expects( $this->atLeastOnce() )
136 ->method( 'getConnection' )
139 // First call to Title::newFromID, faking no result (db lag?)
140 $db->expects( $this->at( 0 ) )
141 ->method( 'selectRow' )
147 ->willReturn( (object)[
148 'page_namespace' => '1',
149 'page_title' => 'Food',
152 $store = $this->getRevisionStore( $mockLoadBalancer );
153 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
155 $this->assertSame( 1, $title->getNamespace() );
156 $this->assertSame( 'Food', $title->getDBkey() );
159 public function testGetTitle_successFromPageIdOnFallback() {
160 $mockLoadBalancer = $this->getMockLoadBalancer();
161 // Title calls wfGetDB() so we have to set the main service
162 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
164 $db = $this->getMockDatabase();
165 // Title calls wfGetDB() which uses a regular Connection
166 // Assert that the first call uses a REPLICA and the second falls back to master
167 $mockLoadBalancer->expects( $this->exactly( 2 ) )
168 ->method( 'getConnection' )
170 // RevisionStore getTitle uses a ConnectionRef
171 $mockLoadBalancer->expects( $this->atLeastOnce() )
172 ->method( 'getConnectionRef' )
175 // First call to Title::newFromID, faking no result (db lag?)
176 $db->expects( $this->at( 0 ) )
177 ->method( 'selectRow' )
183 ->willReturn( false );
185 // First select using rev_id, faking no result (db lag?)
186 $db->expects( $this->at( 1 ) )
187 ->method( 'selectRow' )
189 [ 'revision', 'page' ],
193 ->willReturn( false );
195 // Second call to Title::newFromID, no result
196 $db->expects( $this->at( 2 ) )
197 ->method( 'selectRow' )
203 ->willReturn( (object)[
204 'page_namespace' => '2',
205 'page_title' => 'Foodey',
208 $store = $this->getRevisionStore( $mockLoadBalancer );
209 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
211 $this->assertSame( 2, $title->getNamespace() );
212 $this->assertSame( 'Foodey', $title->getDBkey() );
215 public function testGetTitle_successFromRevId() {
216 $mockLoadBalancer = $this->getMockLoadBalancer();
217 // Title calls wfGetDB() so we have to set the main service
218 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
220 $db = $this->getMockDatabase();
221 // Title calls wfGetDB() which uses a regular Connection
222 $mockLoadBalancer->expects( $this->atLeastOnce() )
223 ->method( 'getConnection' )
225 // RevisionStore getTitle uses a ConnectionRef
226 $mockLoadBalancer->expects( $this->atLeastOnce() )
227 ->method( 'getConnectionRef' )
230 // First call to Title::newFromID, faking no result (db lag?)
231 $db->expects( $this->at( 0 ) )
232 ->method( 'selectRow' )
238 ->willReturn( false );
240 // First select using rev_id, faking no result (db lag?)
241 $db->expects( $this->at( 1 ) )
242 ->method( 'selectRow' )
244 [ 'revision', 'page' ],
248 ->willReturn( (object)[
249 'page_namespace' => '1',
250 'page_title' => 'Food2',
253 $store = $this->getRevisionStore( $mockLoadBalancer );
254 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
256 $this->assertSame( 1, $title->getNamespace() );
257 $this->assertSame( 'Food2', $title->getDBkey() );
260 public function testGetTitle_successFromRevIdOnFallback() {
261 $mockLoadBalancer = $this->getMockLoadBalancer();
262 // Title calls wfGetDB() so we have to set the main service
263 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
265 $db = $this->getMockDatabase();
266 // Title calls wfGetDB() which uses a regular Connection
267 // Assert that the first call uses a REPLICA and the second falls back to master
268 $mockLoadBalancer->expects( $this->exactly( 2 ) )
269 ->method( 'getConnection' )
271 // RevisionStore getTitle uses a ConnectionRef
272 $mockLoadBalancer->expects( $this->atLeastOnce() )
273 ->method( 'getConnectionRef' )
276 // First call to Title::newFromID, faking no result (db lag?)
277 $db->expects( $this->at( 0 ) )
278 ->method( 'selectRow' )
284 ->willReturn( false );
286 // First select using rev_id, faking no result (db lag?)
287 $db->expects( $this->at( 1 ) )
288 ->method( 'selectRow' )
290 [ 'revision', 'page' ],
294 ->willReturn( false );
296 // Second call to Title::newFromID, no result
297 $db->expects( $this->at( 2 ) )
298 ->method( 'selectRow' )
304 ->willReturn( false );
306 // Second select using rev_id, result
307 $db->expects( $this->at( 3 ) )
308 ->method( 'selectRow' )
310 [ 'revision', 'page' ],
314 ->willReturn( (object)[
315 'page_namespace' => '2',
316 'page_title' => 'Foodey',
319 $store = $this->getRevisionStore( $mockLoadBalancer );
320 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
322 $this->assertSame( 2, $title->getNamespace() );
323 $this->assertSame( 'Foodey', $title->getDBkey() );
327 * @covers \MediaWiki\Storage\RevisionStore::getTitle
329 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
330 $mockLoadBalancer = $this->getMockLoadBalancer();
331 // Title calls wfGetDB() so we have to set the main service
332 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
334 $db = $this->getMockDatabase();
335 // Title calls wfGetDB() which uses a regular Connection
336 // Assert that the first call uses a REPLICA and the second falls back to master
338 // RevisionStore getTitle uses getConnectionRef
339 // Title::newFromID uses getConnection
340 foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
341 $mockLoadBalancer->expects( $this->exactly( 2 ) )
343 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
344 static $callCounter = 0;
346 // The first call should be to a REPLICA, and the second a MASTER.
347 if ( $callCounter === 1 ) {
348 $this->assertSame( DB_REPLICA
, $masterOrReplica );
349 } elseif ( $callCounter === 2 ) {
350 $this->assertSame( DB_MASTER
, $masterOrReplica );
355 // First and third call to Title::newFromID, faking no result
356 foreach ( [ 0, 2 ] as $counter ) {
357 $db->expects( $this->at( $counter ) )
358 ->method( 'selectRow' )
364 ->willReturn( false );
367 foreach ( [ 1, 3 ] as $counter ) {
368 $db->expects( $this->at( $counter ) )
369 ->method( 'selectRow' )
371 [ 'revision', 'page' ],
375 ->willReturn( false );
378 $store = $this->getRevisionStore( $mockLoadBalancer );
380 $this->setExpectedException( RevisionAccessException
::class );
381 $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
384 public function provideNewRevisionFromRow_legacyEncoding_applied() {
385 yield
'windows-1252, old_flags is empty' => [
390 'old_text' => "S\xF6me Content",
395 yield
'windows-1252, old_flags is null' => [
400 'old_text' => "S\xF6me Content",
407 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
409 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
411 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
412 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
413 $lb = MediaWikiServices
::getInstance()->getDBLoadBalancer();
415 $blobStore = new SqlBlobStore( $lb, $cache );
416 $blobStore->setLegacyEncoding( $encoding, Language
::factory( $locale ) );
418 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
420 $record = $store->newRevisionFromRow(
421 $this->makeRow( $row ),
423 Title
::newFromText( __METHOD__
. '-UTPage' )
426 $this->assertSame( $text, $record->getContent( 'main' )->serialize() );
430 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
432 public function testNewRevisionFromRow_legacyEncoding_ignored() {
434 'old_flags' => 'utf-8',
435 'old_text' => 'Söme Content',
438 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
439 $lb = MediaWikiServices
::getInstance()->getDBLoadBalancer();
441 $blobStore = new SqlBlobStore( $lb, $cache );
442 $blobStore->setLegacyEncoding( 'windows-1252', Language
::factory( 'en' ) );
444 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
446 $record = $store->newRevisionFromRow(
447 $this->makeRow( $row ),
449 Title
::newFromText( __METHOD__
. '-UTPage' )
451 $this->assertSame( 'Söme Content', $record->getContent( 'main' )->serialize() );
454 private function makeRow( array $array ) {
459 'rev_timestamp' => '20110101000000',
460 'rev_user_text' => 'Tester',
462 'rev_minor_edit' => 0,
465 'rev_parent_id' => 0,
466 'rev_sha1' => 'deadbeef',
467 'rev_comment_text' => 'Testing',
468 'rev_comment_data' => '{}',
469 'rev_comment_cid' => 111,
470 'rev_content_format' => CONTENT_FORMAT_TEXT
,
471 'rev_content_model' => CONTENT_MODEL_TEXT
,
472 'page_namespace' => 0,
473 'page_title' => 'TEST',
476 'page_is_redirect' => 0,
478 'user_name' => 'Tester',
480 'old_text' => 'Hello World',
481 'old_flags' => 'utf-8',
487 public function provideMigrationConstruction() {
489 [ SCHEMA_COMPAT_OLD
, false ],
490 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
491 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
492 [ SCHEMA_COMPAT_NEW
, false ],
493 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH
, true ],
494 [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH
, true ],
495 [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH
, true ],
500 * @covers \MediaWiki\Storage\RevisionStore::__construct
501 * @dataProvider provideMigrationConstruction
503 public function testMigrationConstruction( $migration, $expectException ) {
504 if ( $expectException ) {
505 $this->setExpectedException( InvalidArgumentException
::class );
507 $loadBalancer = $this->getMockLoadBalancer();
508 $blobStore = $this->getMockSqlBlobStore();
509 $cache = $this->getHashWANObjectCache();
510 $commentStore = $this->getMockCommentStore();
511 $contentModelStore = MediaWikiServices
::getInstance()->getContentModelStore();
512 $slotRoleStore = MediaWikiServices
::getInstance()->getSlotRoleStore();
513 $store = new RevisionStore(
518 MediaWikiServices
::getInstance()->getContentModelStore(),
519 MediaWikiServices
::getInstance()->getSlotRoleStore(),
521 MediaWikiServices
::getInstance()->getActorMigration()
523 if ( !$expectException ) {
524 $store = TestingAccessWrapper
::newFromObject( $store );
525 $this->assertSame( $loadBalancer, $store->loadBalancer
);
526 $this->assertSame( $blobStore, $store->blobStore
);
527 $this->assertSame( $cache, $store->cache
);
528 $this->assertSame( $commentStore, $store->commentStore
);
529 $this->assertSame( $contentModelStore, $store->contentModelStore
);
530 $this->assertSame( $slotRoleStore, $store->slotRoleStore
);
531 $this->assertSame( $migration, $store->mcrMigrationStage
);