Merge "jquery.textSelection: Implement 'encapsulateSelection' in terms of the other...
[lhc/web/wiklou.git] / tests / phpunit / includes / Storage / RevisionStoreTest.php
1 <?php
2
3 namespace MediaWiki\Tests\Storage;
4
5 use HashBagOStuff;
6 use Language;
7 use MediaWiki\MediaWikiServices;
8 use MediaWiki\Storage\RevisionAccessException;
9 use MediaWiki\Storage\RevisionStore;
10 use MediaWiki\Storage\SqlBlobStore;
11 use MediaWikiTestCase;
12 use Title;
13 use WANObjectCache;
14 use Wikimedia\Rdbms\Database;
15 use Wikimedia\Rdbms\LoadBalancer;
16
17 class RevisionStoreTest extends MediaWikiTestCase {
18
19 /**
20 * @param LoadBalancer $loadBalancer
21 * @param SqlBlobStore $blobStore
22 * @param WANObjectCache $WANObjectCache
23 *
24 * @return RevisionStore
25 */
26 private function getRevisionStore(
27 $loadBalancer = null,
28 $blobStore = null,
29 $WANObjectCache = null
30 ) {
31 return new RevisionStore(
32 $loadBalancer ? $loadBalancer : $this->getMockLoadBalancer(),
33 $blobStore ? $blobStore : $this->getMockSqlBlobStore(),
34 $WANObjectCache ? $WANObjectCache : $this->getHashWANObjectCache(),
35 MediaWikiServices::getInstance()->getCommentStore()
36 );
37 }
38
39 /**
40 * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
41 */
42 private function getMockLoadBalancer() {
43 return $this->getMockBuilder( LoadBalancer::class )
44 ->disableOriginalConstructor()->getMock();
45 }
46
47 /**
48 * @return \PHPUnit_Framework_MockObject_MockObject|Database
49 */
50 private function getMockDatabase() {
51 return $this->getMockBuilder( Database::class )
52 ->disableOriginalConstructor()->getMock();
53 }
54
55 /**
56 * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
57 */
58 private function getMockSqlBlobStore() {
59 return $this->getMockBuilder( SqlBlobStore::class )
60 ->disableOriginalConstructor()->getMock();
61 }
62
63 private function getHashWANObjectCache() {
64 return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
65 }
66
67 /**
68 * @covers \MediaWiki\Storage\RevisionStore::getContentHandlerUseDB
69 * @covers \MediaWiki\Storage\RevisionStore::setContentHandlerUseDB
70 */
71 public function testGetSetContentHandlerDb() {
72 $store = $this->getRevisionStore();
73 $this->assertTrue( $store->getContentHandlerUseDB() );
74 $store->setContentHandlerUseDB( false );
75 $this->assertFalse( $store->getContentHandlerUseDB() );
76 $store->setContentHandlerUseDB( true );
77 $this->assertTrue( $store->getContentHandlerUseDB() );
78 }
79
80 private function getDefaultQueryFields() {
81 return [
82 'rev_id',
83 'rev_page',
84 'rev_text_id',
85 'rev_timestamp',
86 'rev_user_text',
87 'rev_user',
88 'rev_minor_edit',
89 'rev_deleted',
90 'rev_len',
91 'rev_parent_id',
92 'rev_sha1',
93 ];
94 }
95
96 private function getCommentQueryFields() {
97 return [
98 'rev_comment_text' => 'rev_comment',
99 'rev_comment_data' => 'NULL',
100 'rev_comment_cid' => 'NULL',
101 ];
102 }
103
104 private function getContentHandlerQueryFields() {
105 return [
106 'rev_content_format',
107 'rev_content_model',
108 ];
109 }
110
111 public function provideGetQueryInfo() {
112 yield [
113 true,
114 [],
115 [
116 'tables' => [ 'revision' ],
117 'fields' => array_merge(
118 $this->getDefaultQueryFields(),
119 $this->getCommentQueryFields(),
120 $this->getContentHandlerQueryFields()
121 ),
122 'joins' => [],
123 ]
124 ];
125 yield [
126 false,
127 [],
128 [
129 'tables' => [ 'revision' ],
130 'fields' => array_merge(
131 $this->getDefaultQueryFields(),
132 $this->getCommentQueryFields()
133 ),
134 'joins' => [],
135 ]
136 ];
137 yield [
138 false,
139 [ 'page' ],
140 [
141 'tables' => [ 'revision', 'page' ],
142 'fields' => array_merge(
143 $this->getDefaultQueryFields(),
144 $this->getCommentQueryFields(),
145 [
146 'page_namespace',
147 'page_title',
148 'page_id',
149 'page_latest',
150 'page_is_redirect',
151 'page_len',
152 ]
153 ),
154 'joins' => [
155 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
156 ],
157 ]
158 ];
159 yield [
160 false,
161 [ 'user' ],
162 [
163 'tables' => [ 'revision', 'user' ],
164 'fields' => array_merge(
165 $this->getDefaultQueryFields(),
166 $this->getCommentQueryFields(),
167 [
168 'user_name',
169 ]
170 ),
171 'joins' => [
172 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
173 ],
174 ]
175 ];
176 yield [
177 false,
178 [ 'text' ],
179 [
180 'tables' => [ 'revision', 'text' ],
181 'fields' => array_merge(
182 $this->getDefaultQueryFields(),
183 $this->getCommentQueryFields(),
184 [
185 'old_text',
186 'old_flags',
187 ]
188 ),
189 'joins' => [
190 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
191 ],
192 ]
193 ];
194 yield [
195 true,
196 [ 'page', 'user', 'text' ],
197 [
198 'tables' => [ 'revision', 'page', 'user', 'text' ],
199 'fields' => array_merge(
200 $this->getDefaultQueryFields(),
201 $this->getCommentQueryFields(),
202 $this->getContentHandlerQueryFields(),
203 [
204 'page_namespace',
205 'page_title',
206 'page_id',
207 'page_latest',
208 'page_is_redirect',
209 'page_len',
210 'user_name',
211 'old_text',
212 'old_flags',
213 ]
214 ),
215 'joins' => [
216 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
217 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
218 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
219 ],
220 ]
221 ];
222 }
223
224 /**
225 * @dataProvider provideGetQueryInfo
226 * @covers \MediaWiki\Storage\RevisionStore::getQueryInfo
227 */
228 public function testGetQueryInfo( $contentHandlerUseDb, $options, $expected ) {
229 $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
230 $this->overrideMwServices();
231 $store = $this->getRevisionStore();
232 $store->setContentHandlerUseDB( $contentHandlerUseDb );
233 $this->assertEquals( $expected, $store->getQueryInfo( $options ) );
234 }
235
236 private function getDefaultArchiveFields() {
237 return [
238 'ar_id',
239 'ar_page_id',
240 'ar_namespace',
241 'ar_title',
242 'ar_rev_id',
243 'ar_text',
244 'ar_text_id',
245 'ar_timestamp',
246 'ar_user_text',
247 'ar_user',
248 'ar_minor_edit',
249 'ar_deleted',
250 'ar_len',
251 'ar_parent_id',
252 'ar_sha1',
253 ];
254 }
255
256 /**
257 * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo
258 */
259 public function testGetArchiveQueryInfo_contentHandlerDb() {
260 $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
261 $this->overrideMwServices();
262 $store = $this->getRevisionStore();
263 $store->setContentHandlerUseDB( true );
264 $this->assertEquals(
265 [
266 'tables' => [
267 'archive'
268 ],
269 'fields' => array_merge(
270 $this->getDefaultArchiveFields(),
271 [
272 'ar_comment_text' => 'ar_comment',
273 'ar_comment_data' => 'NULL',
274 'ar_comment_cid' => 'NULL',
275 'ar_content_format',
276 'ar_content_model',
277 ]
278 ),
279 'joins' => [],
280 ],
281 $store->getArchiveQueryInfo()
282 );
283 }
284
285 /**
286 * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo
287 */
288 public function testGetArchiveQueryInfo_noContentHandlerDb() {
289 $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
290 $this->overrideMwServices();
291 $store = $this->getRevisionStore();
292 $store->setContentHandlerUseDB( false );
293 $this->assertEquals(
294 [
295 'tables' => [
296 'archive'
297 ],
298 'fields' => array_merge(
299 $this->getDefaultArchiveFields(),
300 [
301 'ar_comment_text' => 'ar_comment',
302 'ar_comment_data' => 'NULL',
303 'ar_comment_cid' => 'NULL',
304 ]
305 ),
306 'joins' => [],
307 ],
308 $store->getArchiveQueryInfo()
309 );
310 }
311
312 public function testGetTitle_successFromPageId() {
313 $mockLoadBalancer = $this->getMockLoadBalancer();
314 // Title calls wfGetDB() so we have to set the main service
315 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
316
317 $db = $this->getMockDatabase();
318 // Title calls wfGetDB() which uses a regular Connection
319 $mockLoadBalancer->expects( $this->atLeastOnce() )
320 ->method( 'getConnection' )
321 ->willReturn( $db );
322
323 // First call to Title::newFromID, faking no result (db lag?)
324 $db->expects( $this->at( 0 ) )
325 ->method( 'selectRow' )
326 ->with(
327 'page',
328 $this->anything(),
329 [ 'page_id' => 1 ]
330 )
331 ->willReturn( (object)[
332 'page_namespace' => '1',
333 'page_title' => 'Food',
334 ] );
335
336 $store = $this->getRevisionStore( $mockLoadBalancer );
337 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
338
339 $this->assertSame( 1, $title->getNamespace() );
340 $this->assertSame( 'Food', $title->getDBkey() );
341 }
342
343 public function testGetTitle_successFromPageIdOnFallback() {
344 $mockLoadBalancer = $this->getMockLoadBalancer();
345 // Title calls wfGetDB() so we have to set the main service
346 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
347
348 $db = $this->getMockDatabase();
349 // Title calls wfGetDB() which uses a regular Connection
350 // Assert that the first call uses a REPLICA and the second falls back to master
351 $mockLoadBalancer->expects( $this->exactly( 2 ) )
352 ->method( 'getConnection' )
353 ->willReturn( $db );
354 // RevisionStore getTitle uses a ConnectionRef
355 $mockLoadBalancer->expects( $this->atLeastOnce() )
356 ->method( 'getConnectionRef' )
357 ->willReturn( $db );
358
359 // First call to Title::newFromID, faking no result (db lag?)
360 $db->expects( $this->at( 0 ) )
361 ->method( 'selectRow' )
362 ->with(
363 'page',
364 $this->anything(),
365 [ 'page_id' => 1 ]
366 )
367 ->willReturn( false );
368
369 // First select using rev_id, faking no result (db lag?)
370 $db->expects( $this->at( 1 ) )
371 ->method( 'selectRow' )
372 ->with(
373 [ 'revision', 'page' ],
374 $this->anything(),
375 [ 'rev_id' => 2 ]
376 )
377 ->willReturn( false );
378
379 // Second call to Title::newFromID, no result
380 $db->expects( $this->at( 2 ) )
381 ->method( 'selectRow' )
382 ->with(
383 'page',
384 $this->anything(),
385 [ 'page_id' => 1 ]
386 )
387 ->willReturn( (object)[
388 'page_namespace' => '2',
389 'page_title' => 'Foodey',
390 ] );
391
392 $store = $this->getRevisionStore( $mockLoadBalancer );
393 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
394
395 $this->assertSame( 2, $title->getNamespace() );
396 $this->assertSame( 'Foodey', $title->getDBkey() );
397 }
398
399 public function testGetTitle_successFromRevId() {
400 $mockLoadBalancer = $this->getMockLoadBalancer();
401 // Title calls wfGetDB() so we have to set the main service
402 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
403
404 $db = $this->getMockDatabase();
405 // Title calls wfGetDB() which uses a regular Connection
406 $mockLoadBalancer->expects( $this->atLeastOnce() )
407 ->method( 'getConnection' )
408 ->willReturn( $db );
409 // RevisionStore getTitle uses a ConnectionRef
410 $mockLoadBalancer->expects( $this->atLeastOnce() )
411 ->method( 'getConnectionRef' )
412 ->willReturn( $db );
413
414 // First call to Title::newFromID, faking no result (db lag?)
415 $db->expects( $this->at( 0 ) )
416 ->method( 'selectRow' )
417 ->with(
418 'page',
419 $this->anything(),
420 [ 'page_id' => 1 ]
421 )
422 ->willReturn( false );
423
424 // First select using rev_id, faking no result (db lag?)
425 $db->expects( $this->at( 1 ) )
426 ->method( 'selectRow' )
427 ->with(
428 [ 'revision', 'page' ],
429 $this->anything(),
430 [ 'rev_id' => 2 ]
431 )
432 ->willReturn( (object)[
433 'page_namespace' => '1',
434 'page_title' => 'Food2',
435 ] );
436
437 $store = $this->getRevisionStore( $mockLoadBalancer );
438 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
439
440 $this->assertSame( 1, $title->getNamespace() );
441 $this->assertSame( 'Food2', $title->getDBkey() );
442 }
443
444 public function testGetTitle_successFromRevIdOnFallback() {
445 $mockLoadBalancer = $this->getMockLoadBalancer();
446 // Title calls wfGetDB() so we have to set the main service
447 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
448
449 $db = $this->getMockDatabase();
450 // Title calls wfGetDB() which uses a regular Connection
451 // Assert that the first call uses a REPLICA and the second falls back to master
452 $mockLoadBalancer->expects( $this->exactly( 2 ) )
453 ->method( 'getConnection' )
454 ->willReturn( $db );
455 // RevisionStore getTitle uses a ConnectionRef
456 $mockLoadBalancer->expects( $this->atLeastOnce() )
457 ->method( 'getConnectionRef' )
458 ->willReturn( $db );
459
460 // First call to Title::newFromID, faking no result (db lag?)
461 $db->expects( $this->at( 0 ) )
462 ->method( 'selectRow' )
463 ->with(
464 'page',
465 $this->anything(),
466 [ 'page_id' => 1 ]
467 )
468 ->willReturn( false );
469
470 // First select using rev_id, faking no result (db lag?)
471 $db->expects( $this->at( 1 ) )
472 ->method( 'selectRow' )
473 ->with(
474 [ 'revision', 'page' ],
475 $this->anything(),
476 [ 'rev_id' => 2 ]
477 )
478 ->willReturn( false );
479
480 // Second call to Title::newFromID, no result
481 $db->expects( $this->at( 2 ) )
482 ->method( 'selectRow' )
483 ->with(
484 'page',
485 $this->anything(),
486 [ 'page_id' => 1 ]
487 )
488 ->willReturn( false );
489
490 // Second select using rev_id, result
491 $db->expects( $this->at( 3 ) )
492 ->method( 'selectRow' )
493 ->with(
494 [ 'revision', 'page' ],
495 $this->anything(),
496 [ 'rev_id' => 2 ]
497 )
498 ->willReturn( (object)[
499 'page_namespace' => '2',
500 'page_title' => 'Foodey',
501 ] );
502
503 $store = $this->getRevisionStore( $mockLoadBalancer );
504 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
505
506 $this->assertSame( 2, $title->getNamespace() );
507 $this->assertSame( 'Foodey', $title->getDBkey() );
508 }
509
510 /**
511 * @covers \MediaWiki\Storage\RevisionStore::getTitle
512 */
513 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
514 $mockLoadBalancer = $this->getMockLoadBalancer();
515 // Title calls wfGetDB() so we have to set the main service
516 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
517
518 $db = $this->getMockDatabase();
519 // Title calls wfGetDB() which uses a regular Connection
520 // Assert that the first call uses a REPLICA and the second falls back to master
521
522 // RevisionStore getTitle uses getConnectionRef
523 // Title::newFromID uses getConnection
524 foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
525 $mockLoadBalancer->expects( $this->exactly( 2 ) )
526 ->method( $method )
527 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
528 static $callCounter = 0;
529 $callCounter++;
530 // The first call should be to a REPLICA, and the second a MASTER.
531 if ( $callCounter === 1 ) {
532 $this->assertSame( DB_REPLICA, $masterOrReplica );
533 } elseif ( $callCounter === 2 ) {
534 $this->assertSame( DB_MASTER, $masterOrReplica );
535 }
536 return $db;
537 } );
538 }
539 // First and third call to Title::newFromID, faking no result
540 foreach ( [ 0, 2 ] as $counter ) {
541 $db->expects( $this->at( $counter ) )
542 ->method( 'selectRow' )
543 ->with(
544 'page',
545 $this->anything(),
546 [ 'page_id' => 1 ]
547 )
548 ->willReturn( false );
549 }
550
551 foreach ( [ 1, 3 ] as $counter ) {
552 $db->expects( $this->at( $counter ) )
553 ->method( 'selectRow' )
554 ->with(
555 [ 'revision', 'page' ],
556 $this->anything(),
557 [ 'rev_id' => 2 ]
558 )
559 ->willReturn( false );
560 }
561
562 $store = $this->getRevisionStore( $mockLoadBalancer );
563
564 $this->setExpectedException( RevisionAccessException::class );
565 $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
566 }
567
568 public function provideNewRevisionFromRow_legacyEncoding_applied() {
569 yield 'windows-1252, old_flags is empty' => [
570 'windows-1252',
571 'en',
572 [
573 'old_flags' => '',
574 'old_text' => "S\xF6me Content",
575 ],
576 'Söme Content'
577 ];
578
579 yield 'windows-1252, old_flags is null' => [
580 'windows-1252',
581 'en',
582 [
583 'old_flags' => null,
584 'old_text' => "S\xF6me Content",
585 ],
586 'Söme Content'
587 ];
588 }
589
590 /**
591 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
592 *
593 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
594 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
595 */
596 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
597 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
598
599 $blobStore = new SqlBlobStore( wfGetLB(), $cache );
600 $blobStore->setLegacyEncoding( $encoding, Language::factory( $locale ) );
601
602 $store = $this->getRevisionStore( wfGetLB(), $blobStore, $cache );
603
604 $record = $store->newRevisionFromRow(
605 $this->makeRow( $row ),
606 0,
607 Title::newFromText( __METHOD__ . '-UTPage' )
608 );
609
610 $this->assertSame( $text, $record->getContent( 'main' )->serialize() );
611 }
612
613 /**
614 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
615 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
616 */
617 public function testNewRevisionFromRow_legacyEncoding_ignored() {
618 $row = [
619 'old_flags' => 'utf-8',
620 'old_text' => 'Söme Content',
621 ];
622
623 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
624
625 $blobStore = new SqlBlobStore( wfGetLB(), $cache );
626 $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
627
628 $store = $this->getRevisionStore( wfGetLB(), $blobStore, $cache );
629
630 $record = $store->newRevisionFromRow(
631 $this->makeRow( $row ),
632 0,
633 Title::newFromText( __METHOD__ . '-UTPage' )
634 );
635 $this->assertSame( 'Söme Content', $record->getContent( 'main' )->serialize() );
636 }
637
638 private function makeRow( array $array ) {
639 $row = $array + [
640 'rev_id' => 7,
641 'rev_page' => 5,
642 'rev_text_id' => 11,
643 'rev_timestamp' => '20110101000000',
644 'rev_user_text' => 'Tester',
645 'rev_user' => 17,
646 'rev_minor_edit' => 0,
647 'rev_deleted' => 0,
648 'rev_len' => 100,
649 'rev_parent_id' => 0,
650 'rev_sha1' => 'deadbeef',
651 'rev_comment_text' => 'Testing',
652 'rev_comment_data' => '{}',
653 'rev_comment_cid' => 111,
654 'rev_content_format' => CONTENT_FORMAT_TEXT,
655 'rev_content_model' => CONTENT_MODEL_TEXT,
656 'page_namespace' => 0,
657 'page_title' => 'TEST',
658 'page_id' => 5,
659 'page_latest' => 7,
660 'page_is_redirect' => 0,
661 'page_len' => 100,
662 'user_name' => 'Tester',
663 'old_is' => 13,
664 'old_text' => 'Hello World',
665 'old_flags' => 'utf-8',
666 ];
667
668 return (object)$row;
669 }
670
671 }