wiki.techinc.nl/tests/phpunit/includes/api/ApiLoginTest.php
daniel bf092744c9 PHPUnit: introduce setMainCache
The main object cache is disabled during testing. Some integration tests
need it though. This provides a clean way to enable it, to replace the hacks
that were used so far.

Note that we may want to enable the main cache during testing soon. When
that happens, this method is still useful to disable the cache in certain
tests, and to set a specific cache instance.

Change-Id: I04ae1bf1b6b2c8f6310acd2edf89459d01a9c870
2022-07-07 16:25:59 +10:00

388 lines
11 KiB
PHP

<?php
use MediaWiki\Session\BotPasswordSessionProvider;
use MediaWiki\Session\SessionManager;
use Wikimedia\TestingAccessWrapper;
/**
* @group API
* @group Database
* @group medium
*
* @covers ApiLogin
*/
class ApiLoginTest extends ApiTestCase {
protected function setUp(): void {
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 testNoName() {
$session = [
'wsTokenSecrets' => [ 'login' => 'foobar' ],
];
$ret = $this->doApiRequest( [
'action' => 'login',
'lgname' => '',
'lgpassword' => self::$users['sysop']->getPassword(),
'lgtoken' => (string)( new MediaWiki\Session\Token( 'foobar', '' ) ),
], $session );
$this->assertSame( 'Failed', $ret[0]['login']['result'] );
}
/**
* @dataProvider provideEnableBotPasswords
*/
public function testDeprecatedUserLogin( $enableBotPasswords ) {
$this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
$user = $this->getTestUser();
$ret = $this->doApiRequest( [
'action' => 'login',
'lgname' => $user->getUser()->getName(),
] );
$this->assertSame(
[ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
'apiwarn-deprecation-login-token' )->text() ) ],
$ret[0]['warnings']['login']
);
$this->assertSame( 'NeedToken', $ret[0]['login']['result'] );
$ret = $this->doApiRequest( [
'action' => 'login',
'lgtoken' => $ret[0]['login']['token'],
'lgname' => $user->getUser()->getName(),
'lgpassword' => $user->getPassword(),
], $ret[2] );
$this->assertSame(
[ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
'apiwarn-deprecation-login-' . ( $enableBotPasswords ? '' : 'no' ) . 'botpw' )
->text() ) ],
$ret[0]['warnings']['login']
);
$this->assertSame(
[
'result' => 'Success',
'lguserid' => $user->getUser()->getId(),
'lgusername' => $user->getUser()->getName(),
],
$ret[0]['login']
);
}
/**
* 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->assertArrayNotHasKey( 'warnings', $ret );
return $this->doApiRequest( array_merge(
[
'action' => 'login',
'lgtoken' => $ret[0]['query']['tokens']['logintoken'],
'lgname' => $name,
'lgpassword' => $password,
], $params
), $ret[2] );
}
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' ] );
$this->assertSame( 'WrongToken', $ret[0]['login']['result'] );
}
public function testLostSession() {
$user = self::$users['sysop'];
$userName = $user->getUser()->getName();
$password = $user->getPassword();
$user->getUser()->logout();
$ret = $this->doApiRequest( [
'action' => 'query',
'meta' => 'tokens',
'type' => 'login',
] );
$this->assertArrayNotHasKey( 'warnings', $ret );
// Lose the session
MediaWiki\Session\SessionManager::getGlobalSession()->clear();
$ret[2] = [];
$ret = $this->doApiRequest( [
'action' => 'login',
'lgtoken' => $ret[0]['query']['tokens']['logintoken'],
'lgname' => $userName,
'lgpassword' => $password,
'errorformat' => 'raw',
], $ret[2] );
$this->assertSame( [
'result' => 'Failed',
'reason' => [
'code' => 'sessionlost',
'key' => 'authpage-cannot-login-continue',
'params' => [],
],
], $ret[0]['login'] );
}
public function testBadPass() {
$user = self::$users['sysop'];
$userName = $user->getUser()->getName();
$user->getUser()->logout();
$ret = $this->doUserLogin( $userName, 'bad', [ 'errorformat' => 'raw' ] );
$this->assertSame( [
'result' => 'Failed',
'reason' => [
'code' => 'wrongpassword',
'key' => 'wrongpassword',
'params' => [],
],
], $ret[0]['login'] );
}
/**
* @dataProvider provideEnableBotPasswords
*/
public function testGoodPass( $enableBotPasswords ) {
$this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
$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' => static function () use ( $mockProvider ) {
return $mockProvider;
},
] ],
] );
$user = self::$users['sysop'];
$userName = $user->getUser()->getName();
$password = $user->getPassword();
$user->getUser()->logout();
$ret = $this->doUserLogin( $userName, $password );
$this->assertSame( [ 'login' => [
'result' => 'Aborted',
'reason' => ApiErrorFormatter::stripMarkup( wfMessage(
'api-login-fail-aborted' . ( $enableBotPasswords ? '' : '-nobotpw' ) )->text() ),
] ], $ret[0] );
}
/**
* @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' => BotPasswordSessionProvider::class,
'args' => [ [ 'priority' => 40 ] ],
'services' => [ 'GrantsInfo' ],
],
] ),
'wgEnableBotPasswords' => true,
'wgBotPasswordsDatabase' => false,
'wgCentralIdLookupProvider' => 'local',
'wgGrantPermissions' => [
'test' => [ 'read' => true ],
],
] );
// Make sure our session provider is present
$manager = TestingAccessWrapper::newFromObject( SessionManager::singleton() );
if ( !isset( $manager->sessionProviders[BotPasswordSessionProvider::class] ) ) {
$tmp = $manager->sessionProviders;
$manager->sessionProviders = null;
$manager->sessionProviders = $tmp + $manager->getProviders();
}
$this->assertNotNull(
SessionManager::singleton()->getProvider( BotPasswordSessionProvider::class )
);
$user = self::$users['sysop'];
$centralId = $this->getServiceContainer()
->getCentralIdLookup()
->centralIdFromLocalUser( $user->getUser() );
$this->assertNotSame( 0, $centralId );
$password = 'ngfhmjm64hv0854493hsj5nncjud2clk';
$passwordFactory = $this->getServiceContainer()->getPasswordFactory();
// A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
$passwordHash = $passwordFactory->newFromPlaintext( $password );
$dbw = wfGetDB( DB_PRIMARY );
$dbw->insert(
'bot_passwords',
[
'bp_user' => $centralId,
'bp_app_id' => 'foo',
'bp_password' => $passwordHash->toString(),
'bp_token' => '',
'bp_restrictions' => MWRestrictions::newDefault()->toJson(),
'bp_grants' => '["test"]',
],
__METHOD__
);
$lgName = $user->getUser()->getName() . BotPassword::getSeparator() . 'foo';
return [ $lgName, $password ];
}
public function testBotPassword() {
$ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
$this->assertSame( 'Success', $ret[0]['login']['result'] );
}
public function testBotPasswordThrottled() {
// Undo high count from DevelopmentSettings.php
$throttle = [
[ 'count' => 5, 'seconds' => 30 ],
[ 'count' => 100, 'seconds' => 60 * 60 * 48 ],
];
$this->setGroupPermissions( 'sysop', 'noratelimit', false );
$this->setMainCache( CACHE_HASH );
$this->overrideConfigValue( 'PasswordAttemptThrottle', $throttle );
list( $name, $password ) = $this->setUpForBotPassword();
for ( $i = 0; $i < $throttle[0]['count']; $i++ ) {
$this->doUserLogin( $name, 'incorrectpasswordincorrectpassword' );
}
$ret = $this->doUserLogin( $name, $password );
$this->assertSame( [
'result' => 'Failed',
'reason' => ApiErrorFormatter::stripMarkup( wfMessage( 'login-throttled' )->
durationParams( $throttle[0]['seconds'] )->text() ),
], $ret[0]['login'] );
}
public function testBotPasswordLocked() {
$this->setTemporaryHook( 'UserIsLocked', static 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',
static function () {
return false;
}
);
$ret = $this->doApiRequest( [
'action' => 'login',
'errorformat' => 'plaintext',
] )[0]['login'];
$this->assertSame( [
'result' => 'Aborted',
'reason' => [
'code' => 'api-login-fail-sameorigin',
'text' => 'Cannot log in when the same-origin policy is not applied.',
],
], $ret );
}
}