diff options
Diffstat (limited to 'www/wiki/tests/phpunit/includes/specialpage')
5 files changed, 1662 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/specialpage/AbstractChangesListSpecialPageTestCase.php b/www/wiki/tests/phpunit/includes/specialpage/AbstractChangesListSpecialPageTestCase.php new file mode 100644 index 00000000..8b8ba0c0 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specialpage/AbstractChangesListSpecialPageTestCase.php @@ -0,0 +1,151 @@ +<?php + +/** + * Abstract base class for shared logic when testing ChangesListSpecialPage + * and subclasses + * + * @group Database + */ +abstract class AbstractChangesListSpecialPageTestCase extends MediaWikiTestCase { + // Must be initialized by subclass + /** + * @var ChangesListSpecialPage + */ + protected $changesListSpecialPage; + + protected $oldPatrollersGroup; + + protected function setUp() { + global $wgGroupPermissions; + + parent::setUp(); + $this->setMwGlobals( [ + 'wgRCWatchCategoryMembership' => true, + 'wgUseRCPatrol' => true, + ] ); + + if ( isset( $wgGroupPermissions['patrollers'] ) ) { + $this->oldPatrollersGroup = $wgGroupPermissions['patrollers']; + } + + $wgGroupPermissions['patrollers'] = [ + 'patrol' => true, + ]; + + // Deprecated + $this->setTemporaryHook( + 'ChangesListSpecialPageFilters', + null + ); + + # setup the ChangesListSpecialPage (or subclass) object + $this->changesListSpecialPage = $this->getPage(); + $context = $this->changesListSpecialPage->getContext(); + $context = new DerivativeContext( $context ); + $context->setUser( $this->getTestUser( [ 'patrollers' ] )->getUser() ); + $this->changesListSpecialPage->setContext( $context ); + $this->changesListSpecialPage->registerFilters(); + } + + abstract protected function getPage(); + + protected function tearDown() { + global $wgGroupPermissions; + + parent::tearDown(); + + if ( $this->oldPatrollersGroup !== null ) { + $wgGroupPermissions['patrollers'] = $this->oldPatrollersGroup; + } + } + + abstract public function provideParseParameters(); + + /** + * @dataProvider provideParseParameters + */ + public function testParseParameters( $params, $expected ) { + $opts = new FormOptions(); + foreach ( $expected as $key => $value ) { + // Register it as null so sets aren't rejected. + $opts->add( + $key, + null, + FormOptions::guessType( $expected ) + ); + } + + $this->changesListSpecialPage->parseParameters( + $params, + $opts + ); + + $this->assertArrayEquals( + $expected, + $opts->getAllValues(), + /** ordered= */ false, + /** named= */ true + ); + } + + /** + * @dataProvider validateOptionsProvider + */ + public function testValidateOptions( $optionsToSet, $expectedRedirect, $expectedRedirectOptions ) { + $redirectQuery = []; + $redirected = false; + $output = $this->getMockBuilder( OutputPage::class ) + ->disableProxyingToOriginalMethods() + ->disableOriginalConstructor() + ->getMock(); + $output->method( 'redirect' )->willReturnCallback( + function ( $url ) use ( &$redirectQuery, &$redirected ) { + $urlParts = wfParseUrl( $url ); + $query = isset( $urlParts[ 'query' ] ) ? $urlParts[ 'query' ] : ''; + parse_str( $query, $redirectQuery ); + $redirected = true; + } + ); + $ctx = new RequestContext(); + + // Give users patrol permissions so we can test that. + $user = $this->getTestSysop()->getUser(); + $ctx->setUser( $user ); + + // Disable this hook or it could break changeType + // depending on which other extensions are running. + $this->setTemporaryHook( + 'ChangesListSpecialPageStructuredFilters', + null + ); + + $ctx->setOutput( $output ); + $clsp = $this->changesListSpecialPage; + $clsp->setContext( $ctx ); + $opts = $clsp->getDefaultOptions(); + + foreach ( $optionsToSet as $option => $value ) { + $opts->setValue( $option, $value ); + } + + $clsp->validateOptions( $opts ); + + $this->assertEquals( $expectedRedirect, $redirected, 'redirection' ); + + if ( $expectedRedirect ) { + if ( count( $expectedRedirectOptions ) > 0 ) { + $expectedRedirectOptions += [ + 'title' => $clsp->getPageTitle()->getPrefixedText(), + ]; + } + + $this->assertArrayEquals( + $expectedRedirectOptions, + $redirectQuery, + /* $ordered= */ false, + /* $named= */ true, + 'redirection query' + ); + } + } +} diff --git a/www/wiki/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php b/www/wiki/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php new file mode 100644 index 00000000..aeaa1aee --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php @@ -0,0 +1,1098 @@ +<?php + +use Wikimedia\TestingAccessWrapper; + +/** + * Test class for ChangesListSpecialPage class + * + * Copyright © 2011-, Antoine Musso, Stephane Bisson, Matthew Flaschen + * + * @author Antoine Musso + * @author Stephane Bisson + * @author Matthew Flaschen + * @group Database + * + * @covers ChangesListSpecialPage + */ +class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase { + public function setUp() { + parent::setUp(); + $this->setMwGlobals( [ + 'wgStructuredChangeFiltersShowPreference' => true, + ] ); + } + + protected function getPage() { + $mock = $this->getMockBuilder( ChangesListSpecialPage::class ) + ->setConstructorArgs( + [ + 'ChangesListSpecialPage', + '' + ] + ) + ->setMethods( [ 'getPageTitle' ] ) + ->getMockForAbstractClass(); + + $mock->method( 'getPageTitle' )->willReturn( + Title::makeTitle( NS_SPECIAL, 'ChangesListSpecialPage' ) + ); + + $mock = TestingAccessWrapper::newFromObject( + $mock + ); + + return $mock; + } + + private function buildQuery( + $requestOptions = null, + $user = null + ) { + $context = new RequestContext; + $context->setRequest( new FauxRequest( $requestOptions ) ); + if ( $user ) { + $context->setUser( $user ); + } + + $this->changesListSpecialPage->setContext( $context ); + $this->changesListSpecialPage->filterGroups = []; + $formOptions = $this->changesListSpecialPage->setup( null ); + + # Filter out rc_timestamp conditions which depends on the test runtime + # This condition is not needed as of march 2, 2011 -- hashar + # @todo FIXME: Find a way to generate the correct rc_timestamp + + $tables = []; + $fields = []; + $queryConditions = []; + $query_options = []; + $join_conds = []; + + call_user_func_array( + [ $this->changesListSpecialPage, 'buildQuery' ], + [ + &$tables, + &$fields, + &$queryConditions, + &$query_options, + &$join_conds, + $formOptions + ] + ); + + $queryConditions = array_filter( + $queryConditions, + 'ChangesListSpecialPageTest::filterOutRcTimestampCondition' + ); + + return $queryConditions; + } + + /** helper to test SpecialRecentchanges::buildQuery() */ + private function assertConditions( + $expected, + $requestOptions = null, + $message = '', + $user = null + ) { + $queryConditions = $this->buildQuery( $requestOptions, $user ); + + $this->assertEquals( + self::normalizeCondition( $expected ), + self::normalizeCondition( $queryConditions ), + $message + ); + } + + private static function normalizeCondition( $conds ) { + $dbr = wfGetDB( DB_REPLICA ); + $normalized = array_map( + function ( $k, $v ) use ( $dbr ) { + if ( is_array( $v ) ) { + sort( $v ); + } + // (Ab)use makeList() to format only this entry + return $dbr->makeList( [ $k => $v ], Database::LIST_AND ); + }, + array_keys( $conds ), + $conds + ); + sort( $normalized ); + return $normalized; + } + + /** return false if condition begins with 'rc_timestamp ' */ + private static function filterOutRcTimestampCondition( $var ) { + return ( is_array( $var ) || false === strpos( $var, 'rc_timestamp ' ) ); + } + + public function testRcNsFilter() { + $this->assertConditions( + [ # expected + "rc_namespace = '0'", + ], + [ + 'namespace' => NS_MAIN, + ], + "rc conditions with one namespace" + ); + } + + public function testRcNsFilterInversion() { + $this->assertConditions( + [ # expected + "rc_namespace != '0'", + ], + [ + 'namespace' => NS_MAIN, + 'invert' => 1, + ], + "rc conditions with namespace inverted" + ); + } + + public function testRcNsFilterMultiple() { + $this->assertConditions( + [ # expected + "rc_namespace IN ('1','2','3')", + ], + [ + 'namespace' => '1;2;3', + ], + "rc conditions with multiple namespaces" + ); + } + + public function testRcNsFilterMultipleAssociated() { + $this->assertConditions( + [ # expected + "rc_namespace IN ('0','1','4','5','6','7')", + ], + [ + 'namespace' => '1;4;7', + 'associated' => 1, + ], + "rc conditions with multiple namespaces and associated" + ); + } + + public function testRcNsFilterMultipleAssociatedInvert() { + $this->assertConditions( + [ # expected + "rc_namespace NOT IN ('2','3','8','9')", + ], + [ + 'namespace' => '2;3;9', + 'associated' => 1, + 'invert' => 1 + ], + "rc conditions with multiple namespaces, associated and inverted" + ); + } + + public function testRcNsFilterMultipleInvert() { + $this->assertConditions( + [ # expected + "rc_namespace NOT IN ('1','2','3')", + ], + [ + 'namespace' => '1;2;3', + 'invert' => 1, + ], + "rc conditions with multiple namespaces inverted" + ); + } + + public function testRcHidemyselfFilter() { + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $this->overrideMwServices(); + + $user = $this->getTestUser()->getUser(); + $user->getActorId( wfGetDB( DB_MASTER ) ); + $this->assertConditions( + [ # expected + "NOT((rc_actor = '{$user->getActorId()}') OR " + . "(rc_actor = '0' AND rc_user = '{$user->getId()}'))", + ], + [ + 'hidemyself' => 1, + ], + "rc conditions: hidemyself=1 (logged in)", + $user + ); + + $user = User::newFromName( '10.11.12.13', false ); + $id = $user->getActorId( wfGetDB( DB_MASTER ) ); + $this->assertConditions( + [ # expected + "NOT((rc_actor = '$id') OR (rc_actor = '0' AND rc_user_text = '10.11.12.13'))", + ], + [ + 'hidemyself' => 1, + ], + "rc conditions: hidemyself=1 (anon)", + $user + ); + } + + public function testRcHidebyothersFilter() { + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $this->overrideMwServices(); + + $user = $this->getTestUser()->getUser(); + $user->getActorId( wfGetDB( DB_MASTER ) ); + $this->assertConditions( + [ # expected + "(rc_actor = '{$user->getActorId()}') OR " + . "(rc_actor = '0' AND rc_user_text = '{$user->getName()}')", + ], + [ + 'hidebyothers' => 1, + ], + "rc conditions: hidebyothers=1 (logged in)", + $user + ); + + $user = User::newFromName( '10.11.12.13', false ); + $id = $user->getActorId( wfGetDB( DB_MASTER ) ); + $this->assertConditions( + [ # expected + "(rc_actor = '$id') OR (rc_actor = '0' AND rc_user_text = '10.11.12.13')", + ], + [ + 'hidebyothers' => 1, + ], + "rc conditions: hidebyothers=1 (anon)", + $user + ); + } + + public function testRcHidepageedits() { + $this->assertConditions( + [ # expected + "rc_type != '0'", + ], + [ + 'hidepageedits' => 1, + ], + "rc conditions: hidepageedits=1" + ); + } + + public function testRcHidenewpages() { + $this->assertConditions( + [ # expected + "rc_type != '1'", + ], + [ + 'hidenewpages' => 1, + ], + "rc conditions: hidenewpages=1" + ); + } + + public function testRcHidelog() { + $this->assertConditions( + [ # expected + "rc_type != '3'", + ], + [ + 'hidelog' => 1, + ], + "rc conditions: hidelog=1" + ); + } + + public function testRcHidehumans() { + $this->assertConditions( + [ # expected + 'rc_bot' => 1, + ], + [ + 'hidebots' => 0, + 'hidehumans' => 1, + ], + "rc conditions: hidebots=0 hidehumans=1" + ); + } + + public function testRcHidepatrolledDisabledFilter() { + $this->setMwGlobals( 'wgUseRCPatrol', false ); + $user = $this->getTestUser()->getUser(); + $this->assertConditions( + [ # expected + ], + [ + 'hidepatrolled' => 1, + ], + "rc conditions: hidepatrolled=1 (user not allowed)", + $user + ); + } + + public function testRcHideunpatrolledDisabledFilter() { + $this->setMwGlobals( 'wgUseRCPatrol', false ); + $user = $this->getTestUser()->getUser(); + $this->assertConditions( + [ # expected + ], + [ + 'hideunpatrolled' => 1, + ], + "rc conditions: hideunpatrolled=1 (user not allowed)", + $user + ); + } + public function testRcHidepatrolledFilter() { + $user = $this->getTestSysop()->getUser(); + $this->assertConditions( + [ # expected + 'rc_patrolled' => 0, + ], + [ + 'hidepatrolled' => 1, + ], + "rc conditions: hidepatrolled=1", + $user + ); + } + + public function testRcHideunpatrolledFilter() { + $user = $this->getTestSysop()->getUser(); + $this->assertConditions( + [ # expected + 'rc_patrolled' => [ 1, 2 ], + ], + [ + 'hideunpatrolled' => 1, + ], + "rc conditions: hideunpatrolled=1", + $user + ); + } + + public function testRcReviewStatusFilter() { + $user = $this->getTestSysop()->getUser(); + $this->assertConditions( + [ #expected + 'rc_patrolled' => 1, + ], + [ + 'reviewStatus' => 'manual' + ], + "rc conditions: reviewStatus=manual", + $user + ); + $this->assertConditions( + [ #expected + 'rc_patrolled' => [ 0, 2 ], + ], + [ + 'reviewStatus' => 'unpatrolled;auto' + ], + "rc conditions: reviewStatus=unpatrolled;auto", + $user + ); + } + + public function testRcHideminorFilter() { + $this->assertConditions( + [ # expected + "rc_minor = 0", + ], + [ + 'hideminor' => 1, + ], + "rc conditions: hideminor=1" + ); + } + + public function testRcHidemajorFilter() { + $this->assertConditions( + [ # expected + "rc_minor = 1", + ], + [ + 'hidemajor' => 1, + ], + "rc conditions: hidemajor=1" + ); + } + + public function testHideCategorization() { + $this->assertConditions( + [ + # expected + "rc_type != '6'" + ], + [ + 'hidecategorization' => 1 + ], + "rc conditions: hidecategorization=1" + ); + } + + public function testFilterUserExpLevelAll() { + $this->assertConditions( + [ + # expected + ], + [ + 'userExpLevel' => 'registered;unregistered;newcomer;learner;experienced', + ], + "rc conditions: userExpLevel=registered;unregistered;newcomer;learner;experienced" + ); + } + + public function testFilterUserExpLevelRegisteredUnregistered() { + $this->assertConditions( + [ + # expected + ], + [ + 'userExpLevel' => 'registered;unregistered', + ], + "rc conditions: userExpLevel=registered;unregistered" + ); + } + + public function testFilterUserExpLevelRegisteredUnregisteredLearner() { + $this->assertConditions( + [ + # expected + ], + [ + 'userExpLevel' => 'registered;unregistered;learner', + ], + "rc conditions: userExpLevel=registered;unregistered;learner" + ); + } + + public function testFilterUserExpLevelAllExperienceLevels() { + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $this->overrideMwServices(); + + $this->assertConditions( + [ + # expected + 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0', + ], + [ + 'userExpLevel' => 'newcomer;learner;experienced', + ], + "rc conditions: userExpLevel=newcomer;learner;experienced" + ); + } + + public function testFilterUserExpLevelRegistrered() { + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $this->overrideMwServices(); + + $this->assertConditions( + [ + # expected + 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0', + ], + [ + 'userExpLevel' => 'registered', + ], + "rc conditions: userExpLevel=registered" + ); + } + + public function testFilterUserExpLevelUnregistrered() { + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $this->overrideMwServices(); + + $this->assertConditions( + [ + # expected + 'COALESCE( actor_rc_user.actor_user, rc_user ) = 0', + ], + [ + 'userExpLevel' => 'unregistered', + ], + "rc conditions: userExpLevel=unregistered" + ); + } + + public function testFilterUserExpLevelRegistreredOrLearner() { + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $this->overrideMwServices(); + + $this->assertConditions( + [ + # expected + 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0', + ], + [ + 'userExpLevel' => 'registered;learner', + ], + "rc conditions: userExpLevel=registered;learner" + ); + } + + public function testFilterUserExpLevelUnregistreredOrExperienced() { + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $this->overrideMwServices(); + + $conds = $this->buildQuery( [ 'userExpLevel' => 'unregistered;experienced' ] ); + + $this->assertRegExp( + '/\(COALESCE\( actor_rc_user.actor_user, rc_user \) = 0\) OR ' + . '\(\(user_editcount >= 500\) AND \(user_registration <= \'[^\']+\'\)\)/', + reset( $conds ), + "rc conditions: userExpLevel=unregistered;experienced" + ); + } + + public function testFilterUserExpLevel() { + $now = time(); + $this->setMwGlobals( [ + 'wgLearnerEdits' => 10, + 'wgLearnerMemberSince' => 4, + 'wgExperiencedUserEdits' => 500, + 'wgExperiencedUserMemberSince' => 30, + ] ); + + $this->createUsers( [ + 'Newcomer1' => [ 'edits' => 2, 'days' => 2 ], + 'Newcomer2' => [ 'edits' => 12, 'days' => 3 ], + 'Newcomer3' => [ 'edits' => 8, 'days' => 5 ], + 'Learner1' => [ 'edits' => 15, 'days' => 10 ], + 'Learner2' => [ 'edits' => 450, 'days' => 20 ], + 'Learner3' => [ 'edits' => 460, 'days' => 33 ], + 'Learner4' => [ 'edits' => 525, 'days' => 28 ], + 'Experienced1' => [ 'edits' => 538, 'days' => 33 ], + ], $now ); + + // newcomers only + $this->assertArrayEquals( + [ 'Newcomer1', 'Newcomer2', 'Newcomer3' ], + $this->fetchUsers( [ 'newcomer' ], $now ) + ); + + // newcomers and learner + $this->assertArrayEquals( + [ + 'Newcomer1', 'Newcomer2', 'Newcomer3', + 'Learner1', 'Learner2', 'Learner3', 'Learner4', + ], + $this->fetchUsers( [ 'newcomer', 'learner' ], $now ) + ); + + // newcomers and more learner + $this->assertArrayEquals( + [ + 'Newcomer1', 'Newcomer2', 'Newcomer3', + 'Experienced1', + ], + $this->fetchUsers( [ 'newcomer', 'experienced' ], $now ) + ); + + // learner only + $this->assertArrayEquals( + [ 'Learner1', 'Learner2', 'Learner3', 'Learner4' ], + $this->fetchUsers( [ 'learner' ], $now ) + ); + + // more experienced only + $this->assertArrayEquals( + [ 'Experienced1' ], + $this->fetchUsers( [ 'experienced' ], $now ) + ); + + // learner and more experienced + $this->assertArrayEquals( + [ + 'Learner1', 'Learner2', 'Learner3', 'Learner4', + 'Experienced1', + ], + $this->fetchUsers( [ 'learner', 'experienced' ], $now ), + 'Learner and more experienced' + ); + } + + private function createUsers( $specs, $now ) { + $dbw = wfGetDB( DB_MASTER ); + foreach ( $specs as $name => $spec ) { + User::createNew( + $name, + [ + 'editcount' => $spec['edits'], + 'registration' => $dbw->timestamp( $this->daysAgo( $spec['days'], $now ) ), + 'email' => 'ut', + ] + ); + } + } + + private function fetchUsers( $filters, $now ) { + $tables = []; + $conds = []; + $fields = []; + $query_options = []; + $join_conds = []; + + sort( $filters ); + + call_user_func_array( + [ $this->changesListSpecialPage, 'filterOnUserExperienceLevel' ], + [ + get_class( $this->changesListSpecialPage ), + $this->changesListSpecialPage->getContext(), + $this->changesListSpecialPage->getDB(), + &$tables, + &$fields, + &$conds, + &$query_options, + &$join_conds, + $filters, + $now + ] + ); + + // @todo: This is not at all safe or sane. It just blindly assumes + // nothing in $conds depends on any other tables. + $result = wfGetDB( DB_MASTER )->select( + 'user', + 'user_name', + array_filter( $conds ) + [ 'user_email' => 'ut' ] + ); + + $usernames = []; + foreach ( $result as $row ) { + $usernames[] = $row->user_name; + } + + return $usernames; + } + + private function daysAgo( $days, $now ) { + $secondsPerDay = 86400; + return $now - $days * $secondsPerDay; + } + + public function testGetFilterGroupDefinitionFromLegacyCustomFilters() { + $customFilters = [ + 'hidefoo' => [ + 'msg' => 'showhidefoo', + 'default' => true, + ], + + 'hidebar' => [ + 'msg' => 'showhidebar', + 'default' => false, + ], + ]; + + $this->assertEquals( + [ + 'name' => 'unstructured', + 'class' => ChangesListBooleanFilterGroup::class, + 'priority' => -1, + 'filters' => [ + [ + 'name' => 'hidefoo', + 'showHide' => 'showhidefoo', + 'default' => true, + ], + [ + 'name' => 'hidebar', + 'showHide' => 'showhidebar', + 'default' => false, + ] + ], + ], + $this->changesListSpecialPage->getFilterGroupDefinitionFromLegacyCustomFilters( + $customFilters + ) + ); + } + + public function testGetStructuredFilterJsData() { + $this->changesListSpecialPage->filterGroups = []; + + $definition = [ + [ + 'name' => 'gub-group', + 'title' => 'gub-group-title', + 'class' => ChangesListBooleanFilterGroup::class, + 'filters' => [ + [ + 'name' => 'hidefoo', + 'label' => 'foo-label', + 'description' => 'foo-description', + 'default' => true, + 'showHide' => 'showhidefoo', + 'priority' => 2, + ], + [ + 'name' => 'hidebar', + 'label' => 'bar-label', + 'description' => 'bar-description', + 'default' => false, + 'priority' => 4, + ] + ], + ], + + [ + 'name' => 'des-group', + 'title' => 'des-group-title', + 'class' => ChangesListStringOptionsFilterGroup::class, + 'isFullCoverage' => true, + 'filters' => [ + [ + 'name' => 'grault', + 'label' => 'grault-label', + 'description' => 'grault-description', + ], + [ + 'name' => 'garply', + 'label' => 'garply-label', + 'description' => 'garply-description', + ], + ], + 'queryCallable' => function () { + }, + 'default' => ChangesListStringOptionsFilterGroup::NONE, + ], + + [ + 'name' => 'unstructured', + 'class' => ChangesListBooleanFilterGroup::class, + 'filters' => [ + [ + 'name' => 'hidethud', + 'showHide' => 'showhidethud', + 'default' => true, + ], + + [ + 'name' => 'hidemos', + 'showHide' => 'showhidemos', + 'default' => false, + ], + ], + ], + + ]; + + $this->changesListSpecialPage->registerFiltersFromDefinitions( $definition ); + + $this->assertArrayEquals( + [ + // Filters that only display in the unstructured UI are + // are not included, and neither are groups that would + // be empty due to the above. + 'groups' => [ + [ + 'name' => 'gub-group', + 'title' => 'gub-group-title', + 'type' => ChangesListBooleanFilterGroup::TYPE, + 'priority' => -1, + 'filters' => [ + [ + 'name' => 'hidebar', + 'label' => 'bar-label', + 'description' => 'bar-description', + 'default' => false, + 'priority' => 4, + 'cssClass' => null, + 'conflicts' => [], + 'subset' => [], + 'defaultHighlightColor' => null + ], + [ + 'name' => 'hidefoo', + 'label' => 'foo-label', + 'description' => 'foo-description', + 'default' => true, + 'priority' => 2, + 'cssClass' => null, + 'conflicts' => [], + 'subset' => [], + 'defaultHighlightColor' => null + ], + ], + 'fullCoverage' => true, + 'conflicts' => [], + ], + + [ + 'name' => 'des-group', + 'title' => 'des-group-title', + 'type' => ChangesListStringOptionsFilterGroup::TYPE, + 'priority' => -2, + 'fullCoverage' => true, + 'filters' => [ + [ + 'name' => 'grault', + 'label' => 'grault-label', + 'description' => 'grault-description', + 'cssClass' => null, + 'priority' => -2, + 'conflicts' => [], + 'subset' => [], + 'defaultHighlightColor' => null + ], + [ + 'name' => 'garply', + 'label' => 'garply-label', + 'description' => 'garply-description', + 'cssClass' => null, + 'priority' => -3, + 'conflicts' => [], + 'subset' => [], + 'defaultHighlightColor' => null + ], + ], + 'conflicts' => [], + 'separator' => ';', + 'default' => ChangesListStringOptionsFilterGroup::NONE, + ], + ], + 'messageKeys' => [ + 'gub-group-title', + 'bar-label', + 'bar-description', + 'foo-label', + 'foo-description', + 'des-group-title', + 'grault-label', + 'grault-description', + 'garply-label', + 'garply-description', + ], + ], + $this->changesListSpecialPage->getStructuredFilterJsData(), + /** ordered= */ false, + /** named= */ true + ); + } + + public function provideParseParameters() { + return [ + [ 'hidebots', [ 'hidebots' => true ] ], + + [ 'bots', [ 'hidebots' => false ] ], + + [ 'hideminor', [ 'hideminor' => true ] ], + + [ 'minor', [ 'hideminor' => false ] ], + + [ 'hidemajor', [ 'hidemajor' => true ] ], + + [ 'hideliu', [ 'hideliu' => true ] ], + + [ 'hidepatrolled', [ 'hidepatrolled' => true ] ], + + [ 'hideunpatrolled', [ 'hideunpatrolled' => true ] ], + + [ 'hideanons', [ 'hideanons' => true ] ], + + [ 'hidemyself', [ 'hidemyself' => true ] ], + + [ 'hidebyothers', [ 'hidebyothers' => true ] ], + + [ 'hidehumans', [ 'hidehumans' => true ] ], + + [ 'hidepageedits', [ 'hidepageedits' => true ] ], + + [ 'pagedits', [ 'hidepageedits' => false ] ], + + [ 'hidenewpages', [ 'hidenewpages' => true ] ], + + [ 'hidecategorization', [ 'hidecategorization' => true ] ], + + [ 'hidelog', [ 'hidelog' => true ] ], + + [ + 'userExpLevel=learner;experienced', + [ + 'userExpLevel' => 'learner;experienced' + ], + ], + + // A few random combos + [ + 'bots,hideliu,hidemyself', + [ + 'hidebots' => false, + 'hideliu' => true, + 'hidemyself' => true, + ], + ], + + [ + 'minor,hideanons,categorization', + [ + 'hideminor' => false, + 'hideanons' => true, + 'hidecategorization' => false, + ] + ], + + [ + 'hidehumans,bots,hidecategorization', + [ + 'hidehumans' => true, + 'hidebots' => false, + 'hidecategorization' => true, + ], + ], + + [ + 'hidemyself,userExpLevel=newcomer;learner,hideminor', + [ + 'hidemyself' => true, + 'hideminor' => true, + 'userExpLevel' => 'newcomer;learner', + ], + ], + ]; + } + + public function provideGetFilterConflicts() { + return [ + [ + "parameters" => [], + "expectedConflicts" => false, + ], + [ + "parameters" => [ + "hideliu" => true, + "userExpLevel" => "newcomer", + ], + "expectedConflicts" => false, + ], + [ + "parameters" => [ + "hideanons" => true, + "userExpLevel" => "learner", + ], + "expectedConflicts" => false, + ], + [ + "parameters" => [ + "hidemajor" => true, + "hidenewpages" => true, + "hidepageedits" => true, + "hidecategorization" => false, + "hidelog" => true, + "hideWikidata" => true, + ], + "expectedConflicts" => true, + ], + [ + "parameters" => [ + "hidemajor" => true, + "hidenewpages" => false, + "hidepageedits" => true, + "hidecategorization" => false, + "hidelog" => false, + "hideWikidata" => true, + ], + "expectedConflicts" => true, + ], + [ + "parameters" => [ + "hidemajor" => true, + "hidenewpages" => false, + "hidepageedits" => false, + "hidecategorization" => true, + "hidelog" => true, + "hideWikidata" => true, + ], + "expectedConflicts" => false, + ], + [ + "parameters" => [ + "hideminor" => true, + "hidenewpages" => true, + "hidepageedits" => true, + "hidecategorization" => false, + "hidelog" => true, + "hideWikidata" => true, + ], + "expectedConflicts" => false, + ], + ]; + } + + /** + * @dataProvider provideGetFilterConflicts + */ + public function testGetFilterConflicts( $parameters, $expectedConflicts ) { + $context = new RequestContext; + $context->setRequest( new FauxRequest( $parameters ) ); + $this->changesListSpecialPage->setContext( $context ); + + $this->assertEquals( + $expectedConflicts, + $this->changesListSpecialPage->areFiltersInConflict() + ); + } + + public function validateOptionsProvider() { + return [ + [ + [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 1 ], + true, + [ 'userExpLevel' => 'unregistered', 'hidebots' => 1, ], + ], + [ + [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 0 ], + true, + [ 'hidebots' => 0, 'hidehumans' => 1 ], + ], + [ + [ 'hideanons' => 1 ], + true, + [ 'userExpLevel' => 'registered' ] + ], + [ + [ 'hideliu' => 1 ], + true, + [ 'userExpLevel' => 'unregistered' ] + ], + [ + [ 'hideanons' => 1, 'hidebots' => 1 ], + true, + [ 'userExpLevel' => 'registered', 'hidebots' => 1 ] + ], + [ + [ 'hideliu' => 1, 'hidebots' => 0 ], + true, + [ 'userExpLevel' => 'unregistered', 'hidebots' => 0 ] + ], + [ + [ 'hidemyself' => 1, 'hidebyothers' => 1 ], + true, + [], + ], + [ + [ 'hidebots' => 1, 'hidehumans' => 1 ], + true, + [], + ], + [ + [ 'hidepatrolled' => 1, 'hideunpatrolled' => 1 ], + true, + [], + ], + [ + [ 'hideminor' => 1, 'hidemajor' => 1 ], + true, + [], + ], + [ + // changeType + [ 'hidepageedits' => 1, 'hidenewpages' => 1, 'hidecategorization' => 1, 'hidelog' => 1, ], + true, + [], + ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/specialpage/SpecialPageFactoryTest.php b/www/wiki/tests/phpunit/includes/specialpage/SpecialPageFactoryTest.php new file mode 100644 index 00000000..9ac546dc --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specialpage/SpecialPageFactoryTest.php @@ -0,0 +1,285 @@ +<?php +use Wikimedia\ScopedCallback; + +/** + * Factory for handling the special page list and generating SpecialPage objects. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @covers SpecialPageFactory + * @group SpecialPage + */ +class SpecialPageFactoryTest extends MediaWikiTestCase { + + protected function tearDown() { + parent::tearDown(); + + SpecialPageFactory::resetList(); + } + + public function testResetList() { + SpecialPageFactory::resetList(); + $this->assertContains( 'Specialpages', SpecialPageFactory::getNames() ); + } + + public function testHookNotCalledTwice() { + $count = 0; + $this->mergeMwGlobalArrayValue( 'wgHooks', [ + 'SpecialPage_initList' => [ + function () use ( &$count ) { + $count++; + } + ] ] ); + SpecialPageFactory::resetList(); + SpecialPageFactory::getNames(); + SpecialPageFactory::getNames(); + $this->assertEquals( 1, $count ); + } + + public function newSpecialAllPages() { + return new SpecialAllPages(); + } + + public function specialPageProvider() { + $specialPageTestHelper = new SpecialPageTestHelper(); + + return [ + 'class name' => [ 'SpecialAllPages', false ], + 'closure' => [ function () { + return new SpecialAllPages(); + }, false ], + 'function' => [ [ $this, 'newSpecialAllPages' ], false ], + 'callback string' => [ 'SpecialPageTestHelper::newSpecialAllPages', false ], + 'callback with object' => [ + [ $specialPageTestHelper, 'newSpecialAllPages' ], + false + ], + 'callback array' => [ + [ 'SpecialPageTestHelper', 'newSpecialAllPages' ], + false + ] + ]; + } + + /** + * @covers SpecialPageFactory::getPage + * @dataProvider specialPageProvider + */ + public function testGetPage( $spec, $shouldReuseInstance ) { + $this->mergeMwGlobalArrayValue( 'wgSpecialPages', [ 'testdummy' => $spec ] ); + SpecialPageFactory::resetList(); + + $page = SpecialPageFactory::getPage( 'testdummy' ); + $this->assertInstanceOf( SpecialPage::class, $page ); + + $page2 = SpecialPageFactory::getPage( 'testdummy' ); + $this->assertEquals( $shouldReuseInstance, $page2 === $page, "Should re-use instance:" ); + } + + /** + * @covers SpecialPageFactory::getNames + */ + public function testGetNames() { + $this->mergeMwGlobalArrayValue( 'wgSpecialPages', [ 'testdummy' => SpecialAllPages::class ] ); + SpecialPageFactory::resetList(); + + $names = SpecialPageFactory::getNames(); + $this->assertInternalType( 'array', $names ); + $this->assertContains( 'testdummy', $names ); + } + + /** + * @covers SpecialPageFactory::resolveAlias + */ + public function testResolveAlias() { + $this->setMwGlobals( 'wgContLang', Language::factory( 'de' ) ); + SpecialPageFactory::resetList(); + + list( $name, $param ) = SpecialPageFactory::resolveAlias( 'Spezialseiten/Foo' ); + $this->assertEquals( 'Specialpages', $name ); + $this->assertEquals( 'Foo', $param ); + } + + /** + * @covers SpecialPageFactory::getLocalNameFor + */ + public function testGetLocalNameFor() { + $this->setMwGlobals( 'wgContLang', Language::factory( 'de' ) ); + SpecialPageFactory::resetList(); + + $name = SpecialPageFactory::getLocalNameFor( 'Specialpages', 'Foo' ); + $this->assertEquals( 'Spezialseiten/Foo', $name ); + } + + /** + * @covers SpecialPageFactory::getTitleForAlias + */ + public function testGetTitleForAlias() { + $this->setMwGlobals( 'wgContLang', Language::factory( 'de' ) ); + SpecialPageFactory::resetList(); + + $title = SpecialPageFactory::getTitleForAlias( 'Specialpages/Foo' ); + $this->assertEquals( 'Spezialseiten/Foo', $title->getText() ); + $this->assertEquals( NS_SPECIAL, $title->getNamespace() ); + } + + /** + * @dataProvider provideTestConflictResolution + */ + public function testConflictResolution( + $test, $aliasesList, $alias, $expectedName, $expectedAlias, $expectWarnings + ) { + global $wgContLang; + $lang = clone $wgContLang; + $lang->mExtendedSpecialPageAliases = $aliasesList; + $this->setMwGlobals( 'wgContLang', $lang ); + $this->setMwGlobals( 'wgSpecialPages', + array_combine( array_keys( $aliasesList ), array_keys( $aliasesList ) ) + ); + SpecialPageFactory::resetList(); + + // Catch the warnings we expect to be raised + $warnings = []; + $this->setMwGlobals( 'wgDevelopmentWarnings', true ); + set_error_handler( function ( $errno, $errstr ) use ( &$warnings ) { + if ( preg_match( '/First alias \'[^\']*\' for .*/', $errstr ) || + preg_match( '/Did not find a usable alias for special page .*/', $errstr ) + ) { + $warnings[] = $errstr; + return true; + } + return false; + } ); + $reset = new ScopedCallback( 'restore_error_handler' ); + + list( $name, /*...*/ ) = SpecialPageFactory::resolveAlias( $alias ); + $this->assertEquals( $expectedName, $name, "$test: Alias to name" ); + $result = SpecialPageFactory::getLocalNameFor( $name ); + $this->assertEquals( $expectedAlias, $result, "$test: Alias to name to alias" ); + + $gotWarnings = count( $warnings ); + if ( $gotWarnings !== $expectWarnings ) { + $this->fail( "Expected $expectWarnings warning(s), but got $gotWarnings:\n" . + implode( "\n", $warnings ) + ); + } + } + + /** + * @dataProvider provideTestConflictResolution + */ + public function testConflictResolutionReversed( + $test, $aliasesList, $alias, $expectedName, $expectedAlias, $expectWarnings + ) { + // Make sure order doesn't matter by reversing the list + $aliasesList = array_reverse( $aliasesList ); + return $this->testConflictResolution( + $test, $aliasesList, $alias, $expectedName, $expectedAlias, $expectWarnings + ); + } + + public function provideTestConflictResolution() { + return [ + [ + 'Canonical name wins', + [ 'Foo' => [ 'Foo', 'Bar' ], 'Baz' => [ 'Foo', 'BazPage', 'Baz2' ] ], + 'Foo', + 'Foo', + 'Foo', + 1, + ], + + [ + 'Doesn\'t redirect to a different special page\'s canonical name', + [ 'Foo' => [ 'Foo', 'Bar' ], 'Baz' => [ 'Foo', 'BazPage', 'Baz2' ] ], + 'Baz', + 'Baz', + 'BazPage', + 1, + ], + + [ + 'Canonical name wins even if not aliased', + [ 'Foo' => [ 'FooPage' ], 'Baz' => [ 'Foo', 'BazPage', 'Baz2' ] ], + 'Foo', + 'Foo', + 'FooPage', + 1, + ], + + [ + 'Doesn\'t redirect to a different special page\'s canonical name even if not aliased', + [ 'Foo' => [ 'FooPage' ], 'Baz' => [ 'Foo', 'BazPage', 'Baz2' ] ], + 'Baz', + 'Baz', + 'BazPage', + 1, + ], + + [ + 'First local name beats non-first', + [ 'First' => [ 'Foo' ], 'NonFirst' => [ 'Bar', 'Foo' ] ], + 'Foo', + 'First', + 'Foo', + 0, + ], + + [ + 'Doesn\'t redirect to a different special page\'s first alias', + [ + 'Foo' => [ 'Foo' ], + 'First' => [ 'Bar' ], + 'Baz' => [ 'Foo', 'Bar', 'BazPage', 'Baz2' ] + ], + 'Baz', + 'Baz', + 'BazPage', + 1, + ], + + [ + 'Doesn\'t redirect wrong even if all aliases conflict', + [ + 'Foo' => [ 'Foo' ], + 'First' => [ 'Bar' ], + 'Baz' => [ 'Foo', 'Bar' ] + ], + 'Baz', + 'Baz', + 'Baz', + 2, + ], + + ]; + } + + public function testGetAliasListRecursion() { + $called = false; + $this->mergeMwGlobalArrayValue( 'wgHooks', [ + 'SpecialPage_initList' => [ + function () use ( &$called ) { + SpecialPageFactory::getLocalNameFor( 'Specialpages' ); + $called = true; + } + ], + ] ); + SpecialPageFactory::resetList(); + SpecialPageFactory::getLocalNameFor( 'Specialpages' ); + $this->assertTrue( $called, 'Recursive call succeeded' ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/specialpage/SpecialPageTest.php b/www/wiki/tests/phpunit/includes/specialpage/SpecialPageTest.php new file mode 100644 index 00000000..2ad39729 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specialpage/SpecialPageTest.php @@ -0,0 +1,104 @@ +<?php + +/** + * @covers SpecialPage + * + * @group Database + * + * @author Katie Filbert < aude.wiki@gmail.com > + */ +class SpecialPageTest extends MediaWikiTestCase { + + protected function setUp() { + parent::setUp(); + + $this->setMwGlobals( [ + 'wgScript' => '/index.php', + 'wgContLang' => Language::factory( 'en' ) + ] ); + } + + /** + * @dataProvider getTitleForProvider + */ + public function testGetTitleFor( $expectedName, $name ) { + $title = SpecialPage::getTitleFor( $name ); + $expected = Title::makeTitle( NS_SPECIAL, $expectedName ); + $this->assertEquals( $expected, $title ); + } + + public function getTitleForProvider() { + return [ + [ 'UserLogin', 'Userlogin' ] + ]; + } + + /** + * @expectedException PHPUnit_Framework_Error_Notice + */ + public function testInvalidGetTitleFor() { + $title = SpecialPage::getTitleFor( 'cat' ); + $expected = Title::makeTitle( NS_SPECIAL, 'Cat' ); + $this->assertEquals( $expected, $title ); + } + + /** + * @expectedException PHPUnit_Framework_Error_Notice + * @dataProvider getTitleForWithWarningProvider + */ + public function testGetTitleForWithWarning( $expected, $name ) { + $title = SpecialPage::getTitleFor( $name ); + $this->assertEquals( $expected, $title ); + } + + public function getTitleForWithWarningProvider() { + return [ + [ Title::makeTitle( NS_SPECIAL, 'UserLogin' ), 'UserLogin' ] + ]; + } + + /** + * @dataProvider requireLoginAnonProvider + */ + public function testRequireLoginAnon( $expected, $reason, $title ) { + $specialPage = new SpecialPage( 'Watchlist', 'viewmywatchlist' ); + + $user = User::newFromId( 0 ); + $specialPage->getContext()->setUser( $user ); + $specialPage->getContext()->setLanguage( Language::factory( 'en' ) ); + + $this->setExpectedException( UserNotLoggedIn::class, $expected ); + + // $specialPage->requireLogin( [ $reason [, $title ] ] ) + call_user_func_array( + [ $specialPage, 'requireLogin' ], + array_filter( [ $reason, $title ] ) + ); + } + + public function requireLoginAnonProvider() { + $lang = 'en'; + + $expected1 = wfMessage( 'exception-nologin-text' )->inLanguage( $lang )->text(); + $expected2 = wfMessage( 'about' )->inLanguage( $lang )->text(); + + return [ + [ $expected1, null, null ], + [ $expected2, 'about', null ], + [ $expected2, 'about', 'about' ], + ]; + } + + public function testRequireLoginNotAnon() { + $specialPage = new SpecialPage( 'Watchlist', 'viewmywatchlist' ); + + $user = User::newFromName( "UTSysop" ); + $specialPage->getContext()->setUser( $user ); + + $specialPage->requireLogin(); + + // no exception thrown, logged in use can access special page + $this->assertTrue( true ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/specialpage/SpecialPageTestHelper.php b/www/wiki/tests/phpunit/includes/specialpage/SpecialPageTestHelper.php new file mode 100644 index 00000000..37e29dcb --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specialpage/SpecialPageTestHelper.php @@ -0,0 +1,24 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ +class SpecialPageTestHelper { + + public static function newSpecialAllPages() { + return new SpecialAllPages(); + } + +} |