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