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 public function setUp() {
20 $this->setMwGlobals( [
21 'wgStructuredChangeFiltersShowPreference' => true,
25 protected function getPage() {
26 $mock = $this->getMockBuilder( ChangesListSpecialPage
::class )
29 'ChangesListSpecialPage',
33 ->setMethods( [ 'getPageTitle' ] )
34 ->getMockForAbstractClass();
36 $mock->method( 'getPageTitle' )->willReturn(
37 Title
::makeTitle( NS_SPECIAL
, 'ChangesListSpecialPage' )
40 $mock = TestingAccessWrapper
::newFromObject(
47 private function buildQuery(
48 $requestOptions = null,
51 $context = new RequestContext
;
52 $context->setRequest( new FauxRequest( $requestOptions ) );
54 $context->setUser( $user );
57 $this->changesListSpecialPage
->setContext( $context );
58 $this->changesListSpecialPage
->filterGroups
= [];
59 $formOptions = $this->changesListSpecialPage
->setup( null );
61 # Filter out rc_timestamp conditions which depends on the test runtime
62 # This condition is not needed as of march 2, 2011 -- hashar
63 # @todo FIXME: Find a way to generate the correct rc_timestamp
67 $queryConditions = [];
72 [ $this->changesListSpecialPage
, 'buildQuery' ],
83 $queryConditions = array_filter(
85 'ChangesListSpecialPageTest::filterOutRcTimestampCondition'
88 return $queryConditions;
91 /** helper to test SpecialRecentchanges::buildQuery() */
92 private function assertConditions(
94 $requestOptions = null,
98 $queryConditions = $this->buildQuery( $requestOptions, $user );
101 self
::normalizeCondition( $expected ),
102 self
::normalizeCondition( $queryConditions ),
107 private static function normalizeCondition( $conds ) {
108 $dbr = wfGetDB( DB_REPLICA
);
109 $normalized = array_map(
110 function ( $k, $v ) use ( $dbr ) {
111 if ( is_array( $v ) ) {
114 // (Ab)use makeList() to format only this entry
115 return $dbr->makeList( [ $k => $v ], Database
::LIST_AND
);
117 array_keys( $conds ),
124 /** return false if condition begins with 'rc_timestamp ' */
125 private static function filterOutRcTimestampCondition( $var ) {
126 return ( is_array( $var ) ||
false === strpos( $var, 'rc_timestamp ' ) );
129 public function testRcNsFilter() {
130 $this->assertConditions(
132 "rc_namespace = '0'",
135 'namespace' => NS_MAIN
,
137 "rc conditions with one namespace"
141 public function testRcNsFilterInversion() {
142 $this->assertConditions(
144 "rc_namespace != '0'",
147 'namespace' => NS_MAIN
,
150 "rc conditions with namespace inverted"
154 public function testRcNsFilterMultiple() {
155 $this->assertConditions(
157 "rc_namespace IN ('1','2','3')",
160 'namespace' => '1;2;3',
162 "rc conditions with multiple namespaces"
166 public function testRcNsFilterMultipleAssociated() {
167 $this->assertConditions(
169 "rc_namespace IN ('0','1','4','5','6','7')",
172 'namespace' => '1;4;7',
175 "rc conditions with multiple namespaces and associated"
179 public function testRcNsFilterMultipleAssociatedInvert() {
180 $this->assertConditions(
182 "rc_namespace NOT IN ('2','3','8','9')",
185 'namespace' => '2;3;9',
189 "rc conditions with multiple namespaces, associated and inverted"
193 public function testRcNsFilterMultipleInvert() {
194 $this->assertConditions(
196 "rc_namespace NOT IN ('1','2','3')",
199 'namespace' => '1;2;3',
202 "rc conditions with multiple namespaces inverted"
206 public function testRcHidemyselfFilter() {
207 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
208 $this->overrideMwServices();
210 $user = $this->getTestUser()->getUser();
211 $user->getActorId( wfGetDB( DB_MASTER
) );
212 $this->assertConditions(
214 "NOT((rc_actor = '{$user->getActorId()}') OR "
215 . "(rc_actor = '0' AND rc_user = '{$user->getId()}'))",
220 "rc conditions: hidemyself=1 (logged in)",
224 $user = User
::newFromName( '10.11.12.13', false );
225 $id = $user->getActorId( wfGetDB( DB_MASTER
) );
226 $this->assertConditions(
228 "NOT((rc_actor = '$id') OR (rc_actor = '0' AND rc_user_text = '10.11.12.13'))",
233 "rc conditions: hidemyself=1 (anon)",
238 public function testRcHidebyothersFilter() {
239 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
240 $this->overrideMwServices();
242 $user = $this->getTestUser()->getUser();
243 $user->getActorId( wfGetDB( DB_MASTER
) );
244 $this->assertConditions(
246 "(rc_actor = '{$user->getActorId()}') OR "
247 . "(rc_actor = '0' AND rc_user_text = '{$user->getName()}')",
252 "rc conditions: hidebyothers=1 (logged in)",
256 $user = User
::newFromName( '10.11.12.13', false );
257 $id = $user->getActorId( wfGetDB( DB_MASTER
) );
258 $this->assertConditions(
260 "(rc_actor = '$id') OR (rc_actor = '0' AND rc_user_text = '10.11.12.13')",
265 "rc conditions: hidebyothers=1 (anon)",
270 public function testRcHidepageedits() {
271 $this->assertConditions(
276 'hidepageedits' => 1,
278 "rc conditions: hidepageedits=1"
282 public function testRcHidenewpages() {
283 $this->assertConditions(
290 "rc conditions: hidenewpages=1"
294 public function testRcHidelog() {
295 $this->assertConditions(
302 "rc conditions: hidelog=1"
306 public function testRcHidehumans() {
307 $this->assertConditions(
315 "rc conditions: hidebots=0 hidehumans=1"
319 public function testRcHidepatrolledDisabledFilter() {
320 $this->setMwGlobals( 'wgUseRCPatrol', false );
321 $user = $this->getTestUser()->getUser();
322 $this->assertConditions(
326 'hidepatrolled' => 1,
328 "rc conditions: hidepatrolled=1 (user not allowed)",
333 public function testRcHideunpatrolledDisabledFilter() {
334 $this->setMwGlobals( 'wgUseRCPatrol', false );
335 $user = $this->getTestUser()->getUser();
336 $this->assertConditions(
340 'hideunpatrolled' => 1,
342 "rc conditions: hideunpatrolled=1 (user not allowed)",
346 public function testRcHidepatrolledFilter() {
347 $user = $this->getTestSysop()->getUser();
348 $this->assertConditions(
353 'hidepatrolled' => 1,
355 "rc conditions: hidepatrolled=1",
360 public function testRcHideunpatrolledFilter() {
361 $user = $this->getTestSysop()->getUser();
362 $this->assertConditions(
364 'rc_patrolled' => [ 1, 2 ],
367 'hideunpatrolled' => 1,
369 "rc conditions: hideunpatrolled=1",
374 public function testRcReviewStatusFilter() {
375 $user = $this->getTestSysop()->getUser();
376 $this->assertConditions(
381 'reviewStatus' => 'manual'
383 "rc conditions: reviewStatus=manual",
386 $this->assertConditions(
388 'rc_patrolled' => [ 0, 2 ],
391 'reviewStatus' => 'unpatrolled;auto'
393 "rc conditions: reviewStatus=unpatrolled;auto",
398 public function testRcHideminorFilter() {
399 $this->assertConditions(
406 "rc conditions: hideminor=1"
410 public function testRcHidemajorFilter() {
411 $this->assertConditions(
418 "rc conditions: hidemajor=1"
422 public function testHideCategorization() {
423 $this->assertConditions(
429 'hidecategorization' => 1
431 "rc conditions: hidecategorization=1"
435 public function testFilterUserExpLevelAll() {
436 $this->assertConditions(
441 'userExpLevel' => 'registered;unregistered;newcomer;learner;experienced',
443 "rc conditions: userExpLevel=registered;unregistered;newcomer;learner;experienced"
447 public function testFilterUserExpLevelRegisteredUnregistered() {
448 $this->assertConditions(
453 'userExpLevel' => 'registered;unregistered',
455 "rc conditions: userExpLevel=registered;unregistered"
459 public function testFilterUserExpLevelRegisteredUnregisteredLearner() {
460 $this->assertConditions(
465 'userExpLevel' => 'registered;unregistered;learner',
467 "rc conditions: userExpLevel=registered;unregistered;learner"
471 public function testFilterUserExpLevelAllExperienceLevels() {
472 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
473 $this->overrideMwServices();
475 $this->assertConditions(
478 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
481 'userExpLevel' => 'newcomer;learner;experienced',
483 "rc conditions: userExpLevel=newcomer;learner;experienced"
487 public function testFilterUserExpLevelRegistrered() {
488 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
489 $this->overrideMwServices();
491 $this->assertConditions(
494 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
497 'userExpLevel' => 'registered',
499 "rc conditions: userExpLevel=registered"
503 public function testFilterUserExpLevelUnregistrered() {
504 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
505 $this->overrideMwServices();
507 $this->assertConditions(
510 'COALESCE( actor_rc_user.actor_user, rc_user ) = 0',
513 'userExpLevel' => 'unregistered',
515 "rc conditions: userExpLevel=unregistered"
519 public function testFilterUserExpLevelRegistreredOrLearner() {
520 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
521 $this->overrideMwServices();
523 $this->assertConditions(
526 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
529 'userExpLevel' => 'registered;learner',
531 "rc conditions: userExpLevel=registered;learner"
535 public function testFilterUserExpLevelUnregistreredOrExperienced() {
536 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
537 $this->overrideMwServices();
539 $conds = $this->buildQuery( [ 'userExpLevel' => 'unregistered;experienced' ] );
542 '/\(COALESCE\( actor_rc_user.actor_user, rc_user \) = 0\) OR '
543 . '\(\(user_editcount >= 500\) AND \(user_registration <= \'[^\']+\'\)\)/',
545 "rc conditions: userExpLevel=unregistered;experienced"
549 public function testFilterUserExpLevel() {
551 $this->setMwGlobals( [
552 'wgLearnerEdits' => 10,
553 'wgLearnerMemberSince' => 4,
554 'wgExperiencedUserEdits' => 500,
555 'wgExperiencedUserMemberSince' => 30,
558 $this->createUsers( [
559 'Newcomer1' => [ 'edits' => 2, 'days' => 2 ],
560 'Newcomer2' => [ 'edits' => 12, 'days' => 3 ],
561 'Newcomer3' => [ 'edits' => 8, 'days' => 5 ],
562 'Learner1' => [ 'edits' => 15, 'days' => 10 ],
563 'Learner2' => [ 'edits' => 450, 'days' => 20 ],
564 'Learner3' => [ 'edits' => 460, 'days' => 33 ],
565 'Learner4' => [ 'edits' => 525, 'days' => 28 ],
566 'Experienced1' => [ 'edits' => 538, 'days' => 33 ],
570 $this->assertArrayEquals(
571 [ 'Newcomer1', 'Newcomer2', 'Newcomer3' ],
572 $this->fetchUsers( [ 'newcomer' ], $now )
575 // newcomers and learner
576 $this->assertArrayEquals(
578 'Newcomer1', 'Newcomer2', 'Newcomer3',
579 'Learner1', 'Learner2', 'Learner3', 'Learner4',
581 $this->fetchUsers( [ 'newcomer', 'learner' ], $now )
584 // newcomers and more learner
585 $this->assertArrayEquals(
587 'Newcomer1', 'Newcomer2', 'Newcomer3',
590 $this->fetchUsers( [ 'newcomer', 'experienced' ], $now )
594 $this->assertArrayEquals(
595 [ 'Learner1', 'Learner2', 'Learner3', 'Learner4' ],
596 $this->fetchUsers( [ 'learner' ], $now )
599 // more experienced only
600 $this->assertArrayEquals(
602 $this->fetchUsers( [ 'experienced' ], $now )
605 // learner and more experienced
606 $this->assertArrayEquals(
608 'Learner1', 'Learner2', 'Learner3', 'Learner4',
611 $this->fetchUsers( [ 'learner', 'experienced' ], $now ),
612 'Learner and more experienced'
616 private function createUsers( $specs, $now ) {
617 $dbw = wfGetDB( DB_MASTER
);
618 foreach ( $specs as $name => $spec ) {
622 'editcount' => $spec['edits'],
623 'registration' => $dbw->timestamp( $this->daysAgo( $spec['days'], $now ) ),
630 private function fetchUsers( $filters, $now ) {
639 call_user_func_array(
640 [ $this->changesListSpecialPage
, 'filterOnUserExperienceLevel' ],
642 get_class( $this->changesListSpecialPage
),
643 $this->changesListSpecialPage
->getContext(),
644 $this->changesListSpecialPage
->getDB(),
655 // @todo: This is not at all safe or sane. It just blindly assumes
656 // nothing in $conds depends on any other tables.
657 $result = wfGetDB( DB_MASTER
)->select(
660 array_filter( $conds ) +
[ 'user_email' => 'ut' ]
664 foreach ( $result as $row ) {
665 $usernames[] = $row->user_name
;
671 private function daysAgo( $days, $now ) {
672 $secondsPerDay = 86400;
673 return $now - $days * $secondsPerDay;
676 public function testGetStructuredFilterJsData() {
677 $this->changesListSpecialPage
->filterGroups
= [];
681 'name' => 'gub-group',
682 'title' => 'gub-group-title',
683 'class' => ChangesListBooleanFilterGroup
::class,
687 'label' => 'foo-label',
688 'description' => 'foo-description',
690 'showHide' => 'showhidefoo',
695 'label' => 'bar-label',
696 'description' => 'bar-description',
704 'name' => 'des-group',
705 'title' => 'des-group-title',
706 'class' => ChangesListStringOptionsFilterGroup
::class,
707 'isFullCoverage' => true,
711 'label' => 'grault-label',
712 'description' => 'grault-description',
716 'label' => 'garply-label',
717 'description' => 'garply-description',
720 'queryCallable' => function () {
722 'default' => ChangesListStringOptionsFilterGroup
::NONE
,
726 'name' => 'unstructured',
727 'class' => ChangesListBooleanFilterGroup
::class,
730 'name' => 'hidethud',
731 'showHide' => 'showhidethud',
737 'showHide' => 'showhidemos',
745 $this->changesListSpecialPage
->registerFiltersFromDefinitions( $definition );
747 $this->assertArrayEquals(
749 // Filters that only display in the unstructured UI are
750 // are not included, and neither are groups that would
751 // be empty due to the above.
754 'name' => 'gub-group',
755 'title' => 'gub-group-title',
756 'type' => ChangesListBooleanFilterGroup
::TYPE
,
761 'label' => 'bar-label',
762 'description' => 'bar-description',
768 'defaultHighlightColor' => null
772 'label' => 'foo-label',
773 'description' => 'foo-description',
779 'defaultHighlightColor' => null
782 'fullCoverage' => true,
787 'name' => 'des-group',
788 'title' => 'des-group-title',
789 'type' => ChangesListStringOptionsFilterGroup
::TYPE
,
791 'fullCoverage' => true,
795 'label' => 'grault-label',
796 'description' => 'grault-description',
801 'defaultHighlightColor' => null
805 'label' => 'garply-label',
806 'description' => 'garply-description',
811 'defaultHighlightColor' => null
816 'default' => ChangesListStringOptionsFilterGroup
::NONE
,
827 'grault-description',
829 'garply-description',
832 $this->changesListSpecialPage
->getStructuredFilterJsData(),
833 /** ordered= */ false,
838 public function provideParseParameters() {
840 [ 'hidebots', [ 'hidebots' => true ] ],
842 [ 'bots', [ 'hidebots' => false ] ],
844 [ 'hideminor', [ 'hideminor' => true ] ],
846 [ 'minor', [ 'hideminor' => false ] ],
848 [ 'hidemajor', [ 'hidemajor' => true ] ],
850 [ 'hideliu', [ 'hideliu' => true ] ],
852 [ 'hidepatrolled', [ 'hidepatrolled' => true ] ],
854 [ 'hideunpatrolled', [ 'hideunpatrolled' => true ] ],
856 [ 'hideanons', [ 'hideanons' => true ] ],
858 [ 'hidemyself', [ 'hidemyself' => true ] ],
860 [ 'hidebyothers', [ 'hidebyothers' => true ] ],
862 [ 'hidehumans', [ 'hidehumans' => true ] ],
864 [ 'hidepageedits', [ 'hidepageedits' => true ] ],
866 [ 'pagedits', [ 'hidepageedits' => false ] ],
868 [ 'hidenewpages', [ 'hidenewpages' => true ] ],
870 [ 'hidecategorization', [ 'hidecategorization' => true ] ],
872 [ 'hidelog', [ 'hidelog' => true ] ],
875 'userExpLevel=learner;experienced',
877 'userExpLevel' => 'learner;experienced'
881 // A few random combos
883 'bots,hideliu,hidemyself',
887 'hidemyself' => true,
892 'minor,hideanons,categorization',
894 'hideminor' => false,
896 'hidecategorization' => false,
901 'hidehumans,bots,hidecategorization',
903 'hidehumans' => true,
905 'hidecategorization' => true,
910 'hidemyself,userExpLevel=newcomer;learner,hideminor',
912 'hidemyself' => true,
914 'userExpLevel' => 'newcomer;learner',
920 public function provideGetFilterConflicts() {
924 "expectedConflicts" => false,
929 "userExpLevel" => "newcomer",
931 "expectedConflicts" => false,
936 "userExpLevel" => "learner",
938 "expectedConflicts" => false,
943 "hidenewpages" => true,
944 "hidepageedits" => true,
945 "hidecategorization" => false,
947 "hideWikidata" => true,
949 "expectedConflicts" => true,
954 "hidenewpages" => false,
955 "hidepageedits" => true,
956 "hidecategorization" => false,
958 "hideWikidata" => true,
960 "expectedConflicts" => true,
965 "hidenewpages" => false,
966 "hidepageedits" => false,
967 "hidecategorization" => true,
969 "hideWikidata" => true,
971 "expectedConflicts" => false,
976 "hidenewpages" => true,
977 "hidepageedits" => true,
978 "hidecategorization" => false,
980 "hideWikidata" => true,
982 "expectedConflicts" => false,
988 * @dataProvider provideGetFilterConflicts
990 public function testGetFilterConflicts( $parameters, $expectedConflicts ) {
991 $context = new RequestContext
;
992 $context->setRequest( new FauxRequest( $parameters ) );
993 $this->changesListSpecialPage
->setContext( $context );
997 $this->changesListSpecialPage
->areFiltersInConflict()
1001 public function validateOptionsProvider() {
1004 [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 1 ],
1006 [ 'userExpLevel' => 'unregistered', 'hidebots' => 1, ],
1009 [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 0 ],
1011 [ 'hidebots' => 0, 'hidehumans' => 1 ],
1014 [ 'hideanons' => 1 ],
1016 [ 'userExpLevel' => 'registered' ]
1021 [ 'userExpLevel' => 'unregistered' ]
1024 [ 'hideanons' => 1, 'hidebots' => 1 ],
1026 [ 'userExpLevel' => 'registered', 'hidebots' => 1 ]
1029 [ 'hideliu' => 1, 'hidebots' => 0 ],
1031 [ 'userExpLevel' => 'unregistered', 'hidebots' => 0 ]
1034 [ 'hidemyself' => 1, 'hidebyothers' => 1 ],
1039 [ 'hidebots' => 1, 'hidehumans' => 1 ],
1044 [ 'hidepatrolled' => 1, 'hideunpatrolled' => 1 ],
1049 [ 'hideminor' => 1, 'hidemajor' => 1 ],
1055 [ 'hidepageedits' => 1, 'hidenewpages' => 1, 'hidecategorization' => 1, 'hidelog' => 1, ],