Remove hard deprecation of PasswordPolicyChecks::checkPopularPasswordBlacklist
[lhc/web/wiklou.git] / tests / phpunit / includes / api / ApiLoginTest.php
index 384d779..15486fe 100644 (file)
@@ -1,6 +1,8 @@
 <?php
 
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Session\BotPasswordSessionProvider;
+use MediaWiki\Session\SessionManager;
 use Wikimedia\TestingAccessWrapper;
 
 /**
@@ -11,201 +13,267 @@ use Wikimedia\TestingAccessWrapper;
  * @covers ApiLogin
  */
 class ApiLoginTest extends ApiTestCase {
+       public function setUp() {
+               parent::setUp();
+
+               $this->tablesUsed[] = 'bot_passwords';
+       }
+
+       public static function provideEnableBotPasswords() {
+               return [
+                       'Bot passwords enabled' => [ true ],
+                       'Bot passwords disabled' => [ false ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideEnableBotPasswords
+        */
+       public function testExtendedDescription( $enableBotPasswords ) {
+               $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
+               $ret = $this->doApiRequest( [
+                       'action' => 'paraminfo',
+                       'modules' => 'login',
+                       'helpformat' => 'raw',
+               ] );
+               $this->assertSame(
+                       'apihelp-login-extended-description' . ( $enableBotPasswords ? '' : '-nobotpasswords' ),
+                       $ret[0]['paraminfo']['modules'][0]['description'][1]['key']
+               );
+       }
 
        /**
         * Test result of attempted login with an empty username
         */
-       public function testApiLoginNoName() {
+       public function testNoName() {
                $session = [
                        'wsTokenSecrets' => [ 'login' => 'foobar' ],
                ];
-               $data = $this->doApiRequest( [ 'action' => 'login',
-                       'lgname' => '', 'lgpassword' => self::$users['sysop']->getPassword(),
-                       'lgtoken' => (string)( new MediaWiki\Session\Token( 'foobar', '' ) )
+               $ret = $this->doApiRequest( [
+                       'action' => 'login',
+                       'lgname' => '',
+                       'lgpassword' => self::$users['sysop']->getPassword(),
+                       'lgtoken' => (string)( new MediaWiki\Session\Token( 'foobar', '' ) ),
                ], $session );
-               $this->assertEquals( 'Failed', $data[0]['login']['result'] );
+               $this->assertSame( 'Failed', $ret[0]['login']['result'] );
        }
 
-       public function testApiLoginBadPass() {
-               global $wgServer;
+       /**
+        * @dataProvider provideEnableBotPasswords
+        */
+       public function testDeprecatedUserLogin( $enableBotPasswords ) {
+               $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
 
-               $user = self::$users['sysop'];
-               $userName = $user->getUser()->getName();
-               $user->getUser()->logout();
+               $user = $this->getTestUser();
 
-               if ( !isset( $wgServer ) ) {
-                       $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
-               }
                $ret = $this->doApiRequest( [
-                       "action" => "login",
-                       "lgname" => $userName,
-                       "lgpassword" => "bad",
+                       'action' => 'login',
+                       'lgname' => $user->getUser()->getName(),
                ] );
 
-               $result = $ret[0];
-
-               $this->assertNotInternalType( "bool", $result );
-               $a = $result["login"]["result"];
-               $this->assertEquals( "NeedToken", $a );
+               $this->assertSame(
+                       [ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
+                               'apiwarn-deprecation-login-token' )->text() ) ],
+                       $ret[0]['warnings']['login']
+               );
+               $this->assertSame( 'NeedToken', $ret[0]['login']['result'] );
 
-               $token = $result["login"]["token"];
+               $ret = $this->doApiRequest( [
+                       'action' => 'login',
+                       'lgtoken' => $ret[0]['login']['token'],
+                       'lgname' => $user->getUser()->getName(),
+                       'lgpassword' => $user->getPassword(),
+               ], $ret[2] );
 
-               $ret = $this->doApiRequest(
+               $this->assertSame(
+                       [ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
+                               'apiwarn-deprecation-login-' . ( $enableBotPasswords ? '' : 'no' ) . 'botpw' )
+                               ->text() ) ],
+                       $ret[0]['warnings']['login']
+               );
+               $this->assertSame(
                        [
-                               "action" => "login",
-                               "lgtoken" => $token,
-                               "lgname" => $userName,
-                               "lgpassword" => "badnowayinhell",
+                               'result' => 'Success',
+                               'lguserid' => $user->getUser()->getId(),
+                               'lgusername' => $user->getUser()->getName(),
                        ],
-                       $ret[2]
+                       $ret[0]['login']
                );
+       }
 
-               $result = $ret[0];
+       /**
+        * Attempts to log in with the given name and password, retrieves the returned token, and makes
+        * a second API request to actually log in with the token.
+        *
+        * @param string $name
+        * @param string $password
+        * @param array $params To pass to second request
+        * @return array Result of second doApiRequest
+        */
+       private function doUserLogin( $name, $password, array $params = [] ) {
+               $ret = $this->doApiRequest( [
+                       'action' => 'query',
+                       'meta' => 'tokens',
+                       'type' => 'login',
+               ] );
 
-               $this->assertNotInternalType( "bool", $result );
-               $a = $result["login"]["result"];
+               $this->assertArrayNotHasKey( 'warnings', $ret );
 
-               $this->assertEquals( 'Failed', $a );
+               return $this->doApiRequest( array_merge(
+                       [
+                               'action' => 'login',
+                               'lgtoken' => $ret[0]['query']['tokens']['logintoken'],
+                               'lgname' => $name,
+                               'lgpassword' => $password,
+                       ], $params
+               ), $ret[2] );
        }
 
-       public function testApiLoginGoodPass() {
-               global $wgServer;
+       public function testBadToken() {
+               $user = self::$users['sysop'];
+               $userName = $user->getUser()->getName();
+               $password = $user->getPassword();
+               $user->getUser()->logout();
+
+               $ret = $this->doUserLogin( $userName, $password, [ 'lgtoken' => 'invalid token' ] );
 
-               if ( !isset( $wgServer ) ) {
-                       $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
-               }
+               $this->assertSame( 'WrongToken', $ret[0]['login']['result'] );
+       }
 
+       public function testBadPass() {
                $user = self::$users['sysop'];
                $userName = $user->getUser()->getName();
-               $password = $user->getPassword();
                $user->getUser()->logout();
 
-               $ret = $this->doApiRequest( [
-                               "action" => "login",
-                               "lgname" => $userName,
-                               "lgpassword" => $password,
-                       ]
-               );
+               $ret = $this->doUserLogin( $userName, 'bad' );
 
-               $result = $ret[0];
-               $this->assertNotInternalType( "bool", $result );
-               $this->assertNotInternalType( "null", $result["login"] );
+               $this->assertSame( 'Failed', $ret[0]['login']['result'] );
+       }
 
-               $a = $result["login"]["result"];
-               $this->assertEquals( "NeedToken", $a );
-               $token = $result["login"]["token"];
+       /**
+        * @dataProvider provideEnableBotPasswords
+        */
+       public function testGoodPass( $enableBotPasswords ) {
+               $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
 
-               $ret = $this->doApiRequest(
-                       [
-                               "action" => "login",
-                               "lgtoken" => $token,
-                               "lgname" => $userName,
-                               "lgpassword" => $password,
-                       ],
-                       $ret[2]
+               $user = self::$users['sysop'];
+               $userName = $user->getUser()->getName();
+               $password = $user->getPassword();
+               $user->getUser()->logout();
+
+               $ret = $this->doUserLogin( $userName, $password );
+
+               $this->assertSame( 'Success', $ret[0]['login']['result'] );
+               $this->assertSame(
+                       [ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
+                               'apiwarn-deprecation-login-' . ( $enableBotPasswords ? '' : 'no' ) . 'botpw' )->
+                               text() ) ],
+                       $ret[0]['warnings']['login']
+               );
+       }
+
+       /**
+        * @dataProvider provideEnableBotPasswords
+        */
+       public function testUnsupportedAuthResponseType( $enableBotPasswords ) {
+               $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
+
+               $mockProvider = $this->createMock(
+                       MediaWiki\Auth\AbstractSecondaryAuthenticationProvider::class );
+               $mockProvider->method( 'beginSecondaryAuthentication' )->willReturn(
+                       MediaWiki\Auth\AuthenticationResponse::newUI(
+                               [ new MediaWiki\Auth\UsernameAuthenticationRequest ],
+                               // Slightly silly message here
+                               wfMessage( 'mainpage' )
+                       )
                );
+               $mockProvider->method( 'getAuthenticationRequests' )
+                       ->willReturn( [] );
+
+               $this->mergeMwGlobalArrayValue( 'wgAuthManagerConfig', [
+                       'secondaryauth' => [ [
+                               'factory' => function () use ( $mockProvider ) {
+                                       return $mockProvider;
+                               },
+                       ] ],
+               ] );
 
-               $result = $ret[0];
+               $user = self::$users['sysop'];
+               $userName = $user->getUser()->getName();
+               $password = $user->getPassword();
+               $user->getUser()->logout();
 
-               $this->assertNotInternalType( "bool", $result );
-               $a = $result["login"]["result"];
+               $ret = $this->doUserLogin( $userName, $password );
 
-               $this->assertEquals( "Success", $a );
+               $this->assertSame( [ 'login' => [
+                       'result' => 'Aborted',
+                       'reason' => ApiErrorFormatter::stripMarkup( wfMessage(
+                               'api-login-fail-aborted' . ( $enableBotPasswords ? '' : '-nobotpw' ) )->text() ),
+               ] ], $ret[0] );
        }
 
        /**
+        * @todo Should this test just be deleted?
         * @group Broken
         */
-       public function testApiLoginGotCookie() {
+       public function testGotCookie() {
                $this->markTestIncomplete( "The server can't do external HTTP requests, "
                        . "and the internal one won't give cookies" );
 
                global $wgServer, $wgScriptPath;
 
-               if ( !isset( $wgServer ) ) {
-                       $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
-               }
                $user = self::$users['sysop'];
                $userName = $user->getUser()->getName();
                $password = $user->getPassword();
 
-               $req = MWHttpRequest::factory( self::$apiUrl . "?action=login&format=xml",
-                       [ "method" => "POST",
-                               "postData" => [
-                                       "lgname" => $userName,
-                                       "lgpassword" => $password
-                               ]
+               $req = MWHttpRequest::factory(
+                       self::$apiUrl . '?action=login&format=json',
+                       [
+                               'method' => 'POST',
+                               'postData' => [
+                                       'lgname' => $userName,
+                                       'lgpassword' => $password,
+                               ],
                        ],
                        __METHOD__
                );
                $req->execute();
 
-               libxml_use_internal_errors( true );
-               $sxe = simplexml_load_string( $req->getContent() );
-               $this->assertNotInternalType( "bool", $sxe );
-               $this->assertThat( $sxe, $this->isInstanceOf( SimpleXMLElement::class ) );
-               $this->assertNotInternalType( "null", $sxe->login[0] );
+               $content = json_decode( $req->getContent() );
 
-               $a = $sxe->login[0]->attributes()->result[0];
-               $this->assertEquals( ' result="NeedToken"', $a->asXML() );
-               $token = (string)$sxe->login[0]->attributes()->token;
+               $this->assertSame( 'NeedToken', $content->login->result );
 
                $req->setData( [
-                       "lgtoken" => $token,
-                       "lgname" => $userName,
-                       "lgpassword" => $password ] );
+                       'lgtoken' => $content->login->token,
+                       'lgname' => $userName,
+                       'lgpassword' => $password,
+               ] );
                $req->execute();
 
                $cj = $req->getCookieJar();
                $serverName = parse_url( $wgServer, PHP_URL_HOST );
                $this->assertNotEquals( false, $serverName );
                $serializedCookie = $cj->serializeToHttpRequest( $wgScriptPath, $serverName );
-               $this->assertNotEquals( '', $serializedCookie );
                $this->assertRegExp(
-                       '/_session=[^;]*; .*UserID=[0-9]*; .*UserName=' . $user->userName . '; .*Token=/',
+                       '/_session=[^;]*; .*UserID=[0-9]*; .*UserName=' . $userName . '; .*Token=/',
                        $serializedCookie
                );
        }
 
-       public function testRunLogin() {
-               $user = self::$users['sysop'];
-               $userName = $user->getUser()->getName();
-               $password = $user->getPassword();
-
-               $data = $this->doApiRequest( [
-                       'action' => 'login',
-                       'lgname' => $userName,
-                       'lgpassword' => $password ] );
-
-               $this->assertArrayHasKey( "login", $data[0] );
-               $this->assertArrayHasKey( "result", $data[0]['login'] );
-               $this->assertEquals( "NeedToken", $data[0]['login']['result'] );
-               $token = $data[0]['login']['token'];
-
-               $data = $this->doApiRequest( [
-                       'action' => 'login',
-                       "lgtoken" => $token,
-                       "lgname" => $userName,
-                       "lgpassword" => $password ], $data[2] );
-
-               $this->assertArrayHasKey( "login", $data[0] );
-               $this->assertArrayHasKey( "result", $data[0]['login'] );
-               $this->assertEquals( "Success", $data[0]['login']['result'] );
-       }
-
-       public function testBotPassword() {
-               global $wgServer, $wgSessionProviders;
-
-               if ( !isset( $wgServer ) ) {
-                       $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
-               }
+       /**
+        * @return [ $username, $password ] suitable for passing to an API request for successful login
+        */
+       private function setUpForBotPassword() {
+               global $wgSessionProviders;
 
                $this->setMwGlobals( [
+                       // We can't use mergeMwGlobalArrayValue because it will overwrite the existing entry
+                       // with index 0
                        'wgSessionProviders' => array_merge( $wgSessionProviders, [
                                [
-                                       'class' => MediaWiki\Session\BotPasswordSessionProvider::class,
+                                       'class' => BotPasswordSessionProvider::class,
                                        'args' => [ [ 'priority' => 40 ] ],
-                               ]
+                               ],
                        ] ),
                        'wgEnableBotPasswords' => true,
                        'wgBotPasswordsDatabase' => false,
@@ -216,22 +284,20 @@ class ApiLoginTest extends ApiTestCase {
                ] );
 
                // Make sure our session provider is present
-               $manager = TestingAccessWrapper::newFromObject( MediaWiki\Session\SessionManager::singleton() );
-               if ( !isset( $manager->sessionProviders[MediaWiki\Session\BotPasswordSessionProvider::class] ) ) {
+               $manager = TestingAccessWrapper::newFromObject( SessionManager::singleton() );
+               if ( !isset( $manager->sessionProviders[BotPasswordSessionProvider::class] ) ) {
                        $tmp = $manager->sessionProviders;
                        $manager->sessionProviders = null;
                        $manager->sessionProviders = $tmp + $manager->getProviders();
                }
                $this->assertNotNull(
-                       MediaWiki\Session\SessionManager::singleton()->getProvider(
-                               MediaWiki\Session\BotPasswordSessionProvider::class
-                       ),
+                       SessionManager::singleton()->getProvider( BotPasswordSessionProvider::class ),
                        'sanity check'
                );
 
                $user = self::$users['sysop'];
                $centralId = CentralIdLookup::factory()->centralIdFromLocalUser( $user->getUser() );
-               $this->assertNotEquals( 0, $centralId, 'sanity check' );
+               $this->assertNotSame( 0, $centralId, 'sanity check' );
 
                $password = 'ngfhmjm64hv0854493hsj5nncjud2clk';
                $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
@@ -254,48 +320,68 @@ class ApiLoginTest extends ApiTestCase {
 
                $lgName = $user->getUser()->getName() . BotPassword::getSeparator() . 'foo';
 
-               $ret = $this->doApiRequest( [
-                       'action' => 'login',
-                       'lgname' => $lgName,
-                       'lgpassword' => $password,
-               ] );
+               return [ $lgName, $password ];
+       }
 
-               $result = $ret[0];
-               $this->assertNotInternalType( 'bool', $result );
-               $this->assertNotInternalType( 'null', $result['login'] );
+       public function testBotPassword() {
+               $ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
 
-               $a = $result['login']['result'];
-               $this->assertEquals( 'NeedToken', $a );
-               $token = $result['login']['token'];
+               $this->assertSame( 'Success', $ret[0]['login']['result'] );
+       }
 
-               $ret = $this->doApiRequest( [
-                       'action' => 'login',
-                       'lgtoken' => $token,
-                       'lgname' => $lgName,
-                       'lgpassword' => $password,
-               ], $ret[2] );
+       public function testBotPasswordThrottled() {
+               global $wgPasswordAttemptThrottle;
+
+               $this->setGroupPermissions( 'sysop', 'noratelimit', false );
+               $this->setMwGlobals( 'wgMainCacheType', 'hash' );
 
-               $result = $ret[0];
-               $this->assertNotInternalType( 'bool', $result );
-               $a = $result['login']['result'];
+               list( $name, $password ) = $this->setUpForBotPassword();
+
+               for ( $i = 0; $i < $wgPasswordAttemptThrottle[0]['count']; $i++ ) {
+                       $this->doUserLogin( $name, 'incorrectpasswordincorrectpassword' );
+               }
 
-               $this->assertEquals( 'Success', $a );
+               $ret = $this->doUserLogin( $name, $password );
+
+               $this->assertSame( [
+                       'result' => 'Failed',
+                       'reason' => ApiErrorFormatter::stripMarkup( wfMessage( 'login-throttled' )->
+                               durationParams( $wgPasswordAttemptThrottle[0]['seconds'] )->text() ),
+               ], $ret[0]['login'] );
        }
 
-       public function testLoginWithNoSameOriginSecurity() {
+       public function testBotPasswordLocked() {
+               $this->setTemporaryHook( 'UserIsLocked', function ( User $unused, &$isLocked ) {
+                       $isLocked = true;
+                       return true;
+               } );
+
+               $ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
+
+               $this->assertSame( [
+                       'result' => 'Failed',
+                       'reason' => wfMessage( 'botpasswords-locked' )->text(),
+               ], $ret[0]['login'] );
+       }
+
+       public function testNoSameOriginSecurity() {
                $this->setTemporaryHook( 'RequestHasSameOriginSecurity',
                        function () {
                                return false;
                        }
                );
 
-               $result = $this->doApiRequest( [
+               $ret = $this->doApiRequest( [
                        'action' => 'login',
+                       'errorformat' => 'plaintext',
                ] )[0]['login'];
 
                $this->assertSame( [
                        'result' => 'Aborted',
-                       'reason' => 'Cannot log in when the same-origin policy is not applied.',
-               ], $result );
+                       'reason' => [
+                               'code' => 'api-login-fail-sameorigin',
+                               'text' => 'Cannot log in when the same-origin policy is not applied.',
+                       ],
+               ], $ret );
        }
 }