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