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