e10a97f22269765ce363a72970075e515d984e46
[lhc/web/wiklou.git] / tests / phpunit / includes / specialpage / ChangesListSpecialPageTest.php
1 <?php
2 /**
3 * Test class for ChangesListSpecialPage class
4 *
5 * Copyright © 2011-, Antoine Musso, Stephane Bisson, Matthew Flaschen
6 *
7 * @author Antoine Musso
8 * @author Stephane Bisson
9 * @author Matthew Flaschen
10 * @group Database
11 *
12 * @covers ChangesListSpecialPage
13 */
14 class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase {
15 protected function setUp() {
16 parent::setUp();
17
18 # setup the rc object
19 $this->changesListSpecialPage = $this->getPage();
20 }
21
22 protected function getPage() {
23 return TestingAccessWrapper::newFromObject(
24 $this->getMockForAbstractClass(
25 'ChangesListSpecialPage',
26 [
27 'ChangesListSpecialPage',
28 ''
29 ]
30 )
31 );
32 }
33
34 /** helper to test SpecialRecentchanges::buildMainQueryConds() */
35 private function assertConditions(
36 $expected,
37 $requestOptions = null,
38 $message = '',
39 $user = null
40 ) {
41 $context = new RequestContext;
42 $context->setRequest( new FauxRequest( $requestOptions ) );
43 if ( $user ) {
44 $context->setUser( $user );
45 }
46
47 $this->changesListSpecialPage->setContext( $context );
48 $formOptions = $this->changesListSpecialPage->setup( null );
49
50 #  Filter out rc_timestamp conditions which depends on the test runtime
51 # This condition is not needed as of march 2, 2011 -- hashar
52 # @todo FIXME: Find a way to generate the correct rc_timestamp
53
54 $tables = [];
55 $fields = [];
56 $queryConditions = [];
57 $query_options = [];
58 $join_conds = [];
59
60 call_user_func_array(
61 [ $this->changesListSpecialPage, 'buildQuery' ],
62 [
63 &$tables,
64 &$fields,
65 &$queryConditions,
66 &$query_options,
67 &$join_conds,
68 $formOptions
69 ]
70 );
71
72 $queryConditions = array_filter(
73 $queryConditions,
74 'ChangesListSpecialPageTest::filterOutRcTimestampCondition'
75 );
76
77 $this->assertEquals(
78 self::normalizeCondition( $expected ),
79 self::normalizeCondition( $queryConditions ),
80 $message
81 );
82 }
83
84 private static function normalizeCondition( $conds ) {
85 $normalized = array_map(
86 function ( $k, $v ) {
87 return is_numeric( $k ) ? $v : "$k = $v";
88 },
89 array_keys( $conds ),
90 $conds
91 );
92 sort( $normalized );
93 return $normalized;
94 }
95
96 /** return false if condition begin with 'rc_timestamp ' */
97 private static function filterOutRcTimestampCondition( $var ) {
98 return ( false === strpos( $var, 'rc_timestamp ' ) );
99 }
100
101 public function testRcNsFilter() {
102 $this->assertConditions(
103 [ # expected
104 "rc_namespace = '0'",
105 ],
106 [
107 'namespace' => NS_MAIN,
108 ],
109 "rc conditions with no options (aka default setting)"
110 );
111 }
112
113 public function testRcNsFilterInversion() {
114 $this->assertConditions(
115 [ # expected
116 "rc_namespace != '0'",
117 ],
118 [
119 'namespace' => NS_MAIN,
120 'invert' => 1,
121 ],
122 "rc conditions with namespace inverted"
123 );
124 }
125
126 /**
127 * T4429
128 * @dataProvider provideNamespacesAssociations
129 */
130 public function testRcNsFilterAssociation( $ns1, $ns2 ) {
131 $this->assertConditions(
132 [ # expected
133 "(rc_namespace = '$ns1' OR rc_namespace = '$ns2')",
134 ],
135 [
136 'namespace' => $ns1,
137 'associated' => 1,
138 ],
139 "rc conditions with namespace inverted"
140 );
141 }
142
143 /**
144 * T4429
145 * @dataProvider provideNamespacesAssociations
146 */
147 public function testRcNsFilterAssociationWithInversion( $ns1, $ns2 ) {
148 $this->assertConditions(
149 [ # expected
150 "(rc_namespace != '$ns1' AND rc_namespace != '$ns2')",
151 ],
152 [
153 'namespace' => $ns1,
154 'associated' => 1,
155 'invert' => 1,
156 ],
157 "rc conditions with namespace inverted"
158 );
159 }
160
161 /**
162 * Provides associated namespaces to test recent changes
163 * namespaces association filtering.
164 */
165 public static function provideNamespacesAssociations() {
166 return [ # (NS => Associated_NS)
167 [ NS_MAIN, NS_TALK ],
168 [ NS_TALK, NS_MAIN ],
169 ];
170 }
171
172 public function testRcHidemyselfFilter() {
173 $user = $this->getTestUser()->getUser();
174 $this->assertConditions(
175 [ # expected
176 "rc_user_text != '{$user->getName()}'",
177 ],
178 [
179 'hidemyself' => 1,
180 ],
181 "rc conditions: hidemyself=1 (logged in)",
182 $user
183 );
184
185 $user = User::newFromName( '10.11.12.13', false );
186 $this->assertConditions(
187 [ # expected
188 "rc_user_text != '10.11.12.13'",
189 ],
190 [
191 'hidemyself' => 1,
192 ],
193 "rc conditions: hidemyself=1 (anon)",
194 $user
195 );
196 }
197
198 public function testRcHidebyothersFilter() {
199 $user = $this->getTestUser()->getUser();
200 $this->assertConditions(
201 [ # expected
202 "rc_user_text = '{$user->getName()}'",
203 ],
204 [
205 'hidebyothers' => 1,
206 ],
207 "rc conditions: hidebyothers=1 (logged in)",
208 $user
209 );
210
211 $user = User::newFromName( '10.11.12.13', false );
212 $this->assertConditions(
213 [ # expected
214 "rc_user_text = '10.11.12.13'",
215 ],
216 [
217 'hidebyothers' => 1,
218 ],
219 "rc conditions: hidebyothers=1 (anon)",
220 $user
221 );
222 }
223
224 public function testRcHidemyselfHidebyothersFilter() {
225 $user = $this->getTestUser()->getUser();
226 $this->assertConditions(
227 [ # expected
228 "rc_user_text != '{$user->getName()}'",
229 "rc_user_text = '{$user->getName()}'",
230 ],
231 [
232 'hidemyself' => 1,
233 'hidebyothers' => 1,
234 ],
235 "rc conditions: hidemyself=1 hidebyothers=1 (logged in)",
236 $user
237 );
238 }
239
240 public function testRcHidepageedits() {
241 $this->assertConditions(
242 [ # expected
243 "rc_type != '0'",
244 ],
245 [
246 'hidepageedits' => 1,
247 ],
248 "rc conditions: hidepageedits=1"
249 );
250 }
251
252 public function testRcHidenewpages() {
253 $this->assertConditions(
254 [ # expected
255 "rc_type != '1'",
256 ],
257 [
258 'hidenewpages' => 1,
259 ],
260 "rc conditions: hidenewpages=1"
261 );
262 }
263
264 public function testRcHidelog() {
265 $this->assertConditions(
266 [ # expected
267 "rc_type != '3'",
268 ],
269 [
270 'hidelog' => 1,
271 ],
272 "rc conditions: hidelog=1"
273 );
274 }
275
276 public function testRcHidehumans() {
277 $this->assertConditions(
278 [ # expected
279 'rc_bot' => 1,
280 ],
281 [
282 'hidebots' => 0,
283 'hidehumans' => 1,
284 ],
285 "rc conditions: hidebots=0 hidehumans=1"
286 );
287 }
288
289 public function testRcHidepatrolledDisabledFilter() {
290 $user = $this->getTestUser()->getUser();
291 $this->assertConditions(
292 [ # expected
293 ],
294 [
295 'hidepatrolled' => 1,
296 ],
297 "rc conditions: hidepatrolled=1 (user not allowed)",
298 $user
299 );
300 }
301
302 public function testRcHideunpatrolledDisabledFilter() {
303 $user = $this->getTestUser()->getUser();
304 $this->assertConditions(
305 [ # expected
306 ],
307 [
308 'hideunpatrolled' => 1,
309 ],
310 "rc conditions: hideunpatrolled=1 (user not allowed)",
311 $user
312 );
313 }
314 public function testRcHidepatrolledFilter() {
315 $user = $this->getTestSysop()->getUser();
316 $this->assertConditions(
317 [ # expected
318 "rc_patrolled = 0",
319 ],
320 [
321 'hidepatrolled' => 1,
322 ],
323 "rc conditions: hidepatrolled=1",
324 $user
325 );
326 }
327
328 public function testRcHideunpatrolledFilter() {
329 $user = $this->getTestSysop()->getUser();
330 $this->assertConditions(
331 [ # expected
332 "rc_patrolled = 1",
333 ],
334 [
335 'hideunpatrolled' => 1,
336 ],
337 "rc conditions: hideunpatrolled=1",
338 $user
339 );
340 }
341
342 public function testRcHideminorFilter() {
343 $this->assertConditions(
344 [ # expected
345 "rc_minor = 0",
346 ],
347 [
348 'hideminor' => 1,
349 ],
350 "rc conditions: hideminor=1"
351 );
352 }
353
354 public function testRcHidemajorFilter() {
355 $this->assertConditions(
356 [ # expected
357 "rc_minor = 1",
358 ],
359 [
360 'hidemajor' => 1,
361 ],
362 "rc conditions: hidemajor=1"
363 );
364 }
365
366 public function testRcHidepatrolledHideunpatrolledFilter() {
367 $user = $this->getTestSysop()->getUser();
368 $this->assertConditions(
369 [ # expected
370 "rc_patrolled = 0",
371 "rc_patrolled = 1",
372 ],
373 [
374 'hidepatrolled' => 1,
375 'hideunpatrolled' => 1,
376 ],
377 "rc conditions: hidepatrolled=1 hideunpatrolled=1",
378 $user
379 );
380 }
381
382 public function testHideCategorization() {
383 $this->assertConditions(
384 [
385 # expected
386 "rc_type != '6'"
387 ],
388 [
389 'hidecategorization' => 1
390 ],
391 "rc conditions: hidecategorization=1"
392 );
393 }
394
395 public function testFilterUserExpLevel() {
396 $this->setMwGlobals( [
397 'wgLearnerEdits' => 10,
398 'wgLearnerMemberSince' => 4,
399 'wgExperiencedUserEdits' => 500,
400 'wgExperiencedUserMemberSince' => 30,
401 ] );
402
403 $this->createUsers( [
404 'Newcomer1' => [ 'edits' => 2, 'days' => 2 ],
405 'Newcomer2' => [ 'edits' => 12, 'days' => 3 ],
406 'Newcomer3' => [ 'edits' => 8, 'days' => 5 ],
407 'Learner1' => [ 'edits' => 15, 'days' => 10 ],
408 'Learner2' => [ 'edits' => 450, 'days' => 20 ],
409 'Learner3' => [ 'edits' => 460, 'days' => 33 ],
410 'Learner4' => [ 'edits' => 525, 'days' => 28 ],
411 'Experienced1' => [ 'edits' => 538, 'days' => 33 ],
412 ] );
413
414 // newcomers only
415 $this->assertArrayEquals(
416 [ 'Newcomer1', 'Newcomer2', 'Newcomer3' ],
417 $this->fetchUsers( [ 'newcomer' ] )
418 );
419
420 // newcomers and learner
421 $this->assertArrayEquals(
422 [
423 'Newcomer1', 'Newcomer2', 'Newcomer3',
424 'Learner1', 'Learner2', 'Learner3', 'Learner4',
425 ],
426 $this->fetchUsers( [ 'newcomer', 'learner' ] )
427 );
428
429 // newcomers and more learner
430 $this->assertArrayEquals(
431 [
432 'Newcomer1', 'Newcomer2', 'Newcomer3',
433 'Experienced1',
434 ],
435 $this->fetchUsers( [ 'newcomer', 'experienced' ] )
436 );
437
438 // learner only
439 $this->assertArrayEquals(
440 [ 'Learner1', 'Learner2', 'Learner3', 'Learner4' ],
441 $this->fetchUsers( [ 'learner' ] )
442 );
443
444 // more experienced only
445 $this->assertArrayEquals(
446 [ 'Experienced1' ],
447 $this->fetchUsers( [ 'experienced' ] )
448 );
449
450 // learner and more experienced
451 $this->assertArrayEquals(
452 [
453 'Learner1', 'Learner2', 'Learner3', 'Learner4',
454 'Experienced1',
455 ],
456 $this->fetchUsers( [ 'learner', 'experienced' ] ),
457 'Learner and more experienced'
458 );
459
460 // newcomers, learner, and more experienced
461 // TOOD: Fix test. This needs to test that anons are excluded,
462 // and right now the join fails.
463 /* $this->assertArrayEquals( */
464 /* [ */
465 /* 'Newcomer1', 'Newcomer2', 'Newcomer3', */
466 /* 'Learner1', 'Learner2', 'Learner3', 'Learner4', */
467 /* 'Experienced1', */
468 /* ], */
469 /* $this->fetchUsers( [ 'newcomer', 'learner', 'experienced' ] ) */
470 /* ); */
471 }
472
473 private function createUsers( $specs ) {
474 $dbw = wfGetDB( DB_MASTER );
475 foreach ( $specs as $name => $spec ) {
476 User::createNew(
477 $name,
478 [
479 'editcount' => $spec['edits'],
480 'registration' => $dbw->timestamp( $this->daysAgo( $spec['days'] ) ),
481 'email' => 'ut',
482 ]
483 );
484 }
485 }
486
487 private function fetchUsers( $filters ) {
488 $tables = [];
489 $conds = [];
490 $fields = [];
491 $query_options = [];
492 $join_conds = [];
493
494 sort( $filters );
495
496 call_user_func_array(
497 [ $this->changesListSpecialPage, 'filterOnUserExperienceLevel' ],
498 [
499 get_class( $this->changesListSpecialPage ),
500 $this->changesListSpecialPage->getContext(),
501 $this->changesListSpecialPage->getDB(),
502 &$tables,
503 &$fields,
504 &$conds,
505 &$query_options,
506 &$join_conds,
507 $filters
508 ]
509 );
510
511 $result = wfGetDB( DB_MASTER )->select(
512 'user',
513 'user_name',
514 array_filter( $conds ) + [ 'user_email' => 'ut' ]
515 );
516
517 $usernames = [];
518 foreach ( $result as $row ) {
519 $usernames[] = $row->user_name;
520 }
521
522 return $usernames;
523 }
524
525 private function daysAgo( $days ) {
526 $secondsPerDay = 86400;
527 return time() - $days * $secondsPerDay;
528 }
529
530 public function testGetFilterGroupDefinitionFromLegacyCustomFilters() {
531 $customFilters = [
532 'hidefoo' => [
533 'msg' => 'showhidefoo',
534 'default' => true,
535 ],
536
537 'hidebar' => [
538 'msg' => 'showhidebar',
539 'default' => false,
540 ],
541 ];
542
543 $this->assertEquals(
544 [
545 'name' => 'unstructured',
546 'class' => ChangesListBooleanFilterGroup::class,
547 'priority' => -1,
548 'filters' => [
549 [
550 'name' => 'hidefoo',
551 'showHide' => 'showhidefoo',
552 'default' => true,
553 ],
554 [
555 'name' => 'hidebar',
556 'showHide' => 'showhidebar',
557 'default' => false,
558 ]
559 ],
560 ],
561 $this->changesListSpecialPage->getFilterGroupDefinitionFromLegacyCustomFilters(
562 $customFilters
563 )
564 );
565 }
566
567 public function testGetStructuredFilterJsData() {
568 $definition = [
569 [
570 'name' => 'gub-group',
571 'title' => 'gub-group-title',
572 'class' => ChangesListBooleanFilterGroup::class,
573 'filters' => [
574 [
575 'name' => 'hidefoo',
576 'label' => 'foo-label',
577 'description' => 'foo-description',
578 'default' => true,
579 'showHide' => 'showhidefoo',
580 'priority' => 2,
581 ],
582 [
583 'name' => 'hidebar',
584 'label' => 'bar-label',
585 'description' => 'bar-description',
586 'default' => false,
587 'priority' => 4,
588 ]
589 ],
590 ],
591
592 [
593 'name' => 'des-group',
594 'title' => 'des-group-title',
595 'class' => ChangesListStringOptionsFilterGroup::class,
596 'isFullCoverage' => true,
597 'filters' => [
598 [
599 'name' => 'grault',
600 'label' => 'grault-label',
601 'description' => 'grault-description',
602 ],
603 [
604 'name' => 'garply',
605 'label' => 'garply-label',
606 'description' => 'garply-description',
607 ],
608 ],
609 'queryCallable' => function () {
610 },
611 'default' => ChangesListStringOptionsFilterGroup::NONE,
612 ],
613
614 [
615 'name' => 'unstructured',
616 'class' => ChangesListBooleanFilterGroup::class,
617 'filters' => [
618 [
619 'name' => 'hidethud',
620 'showHide' => 'showhidethud',
621 'default' => true,
622 ],
623
624 [
625 'name' => 'hidemos',
626 'showHide' => 'showhidemos',
627 'default' => false,
628 ],
629 ],
630 ],
631
632 ];
633
634 $this->changesListSpecialPage->registerFiltersFromDefinitions( $definition );
635
636 $this->assertArrayEquals(
637 [
638 // Filters that only display in the unstructured UI are
639 // are not included, and neither are groups that would
640 // be empty due to the above.
641 'groups' => [
642 [
643 'name' => 'gub-group',
644 'title' => 'gub-group-title',
645 'type' => ChangesListBooleanFilterGroup::TYPE,
646 'priority' => -1,
647 'filters' => [
648 [
649 'name' => 'hidebar',
650 'label' => 'bar-label',
651 'description' => 'bar-description',
652 'default' => false,
653 'priority' => 4,
654 'cssClass' => null,
655 'conflicts' => [],
656 'subset' => [],
657 ],
658 [
659 'name' => 'hidefoo',
660 'label' => 'foo-label',
661 'description' => 'foo-description',
662 'default' => true,
663 'priority' => 2,
664 'cssClass' => null,
665 'conflicts' => [],
666 'subset' => [],
667 ],
668 ],
669 'fullCoverage' => true,
670 'conflicts' => [],
671 ],
672
673 [
674 'name' => 'des-group',
675 'title' => 'des-group-title',
676 'type' => ChangesListStringOptionsFilterGroup::TYPE,
677 'priority' => -2,
678 'fullCoverage' => true,
679 'filters' => [
680 [
681 'name' => 'grault',
682 'label' => 'grault-label',
683 'description' => 'grault-description',
684 'cssClass' => null,
685 'priority' => -2,
686 'conflicts' => [],
687 'subset' => [],
688 ],
689 [
690 'name' => 'garply',
691 'label' => 'garply-label',
692 'description' => 'garply-description',
693 'cssClass' => null,
694 'priority' => -3,
695 'conflicts' => [],
696 'subset' => [],
697 ],
698 ],
699 'conflicts' => [],
700 'separator' => ';',
701 'default' => ChangesListStringOptionsFilterGroup::NONE,
702 ],
703 ],
704 'messageKeys' => [
705 'gub-group-title',
706 'bar-label',
707 'bar-description',
708 'foo-label',
709 'foo-description',
710 'des-group-title',
711 'grault-label',
712 'grault-description',
713 'garply-label',
714 'garply-description',
715 ],
716 ],
717 $this->changesListSpecialPage->getStructuredFilterJsData(),
718 /** ordered= */ false,
719 /** named= */ true
720 );
721 }
722
723 public function provideParseParameters() {
724 return [
725 [ 'hidebots', [ 'hidebots' => true ] ],
726
727 [ 'bots', [ 'hidebots' => false ] ],
728
729 [ 'hideminor', [ 'hideminor' => true ] ],
730
731 [ 'minor', [ 'hideminor' => false ] ],
732
733 [ 'hidemajor', [ 'hidemajor' => true ] ],
734
735 [ 'hideliu', [ 'hideliu' => true ] ],
736
737 [ 'hidepatrolled', [ 'hidepatrolled' => true ] ],
738
739 [ 'hideunpatrolled', [ 'hideunpatrolled' => true ] ],
740
741 [ 'hideanons', [ 'hideanons' => true ] ],
742
743 [ 'hidemyself', [ 'hidemyself' => true ] ],
744
745 [ 'hidebyothers', [ 'hidebyothers' => true ] ],
746
747 [ 'hidehumans', [ 'hidehumans' => true ] ],
748
749 [ 'hidepageedits', [ 'hidepageedits' => true ] ],
750
751 [ 'pagedits', [ 'hidepageedits' => false ] ],
752
753 [ 'hidenewpages', [ 'hidenewpages' => true ] ],
754
755 [ 'hidecategorization', [ 'hidecategorization' => true ] ],
756
757 [ 'hidelog', [ 'hidelog' => true ] ],
758
759 [
760 'userExpLevel=learner;experienced',
761 [
762 'userExpLevel' => 'learner;experienced'
763 ],
764 ],
765
766 // A few random combos
767 [
768 'bots,hideliu,hidemyself',
769 [
770 'hidebots' => false,
771 'hideliu' => true,
772 'hidemyself' => true,
773 ],
774 ],
775
776 [
777 'minor,hideanons,categorization',
778 [
779 'hideminor' => false,
780 'hideanons' => true,
781 'hidecategorization' => false,
782 ]
783 ],
784
785 [
786 'hidehumans,bots,hidecategorization',
787 [
788 'hidehumans' => true,
789 'hidebots' => false,
790 'hidecategorization' => true,
791 ],
792 ],
793
794 [
795 'hidemyself,userExpLevel=newcomer;learner,hideminor',
796 [
797 'hidemyself' => true,
798 'hideminor' => true,
799 'userExpLevel' => 'newcomer;learner',
800 ],
801 ],
802 ];
803 }
804 }