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(
367 'hideunpatrolled' => 1,
369 "rc conditions: hideunpatrolled=1",
374 public function testRcHideminorFilter() {
375 $this->assertConditions(
382 "rc conditions: hideminor=1"
386 public function testRcHidemajorFilter() {
387 $this->assertConditions(
394 "rc conditions: hidemajor=1"
398 public function testHideCategorization() {
399 $this->assertConditions(
405 'hidecategorization' => 1
407 "rc conditions: hidecategorization=1"
411 public function testFilterUserExpLevelAll() {
412 $this->assertConditions(
417 'userExpLevel' => 'registered;unregistered;newcomer;learner;experienced',
419 "rc conditions: userExpLevel=registered;unregistered;newcomer;learner;experienced"
423 public function testFilterUserExpLevelRegisteredUnregistered() {
424 $this->assertConditions(
429 'userExpLevel' => 'registered;unregistered',
431 "rc conditions: userExpLevel=registered;unregistered"
435 public function testFilterUserExpLevelRegisteredUnregisteredLearner() {
436 $this->assertConditions(
441 'userExpLevel' => 'registered;unregistered;learner',
443 "rc conditions: userExpLevel=registered;unregistered;learner"
447 public function testFilterUserExpLevelAllExperienceLevels() {
448 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
449 $this->overrideMwServices();
451 $this->assertConditions(
454 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
457 'userExpLevel' => 'newcomer;learner;experienced',
459 "rc conditions: userExpLevel=newcomer;learner;experienced"
463 public function testFilterUserExpLevelRegistrered() {
464 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
465 $this->overrideMwServices();
467 $this->assertConditions(
470 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
473 'userExpLevel' => 'registered',
475 "rc conditions: userExpLevel=registered"
479 public function testFilterUserExpLevelUnregistrered() {
480 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
481 $this->overrideMwServices();
483 $this->assertConditions(
486 'COALESCE( actor_rc_user.actor_user, rc_user ) = 0',
489 'userExpLevel' => 'unregistered',
491 "rc conditions: userExpLevel=unregistered"
495 public function testFilterUserExpLevelRegistreredOrLearner() {
496 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
497 $this->overrideMwServices();
499 $this->assertConditions(
502 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
505 'userExpLevel' => 'registered;learner',
507 "rc conditions: userExpLevel=registered;learner"
511 public function testFilterUserExpLevelUnregistreredOrExperienced() {
512 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
513 $this->overrideMwServices();
515 $conds = $this->buildQuery( [ 'userExpLevel' => 'unregistered;experienced' ] );
518 '/\(COALESCE\( actor_rc_user.actor_user, rc_user \) = 0\) OR '
519 . '\(\(user_editcount >= 500\) AND \(user_registration <= \'[^\']+\'\)\)/',
521 "rc conditions: userExpLevel=unregistered;experienced"
525 public function testFilterUserExpLevel() {
527 $this->setMwGlobals( [
528 'wgLearnerEdits' => 10,
529 'wgLearnerMemberSince' => 4,
530 'wgExperiencedUserEdits' => 500,
531 'wgExperiencedUserMemberSince' => 30,
534 $this->createUsers( [
535 'Newcomer1' => [ 'edits' => 2, 'days' => 2 ],
536 'Newcomer2' => [ 'edits' => 12, 'days' => 3 ],
537 'Newcomer3' => [ 'edits' => 8, 'days' => 5 ],
538 'Learner1' => [ 'edits' => 15, 'days' => 10 ],
539 'Learner2' => [ 'edits' => 450, 'days' => 20 ],
540 'Learner3' => [ 'edits' => 460, 'days' => 33 ],
541 'Learner4' => [ 'edits' => 525, 'days' => 28 ],
542 'Experienced1' => [ 'edits' => 538, 'days' => 33 ],
546 $this->assertArrayEquals(
547 [ 'Newcomer1', 'Newcomer2', 'Newcomer3' ],
548 $this->fetchUsers( [ 'newcomer' ], $now )
551 // newcomers and learner
552 $this->assertArrayEquals(
554 'Newcomer1', 'Newcomer2', 'Newcomer3',
555 'Learner1', 'Learner2', 'Learner3', 'Learner4',
557 $this->fetchUsers( [ 'newcomer', 'learner' ], $now )
560 // newcomers and more learner
561 $this->assertArrayEquals(
563 'Newcomer1', 'Newcomer2', 'Newcomer3',
566 $this->fetchUsers( [ 'newcomer', 'experienced' ], $now )
570 $this->assertArrayEquals(
571 [ 'Learner1', 'Learner2', 'Learner3', 'Learner4' ],
572 $this->fetchUsers( [ 'learner' ], $now )
575 // more experienced only
576 $this->assertArrayEquals(
578 $this->fetchUsers( [ 'experienced' ], $now )
581 // learner and more experienced
582 $this->assertArrayEquals(
584 'Learner1', 'Learner2', 'Learner3', 'Learner4',
587 $this->fetchUsers( [ 'learner', 'experienced' ], $now ),
588 'Learner and more experienced'
592 private function createUsers( $specs, $now ) {
593 $dbw = wfGetDB( DB_MASTER
);
594 foreach ( $specs as $name => $spec ) {
598 'editcount' => $spec['edits'],
599 'registration' => $dbw->timestamp( $this->daysAgo( $spec['days'], $now ) ),
606 private function fetchUsers( $filters, $now ) {
615 call_user_func_array(
616 [ $this->changesListSpecialPage
, 'filterOnUserExperienceLevel' ],
618 get_class( $this->changesListSpecialPage
),
619 $this->changesListSpecialPage
->getContext(),
620 $this->changesListSpecialPage
->getDB(),
631 // @todo: This is not at all safe or sane. It just blindly assumes
632 // nothing in $conds depends on any other tables.
633 $result = wfGetDB( DB_MASTER
)->select(
636 array_filter( $conds ) +
[ 'user_email' => 'ut' ]
640 foreach ( $result as $row ) {
641 $usernames[] = $row->user_name
;
647 private function daysAgo( $days, $now ) {
648 $secondsPerDay = 86400;
649 return $now - $days * $secondsPerDay;
652 public function testGetFilterGroupDefinitionFromLegacyCustomFilters() {
655 'msg' => 'showhidefoo',
660 'msg' => 'showhidebar',
667 'name' => 'unstructured',
668 'class' => ChangesListBooleanFilterGroup
::class,
673 'showHide' => 'showhidefoo',
678 'showHide' => 'showhidebar',
683 $this->changesListSpecialPage
->getFilterGroupDefinitionFromLegacyCustomFilters(
689 public function testGetStructuredFilterJsData() {
690 $this->changesListSpecialPage
->filterGroups
= [];
694 'name' => 'gub-group',
695 'title' => 'gub-group-title',
696 'class' => ChangesListBooleanFilterGroup
::class,
700 'label' => 'foo-label',
701 'description' => 'foo-description',
703 'showHide' => 'showhidefoo',
708 'label' => 'bar-label',
709 'description' => 'bar-description',
717 'name' => 'des-group',
718 'title' => 'des-group-title',
719 'class' => ChangesListStringOptionsFilterGroup
::class,
720 'isFullCoverage' => true,
724 'label' => 'grault-label',
725 'description' => 'grault-description',
729 'label' => 'garply-label',
730 'description' => 'garply-description',
733 'queryCallable' => function () {
735 'default' => ChangesListStringOptionsFilterGroup
::NONE
,
739 'name' => 'unstructured',
740 'class' => ChangesListBooleanFilterGroup
::class,
743 'name' => 'hidethud',
744 'showHide' => 'showhidethud',
750 'showHide' => 'showhidemos',
758 $this->changesListSpecialPage
->registerFiltersFromDefinitions( $definition );
760 $this->assertArrayEquals(
762 // Filters that only display in the unstructured UI are
763 // are not included, and neither are groups that would
764 // be empty due to the above.
767 'name' => 'gub-group',
768 'title' => 'gub-group-title',
769 'type' => ChangesListBooleanFilterGroup
::TYPE
,
774 'label' => 'bar-label',
775 'description' => 'bar-description',
781 'defaultHighlightColor' => null
785 'label' => 'foo-label',
786 'description' => 'foo-description',
792 'defaultHighlightColor' => null
795 'fullCoverage' => true,
800 'name' => 'des-group',
801 'title' => 'des-group-title',
802 'type' => ChangesListStringOptionsFilterGroup
::TYPE
,
804 'fullCoverage' => true,
808 'label' => 'grault-label',
809 'description' => 'grault-description',
814 'defaultHighlightColor' => null
818 'label' => 'garply-label',
819 'description' => 'garply-description',
824 'defaultHighlightColor' => null
829 'default' => ChangesListStringOptionsFilterGroup
::NONE
,
840 'grault-description',
842 'garply-description',
845 $this->changesListSpecialPage
->getStructuredFilterJsData(),
846 /** ordered= */ false,
851 public function provideParseParameters() {
853 [ 'hidebots', [ 'hidebots' => true ] ],
855 [ 'bots', [ 'hidebots' => false ] ],
857 [ 'hideminor', [ 'hideminor' => true ] ],
859 [ 'minor', [ 'hideminor' => false ] ],
861 [ 'hidemajor', [ 'hidemajor' => true ] ],
863 [ 'hideliu', [ 'hideliu' => true ] ],
865 [ 'hidepatrolled', [ 'hidepatrolled' => true ] ],
867 [ 'hideunpatrolled', [ 'hideunpatrolled' => true ] ],
869 [ 'hideanons', [ 'hideanons' => true ] ],
871 [ 'hidemyself', [ 'hidemyself' => true ] ],
873 [ 'hidebyothers', [ 'hidebyothers' => true ] ],
875 [ 'hidehumans', [ 'hidehumans' => true ] ],
877 [ 'hidepageedits', [ 'hidepageedits' => true ] ],
879 [ 'pagedits', [ 'hidepageedits' => false ] ],
881 [ 'hidenewpages', [ 'hidenewpages' => true ] ],
883 [ 'hidecategorization', [ 'hidecategorization' => true ] ],
885 [ 'hidelog', [ 'hidelog' => true ] ],
888 'userExpLevel=learner;experienced',
890 'userExpLevel' => 'learner;experienced'
894 // A few random combos
896 'bots,hideliu,hidemyself',
900 'hidemyself' => true,
905 'minor,hideanons,categorization',
907 'hideminor' => false,
909 'hidecategorization' => false,
914 'hidehumans,bots,hidecategorization',
916 'hidehumans' => true,
918 'hidecategorization' => true,
923 'hidemyself,userExpLevel=newcomer;learner,hideminor',
925 'hidemyself' => true,
927 'userExpLevel' => 'newcomer;learner',
933 public function provideGetFilterConflicts() {
937 "expectedConflicts" => false,
942 "userExpLevel" => "newcomer",
944 "expectedConflicts" => false,
949 "userExpLevel" => "learner",
951 "expectedConflicts" => false,
956 "hidenewpages" => true,
957 "hidepageedits" => true,
958 "hidecategorization" => false,
960 "hideWikidata" => true,
962 "expectedConflicts" => true,
967 "hidenewpages" => false,
968 "hidepageedits" => true,
969 "hidecategorization" => false,
971 "hideWikidata" => true,
973 "expectedConflicts" => true,
978 "hidenewpages" => false,
979 "hidepageedits" => false,
980 "hidecategorization" => true,
982 "hideWikidata" => true,
984 "expectedConflicts" => false,
989 "hidenewpages" => true,
990 "hidepageedits" => true,
991 "hidecategorization" => false,
993 "hideWikidata" => true,
995 "expectedConflicts" => false,
1001 * @dataProvider provideGetFilterConflicts
1003 public function testGetFilterConflicts( $parameters, $expectedConflicts ) {
1004 $context = new RequestContext
;
1005 $context->setRequest( new FauxRequest( $parameters ) );
1006 $this->changesListSpecialPage
->setContext( $context );
1008 $this->assertEquals(
1010 $this->changesListSpecialPage
->areFiltersInConflict()
1014 public function validateOptionsProvider() {
1017 [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 1 ],
1019 [ 'userExpLevel' => 'unregistered', 'hidebots' => 1, ],
1022 [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 0 ],
1024 [ 'hidebots' => 0, 'hidehumans' => 1 ],
1027 [ 'hideanons' => 1 ],
1029 [ 'userExpLevel' => 'registered' ]
1034 [ 'userExpLevel' => 'unregistered' ]
1037 [ 'hideanons' => 1, 'hidebots' => 1 ],
1039 [ 'userExpLevel' => 'registered', 'hidebots' => 1 ]
1042 [ 'hideliu' => 1, 'hidebots' => 0 ],
1044 [ 'userExpLevel' => 'unregistered', 'hidebots' => 0 ]
1047 [ 'hidemyself' => 1, 'hidebyothers' => 1 ],
1052 [ 'hidebots' => 1, 'hidehumans' => 1 ],
1057 [ 'hidepatrolled' => 1, 'hideunpatrolled' => 1 ],
1062 [ 'hideminor' => 1, 'hidemajor' => 1 ],
1068 [ 'hidepageedits' => 1, 'hidenewpages' => 1, 'hidecategorization' => 1, 'hidelog' => 1, ],