9b26c3761f9b389c3f5d1f1c226b26347a8b8e4a
[lhc/web/wiklou.git] / tests / phpunit / includes / Revision / RevisionStoreTest.php
1 <?php
2
3 namespace MediaWiki\Tests\Revision;
4
5 use CommentStore;
6 use HashBagOStuff;
7 use InvalidArgumentException;
8 use Language;
9 use MediaWiki\MediaWikiServices;
10 use MediaWiki\Revision\RevisionAccessException;
11 use MediaWiki\Revision\RevisionStore;
12 use MediaWiki\Revision\SlotRoleRegistry;
13 use MediaWiki\Revision\SlotRecord;
14 use MediaWiki\Storage\SqlBlobStore;
15 use Wikimedia\Rdbms\ILoadBalancer;
16 use Wikimedia\Rdbms\MaintainableDBConnRef;
17 use MediaWikiTestCase;
18 use MWException;
19 use Title;
20 use WANObjectCache;
21 use Wikimedia\Rdbms\IDatabase;
22 use Wikimedia\Rdbms\LoadBalancer;
23 use Wikimedia\TestingAccessWrapper;
24 use WikitextContent;
25
26 /**
27 * Tests RevisionStore
28 */
29 class RevisionStoreTest extends MediaWikiTestCase {
30
31 private function useTextId() {
32 global $wgMultiContentRevisionSchemaMigrationStage;
33
34 return (bool)( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_OLD );
35 }
36
37 /**
38 * @param LoadBalancer $loadBalancer
39 * @param SqlBlobStore $blobStore
40 * @param WANObjectCache $WANObjectCache
41 *
42 * @return RevisionStore
43 */
44 private function getRevisionStore(
45 $loadBalancer = null,
46 $blobStore = null,
47 $WANObjectCache = null
48 ) {
49 global $wgMultiContentRevisionSchemaMigrationStage;
50 // the migration stage should be irrelevant, since all the tests that interact with
51 // the database are in RevisionStoreDbTest, not here.
52
53 return new RevisionStore(
54 $loadBalancer ?: $this->getMockLoadBalancer(),
55 $blobStore ?: $this->getMockSqlBlobStore(),
56 $WANObjectCache ?: $this->getHashWANObjectCache(),
57 MediaWikiServices::getInstance()->getCommentStore(),
58 MediaWikiServices::getInstance()->getContentModelStore(),
59 MediaWikiServices::getInstance()->getSlotRoleStore(),
60 MediaWikiServices::getInstance()->getSlotRoleRegistry(),
61 $wgMultiContentRevisionSchemaMigrationStage,
62 MediaWikiServices::getInstance()->getActorMigration()
63 );
64 }
65
66 /**
67 * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
68 */
69 private function getMockLoadBalancer() {
70 return $this->getMockBuilder( LoadBalancer::class )
71 ->disableOriginalConstructor()->getMock();
72 }
73
74 /**
75 * @return \PHPUnit_Framework_MockObject_MockObject|IDatabase
76 */
77 private function getMockDatabase() {
78 return $this->getMockBuilder( IDatabase::class )
79 ->disableOriginalConstructor()->getMock();
80 }
81
82 /**
83 * @param ILoadBalancer $mockLoadBalancer
84 * @param Database $db
85 * @return callable
86 */
87 private function getMockDBConnRefCallback( ILoadBalancer $mockLoadBalancer, IDatabase $db ) {
88 return function ( $i, $g, $domain, $flg ) use ( $mockLoadBalancer, $db ) {
89 return new MaintainableDBConnRef( $mockLoadBalancer, $db, $i );
90 };
91 }
92
93 /**
94 * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
95 */
96 private function getMockSqlBlobStore() {
97 return $this->getMockBuilder( SqlBlobStore::class )
98 ->disableOriginalConstructor()->getMock();
99 }
100
101 /**
102 * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
103 */
104 private function getMockCommentStore() {
105 return $this->getMockBuilder( CommentStore::class )
106 ->disableOriginalConstructor()->getMock();
107 }
108
109 /**
110 * @return \PHPUnit_Framework_MockObject_MockObject|SlotRoleRegistry
111 */
112 private function getMockSlotRoleRegistry() {
113 return $this->getMockBuilder( SlotRoleRegistry::class )
114 ->disableOriginalConstructor()->getMock();
115 }
116
117 private function getHashWANObjectCache() {
118 return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
119 }
120
121 public function provideSetContentHandlerUseDB() {
122 return [
123 // ContentHandlerUseDB can be true or false pre migration.
124 // During and after migration it can not be false...
125 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, true ],
126 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, true ],
127 [ false, SCHEMA_COMPAT_NEW, true ],
128 // ...but it can be true.
129 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, false ],
130 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, false ],
131 [ true, SCHEMA_COMPAT_NEW, false ],
132 ];
133 }
134
135 /**
136 * @dataProvider provideSetContentHandlerUseDB
137 * @covers \MediaWiki\Revision\RevisionStore::getContentHandlerUseDB
138 * @covers \MediaWiki\Revision\RevisionStore::setContentHandlerUseDB
139 */
140 public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
141 if ( $expectedFail ) {
142 $this->setExpectedException( MWException::class );
143 }
144
145 $nameTables = MediaWikiServices::getInstance()->getNameTableStoreFactory();
146
147 $store = new RevisionStore(
148 $this->getMockLoadBalancer(),
149 $this->getMockSqlBlobStore(),
150 $this->getHashWANObjectCache(),
151 $this->getMockCommentStore(),
152 $nameTables->getContentModels(),
153 $nameTables->getSlotRoles(),
154 $this->getMockSlotRoleRegistry(),
155 $migrationMode,
156 MediaWikiServices::getInstance()->getActorMigration()
157 );
158
159 $store->setContentHandlerUseDB( $contentHandlerDb );
160 $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
161 }
162
163 /**
164 * @covers \MediaWiki\Revision\RevisionStore::getTitle
165 */
166 public function testGetTitle_successFromPageId() {
167 $mockLoadBalancer = $this->getMockLoadBalancer();
168 // Title calls wfGetDB() so we have to set the main service
169 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
170
171 $db = $this->getMockDatabase();
172 // RevisionStore uses getConnectionRef
173 $mockLoadBalancer->expects( $this->any() )
174 ->method( 'getConnectionRef' )
175 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
176 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
177 $mockLoadBalancer->expects( $this->atLeastOnce() )
178 ->method( 'getMaintenanceConnectionRef' )
179 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
180
181 // First call to Title::newFromID, faking no result (db lag?)
182 $db->expects( $this->at( 0 ) )
183 ->method( 'selectRow' )
184 ->with(
185 'page',
186 $this->anything(),
187 [ 'page_id' => 1 ]
188 )
189 ->willReturn( (object)[
190 'page_namespace' => '1',
191 'page_title' => 'Food',
192 ] );
193
194 $store = $this->getRevisionStore( $mockLoadBalancer );
195 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
196
197 $this->assertSame( 1, $title->getNamespace() );
198 $this->assertSame( 'Food', $title->getDBkey() );
199 }
200
201 /**
202 * @covers \MediaWiki\Revision\RevisionStore::getTitle
203 */
204 public function testGetTitle_successFromPageIdOnFallback() {
205 $mockLoadBalancer = $this->getMockLoadBalancer();
206 // Title calls wfGetDB() so we have to set the main service
207 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
208
209 $db = $this->getMockDatabase();
210 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
211 // Assert that the first call uses a REPLICA and the second falls back to master
212 $mockLoadBalancer->expects( $this->atLeastOnce() )
213 ->method( 'getConnectionRef' )
214 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
215 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
216 $mockLoadBalancer->expects( $this->exactly( 2 ) )
217 ->method( 'getMaintenanceConnectionRef' )
218 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
219
220 // First call to Title::newFromID, faking no result (db lag?)
221 $db->expects( $this->at( 0 ) )
222 ->method( 'selectRow' )
223 ->with(
224 'page',
225 $this->anything(),
226 [ 'page_id' => 1 ]
227 )
228 ->willReturn( false );
229
230 // First select using rev_id, faking no result (db lag?)
231 $db->expects( $this->at( 1 ) )
232 ->method( 'selectRow' )
233 ->with(
234 [ 'revision', 'page' ],
235 $this->anything(),
236 [ 'rev_id' => 2 ]
237 )
238 ->willReturn( false );
239
240 // Second call to Title::newFromID, no result
241 $db->expects( $this->at( 2 ) )
242 ->method( 'selectRow' )
243 ->with(
244 'page',
245 $this->anything(),
246 [ 'page_id' => 1 ]
247 )
248 ->willReturn( (object)[
249 'page_namespace' => '2',
250 'page_title' => 'Foodey',
251 ] );
252
253 $store = $this->getRevisionStore( $mockLoadBalancer );
254 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
255
256 $this->assertSame( 2, $title->getNamespace() );
257 $this->assertSame( 'Foodey', $title->getDBkey() );
258 }
259
260 /**
261 * @covers \MediaWiki\Revision\RevisionStore::getTitle
262 */
263 public function testGetTitle_successFromRevId() {
264 $mockLoadBalancer = $this->getMockLoadBalancer();
265 // Title calls wfGetDB() so we have to set the main service
266 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
267
268 $db = $this->getMockDatabase();
269 $mockLoadBalancer->expects( $this->atLeastOnce() )
270 ->method( 'getConnectionRef' )
271 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
272 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
273 // RevisionStore getTitle uses getMaintenanceConnectionRef
274 $mockLoadBalancer->expects( $this->atLeastOnce() )
275 ->method( 'getMaintenanceConnectionRef' )
276 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
277
278 // First call to Title::newFromID, faking no result (db lag?)
279 $db->expects( $this->at( 0 ) )
280 ->method( 'selectRow' )
281 ->with(
282 'page',
283 $this->anything(),
284 [ 'page_id' => 1 ]
285 )
286 ->willReturn( false );
287
288 // First select using rev_id, faking no result (db lag?)
289 $db->expects( $this->at( 1 ) )
290 ->method( 'selectRow' )
291 ->with(
292 [ 'revision', 'page' ],
293 $this->anything(),
294 [ 'rev_id' => 2 ]
295 )
296 ->willReturn( (object)[
297 'page_namespace' => '1',
298 'page_title' => 'Food2',
299 ] );
300
301 $store = $this->getRevisionStore( $mockLoadBalancer );
302 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
303
304 $this->assertSame( 1, $title->getNamespace() );
305 $this->assertSame( 'Food2', $title->getDBkey() );
306 }
307
308 /**
309 * @covers \MediaWiki\Revision\RevisionStore::getTitle
310 */
311 public function testGetTitle_successFromRevIdOnFallback() {
312 $mockLoadBalancer = $this->getMockLoadBalancer();
313 // Title calls wfGetDB() so we have to set the main service
314 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
315
316 $db = $this->getMockDatabase();
317 // Assert that the first call uses a REPLICA and the second falls back to master
318 // RevisionStore uses getMaintenanceConnectionRef
319 $mockLoadBalancer->expects( $this->atLeastOnce() )
320 ->method( 'getConnectionRef' )
321 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
322 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
323 $mockLoadBalancer->expects( $this->exactly( 2 ) )
324 ->method( 'getMaintenanceConnectionRef' )
325 ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
326
327 // First call to Title::newFromID, faking no result (db lag?)
328 $db->expects( $this->at( 0 ) )
329 ->method( 'selectRow' )
330 ->with(
331 'page',
332 $this->anything(),
333 [ 'page_id' => 1 ]
334 )
335 ->willReturn( false );
336
337 // First select using rev_id, faking no result (db lag?)
338 $db->expects( $this->at( 1 ) )
339 ->method( 'selectRow' )
340 ->with(
341 [ 'revision', 'page' ],
342 $this->anything(),
343 [ 'rev_id' => 2 ]
344 )
345 ->willReturn( false );
346
347 // Second call to Title::newFromID, no result
348 $db->expects( $this->at( 2 ) )
349 ->method( 'selectRow' )
350 ->with(
351 'page',
352 $this->anything(),
353 [ 'page_id' => 1 ]
354 )
355 ->willReturn( false );
356
357 // Second select using rev_id, result
358 $db->expects( $this->at( 3 ) )
359 ->method( 'selectRow' )
360 ->with(
361 [ 'revision', 'page' ],
362 $this->anything(),
363 [ 'rev_id' => 2 ]
364 )
365 ->willReturn( (object)[
366 'page_namespace' => '2',
367 'page_title' => 'Foodey',
368 ] );
369
370 $store = $this->getRevisionStore( $mockLoadBalancer );
371 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
372
373 $this->assertSame( 2, $title->getNamespace() );
374 $this->assertSame( 'Foodey', $title->getDBkey() );
375 }
376
377 /**
378 * @covers \MediaWiki\Revision\RevisionStore::getTitle
379 */
380 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
381 $mockLoadBalancer = $this->getMockLoadBalancer();
382 // Title calls wfGetDB() so we have to set the main service
383 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
384
385 $db = $this->getMockDatabase();
386 // Title calls wfGetDB() which uses getMaintenanceConnectionRef
387 // Assert that the first call uses a REPLICA and the second falls back to master
388
389 // RevisionStore getTitle uses getConnectionRef
390 // Title::newFromID uses getMaintenanceConnectionRef
391 foreach ( [
392 'getConnectionRef', 'getMaintenanceConnectionRef'
393 ] as $method ) {
394 $mockLoadBalancer->expects( $this->exactly( 2 ) )
395 ->method( $method )
396 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
397 static $callCounter = 0;
398 $callCounter++;
399 // The first call should be to a REPLICA, and the second a MASTER.
400 if ( $callCounter === 1 ) {
401 $this->assertSame( DB_REPLICA, $masterOrReplica );
402 } elseif ( $callCounter === 2 ) {
403 $this->assertSame( DB_MASTER, $masterOrReplica );
404 }
405 return $db;
406 } );
407 }
408 // First and third call to Title::newFromID, faking no result
409 foreach ( [ 0, 2 ] as $counter ) {
410 $db->expects( $this->at( $counter ) )
411 ->method( 'selectRow' )
412 ->with(
413 'page',
414 $this->anything(),
415 [ 'page_id' => 1 ]
416 )
417 ->willReturn( false );
418 }
419
420 foreach ( [ 1, 3 ] as $counter ) {
421 $db->expects( $this->at( $counter ) )
422 ->method( 'selectRow' )
423 ->with(
424 [ 'revision', 'page' ],
425 $this->anything(),
426 [ 'rev_id' => 2 ]
427 )
428 ->willReturn( false );
429 }
430
431 $store = $this->getRevisionStore( $mockLoadBalancer );
432
433 $this->setExpectedException( RevisionAccessException::class );
434 $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
435 }
436
437 public function provideNewRevisionFromRow_legacyEncoding_applied() {
438 yield 'windows-1252, old_flags is empty' => [
439 'windows-1252',
440 'en',
441 [
442 'old_flags' => '',
443 'old_text' => "S\xF6me Content",
444 ],
445 'Söme Content'
446 ];
447
448 yield 'windows-1252, old_flags is null' => [
449 'windows-1252',
450 'en',
451 [
452 'old_flags' => null,
453 'old_text' => "S\xF6me Content",
454 ],
455 'Söme Content'
456 ];
457 }
458
459 /**
460 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
461 *
462 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
463 */
464 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
465 if ( !$this->useTextId() ) {
466 $this->markTestSkipped( 'No longer applicable with MCR schema' );
467 }
468
469 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
470 $services = MediaWikiServices::getInstance();
471 $lb = $services->getDBLoadBalancer();
472 $access = $services->getExternalStoreAccess();
473
474 $blobStore = new SqlBlobStore( $lb, $access, $cache );
475
476 $blobStore->setLegacyEncoding( $encoding, Language::factory( $locale ) );
477
478 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
479
480 $record = $store->newRevisionFromRow(
481 $this->makeRow( $row ),
482 0,
483 Title::newFromText( __METHOD__ . '-UTPage' )
484 );
485
486 $this->assertSame( $text, $record->getContent( SlotRecord::MAIN )->serialize() );
487 }
488
489 /**
490 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
491 */
492 public function testNewRevisionFromRow_legacyEncoding_ignored() {
493 if ( !$this->useTextId() ) {
494 $this->markTestSkipped( 'No longer applicable with MCR schema' );
495 }
496
497 $row = [
498 'old_flags' => 'utf-8',
499 'old_text' => 'Söme Content',
500 ];
501
502 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
503 $services = MediaWikiServices::getInstance();
504 $lb = $services->getDBLoadBalancer();
505 $access = $services->getExternalStoreAccess();
506
507 $blobStore = new SqlBlobStore( $lb, $access, $cache );
508 $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
509
510 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
511
512 $record = $store->newRevisionFromRow(
513 $this->makeRow( $row ),
514 0,
515 Title::newFromText( __METHOD__ . '-UTPage' )
516 );
517 $this->assertSame( 'Söme Content', $record->getContent( SlotRecord::MAIN )->serialize() );
518 }
519
520 private function makeRow( array $array ) {
521 $row = $array + [
522 'rev_id' => 7,
523 'rev_page' => 5,
524 'rev_timestamp' => '20110101000000',
525 'rev_user_text' => 'Tester',
526 'rev_user' => 17,
527 'rev_minor_edit' => 0,
528 'rev_deleted' => 0,
529 'rev_len' => 100,
530 'rev_parent_id' => 0,
531 'rev_sha1' => 'deadbeef',
532 'rev_comment_text' => 'Testing',
533 'rev_comment_data' => '{}',
534 'rev_comment_cid' => 111,
535 'page_namespace' => 0,
536 'page_title' => 'TEST',
537 'page_id' => 5,
538 'page_latest' => 7,
539 'page_is_redirect' => 0,
540 'page_len' => 100,
541 'user_name' => 'Tester',
542 ];
543
544 if ( $this->useTextId() ) {
545 $row += [
546 'rev_content_format' => CONTENT_FORMAT_TEXT,
547 'rev_content_model' => CONTENT_MODEL_TEXT,
548 'rev_text_id' => 11,
549 'old_id' => 11,
550 'old_text' => 'Hello World',
551 'old_flags' => 'utf-8',
552 ];
553 } elseif ( !isset( $row['content'] ) && isset( $array['old_text'] ) ) {
554 $row['content'] = [
555 'main' => new WikitextContent( $array['old_text'] ),
556 ];
557 }
558
559 return (object)$row;
560 }
561
562 public function provideMigrationConstruction() {
563 return [
564 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, false ],
565 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, false ],
566 [ SCHEMA_COMPAT_NEW, false ],
567 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH, true ],
568 [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH, true ],
569 [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH, true ],
570 ];
571 }
572
573 /**
574 * @covers \MediaWiki\Revision\RevisionStore::__construct
575 * @dataProvider provideMigrationConstruction
576 */
577 public function testMigrationConstruction( $migration, $expectException ) {
578 if ( $expectException ) {
579 $this->setExpectedException( InvalidArgumentException::class );
580 }
581 $loadBalancer = $this->getMockLoadBalancer();
582 $blobStore = $this->getMockSqlBlobStore();
583 $cache = $this->getHashWANObjectCache();
584 $commentStore = $this->getMockCommentStore();
585 $services = MediaWikiServices::getInstance();
586 $nameTables = $services->getNameTableStoreFactory();
587 $contentModelStore = $nameTables->getContentModels();
588 $slotRoleStore = $nameTables->getSlotRoles();
589 $slotRoleRegistry = $services->getSlotRoleRegistry();
590 $store = new RevisionStore(
591 $loadBalancer,
592 $blobStore,
593 $cache,
594 $commentStore,
595 $nameTables->getContentModels(),
596 $nameTables->getSlotRoles(),
597 $slotRoleRegistry,
598 $migration,
599 $services->getActorMigration()
600 );
601 if ( !$expectException ) {
602 $store = TestingAccessWrapper::newFromObject( $store );
603 $this->assertSame( $loadBalancer, $store->loadBalancer );
604 $this->assertSame( $blobStore, $store->blobStore );
605 $this->assertSame( $cache, $store->cache );
606 $this->assertSame( $commentStore, $store->commentStore );
607 $this->assertSame( $contentModelStore, $store->contentModelStore );
608 $this->assertSame( $slotRoleStore, $store->slotRoleStore );
609 $this->assertSame( $migration, $store->mcrMigrationStage );
610 }
611 }
612
613 }