3 use Wikimedia\TestingAccessWrapper
;
6 * Test class for ChangesListSpecialPage class
8 * Copyright © 2011-, Antoine Musso, Stephane Bisson, Matthew Flaschen
10 * @author Antoine Musso
11 * @author Stephane Bisson
12 * @author Matthew Flaschen
15 * @covers ChangesListSpecialPage
17 class ChangesListSpecialPageTest
extends AbstractChangesListSpecialPageTestCase
{
18 protected function getPage() {
19 $mock = $this->getMockBuilder( ChangesListSpecialPage
::class )
22 'ChangesListSpecialPage',
26 ->setMethods( [ 'getPageTitle' ] )
27 ->getMockForAbstractClass();
29 $mock->method( 'getPageTitle' )->willReturn(
30 Title
::makeTitle( NS_SPECIAL
, 'ChangesListSpecialPage' )
33 $mock = TestingAccessWrapper
::newFromObject(
40 private function buildQuery(
41 $requestOptions = null,
44 $context = new RequestContext
;
45 $context->setRequest( new FauxRequest( $requestOptions ) );
47 $context->setUser( $user );
50 $this->changesListSpecialPage
->setContext( $context );
51 $this->changesListSpecialPage
->filterGroups
= [];
52 $formOptions = $this->changesListSpecialPage
->setup( null );
54 # Filter out rc_timestamp conditions which depends on the test runtime
55 # This condition is not needed as of march 2, 2011 -- hashar
56 # @todo FIXME: Find a way to generate the correct rc_timestamp
60 $queryConditions = [];
65 [ $this->changesListSpecialPage
, 'buildQuery' ],
76 $queryConditions = array_filter(
78 'ChangesListSpecialPageTest::filterOutRcTimestampCondition'
81 return $queryConditions;
84 /** helper to test SpecialRecentchanges::buildQuery() */
85 private function assertConditions(
87 $requestOptions = null,
91 $queryConditions = $this->buildQuery( $requestOptions, $user );
94 self
::normalizeCondition( $expected ),
95 self
::normalizeCondition( $queryConditions ),
100 private static function normalizeCondition( $conds ) {
101 $dbr = wfGetDB( DB_REPLICA
);
102 $normalized = array_map(
103 function ( $k, $v ) use ( $dbr ) {
104 if ( is_array( $v ) ) {
107 // (Ab)use makeList() to format only this entry
108 return $dbr->makeList( [ $k => $v ], Database
::LIST_AND
);
110 array_keys( $conds ),
117 /** return false if condition begins with 'rc_timestamp ' */
118 private static function filterOutRcTimestampCondition( $var ) {
119 return ( is_array( $var ) ||
false === strpos( $var, 'rc_timestamp ' ) );
122 public function testRcNsFilter() {
123 $this->assertConditions(
125 "rc_namespace = '0'",
128 'namespace' => NS_MAIN
,
130 "rc conditions with one namespace"
134 public function testRcNsFilterInversion() {
135 $this->assertConditions(
137 "rc_namespace != '0'",
140 'namespace' => NS_MAIN
,
143 "rc conditions with namespace inverted"
147 public function testRcNsFilterMultiple() {
148 $this->assertConditions(
150 "rc_namespace IN ('1','2','3')",
153 'namespace' => '1;2;3',
155 "rc conditions with multiple namespaces"
159 public function testRcNsFilterMultipleAssociated() {
160 $this->assertConditions(
162 "rc_namespace IN ('0','1','4','5','6','7')",
165 'namespace' => '1;4;7',
168 "rc conditions with multiple namespaces and associated"
172 public function testRcNsFilterMultipleAssociatedInvert() {
173 $this->assertConditions(
175 "rc_namespace NOT IN ('2','3','8','9')",
178 'namespace' => '2;3;9',
182 "rc conditions with multiple namespaces, associated and inverted"
186 public function testRcNsFilterMultipleInvert() {
187 $this->assertConditions(
189 "rc_namespace NOT IN ('1','2','3')",
192 'namespace' => '1;2;3',
195 "rc conditions with multiple namespaces inverted"
199 public function testRcHidemyselfFilter() {
200 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
201 $this->overrideMwServices();
203 $user = $this->getTestUser()->getUser();
204 $user->getActorId( wfGetDB( DB_MASTER
) );
205 $this->assertConditions(
207 "NOT((rc_actor = '{$user->getActorId()}') OR "
208 . "(rc_actor = '0' AND rc_user = '{$user->getId()}'))",
213 "rc conditions: hidemyself=1 (logged in)",
217 $user = User
::newFromName( '10.11.12.13', false );
218 $id = $user->getActorId( wfGetDB( DB_MASTER
) );
219 $this->assertConditions(
221 "NOT((rc_actor = '$id') OR (rc_actor = '0' AND rc_user_text = '10.11.12.13'))",
226 "rc conditions: hidemyself=1 (anon)",
231 public function testRcHidebyothersFilter() {
232 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
233 $this->overrideMwServices();
235 $user = $this->getTestUser()->getUser();
236 $user->getActorId( wfGetDB( DB_MASTER
) );
237 $this->assertConditions(
239 "(rc_actor = '{$user->getActorId()}') OR "
240 . "(rc_actor = '0' AND rc_user_text = '{$user->getName()}')",
245 "rc conditions: hidebyothers=1 (logged in)",
249 $user = User
::newFromName( '10.11.12.13', false );
250 $id = $user->getActorId( wfGetDB( DB_MASTER
) );
251 $this->assertConditions(
253 "(rc_actor = '$id') OR (rc_actor = '0' AND rc_user_text = '10.11.12.13')",
258 "rc conditions: hidebyothers=1 (anon)",
263 public function testRcHidepageedits() {
264 $this->assertConditions(
269 'hidepageedits' => 1,
271 "rc conditions: hidepageedits=1"
275 public function testRcHidenewpages() {
276 $this->assertConditions(
283 "rc conditions: hidenewpages=1"
287 public function testRcHidelog() {
288 $this->assertConditions(
295 "rc conditions: hidelog=1"
299 public function testRcHidehumans() {
300 $this->assertConditions(
308 "rc conditions: hidebots=0 hidehumans=1"
312 public function testRcHidepatrolledDisabledFilter() {
313 $this->setMwGlobals( 'wgUseRCPatrol', false );
314 $user = $this->getTestUser()->getUser();
315 $this->assertConditions(
319 'hidepatrolled' => 1,
321 "rc conditions: hidepatrolled=1 (user not allowed)",
326 public function testRcHideunpatrolledDisabledFilter() {
327 $this->setMwGlobals( 'wgUseRCPatrol', false );
328 $user = $this->getTestUser()->getUser();
329 $this->assertConditions(
333 'hideunpatrolled' => 1,
335 "rc conditions: hideunpatrolled=1 (user not allowed)",
339 public function testRcHidepatrolledFilter() {
340 $user = $this->getTestSysop()->getUser();
341 $this->assertConditions(
346 'hidepatrolled' => 1,
348 "rc conditions: hidepatrolled=1",
353 public function testRcHideunpatrolledFilter() {
354 $user = $this->getTestSysop()->getUser();
355 $this->assertConditions(
357 'rc_patrolled' => [ 1, 2 ],
360 'hideunpatrolled' => 1,
362 "rc conditions: hideunpatrolled=1",
367 public function testRcReviewStatusFilter() {
368 $user = $this->getTestSysop()->getUser();
369 $this->assertConditions(
374 'reviewStatus' => 'manual'
376 "rc conditions: reviewStatus=manual",
379 $this->assertConditions(
381 'rc_patrolled' => [ 0, 2 ],
384 'reviewStatus' => 'unpatrolled;auto'
386 "rc conditions: reviewStatus=unpatrolled;auto",
391 public function testRcHideminorFilter() {
392 $this->assertConditions(
399 "rc conditions: hideminor=1"
403 public function testRcHidemajorFilter() {
404 $this->assertConditions(
411 "rc conditions: hidemajor=1"
415 public function testHideCategorization() {
416 $this->assertConditions(
422 'hidecategorization' => 1
424 "rc conditions: hidecategorization=1"
428 public function testFilterUserExpLevelAll() {
429 $this->assertConditions(
434 'userExpLevel' => 'registered;unregistered;newcomer;learner;experienced',
436 "rc conditions: userExpLevel=registered;unregistered;newcomer;learner;experienced"
440 public function testFilterUserExpLevelRegisteredUnregistered() {
441 $this->assertConditions(
446 'userExpLevel' => 'registered;unregistered',
448 "rc conditions: userExpLevel=registered;unregistered"
452 public function testFilterUserExpLevelRegisteredUnregisteredLearner() {
453 $this->assertConditions(
458 'userExpLevel' => 'registered;unregistered;learner',
460 "rc conditions: userExpLevel=registered;unregistered;learner"
464 public function testFilterUserExpLevelAllExperienceLevels() {
465 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
466 $this->overrideMwServices();
468 $this->assertConditions(
471 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
474 'userExpLevel' => 'newcomer;learner;experienced',
476 "rc conditions: userExpLevel=newcomer;learner;experienced"
480 public function testFilterUserExpLevelRegistrered() {
481 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
482 $this->overrideMwServices();
484 $this->assertConditions(
487 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
490 'userExpLevel' => 'registered',
492 "rc conditions: userExpLevel=registered"
496 public function testFilterUserExpLevelUnregistrered() {
497 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
498 $this->overrideMwServices();
500 $this->assertConditions(
503 'COALESCE( actor_rc_user.actor_user, rc_user ) = 0',
506 'userExpLevel' => 'unregistered',
508 "rc conditions: userExpLevel=unregistered"
512 public function testFilterUserExpLevelRegistreredOrLearner() {
513 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
514 $this->overrideMwServices();
516 $this->assertConditions(
519 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
522 'userExpLevel' => 'registered;learner',
524 "rc conditions: userExpLevel=registered;learner"
528 public function testFilterUserExpLevelUnregistreredOrExperienced() {
529 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
530 $this->overrideMwServices();
532 $conds = $this->buildQuery( [ 'userExpLevel' => 'unregistered;experienced' ] );
535 '/\(COALESCE\( actor_rc_user.actor_user, rc_user \) = 0\) OR '
536 . '\(\(user_editcount >= 500\) AND \(user_registration <= \'[^\']+\'\)\)/',
538 "rc conditions: userExpLevel=unregistered;experienced"
542 public function testFilterUserExpLevel() {
544 $this->setMwGlobals( [
545 'wgLearnerEdits' => 10,
546 'wgLearnerMemberSince' => 4,
547 'wgExperiencedUserEdits' => 500,
548 'wgExperiencedUserMemberSince' => 30,
551 $this->createUsers( [
552 'Newcomer1' => [ 'edits' => 2, 'days' => 2 ],
553 'Newcomer2' => [ 'edits' => 12, 'days' => 3 ],
554 'Newcomer3' => [ 'edits' => 8, 'days' => 5 ],
555 'Learner1' => [ 'edits' => 15, 'days' => 10 ],
556 'Learner2' => [ 'edits' => 450, 'days' => 20 ],
557 'Learner3' => [ 'edits' => 460, 'days' => 33 ],
558 'Learner4' => [ 'edits' => 525, 'days' => 28 ],
559 'Experienced1' => [ 'edits' => 538, 'days' => 33 ],
563 $this->assertArrayEquals(
564 [ 'Newcomer1', 'Newcomer2', 'Newcomer3' ],
565 $this->fetchUsers( [ 'newcomer' ], $now )
568 // newcomers and learner
569 $this->assertArrayEquals(
571 'Newcomer1', 'Newcomer2', 'Newcomer3',
572 'Learner1', 'Learner2', 'Learner3', 'Learner4',
574 $this->fetchUsers( [ 'newcomer', 'learner' ], $now )
577 // newcomers and more learner
578 $this->assertArrayEquals(
580 'Newcomer1', 'Newcomer2', 'Newcomer3',
583 $this->fetchUsers( [ 'newcomer', 'experienced' ], $now )
587 $this->assertArrayEquals(
588 [ 'Learner1', 'Learner2', 'Learner3', 'Learner4' ],
589 $this->fetchUsers( [ 'learner' ], $now )
592 // more experienced only
593 $this->assertArrayEquals(
595 $this->fetchUsers( [ 'experienced' ], $now )
598 // learner and more experienced
599 $this->assertArrayEquals(
601 'Learner1', 'Learner2', 'Learner3', 'Learner4',
604 $this->fetchUsers( [ 'learner', 'experienced' ], $now ),
605 'Learner and more experienced'
609 private function createUsers( $specs, $now ) {
610 $dbw = wfGetDB( DB_MASTER
);
611 foreach ( $specs as $name => $spec ) {
615 'editcount' => $spec['edits'],
616 'registration' => $dbw->timestamp( $this->daysAgo( $spec['days'], $now ) ),
623 private function fetchUsers( $filters, $now ) {
632 call_user_func_array(
633 [ $this->changesListSpecialPage
, 'filterOnUserExperienceLevel' ],
635 get_class( $this->changesListSpecialPage
),
636 $this->changesListSpecialPage
->getContext(),
637 $this->changesListSpecialPage
->getDB(),
648 // @todo: This is not at all safe or sane. It just blindly assumes
649 // nothing in $conds depends on any other tables.
650 $result = wfGetDB( DB_MASTER
)->select(
653 array_filter( $conds ) +
[ 'user_email' => 'ut' ]
657 foreach ( $result as $row ) {
658 $usernames[] = $row->user_name
;
664 private function daysAgo( $days, $now ) {
665 $secondsPerDay = 86400;
666 return $now - $days * $secondsPerDay;
669 public function testGetStructuredFilterJsData() {
670 $this->changesListSpecialPage
->filterGroups
= [];
674 'name' => 'gub-group',
675 'title' => 'gub-group-title',
676 'class' => ChangesListBooleanFilterGroup
::class,
680 'label' => 'foo-label',
681 'description' => 'foo-description',
683 'showHide' => 'showhidefoo',
688 'label' => 'bar-label',
689 'description' => 'bar-description',
697 'name' => 'des-group',
698 'title' => 'des-group-title',
699 'class' => ChangesListStringOptionsFilterGroup
::class,
700 'isFullCoverage' => true,
704 'label' => 'grault-label',
705 'description' => 'grault-description',
709 'label' => 'garply-label',
710 'description' => 'garply-description',
713 'queryCallable' => function () {
715 'default' => ChangesListStringOptionsFilterGroup
::NONE
,
719 'name' => 'unstructured',
720 'class' => ChangesListBooleanFilterGroup
::class,
723 'name' => 'hidethud',
724 'showHide' => 'showhidethud',
730 'showHide' => 'showhidemos',
738 $this->changesListSpecialPage
->registerFiltersFromDefinitions( $definition );
740 $this->assertArrayEquals(
742 // Filters that only display in the unstructured UI are
743 // are not included, and neither are groups that would
744 // be empty due to the above.
747 'name' => 'gub-group',
748 'title' => 'gub-group-title',
749 'type' => ChangesListBooleanFilterGroup
::TYPE
,
754 'label' => 'bar-label',
755 'description' => 'bar-description',
761 'defaultHighlightColor' => null
765 'label' => 'foo-label',
766 'description' => 'foo-description',
772 'defaultHighlightColor' => null
775 'fullCoverage' => true,
780 'name' => 'des-group',
781 'title' => 'des-group-title',
782 'type' => ChangesListStringOptionsFilterGroup
::TYPE
,
784 'fullCoverage' => true,
788 'label' => 'grault-label',
789 'description' => 'grault-description',
794 'defaultHighlightColor' => null
798 'label' => 'garply-label',
799 'description' => 'garply-description',
804 'defaultHighlightColor' => null
809 'default' => ChangesListStringOptionsFilterGroup
::NONE
,
820 'grault-description',
822 'garply-description',
825 $this->changesListSpecialPage
->getStructuredFilterJsData(),
826 /** ordered= */ false,
831 public function provideParseParameters() {
833 [ 'hidebots', [ 'hidebots' => true ] ],
835 [ 'bots', [ 'hidebots' => false ] ],
837 [ 'hideminor', [ 'hideminor' => true ] ],
839 [ 'minor', [ 'hideminor' => false ] ],
841 [ 'hidemajor', [ 'hidemajor' => true ] ],
843 [ 'hideliu', [ 'hideliu' => true ] ],
845 [ 'hidepatrolled', [ 'hidepatrolled' => true ] ],
847 [ 'hideunpatrolled', [ 'hideunpatrolled' => true ] ],
849 [ 'hideanons', [ 'hideanons' => true ] ],
851 [ 'hidemyself', [ 'hidemyself' => true ] ],
853 [ 'hidebyothers', [ 'hidebyothers' => true ] ],
855 [ 'hidehumans', [ 'hidehumans' => true ] ],
857 [ 'hidepageedits', [ 'hidepageedits' => true ] ],
859 [ 'pagedits', [ 'hidepageedits' => false ] ],
861 [ 'hidenewpages', [ 'hidenewpages' => true ] ],
863 [ 'hidecategorization', [ 'hidecategorization' => true ] ],
865 [ 'hidelog', [ 'hidelog' => true ] ],
868 'userExpLevel=learner;experienced',
870 'userExpLevel' => 'learner;experienced'
874 // A few random combos
876 'bots,hideliu,hidemyself',
880 'hidemyself' => true,
885 'minor,hideanons,categorization',
887 'hideminor' => false,
889 'hidecategorization' => false,
894 'hidehumans,bots,hidecategorization',
896 'hidehumans' => true,
898 'hidecategorization' => true,
903 'hidemyself,userExpLevel=newcomer;learner,hideminor',
905 'hidemyself' => true,
907 'userExpLevel' => 'newcomer;learner',
913 public function provideGetFilterConflicts() {
917 "expectedConflicts" => false,
922 "userExpLevel" => "newcomer",
924 "expectedConflicts" => false,
929 "userExpLevel" => "learner",
931 "expectedConflicts" => false,
936 "hidenewpages" => true,
937 "hidepageedits" => true,
938 "hidecategorization" => false,
940 "hideWikidata" => true,
942 "expectedConflicts" => true,
947 "hidenewpages" => false,
948 "hidepageedits" => true,
949 "hidecategorization" => false,
951 "hideWikidata" => true,
953 "expectedConflicts" => true,
958 "hidenewpages" => false,
959 "hidepageedits" => false,
960 "hidecategorization" => true,
962 "hideWikidata" => true,
964 "expectedConflicts" => false,
969 "hidenewpages" => true,
970 "hidepageedits" => true,
971 "hidecategorization" => false,
973 "hideWikidata" => true,
975 "expectedConflicts" => false,
981 * @dataProvider provideGetFilterConflicts
983 public function testGetFilterConflicts( $parameters, $expectedConflicts ) {
984 $context = new RequestContext
;
985 $context->setRequest( new FauxRequest( $parameters ) );
986 $this->changesListSpecialPage
->setContext( $context );
990 $this->changesListSpecialPage
->areFiltersInConflict()
994 public function validateOptionsProvider() {
997 [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 1 ],
999 [ 'userExpLevel' => 'unregistered', 'hidebots' => 1, ],
1003 [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 0 ],
1005 [ 'hidebots' => 0, 'hidehumans' => 1 ],
1009 [ 'hideanons' => 1 ],
1011 [ 'userExpLevel' => 'registered' ],
1017 [ 'userExpLevel' => 'unregistered' ],
1021 [ 'hideanons' => 1, 'hidebots' => 1 ],
1023 [ 'userExpLevel' => 'registered', 'hidebots' => 1 ],
1027 [ 'hideliu' => 1, 'hidebots' => 0 ],
1029 [ 'userExpLevel' => 'unregistered', 'hidebots' => 0 ],
1033 [ 'hidemyself' => 1, 'hidebyothers' => 1 ],
1039 [ 'hidebots' => 1, 'hidehumans' => 1 ],
1045 [ 'hidepatrolled' => 1, 'hideunpatrolled' => 1 ],
1051 [ 'hideminor' => 1, 'hidemajor' => 1 ],
1058 [ 'hidepageedits' => 1, 'hidenewpages' => 1, 'hidecategorization' => 1, 'hidelog' => 1, ],