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