Merge "userExpLevel test: use $tables from function under tests"
[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 $now = time();
400 $this->setMwGlobals( [
401 'wgLearnerEdits' => 10,
402 'wgLearnerMemberSince' => 4,
403 'wgExperiencedUserEdits' => 500,
404 'wgExperiencedUserMemberSince' => 30,
405 ] );
406
407 $this->createUsers( [
408 'Newcomer1' => [ 'edits' => 2, 'days' => 2 ],
409 'Newcomer2' => [ 'edits' => 12, 'days' => 3 ],
410 'Newcomer3' => [ 'edits' => 8, 'days' => 5 ],
411 'Learner1' => [ 'edits' => 15, 'days' => 10 ],
412 'Learner2' => [ 'edits' => 450, 'days' => 20 ],
413 'Learner3' => [ 'edits' => 460, 'days' => 33 ],
414 'Learner4' => [ 'edits' => 525, 'days' => 28 ],
415 'Experienced1' => [ 'edits' => 538, 'days' => 33 ],
416 ], $now );
417
418 // newcomers only
419 $this->assertArrayEquals(
420 [ 'Newcomer1', 'Newcomer2', 'Newcomer3' ],
421 $this->fetchUsers( [ 'newcomer' ], $now )
422 );
423
424 // newcomers and learner
425 $this->assertArrayEquals(
426 [
427 'Newcomer1', 'Newcomer2', 'Newcomer3',
428 'Learner1', 'Learner2', 'Learner3', 'Learner4',
429 ],
430 $this->fetchUsers( [ 'newcomer', 'learner' ], $now )
431 );
432
433 // newcomers and more learner
434 $this->assertArrayEquals(
435 [
436 'Newcomer1', 'Newcomer2', 'Newcomer3',
437 'Experienced1',
438 ],
439 $this->fetchUsers( [ 'newcomer', 'experienced' ], $now )
440 );
441
442 // learner only
443 $this->assertArrayEquals(
444 [ 'Learner1', 'Learner2', 'Learner3', 'Learner4' ],
445 $this->fetchUsers( [ 'learner' ], $now )
446 );
447
448 // more experienced only
449 $this->assertArrayEquals(
450 [ 'Experienced1' ],
451 $this->fetchUsers( [ 'experienced' ], $now )
452 );
453
454 // learner and more experienced
455 $this->assertArrayEquals(
456 [
457 'Learner1', 'Learner2', 'Learner3', 'Learner4',
458 'Experienced1',
459 ],
460 $this->fetchUsers( [ 'learner', 'experienced' ], $now ),
461 'Learner and more experienced'
462 );
463
464 // newcomers, learner, and more experienced
465 // TOOD: Fix test. This needs to test that anons are excluded,
466 // and right now the join fails.
467 /* $this->assertArrayEquals( */
468 /* [ */
469 /* 'Newcomer1', 'Newcomer2', 'Newcomer3', */
470 /* 'Learner1', 'Learner2', 'Learner3', 'Learner4', */
471 /* 'Experienced1', */
472 /* ], */
473 /* $this->fetchUsers( [ 'newcomer', 'learner', 'experienced' ], $now ) */
474 /* ); */
475 }
476
477 private function createUsers( $specs, $now ) {
478 $dbw = wfGetDB( DB_MASTER );
479 foreach ( $specs as $name => $spec ) {
480 User::createNew(
481 $name,
482 [
483 'editcount' => $spec['edits'],
484 'registration' => $dbw->timestamp( $this->daysAgo( $spec['days'], $now ) ),
485 'email' => 'ut',
486 ]
487 );
488 }
489 }
490
491 private function fetchUsers( $filters, $now ) {
492 $tables = [];
493 $conds = [];
494 $fields = [];
495 $query_options = [];
496 $join_conds = [];
497
498 sort( $filters );
499
500 call_user_func_array(
501 [ $this->changesListSpecialPage, 'filterOnUserExperienceLevel' ],
502 [
503 get_class( $this->changesListSpecialPage ),
504 $this->changesListSpecialPage->getContext(),
505 $this->changesListSpecialPage->getDB(),
506 &$tables,
507 &$fields,
508 &$conds,
509 &$query_options,
510 &$join_conds,
511 $filters,
512 $now
513 ]
514 );
515
516 $result = wfGetDB( DB_MASTER )->select(
517 $tables,
518 'user_name',
519 array_filter( $conds ) + [ 'user_email' => 'ut' ]
520 );
521
522 $usernames = [];
523 foreach ( $result as $row ) {
524 $usernames[] = $row->user_name;
525 }
526
527 return $usernames;
528 }
529
530 private function daysAgo( $days, $now ) {
531 $secondsPerDay = 86400;
532 return $now - $days * $secondsPerDay;
533 }
534
535 public function testGetFilterGroupDefinitionFromLegacyCustomFilters() {
536 $customFilters = [
537 'hidefoo' => [
538 'msg' => 'showhidefoo',
539 'default' => true,
540 ],
541
542 'hidebar' => [
543 'msg' => 'showhidebar',
544 'default' => false,
545 ],
546 ];
547
548 $this->assertEquals(
549 [
550 'name' => 'unstructured',
551 'class' => ChangesListBooleanFilterGroup::class,
552 'priority' => -1,
553 'filters' => [
554 [
555 'name' => 'hidefoo',
556 'showHide' => 'showhidefoo',
557 'default' => true,
558 ],
559 [
560 'name' => 'hidebar',
561 'showHide' => 'showhidebar',
562 'default' => false,
563 ]
564 ],
565 ],
566 $this->changesListSpecialPage->getFilterGroupDefinitionFromLegacyCustomFilters(
567 $customFilters
568 )
569 );
570 }
571
572 public function testGetStructuredFilterJsData() {
573 $definition = [
574 [
575 'name' => 'gub-group',
576 'title' => 'gub-group-title',
577 'class' => ChangesListBooleanFilterGroup::class,
578 'filters' => [
579 [
580 'name' => 'hidefoo',
581 'label' => 'foo-label',
582 'description' => 'foo-description',
583 'default' => true,
584 'showHide' => 'showhidefoo',
585 'priority' => 2,
586 ],
587 [
588 'name' => 'hidebar',
589 'label' => 'bar-label',
590 'description' => 'bar-description',
591 'default' => false,
592 'priority' => 4,
593 ]
594 ],
595 ],
596
597 [
598 'name' => 'des-group',
599 'title' => 'des-group-title',
600 'class' => ChangesListStringOptionsFilterGroup::class,
601 'isFullCoverage' => true,
602 'filters' => [
603 [
604 'name' => 'grault',
605 'label' => 'grault-label',
606 'description' => 'grault-description',
607 ],
608 [
609 'name' => 'garply',
610 'label' => 'garply-label',
611 'description' => 'garply-description',
612 ],
613 ],
614 'queryCallable' => function () {
615 },
616 'default' => ChangesListStringOptionsFilterGroup::NONE,
617 ],
618
619 [
620 'name' => 'unstructured',
621 'class' => ChangesListBooleanFilterGroup::class,
622 'filters' => [
623 [
624 'name' => 'hidethud',
625 'showHide' => 'showhidethud',
626 'default' => true,
627 ],
628
629 [
630 'name' => 'hidemos',
631 'showHide' => 'showhidemos',
632 'default' => false,
633 ],
634 ],
635 ],
636
637 ];
638
639 $this->changesListSpecialPage->registerFiltersFromDefinitions( $definition );
640
641 $this->assertArrayEquals(
642 [
643 // Filters that only display in the unstructured UI are
644 // are not included, and neither are groups that would
645 // be empty due to the above.
646 'groups' => [
647 [
648 'name' => 'gub-group',
649 'title' => 'gub-group-title',
650 'type' => ChangesListBooleanFilterGroup::TYPE,
651 'priority' => -1,
652 'filters' => [
653 [
654 'name' => 'hidebar',
655 'label' => 'bar-label',
656 'description' => 'bar-description',
657 'default' => false,
658 'priority' => 4,
659 'cssClass' => null,
660 'conflicts' => [],
661 'subset' => [],
662 ],
663 [
664 'name' => 'hidefoo',
665 'label' => 'foo-label',
666 'description' => 'foo-description',
667 'default' => true,
668 'priority' => 2,
669 'cssClass' => null,
670 'conflicts' => [],
671 'subset' => [],
672 ],
673 ],
674 'fullCoverage' => true,
675 'conflicts' => [],
676 ],
677
678 [
679 'name' => 'des-group',
680 'title' => 'des-group-title',
681 'type' => ChangesListStringOptionsFilterGroup::TYPE,
682 'priority' => -2,
683 'fullCoverage' => true,
684 'filters' => [
685 [
686 'name' => 'grault',
687 'label' => 'grault-label',
688 'description' => 'grault-description',
689 'cssClass' => null,
690 'priority' => -2,
691 'conflicts' => [],
692 'subset' => [],
693 ],
694 [
695 'name' => 'garply',
696 'label' => 'garply-label',
697 'description' => 'garply-description',
698 'cssClass' => null,
699 'priority' => -3,
700 'conflicts' => [],
701 'subset' => [],
702 ],
703 ],
704 'conflicts' => [],
705 'separator' => ';',
706 'default' => ChangesListStringOptionsFilterGroup::NONE,
707 ],
708 ],
709 'messageKeys' => [
710 'gub-group-title',
711 'bar-label',
712 'bar-description',
713 'foo-label',
714 'foo-description',
715 'des-group-title',
716 'grault-label',
717 'grault-description',
718 'garply-label',
719 'garply-description',
720 ],
721 ],
722 $this->changesListSpecialPage->getStructuredFilterJsData(),
723 /** ordered= */ false,
724 /** named= */ true
725 );
726 }
727
728 public function provideParseParameters() {
729 return [
730 [ 'hidebots', [ 'hidebots' => true ] ],
731
732 [ 'bots', [ 'hidebots' => false ] ],
733
734 [ 'hideminor', [ 'hideminor' => true ] ],
735
736 [ 'minor', [ 'hideminor' => false ] ],
737
738 [ 'hidemajor', [ 'hidemajor' => true ] ],
739
740 [ 'hideliu', [ 'hideliu' => true ] ],
741
742 [ 'hidepatrolled', [ 'hidepatrolled' => true ] ],
743
744 [ 'hideunpatrolled', [ 'hideunpatrolled' => true ] ],
745
746 [ 'hideanons', [ 'hideanons' => true ] ],
747
748 [ 'hidemyself', [ 'hidemyself' => true ] ],
749
750 [ 'hidebyothers', [ 'hidebyothers' => true ] ],
751
752 [ 'hidehumans', [ 'hidehumans' => true ] ],
753
754 [ 'hidepageedits', [ 'hidepageedits' => true ] ],
755
756 [ 'pagedits', [ 'hidepageedits' => false ] ],
757
758 [ 'hidenewpages', [ 'hidenewpages' => true ] ],
759
760 [ 'hidecategorization', [ 'hidecategorization' => true ] ],
761
762 [ 'hidelog', [ 'hidelog' => true ] ],
763
764 [
765 'userExpLevel=learner;experienced',
766 [
767 'userExpLevel' => 'learner;experienced'
768 ],
769 ],
770
771 // A few random combos
772 [
773 'bots,hideliu,hidemyself',
774 [
775 'hidebots' => false,
776 'hideliu' => true,
777 'hidemyself' => true,
778 ],
779 ],
780
781 [
782 'minor,hideanons,categorization',
783 [
784 'hideminor' => false,
785 'hideanons' => true,
786 'hidecategorization' => false,
787 ]
788 ],
789
790 [
791 'hidehumans,bots,hidecategorization',
792 [
793 'hidehumans' => true,
794 'hidebots' => false,
795 'hidecategorization' => true,
796 ],
797 ],
798
799 [
800 'hidemyself,userExpLevel=newcomer;learner,hideminor',
801 [
802 'hidemyself' => true,
803 'hideminor' => true,
804 'userExpLevel' => 'newcomer;learner',
805 ],
806 ],
807 ];
808 }
809
810 public function provideGetFilterConflicts() {
811 return [
812 [
813 "parameters" => [],
814 "expectedConflicts" => false,
815 ],
816 [
817 "parameters" => [
818 "hideliu" => true,
819 "userExpLevel" => "newcomer",
820 ],
821 "expectedConflicts" => true,
822 ],
823 [
824 "parameters" => [
825 "hideanons" => true,
826 "userExpLevel" => "learner",
827 ],
828 "expectedConflicts" => false,
829 ],
830 [
831 "parameters" => [
832 "hidemajor" => true,
833 "hidenewpages" => true,
834 "hidepageedits" => true,
835 "hidecategorization" => false,
836 "hidelog" => true,
837 "hideWikidata" => true,
838 ],
839 "expectedConflicts" => true,
840 ],
841 [
842 "parameters" => [
843 "hidemajor" => true,
844 "hidenewpages" => false,
845 "hidepageedits" => true,
846 "hidecategorization" => false,
847 "hidelog" => false,
848 "hideWikidata" => true,
849 ],
850 "expectedConflicts" => true,
851 ],
852 [
853 "parameters" => [
854 "hidemajor" => true,
855 "hidenewpages" => false,
856 "hidepageedits" => false,
857 "hidecategorization" => true,
858 "hidelog" => true,
859 "hideWikidata" => true,
860 ],
861 "expectedConflicts" => false,
862 ],
863 [
864 "parameters" => [
865 "hideminor" => true,
866 "hidenewpages" => true,
867 "hidepageedits" => true,
868 "hidecategorization" => false,
869 "hidelog" => true,
870 "hideWikidata" => true,
871 ],
872 "expectedConflicts" => false,
873 ],
874 ];
875 }
876
877 /**
878 * @dataProvider provideGetFilterConflicts
879 */
880 public function testGetFilterConflicts( $parameters, $expectedConflicts ) {
881 $context = new RequestContext;
882 $context->setRequest( new FauxRequest( $parameters ) );
883 $this->changesListSpecialPage->setContext( $context );
884
885 $this->assertEquals(
886 $expectedConflicts,
887 $this->changesListSpecialPage->areFiltersInConflict()
888 );
889 }
890 }