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