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