Merge "Add @covers for RemexStripTagHandler"
[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 $store = $this->getRevisionStore();
228 $store->setContentHandlerUseDB( $contentHandlerUseDb );
229 $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
230 $this->assertEquals( $expected, $store->getQueryInfo( $options ) );
231 }
232
233 private function getDefaultArchiveFields() {
234 return [
235 'ar_id',
236 'ar_page_id',
237 'ar_namespace',
238 'ar_title',
239 'ar_rev_id',
240 'ar_text',
241 'ar_text_id',
242 'ar_timestamp',
243 'ar_user_text',
244 'ar_user',
245 'ar_minor_edit',
246 'ar_deleted',
247 'ar_len',
248 'ar_parent_id',
249 'ar_sha1',
250 ];
251 }
252
253 /**
254 * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo
255 */
256 public function testGetArchiveQueryInfo_contentHandlerDb() {
257 $store = $this->getRevisionStore();
258 $store->setContentHandlerUseDB( true );
259 $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
260 $this->assertEquals(
261 [
262 'tables' => [
263 'archive'
264 ],
265 'fields' => array_merge(
266 $this->getDefaultArchiveFields(),
267 [
268 'ar_comment_text' => 'ar_comment',
269 'ar_comment_data' => 'NULL',
270 'ar_comment_cid' => 'NULL',
271 'ar_content_format',
272 'ar_content_model',
273 ]
274 ),
275 'joins' => [],
276 ],
277 $store->getArchiveQueryInfo()
278 );
279 }
280
281 /**
282 * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo
283 */
284 public function testGetArchiveQueryInfo_noContentHandlerDb() {
285 $store = $this->getRevisionStore();
286 $store->setContentHandlerUseDB( false );
287 $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
288 $this->assertEquals(
289 [
290 'tables' => [
291 'archive'
292 ],
293 'fields' => array_merge(
294 $this->getDefaultArchiveFields(),
295 [
296 'ar_comment_text' => 'ar_comment',
297 'ar_comment_data' => 'NULL',
298 'ar_comment_cid' => 'NULL',
299 ]
300 ),
301 'joins' => [],
302 ],
303 $store->getArchiveQueryInfo()
304 );
305 }
306
307 public function testGetTitle_successFromPageId() {
308 $mockLoadBalancer = $this->getMockLoadBalancer();
309 // Title calls wfGetDB() so we have to set the main service
310 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
311
312 $db = $this->getMockDatabase();
313 // Title calls wfGetDB() which uses a regular Connection
314 $mockLoadBalancer->expects( $this->atLeastOnce() )
315 ->method( 'getConnection' )
316 ->willReturn( $db );
317
318 // First call to Title::newFromID, faking no result (db lag?)
319 $db->expects( $this->at( 0 ) )
320 ->method( 'selectRow' )
321 ->with(
322 'page',
323 $this->anything(),
324 [ 'page_id' => 1 ]
325 )
326 ->willReturn( (object)[
327 'page_namespace' => '1',
328 'page_title' => 'Food',
329 ] );
330
331 $store = $this->getRevisionStore( $mockLoadBalancer );
332 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
333
334 $this->assertSame( 1, $title->getNamespace() );
335 $this->assertSame( 'Food', $title->getDBkey() );
336 }
337
338 public function testGetTitle_successFromPageIdOnFallback() {
339 $mockLoadBalancer = $this->getMockLoadBalancer();
340 // Title calls wfGetDB() so we have to set the main service
341 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
342
343 $db = $this->getMockDatabase();
344 // Title calls wfGetDB() which uses a regular Connection
345 // Assert that the first call uses a REPLICA and the second falls back to master
346 $mockLoadBalancer->expects( $this->exactly( 2 ) )
347 ->method( 'getConnection' )
348 ->willReturn( $db );
349 // RevisionStore getTitle uses a ConnectionRef
350 $mockLoadBalancer->expects( $this->atLeastOnce() )
351 ->method( 'getConnectionRef' )
352 ->willReturn( $db );
353
354 // First call to Title::newFromID, faking no result (db lag?)
355 $db->expects( $this->at( 0 ) )
356 ->method( 'selectRow' )
357 ->with(
358 'page',
359 $this->anything(),
360 [ 'page_id' => 1 ]
361 )
362 ->willReturn( false );
363
364 // First select using rev_id, faking no result (db lag?)
365 $db->expects( $this->at( 1 ) )
366 ->method( 'selectRow' )
367 ->with(
368 [ 'revision', 'page' ],
369 $this->anything(),
370 [ 'rev_id' => 2 ]
371 )
372 ->willReturn( false );
373
374 // Second call to Title::newFromID, no result
375 $db->expects( $this->at( 2 ) )
376 ->method( 'selectRow' )
377 ->with(
378 'page',
379 $this->anything(),
380 [ 'page_id' => 1 ]
381 )
382 ->willReturn( (object)[
383 'page_namespace' => '2',
384 'page_title' => 'Foodey',
385 ] );
386
387 $store = $this->getRevisionStore( $mockLoadBalancer );
388 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
389
390 $this->assertSame( 2, $title->getNamespace() );
391 $this->assertSame( 'Foodey', $title->getDBkey() );
392 }
393
394 public function testGetTitle_successFromRevId() {
395 $mockLoadBalancer = $this->getMockLoadBalancer();
396 // Title calls wfGetDB() so we have to set the main service
397 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
398
399 $db = $this->getMockDatabase();
400 // Title calls wfGetDB() which uses a regular Connection
401 $mockLoadBalancer->expects( $this->atLeastOnce() )
402 ->method( 'getConnection' )
403 ->willReturn( $db );
404 // RevisionStore getTitle uses a ConnectionRef
405 $mockLoadBalancer->expects( $this->atLeastOnce() )
406 ->method( 'getConnectionRef' )
407 ->willReturn( $db );
408
409 // First call to Title::newFromID, faking no result (db lag?)
410 $db->expects( $this->at( 0 ) )
411 ->method( 'selectRow' )
412 ->with(
413 'page',
414 $this->anything(),
415 [ 'page_id' => 1 ]
416 )
417 ->willReturn( false );
418
419 // First select using rev_id, faking no result (db lag?)
420 $db->expects( $this->at( 1 ) )
421 ->method( 'selectRow' )
422 ->with(
423 [ 'revision', 'page' ],
424 $this->anything(),
425 [ 'rev_id' => 2 ]
426 )
427 ->willReturn( (object)[
428 'page_namespace' => '1',
429 'page_title' => 'Food2',
430 ] );
431
432 $store = $this->getRevisionStore( $mockLoadBalancer );
433 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
434
435 $this->assertSame( 1, $title->getNamespace() );
436 $this->assertSame( 'Food2', $title->getDBkey() );
437 }
438
439 public function testGetTitle_successFromRevIdOnFallback() {
440 $mockLoadBalancer = $this->getMockLoadBalancer();
441 // Title calls wfGetDB() so we have to set the main service
442 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
443
444 $db = $this->getMockDatabase();
445 // Title calls wfGetDB() which uses a regular Connection
446 // Assert that the first call uses a REPLICA and the second falls back to master
447 $mockLoadBalancer->expects( $this->exactly( 2 ) )
448 ->method( 'getConnection' )
449 ->willReturn( $db );
450 // RevisionStore getTitle uses a ConnectionRef
451 $mockLoadBalancer->expects( $this->atLeastOnce() )
452 ->method( 'getConnectionRef' )
453 ->willReturn( $db );
454
455 // First call to Title::newFromID, faking no result (db lag?)
456 $db->expects( $this->at( 0 ) )
457 ->method( 'selectRow' )
458 ->with(
459 'page',
460 $this->anything(),
461 [ 'page_id' => 1 ]
462 )
463 ->willReturn( false );
464
465 // First select using rev_id, faking no result (db lag?)
466 $db->expects( $this->at( 1 ) )
467 ->method( 'selectRow' )
468 ->with(
469 [ 'revision', 'page' ],
470 $this->anything(),
471 [ 'rev_id' => 2 ]
472 )
473 ->willReturn( false );
474
475 // Second call to Title::newFromID, no result
476 $db->expects( $this->at( 2 ) )
477 ->method( 'selectRow' )
478 ->with(
479 'page',
480 $this->anything(),
481 [ 'page_id' => 1 ]
482 )
483 ->willReturn( false );
484
485 // Second select using rev_id, result
486 $db->expects( $this->at( 3 ) )
487 ->method( 'selectRow' )
488 ->with(
489 [ 'revision', 'page' ],
490 $this->anything(),
491 [ 'rev_id' => 2 ]
492 )
493 ->willReturn( (object)[
494 'page_namespace' => '2',
495 'page_title' => 'Foodey',
496 ] );
497
498 $store = $this->getRevisionStore( $mockLoadBalancer );
499 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
500
501 $this->assertSame( 2, $title->getNamespace() );
502 $this->assertSame( 'Foodey', $title->getDBkey() );
503 }
504
505 /**
506 * @covers \MediaWiki\Storage\RevisionStore::getTitle
507 */
508 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
509 $mockLoadBalancer = $this->getMockLoadBalancer();
510 // Title calls wfGetDB() so we have to set the main service
511 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
512
513 $db = $this->getMockDatabase();
514 // Title calls wfGetDB() which uses a regular Connection
515 // Assert that the first call uses a REPLICA and the second falls back to master
516
517 // RevisionStore getTitle uses getConnectionRef
518 // Title::newFromID uses getConnection
519 foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
520 $mockLoadBalancer->expects( $this->exactly( 2 ) )
521 ->method( $method )
522 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
523 static $callCounter = 0;
524 $callCounter++;
525 // The first call should be to a REPLICA, and the second a MASTER.
526 if ( $callCounter === 1 ) {
527 $this->assertSame( DB_REPLICA, $masterOrReplica );
528 } elseif ( $callCounter === 2 ) {
529 $this->assertSame( DB_MASTER, $masterOrReplica );
530 }
531 return $db;
532 } );
533 }
534 // First and third call to Title::newFromID, faking no result
535 foreach ( [ 0, 2 ] as $counter ) {
536 $db->expects( $this->at( $counter ) )
537 ->method( 'selectRow' )
538 ->with(
539 'page',
540 $this->anything(),
541 [ 'page_id' => 1 ]
542 )
543 ->willReturn( false );
544 }
545
546 foreach ( [ 1, 3 ] as $counter ) {
547 $db->expects( $this->at( $counter ) )
548 ->method( 'selectRow' )
549 ->with(
550 [ 'revision', 'page' ],
551 $this->anything(),
552 [ 'rev_id' => 2 ]
553 )
554 ->willReturn( false );
555 }
556
557 $store = $this->getRevisionStore( $mockLoadBalancer );
558
559 $this->setExpectedException( RevisionAccessException::class );
560 $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
561 }
562
563 public function provideNewRevisionFromRow_legacyEncoding_applied() {
564 yield 'windows-1252, old_flags is empty' => [
565 'windows-1252',
566 'en',
567 [
568 'old_flags' => '',
569 'old_text' => "S\xF6me Content",
570 ],
571 'Söme Content'
572 ];
573
574 yield 'windows-1252, old_flags is null' => [
575 'windows-1252',
576 'en',
577 [
578 'old_flags' => null,
579 'old_text' => "S\xF6me Content",
580 ],
581 'Söme Content'
582 ];
583 }
584
585 /**
586 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
587 *
588 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
589 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
590 */
591 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
592 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
593
594 $blobStore = new SqlBlobStore( wfGetLB(), $cache );
595 $blobStore->setLegacyEncoding( $encoding, Language::factory( $locale ) );
596
597 $store = new RevisionStore( wfGetLB(), $blobStore, $cache );
598
599 $record = $store->newRevisionFromRow(
600 $this->makeRow( $row ),
601 0,
602 Title::newFromText( __METHOD__ . '-UTPage' )
603 );
604
605 $this->assertSame( $text, $record->getContent( 'main' )->serialize() );
606 }
607
608 /**
609 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
610 * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
611 */
612 public function testNewRevisionFromRow_legacyEncoding_ignored() {
613 $row = [
614 'old_flags' => 'utf-8',
615 'old_text' => 'Söme Content',
616 ];
617
618 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
619
620 $blobStore = new SqlBlobStore( wfGetLB(), $cache );
621 $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
622
623 $store = new RevisionStore( wfGetLB(), $blobStore, $cache );
624
625 $record = $store->newRevisionFromRow(
626 $this->makeRow( $row ),
627 0,
628 Title::newFromText( __METHOD__ . '-UTPage' )
629 );
630 $this->assertSame( 'Söme Content', $record->getContent( 'main' )->serialize() );
631 }
632
633 private function makeRow( array $array ) {
634 $row = $array + [
635 'rev_id' => 7,
636 'rev_page' => 5,
637 'rev_text_id' => 11,
638 'rev_timestamp' => '20110101000000',
639 'rev_user_text' => 'Tester',
640 'rev_user' => 17,
641 'rev_minor_edit' => 0,
642 'rev_deleted' => 0,
643 'rev_len' => 100,
644 'rev_parent_id' => 0,
645 'rev_sha1' => 'deadbeef',
646 'rev_comment_text' => 'Testing',
647 'rev_comment_data' => '{}',
648 'rev_comment_cid' => 111,
649 'rev_content_format' => CONTENT_FORMAT_TEXT,
650 'rev_content_model' => CONTENT_MODEL_TEXT,
651 'page_namespace' => 0,
652 'page_title' => 'TEST',
653 'page_id' => 5,
654 'page_latest' => 7,
655 'page_is_redirect' => 0,
656 'page_len' => 100,
657 'user_name' => 'Tester',
658 'old_is' => 13,
659 'old_text' => 'Hello World',
660 'old_flags' => 'utf-8',
661 ];
662
663 return (object)$row;
664 }
665
666 }