X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=blobdiff_plain;f=tests%2Fphpunit%2Fincludes%2Fuser%2FPasswordResetTest.php;h=b37a6b45bc2688add3c421bac96023e2258f4f34;hp=ca57c10b6f5ae9e2b2555adabbfe0d0a2bd5cad4;hb=29bee071b23832173a55364453d3132a2fe26101;hpb=9f5c319b091fc84796c1e841699cc70c7566bef7 diff --git a/tests/phpunit/includes/user/PasswordResetTest.php b/tests/phpunit/includes/user/PasswordResetTest.php index ca57c10b6f..b37a6b45bc 100644 --- a/tests/phpunit/includes/user/PasswordResetTest.php +++ b/tests/phpunit/includes/user/PasswordResetTest.php @@ -1,23 +1,30 @@ $passwordResetRoutes, - 'EnableEmail' => $enableEmail, - ] ); + $config = $this->makeConfig( $enableEmail, $passwordResetRoutes, false ); $authManager = $this->getMockBuilder( AuthManager::class )->disableOriginalConstructor() ->getMock(); @@ -28,16 +35,23 @@ class PasswordResetTest extends MediaWikiTestCase { $user->expects( $this->any() )->method( 'getName' )->willReturn( 'Foo' ); $user->expects( $this->any() )->method( 'getBlock' )->willReturn( $block ); $user->expects( $this->any() )->method( 'getGlobalBlock' )->willReturn( $globalBlock ); - $user->expects( $this->any() )->method( 'isAllowed' ) - ->will( $this->returnCallback( function ( $perm ) use ( $canEditPrivate ) { - if ( $perm === 'editmyprivateinfo' ) { - return $canEditPrivate; - } else { - $this->fail( 'Unexpected permission check' ); - } - } ) ); - $passwordReset = new PasswordReset( $config, $authManager ); + $permissionManager = $this->getMockBuilder( PermissionManager::class ) + ->disableOriginalConstructor() + ->getMock(); + $permissionManager->method( 'userHasRight' ) + ->with( $user, 'editmyprivateinfo' ) + ->willReturn( $canEditPrivate ); + + $loadBalancer = $this->getMockBuilder( ILoadBalancer::class )->getMock(); + + $passwordReset = new PasswordReset( + $config, + $authManager, + $permissionManager, + $loadBalancer, + new NullLogger() + ); $this->assertSame( $isAllowed, $passwordReset->isAllowed( $user )->isGood() ); } @@ -85,7 +99,7 @@ class PasswordResetTest extends MediaWikiTestCase { 'enableEmail' => true, 'allowsAuthenticationDataChange' => true, 'canEditPrivate' => true, - 'block' => new Block( [ 'createAccount' => true ] ), + 'block' => new DatabaseBlock( [ 'createAccount' => true ] ), 'globalBlock' => null, 'isAllowed' => false, ], @@ -94,7 +108,7 @@ class PasswordResetTest extends MediaWikiTestCase { 'enableEmail' => true, 'allowsAuthenticationDataChange' => true, 'canEditPrivate' => true, - 'block' => new Block( [] ), + 'block' => new DatabaseBlock( [] ), 'globalBlock' => null, 'isAllowed' => true, ], @@ -140,6 +154,34 @@ class PasswordResetTest extends MediaWikiTestCase { 'globalBlock' => null, 'isAllowed' => false, ], + 'blocked with multiple blocks, all allowing password reset' => [ + 'passwordResetRoutes' => [ 'username' => true ], + 'enableEmail' => true, + 'allowsAuthenticationDataChange' => true, + 'canEditPrivate' => true, + 'block' => new CompositeBlock( [ + 'originalBlocks' => [ + new SystemBlock( [ 'systemBlock' => 'wgSoftBlockRanges', 'anonOnly' => true ] ), + new Block( [] ), + ] + ] ), + 'globalBlock' => null, + 'isAllowed' => true, + ], + 'blocked with multiple blocks, not all allowing password reset' => [ + 'passwordResetRoutes' => [ 'username' => true ], + 'enableEmail' => true, + 'allowsAuthenticationDataChange' => true, + 'canEditPrivate' => true, + 'block' => new CompositeBlock( [ + 'originalBlocks' => [ + new SystemBlock( [ 'systemBlock' => 'wgSoftBlockRanges', 'anonOnly' => true ] ), + new SystemBlock( [ 'systemBlock' => 'proxy' ] ), + ] + ] ), + 'globalBlock' => null, + 'isAllowed' => false, + ], 'all OK' => [ 'passwordResetRoutes' => [ 'username' => true ], 'enableEmail' => true, @@ -152,50 +194,429 @@ class PasswordResetTest extends MediaWikiTestCase { ]; } - public function testExecute_email() { - $config = new HashConfig( [ - 'PasswordResetRoutes' => [ 'username' => true, 'email' => true ], - 'EnableEmail' => true, - ] ); + /** + * @expectedException \LogicException + */ + public function testExecute_notAllowed() { + $user = $this->getMock( User::class ); + /** @var User $user */ + $passwordReset = $this->getMockBuilder( PasswordReset::class ) + ->disableOriginalConstructor() + ->setMethods( [ 'isAllowed' ] ) + ->getMock(); + $passwordReset->expects( $this->any() ) + ->method( 'isAllowed' ) + ->with( $user ) + ->willReturn( Status::newFatal( 'somestatuscode' ) ); + /** @var PasswordReset $passwordReset */ + + $passwordReset->execute( $user ); + } + + /** + * @dataProvider provideExecute + * @param string|bool $expectedError + * @param ServiceOptions $config + * @param User $performingUser + * @param PermissionManager $permissionManager + * @param AuthManager $authManager + * @param string|null $username + * @param string|null $email + * @param User[] $usersWithEmail + */ + public function testExecute( + $expectedError, + ServiceOptions $config, + User $performingUser, + PermissionManager $permissionManager, + AuthManager $authManager, + $username = '', + $email = '', + array $usersWithEmail = [] + ) { // Unregister the hooks for proper unit testing $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'User::mailPasswordInternal' => [], 'SpecialPasswordResetOnSubmit' => [], ] ); - $authManager = $this->getMockBuilder( AuthManager::class )->disableOriginalConstructor() + $loadBalancer = $this->getMockBuilder( ILoadBalancer::class ) ->getMock(); - $authManager->expects( $this->any() )->method( 'allowsAuthenticationDataChange' ) - ->willReturn( Status::newGood() ); - $authManager->expects( $this->exactly( 2 ) )->method( 'changeAuthenticationData' ); - - $request = new FauxRequest(); - $request->setIP( '1.2.3.4' ); - $performingUser = $this->getMockBuilder( User::class )->getMock(); - $performingUser->expects( $this->any() )->method( 'getRequest' )->willReturn( $request ); - $performingUser->expects( $this->any() )->method( 'isAllowed' )->willReturn( true ); - $performingUser->expects( $this->any() )->method( 'getName' )->willReturn( 'Performer' ); - - $targetUser1 = $this->getMockBuilder( User::class )->getMock(); - $targetUser2 = $this->getMockBuilder( User::class )->getMock(); - $targetUser1->expects( $this->any() )->method( 'getName' )->willReturn( 'User1' ); - $targetUser2->expects( $this->any() )->method( 'getName' )->willReturn( 'User2' ); - $targetUser1->expects( $this->any() )->method( 'getId' )->willReturn( 1 ); - $targetUser2->expects( $this->any() )->method( 'getId' )->willReturn( 2 ); - $targetUser1->expects( $this->any() )->method( 'getEmail' )->willReturn( 'foo@bar.baz' ); - $targetUser2->expects( $this->any() )->method( 'getEmail' )->willReturn( 'foo@bar.baz' ); + + $users = $this->makeUsers(); + + $lookupUser = function ( $username ) use ( $users ) { + return $users[ $username ] ?? false; + }; $passwordReset = $this->getMockBuilder( PasswordReset::class ) - ->setMethods( [ 'getUsersByEmail' ] )->setConstructorArgs( [ $config, $authManager ] ) + ->setMethods( [ 'getUsersByEmail', 'isAllowed', 'lookupUser' ] ) + ->setConstructorArgs( [ + $config, + $authManager, + $permissionManager, + $loadBalancer, + new NullLogger() + ] ) ->getMock(); - $passwordReset->expects( $this->any() )->method( 'getUsersByEmail' )->with( 'foo@bar.baz' ) - ->willReturn( [ $targetUser1, $targetUser2 ] ); + $passwordReset->method( 'getUsersByEmail' )->with( $email ) + ->willReturn( array_map( $lookupUser, $usersWithEmail ) ); + $passwordReset->method( 'isAllowed' ) + ->willReturn( Status::newGood() ); + $passwordReset->method( 'lookupUser' ) + ->willReturnCallback( $lookupUser ); - $status = $passwordReset->isAllowed( $performingUser ); - $this->assertTrue( $status->isGood() ); + /** @var PasswordReset $passwordReset */ + $status = $passwordReset->execute( $performingUser, $username, $email ); + $this->assertStatus( $status, $expectedError ); + } + + public function provideExecute() { + $defaultConfig = $this->makeConfig( true, [ 'username' => true, 'email' => true ], false ); + $emailRequiredConfig = $this->makeConfig( true, [ 'username' => true, 'email' => true ], true ); + $performingUser = $this->makePerformingUser( self::VALID_IP, false ); + $throttledUser = $this->makePerformingUser( self::VALID_IP, true ); + $permissionManager = $this->makePermissionManager( $performingUser, true ); - $status = $passwordReset->execute( $performingUser, null, 'foo@bar.baz' ); - $this->assertTrue( $status->isGood() ); + return [ + 'Invalid email' => [ + 'expectedError' => 'passwordreset-invalidemail', + 'config' => $defaultConfig, + 'performingUser' => $throttledUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager(), + 'username' => '', + 'email' => '[invalid email]', + 'usersWithEmail' => [], + ], + 'No username, no email' => [ + 'expectedError' => 'passwordreset-nodata', + 'config' => $defaultConfig, + 'performingUser' => $throttledUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager(), + 'username' => '', + 'email' => '', + 'usersWithEmail' => [], + ], + 'Email route not enabled' => [ + 'expectedError' => 'passwordreset-nodata', + 'config' => $this->makeConfig( true, [ 'username' => true ], false ), + 'performingUser' => $throttledUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager(), + 'username' => '', + 'email' => self::VALID_EMAIL, + 'usersWithEmail' => [], + ], + 'Username route not enabled' => [ + 'expectedError' => 'passwordreset-nodata', + 'config' => $this->makeConfig( true, [ 'email' => true ], false ), + 'performingUser' => $throttledUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager(), + 'username' => 'User1', + 'email' => '', + 'usersWithEmail' => [], + ], + 'No routes enabled' => [ + 'expectedError' => 'passwordreset-nodata', + 'config' => $this->makeConfig( true, [], false ), + 'performingUser' => $throttledUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager(), + 'username' => 'User1', + 'email' => self::VALID_EMAIL, + 'usersWithEmail' => [], + ], + 'Email reqiured for resets, but is empty' => [ + 'expectedError' => 'passwordreset-username-email-required', + 'config' => $emailRequiredConfig, + 'performingUser' => $throttledUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager(), + 'username' => 'User1', + 'email' => '', + 'usersWithEmail' => [], + ], + 'Email reqiured for resets, is invalid' => [ + 'expectedError' => 'passwordreset-invalidemail', + 'config' => $emailRequiredConfig, + 'performingUser' => $throttledUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager(), + 'username' => 'User1', + 'email' => '[invalid email]', + 'usersWithEmail' => [], + ], + 'Throttled' => [ + 'expectedError' => 'actionthrottledtext', + 'config' => $defaultConfig, + 'performingUser' => $throttledUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager(), + 'username' => 'User1', + 'email' => '', + 'usersWithEmail' => [], + ], + 'No user by this username' => [ + 'expectedError' => 'nosuchuser', + 'config' => $defaultConfig, + 'performingUser' => $performingUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager(), + 'username' => 'Nonexistent user', + 'email' => '', + 'usersWithEmail' => [], + ], + 'If no users with this email found, pretend everything is OK' => [ + 'expectedError' => false, + 'config' => $defaultConfig, + 'performingUser' => $performingUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager(), + 'username' => '', + 'email' => 'some@not.found.email', + 'usersWithEmail' => [], + ], + 'No email for the user' => [ + 'expectedError' => 'noemail', + 'config' => $defaultConfig, + 'performingUser' => $performingUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager(), + 'username' => 'BadUser', + 'email' => '', + 'usersWithEmail' => [], + ], + 'Email reqiured for resets, no match' => [ + 'expectedError' => false, + 'config' => $emailRequiredConfig, + 'performingUser' => $performingUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager(), + 'username' => 'User1', + 'email' => 'some@other.email', + 'usersWithEmail' => [], + ], + "Couldn't determine the performing user's IP" => [ + 'expectedError' => 'badipaddress', + 'config' => $defaultConfig, + 'performingUser' => $this->makePerformingUser( null, false ), + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager(), + 'username' => 'User1', + 'email' => '', + 'usersWithEmail' => [], + ], + 'User is allowed, but ignored' => [ + 'expectedError' => 'passwordreset-ignored', + 'config' => $defaultConfig, + 'performingUser' => $performingUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager( [ 'User1' ], 0, [ 'User1' ] ), + 'username' => 'User1', + 'email' => '', + 'usersWithEmail' => [], + ], + 'One of users is ignored' => [ + 'expectedError' => 'passwordreset-ignored', + 'config' => $defaultConfig, + 'performingUser' => $performingUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager( [ 'User1', 'User2' ], 0, [ 'User2' ] ), + 'username' => '', + 'email' => self::VALID_EMAIL, + 'usersWithEmail' => [ 'User1', 'User2' ], + ], + 'User is rejected' => [ + 'expectedError' => 'rejected by test mock', + 'config' => $defaultConfig, + 'performingUser' => $performingUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager(), + 'username' => 'User1', + 'email' => '', + 'usersWithEmail' => [], + ], + 'One of users is rejected' => [ + 'expectedError' => 'rejected by test mock', + 'config' => $defaultConfig, + 'performingUser' => $performingUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager( [ 'User1' ] ), + 'username' => '', + 'email' => self::VALID_EMAIL, + 'usersWithEmail' => [ 'User1', 'User2' ], + ], + 'Reset one user via password' => [ + 'expectedError' => false, + 'config' => $defaultConfig, + 'performingUser' => $performingUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager( [ 'User1' ], 1 ), + 'username' => 'User1', + 'email' => self::VALID_EMAIL, + // Make sure that only the user specified by username is reset + 'usersWithEmail' => [ 'User1', 'User2' ], + ], + 'Reset one user via email' => [ + 'expectedError' => false, + 'config' => $defaultConfig, + 'performingUser' => $performingUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager( [ 'User1' ], 1 ), + 'username' => '', + 'email' => self::VALID_EMAIL, + 'usersWithEmail' => [ 'User1' ], + ], + 'Reset multiple users via email' => [ + 'expectedError' => false, + 'config' => $defaultConfig, + 'performingUser' => $performingUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager( [ 'User1', 'User2' ], 2 ), + 'username' => '', + 'email' => self::VALID_EMAIL, + 'usersWithEmail' => [ 'User1', 'User2' ], + ], + "Email is required for resets, user didn't opt in" => [ + 'expectedError' => false, + 'config' => $emailRequiredConfig, + 'performingUser' => $performingUser, + 'permissionManager' => $permissionManager, + 'authManager' => $this->makeAuthManager( [ 'User2' ], 1 ), + 'username' => 'User2', + 'email' => self::VALID_EMAIL, + 'usersWithEmail' => [ 'User2' ], + ], + ]; + } + + private function assertStatus( StatusValue $status, $error = false ) { + if ( $error === false ) { + $this->assertTrue( $status->isGood(), 'Expected status to be good' ); + } else { + $this->assertFalse( $status->isGood(), 'Expected status to not be good' ); + if ( is_string( $error ) ) { + $this->assertNotEmpty( $status->getErrors() ); + $message = $status->getErrors()[0]['message']; + if ( $message instanceof MessageSpecifier ) { + $message = $message->getKey(); + } + $this->assertSame( $error, $message ); + } + } + } + + private function makeConfig( $enableEmail, array $passwordResetRoutes, $emailForResets ) { + $hash = [ + 'AllowRequiringEmailForResets' => $emailForResets, + 'EnableEmail' => $enableEmail, + 'PasswordResetRoutes' => $passwordResetRoutes, + ]; + + return new ServiceOptions( PasswordReset::$constructorOptions, $hash ); + } + + /** + * @param string|null $ip + * @param bool $pingLimited + * @return User + */ + private function makePerformingUser( $ip, $pingLimited ) : User { + $request = $this->getMockBuilder( WebRequest::class ) + ->getMock(); + $request->method( 'getIP' ) + ->willReturn( $ip ); + /** @var WebRequest $request */ + + $user = $this->getMockBuilder( User::class ) + ->setMethods( [ 'getName', 'pingLimiter', 'getRequest' ] ) + ->getMock(); + + $user->method( 'getName' ) + ->willReturn( 'SomeUser' ); + $user->method( 'pingLimiter' ) + ->with( 'mailpassword' ) + ->willReturn( $pingLimited ); + $user->method( 'getRequest' ) + ->willReturn( $request ); + + /** @var User $user */ + return $user; + } + + private function makePermissionManager( User $performingUser, $isAllowed ) : PermissionManager { + $permissionManager = $this->getMockBuilder( PermissionManager::class ) + ->disableOriginalConstructor() + ->getMock(); + $permissionManager->method( 'userHasRight' ) + ->with( $performingUser, 'editmyprivateinfo' ) + ->willReturn( $isAllowed ); + + /** @var PermissionManager $permissionManager */ + return $permissionManager; + } + + /** + * @param string[] $allowed + * @param int $numUsersToAuth + * @param string[] $ignored + * @return AuthManager + */ + private function makeAuthManager( + array $allowed = [], + $numUsersToAuth = 0, + array $ignored = [] + ) : AuthManager { + $authManager = $this->getMockBuilder( AuthManager::class ) + ->disableOriginalConstructor() + ->getMock(); + $authManager->method( 'allowsAuthenticationDataChange' ) + ->willReturnCallback( + function ( TemporaryPasswordAuthenticationRequest $req ) use ( $allowed, $ignored ) { + $value = in_array( $req->username, $ignored, true ) + ? 'ignored' + : 'okie dokie'; + return in_array( $req->username, $allowed, true ) + ? Status::newGood( $value ) + : Status::newFatal( 'rejected by test mock' ); + } ); + $authManager->expects( $this->exactly( $numUsersToAuth ) ) + ->method( 'changeAuthenticationData' ); + + /** @var AuthManager $authManager */ + return $authManager; + } + + /** + * @return User[] + */ + private function makeUsers() { + $user1 = $this->getMockBuilder( User::class )->getMock(); + $user2 = $this->getMockBuilder( User::class )->getMock(); + $user1->method( 'getName' )->willReturn( 'User1' ); + $user2->method( 'getName' )->willReturn( 'User2' ); + $user1->method( 'getId' )->willReturn( 1 ); + $user2->method( 'getId' )->willReturn( 2 ); + $user1->method( 'getEmail' )->willReturn( self::VALID_EMAIL ); + $user2->method( 'getEmail' )->willReturn( self::VALID_EMAIL ); + + $user1->method( 'getBoolOption' ) + ->with( 'requireemail' ) + ->willReturn( true ); + + $badUser = $this->getMockBuilder( User::class )->getMock(); + $badUser->method( 'getName' )->willReturn( 'BadUser' ); + $badUser->method( 'getId' )->willReturn( 3 ); + $badUser->method( 'getEmail' )->willReturn( null ); + + return [ + 'User1' => $user1, + 'User2' => $user2, + 'BadUser' => $badUser, + ]; } }