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