3 namespace MediaWiki\Tests\Revision
;
7 use InvalidArgumentException
;
9 use MediaWiki\MediaWikiServices
;
10 use MediaWiki\Revision\RevisionAccessException
;
11 use MediaWiki\Revision\RevisionStore
;
12 use MediaWiki\Revision\SlotRecord
;
13 use MediaWiki\Storage\SqlBlobStore
;
14 use MediaWikiTestCase
;
18 use Wikimedia\Rdbms\Database
;
19 use Wikimedia\Rdbms\LoadBalancer
;
20 use Wikimedia\TestingAccessWrapper
;
23 class RevisionStoreTest
extends MediaWikiTestCase
{
25 private function useTextId() {
26 global $wgMultiContentRevisionSchemaMigrationStage;
28 return (bool)( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_OLD
);
32 * @param LoadBalancer $loadBalancer
33 * @param SqlBlobStore $blobStore
34 * @param WANObjectCache $WANObjectCache
36 * @return RevisionStore
38 private function getRevisionStore(
41 $WANObjectCache = null
43 global $wgMultiContentRevisionSchemaMigrationStage;
44 // the migration stage should be irrelevant, since all the tests that interact with
45 // the database are in RevisionStoreDbTest, not here.
47 return new RevisionStore(
48 $loadBalancer ?
: $this->getMockLoadBalancer(),
49 $blobStore ?
: $this->getMockSqlBlobStore(),
50 $WANObjectCache ?
: $this->getHashWANObjectCache(),
51 MediaWikiServices
::getInstance()->getCommentStore(),
52 MediaWikiServices
::getInstance()->getContentModelStore(),
53 MediaWikiServices
::getInstance()->getSlotRoleStore(),
54 $wgMultiContentRevisionSchemaMigrationStage,
55 MediaWikiServices
::getInstance()->getActorMigration()
60 * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
62 private function getMockLoadBalancer() {
63 return $this->getMockBuilder( LoadBalancer
::class )
64 ->disableOriginalConstructor()->getMock();
68 * @return \PHPUnit_Framework_MockObject_MockObject|Database
70 private function getMockDatabase() {
71 return $this->getMockBuilder( Database
::class )
72 ->disableOriginalConstructor()->getMock();
76 * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
78 private function getMockSqlBlobStore() {
79 return $this->getMockBuilder( SqlBlobStore
::class )
80 ->disableOriginalConstructor()->getMock();
84 * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
86 private function getMockCommentStore() {
87 return $this->getMockBuilder( CommentStore
::class )
88 ->disableOriginalConstructor()->getMock();
91 private function getHashWANObjectCache() {
92 return new WANObjectCache( [ 'cache' => new \
HashBagOStuff() ] );
95 public function provideSetContentHandlerUseDB() {
97 // ContentHandlerUseDB can be true of false pre migration.
98 [ false, SCHEMA_COMPAT_OLD
, false ],
99 [ true, SCHEMA_COMPAT_OLD
, false ],
100 // During and after migration it can not be false...
101 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, true ],
102 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, true ],
103 [ false, SCHEMA_COMPAT_NEW
, true ],
104 // ...but it can be true.
105 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
106 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
107 [ true, SCHEMA_COMPAT_NEW
, false ],
112 * @dataProvider provideSetContentHandlerUseDB
113 * @covers \MediaWiki\Revision\RevisionStore::getContentHandlerUseDB
114 * @covers \MediaWiki\Revision\RevisionStore::setContentHandlerUseDB
116 public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
117 if ( $expectedFail ) {
118 $this->setExpectedException( MWException
::class );
121 $nameTables = MediaWikiServices
::getInstance()->getNameTableStoreFactory();
123 $store = new RevisionStore(
124 $this->getMockLoadBalancer(),
125 $this->getMockSqlBlobStore(),
126 $this->getHashWANObjectCache(),
127 $this->getMockCommentStore(),
128 $nameTables->getContentModels(),
129 $nameTables->getSlotRoles(),
131 MediaWikiServices
::getInstance()->getActorMigration()
134 $store->setContentHandlerUseDB( $contentHandlerDb );
135 $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
138 public function testGetTitle_successFromPageId() {
139 $mockLoadBalancer = $this->getMockLoadBalancer();
140 // Title calls wfGetDB() so we have to set the main service
141 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
143 $db = $this->getMockDatabase();
144 // Title calls wfGetDB() which uses a regular Connection
145 $mockLoadBalancer->expects( $this->atLeastOnce() )
146 ->method( 'getConnection' )
149 // First call to Title::newFromID, faking no result (db lag?)
150 $db->expects( $this->at( 0 ) )
151 ->method( 'selectRow' )
157 ->willReturn( (object)[
158 'page_namespace' => '1',
159 'page_title' => 'Food',
162 $store = $this->getRevisionStore( $mockLoadBalancer );
163 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
165 $this->assertSame( 1, $title->getNamespace() );
166 $this->assertSame( 'Food', $title->getDBkey() );
169 public function testGetTitle_successFromPageIdOnFallback() {
170 $mockLoadBalancer = $this->getMockLoadBalancer();
171 // Title calls wfGetDB() so we have to set the main service
172 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
174 $db = $this->getMockDatabase();
175 // Title calls wfGetDB() which uses a regular Connection
176 // Assert that the first call uses a REPLICA and the second falls back to master
177 $mockLoadBalancer->expects( $this->exactly( 2 ) )
178 ->method( 'getConnection' )
180 // RevisionStore getTitle uses a ConnectionRef
181 $mockLoadBalancer->expects( $this->atLeastOnce() )
182 ->method( 'getConnectionRef' )
185 // First call to Title::newFromID, faking no result (db lag?)
186 $db->expects( $this->at( 0 ) )
187 ->method( 'selectRow' )
193 ->willReturn( false );
195 // First select using rev_id, faking no result (db lag?)
196 $db->expects( $this->at( 1 ) )
197 ->method( 'selectRow' )
199 [ 'revision', 'page' ],
203 ->willReturn( false );
205 // Second call to Title::newFromID, no result
206 $db->expects( $this->at( 2 ) )
207 ->method( 'selectRow' )
213 ->willReturn( (object)[
214 'page_namespace' => '2',
215 'page_title' => 'Foodey',
218 $store = $this->getRevisionStore( $mockLoadBalancer );
219 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
221 $this->assertSame( 2, $title->getNamespace() );
222 $this->assertSame( 'Foodey', $title->getDBkey() );
225 public function testGetTitle_successFromRevId() {
226 $mockLoadBalancer = $this->getMockLoadBalancer();
227 // Title calls wfGetDB() so we have to set the main service
228 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
230 $db = $this->getMockDatabase();
231 // Title calls wfGetDB() which uses a regular Connection
232 $mockLoadBalancer->expects( $this->atLeastOnce() )
233 ->method( 'getConnection' )
235 // RevisionStore getTitle uses a ConnectionRef
236 $mockLoadBalancer->expects( $this->atLeastOnce() )
237 ->method( 'getConnectionRef' )
240 // First call to Title::newFromID, faking no result (db lag?)
241 $db->expects( $this->at( 0 ) )
242 ->method( 'selectRow' )
248 ->willReturn( false );
250 // First select using rev_id, faking no result (db lag?)
251 $db->expects( $this->at( 1 ) )
252 ->method( 'selectRow' )
254 [ 'revision', 'page' ],
258 ->willReturn( (object)[
259 'page_namespace' => '1',
260 'page_title' => 'Food2',
263 $store = $this->getRevisionStore( $mockLoadBalancer );
264 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
266 $this->assertSame( 1, $title->getNamespace() );
267 $this->assertSame( 'Food2', $title->getDBkey() );
270 public function testGetTitle_successFromRevIdOnFallback() {
271 $mockLoadBalancer = $this->getMockLoadBalancer();
272 // Title calls wfGetDB() so we have to set the main service
273 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
275 $db = $this->getMockDatabase();
276 // Title calls wfGetDB() which uses a regular Connection
277 // Assert that the first call uses a REPLICA and the second falls back to master
278 $mockLoadBalancer->expects( $this->exactly( 2 ) )
279 ->method( 'getConnection' )
281 // RevisionStore getTitle uses a ConnectionRef
282 $mockLoadBalancer->expects( $this->atLeastOnce() )
283 ->method( 'getConnectionRef' )
286 // First call to Title::newFromID, faking no result (db lag?)
287 $db->expects( $this->at( 0 ) )
288 ->method( 'selectRow' )
294 ->willReturn( false );
296 // First select using rev_id, faking no result (db lag?)
297 $db->expects( $this->at( 1 ) )
298 ->method( 'selectRow' )
300 [ 'revision', 'page' ],
304 ->willReturn( false );
306 // Second call to Title::newFromID, no result
307 $db->expects( $this->at( 2 ) )
308 ->method( 'selectRow' )
314 ->willReturn( false );
316 // Second select using rev_id, result
317 $db->expects( $this->at( 3 ) )
318 ->method( 'selectRow' )
320 [ 'revision', 'page' ],
324 ->willReturn( (object)[
325 'page_namespace' => '2',
326 'page_title' => 'Foodey',
329 $store = $this->getRevisionStore( $mockLoadBalancer );
330 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
332 $this->assertSame( 2, $title->getNamespace() );
333 $this->assertSame( 'Foodey', $title->getDBkey() );
337 * @covers \MediaWiki\Revision\RevisionStore::getTitle
339 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
340 $mockLoadBalancer = $this->getMockLoadBalancer();
341 // Title calls wfGetDB() so we have to set the main service
342 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
344 $db = $this->getMockDatabase();
345 // Title calls wfGetDB() which uses a regular Connection
346 // Assert that the first call uses a REPLICA and the second falls back to master
348 // RevisionStore getTitle uses getConnectionRef
349 // Title::newFromID uses getConnection
350 foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
351 $mockLoadBalancer->expects( $this->exactly( 2 ) )
353 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
354 static $callCounter = 0;
356 // The first call should be to a REPLICA, and the second a MASTER.
357 if ( $callCounter === 1 ) {
358 $this->assertSame( DB_REPLICA
, $masterOrReplica );
359 } elseif ( $callCounter === 2 ) {
360 $this->assertSame( DB_MASTER
, $masterOrReplica );
365 // First and third call to Title::newFromID, faking no result
366 foreach ( [ 0, 2 ] as $counter ) {
367 $db->expects( $this->at( $counter ) )
368 ->method( 'selectRow' )
374 ->willReturn( false );
377 foreach ( [ 1, 3 ] as $counter ) {
378 $db->expects( $this->at( $counter ) )
379 ->method( 'selectRow' )
381 [ 'revision', 'page' ],
385 ->willReturn( false );
388 $store = $this->getRevisionStore( $mockLoadBalancer );
390 $this->setExpectedException( RevisionAccessException
::class );
391 $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
394 public function provideNewRevisionFromRow_legacyEncoding_applied() {
395 yield
'windows-1252, old_flags is empty' => [
400 'old_text' => "S\xF6me Content",
405 yield
'windows-1252, old_flags is null' => [
410 'old_text' => "S\xF6me Content",
417 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
419 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
421 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
422 if ( !$this->useTextId() ) {
423 $this->markTestSkipped( 'No longer applicable with MCR schema' );
426 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
427 $lb = MediaWikiServices
::getInstance()->getDBLoadBalancer();
429 $blobStore = new SqlBlobStore( $lb, $cache );
430 $blobStore->setLegacyEncoding( $encoding, Language
::factory( $locale ) );
432 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
434 $record = $store->newRevisionFromRow(
435 $this->makeRow( $row ),
437 Title
::newFromText( __METHOD__
. '-UTPage' )
440 $this->assertSame( $text, $record->getContent( SlotRecord
::MAIN
)->serialize() );
444 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
446 public function testNewRevisionFromRow_legacyEncoding_ignored() {
447 if ( !$this->useTextId() ) {
448 $this->markTestSkipped( 'No longer applicable with MCR schema' );
452 'old_flags' => 'utf-8',
453 'old_text' => 'Söme Content',
456 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
457 $lb = MediaWikiServices
::getInstance()->getDBLoadBalancer();
459 $blobStore = new SqlBlobStore( $lb, $cache );
460 $blobStore->setLegacyEncoding( 'windows-1252', Language
::factory( 'en' ) );
462 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
464 $record = $store->newRevisionFromRow(
465 $this->makeRow( $row ),
467 Title
::newFromText( __METHOD__
. '-UTPage' )
469 $this->assertSame( 'Söme Content', $record->getContent( SlotRecord
::MAIN
)->serialize() );
472 private function makeRow( array $array ) {
476 'rev_timestamp' => '20110101000000',
477 'rev_user_text' => 'Tester',
479 'rev_minor_edit' => 0,
482 'rev_parent_id' => 0,
483 'rev_sha1' => 'deadbeef',
484 'rev_comment_text' => 'Testing',
485 'rev_comment_data' => '{}',
486 'rev_comment_cid' => 111,
487 'page_namespace' => 0,
488 'page_title' => 'TEST',
491 'page_is_redirect' => 0,
493 'user_name' => 'Tester',
496 if ( $this->useTextId() ) {
498 'rev_content_format' => CONTENT_FORMAT_TEXT
,
499 'rev_content_model' => CONTENT_MODEL_TEXT
,
502 'old_text' => 'Hello World',
503 'old_flags' => 'utf-8',
506 if ( !isset( $row['content'] ) && isset( $array['old_text'] ) ) {
508 'main' => new WikitextContent( $array['old_text'] ),
516 public function provideMigrationConstruction() {
518 [ SCHEMA_COMPAT_OLD
, false ],
519 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
520 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
521 [ SCHEMA_COMPAT_NEW
, false ],
522 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH
, true ],
523 [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH
, true ],
524 [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH
, true ],
529 * @covers \MediaWiki\Revision\RevisionStore::__construct
530 * @dataProvider provideMigrationConstruction
532 public function testMigrationConstruction( $migration, $expectException ) {
533 if ( $expectException ) {
534 $this->setExpectedException( InvalidArgumentException
::class );
536 $loadBalancer = $this->getMockLoadBalancer();
537 $blobStore = $this->getMockSqlBlobStore();
538 $cache = $this->getHashWANObjectCache();
539 $commentStore = $this->getMockCommentStore();
540 $services = MediaWikiServices
::getInstance();
541 $nameTables = $services->getNameTableStoreFactory();
542 $contentModelStore = $nameTables->getContentModels();
543 $slotRoleStore = $nameTables->getSlotRoles();
544 $store = new RevisionStore(
549 $nameTables->getContentModels(),
550 $nameTables->getSlotRoles(),
552 $services->getActorMigration()
554 if ( !$expectException ) {
555 $store = TestingAccessWrapper
::newFromObject( $store );
556 $this->assertSame( $loadBalancer, $store->loadBalancer
);
557 $this->assertSame( $blobStore, $store->blobStore
);
558 $this->assertSame( $cache, $store->cache
);
559 $this->assertSame( $commentStore, $store->commentStore
);
560 $this->assertSame( $contentModelStore, $store->contentModelStore
);
561 $this->assertSame( $slotRoleStore, $store->slotRoleStore
);
562 $this->assertSame( $migration, $store->mcrMigrationStage
);