summaryrefslogtreecommitdiff
path: root/www/wiki/tests/phpunit/includes/auth
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/tests/phpunit/includes/auth')
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AbstractAuthenticationProviderTest.php30
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AbstractPasswordPrimaryAuthenticationProviderTest.php228
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AbstractPreAuthenticationProviderTest.php45
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AbstractPrimaryAuthenticationProviderTest.php174
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AbstractSecondaryAuthenticationProviderTest.php84
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AuthManagerTest.php3629
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AuthPluginPrimaryAuthenticationProviderTest.php716
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTest.php517
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTestCase.php94
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AuthenticationResponseTest.php112
-rw-r--r--www/wiki/tests/phpunit/includes/auth/ButtonAuthenticationRequestTest.php64
-rw-r--r--www/wiki/tests/phpunit/includes/auth/CheckBlocksSecondaryAuthenticationProviderTest.php191
-rw-r--r--www/wiki/tests/phpunit/includes/auth/ConfirmLinkAuthenticationRequestTest.php68
-rw-r--r--www/wiki/tests/phpunit/includes/auth/ConfirmLinkSecondaryAuthenticationProviderTest.php289
-rw-r--r--www/wiki/tests/phpunit/includes/auth/CreateFromLoginAuthenticationRequestTest.php57
-rw-r--r--www/wiki/tests/phpunit/includes/auth/CreatedAccountAuthenticationRequestTest.php30
-rw-r--r--www/wiki/tests/phpunit/includes/auth/CreationReasonAuthenticationRequestTest.php34
-rw-r--r--www/wiki/tests/phpunit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php109
-rw-r--r--www/wiki/tests/phpunit/includes/auth/LegacyHookPreAuthenticationProviderTest.php373
-rw-r--r--www/wiki/tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php658
-rw-r--r--www/wiki/tests/phpunit/includes/auth/PasswordAuthenticationRequestTest.php138
-rw-r--r--www/wiki/tests/phpunit/includes/auth/PasswordDomainAuthenticationRequestTest.php159
-rw-r--r--www/wiki/tests/phpunit/includes/auth/RememberMeAuthenticationRequestTest.php57
-rw-r--r--www/wiki/tests/phpunit/includes/auth/ResetPasswordSecondaryAuthenticationProviderTest.php310
-rw-r--r--www/wiki/tests/phpunit/includes/auth/TemporaryPasswordAuthenticationRequestTest.php79
-rw-r--r--www/wiki/tests/phpunit/includes/auth/TemporaryPasswordPrimaryAuthenticationProviderTest.php720
-rw-r--r--www/wiki/tests/phpunit/includes/auth/ThrottlePreAuthenticationProviderTest.php236
-rw-r--r--www/wiki/tests/phpunit/includes/auth/ThrottlerTest.php238
-rw-r--r--www/wiki/tests/phpunit/includes/auth/UserDataAuthenticationRequestTest.php176
-rw-r--r--www/wiki/tests/phpunit/includes/auth/UsernameAuthenticationRequestTest.php34
30 files changed, 9649 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/auth/AbstractAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/AbstractAuthenticationProviderTest.php
new file mode 100644
index 00000000..b271b701
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/AbstractAuthenticationProviderTest.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\AbstractAuthenticationProvider
+ */
+class AbstractAuthenticationProviderTest extends \MediaWikiTestCase {
+ public function testAbstractAuthenticationProvider() {
+ $provider = $this->getMockForAbstractClass( AbstractAuthenticationProvider::class );
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+
+ $obj = $this->getMockForAbstractClass( \Psr\Log\LoggerInterface::class );
+ $provider->setLogger( $obj );
+ $this->assertSame( $obj, $providerPriv->logger, 'setLogger' );
+
+ $obj = AuthManager::singleton();
+ $provider->setManager( $obj );
+ $this->assertSame( $obj, $providerPriv->manager, 'setManager' );
+
+ $obj = $this->getMockForAbstractClass( \Config::class );
+ $provider->setConfig( $obj );
+ $this->assertSame( $obj, $providerPriv->config, 'setConfig' );
+
+ $this->assertType( 'string', $provider->getUniqueId(), 'getUniqueId' );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/AbstractPasswordPrimaryAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/AbstractPasswordPrimaryAuthenticationProviderTest.php
new file mode 100644
index 00000000..cb015df6
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/AbstractPasswordPrimaryAuthenticationProviderTest.php
@@ -0,0 +1,228 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use MediaWiki\MediaWikiServices;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\AbstractPasswordPrimaryAuthenticationProvider
+ */
+class AbstractPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestCase {
+ public function testConstructor() {
+ $provider = $this->getMockForAbstractClass(
+ AbstractPasswordPrimaryAuthenticationProvider::class
+ );
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+ $this->assertTrue( $providerPriv->authoritative );
+
+ $provider = $this->getMockForAbstractClass(
+ AbstractPasswordPrimaryAuthenticationProvider::class,
+ [ [ 'authoritative' => false ] ]
+ );
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+ $this->assertFalse( $providerPriv->authoritative );
+ }
+
+ public function testGetPasswordFactory() {
+ $provider = $this->getMockForAbstractClass(
+ AbstractPasswordPrimaryAuthenticationProvider::class
+ );
+ $provider->setConfig( MediaWikiServices::getInstance()->getMainConfig() );
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+
+ $obj = $providerPriv->getPasswordFactory();
+ $this->assertInstanceOf( \PasswordFactory::class, $obj );
+ $this->assertSame( $obj, $providerPriv->getPasswordFactory() );
+ }
+
+ public function testGetPassword() {
+ $provider = $this->getMockForAbstractClass(
+ AbstractPasswordPrimaryAuthenticationProvider::class
+ );
+ $provider->setConfig( MediaWikiServices::getInstance()->getMainConfig() );
+ $provider->setLogger( new \Psr\Log\NullLogger() );
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+
+ $obj = $providerPriv->getPassword( null );
+ $this->assertInstanceOf( \Password::class, $obj );
+
+ $obj = $providerPriv->getPassword( 'invalid' );
+ $this->assertInstanceOf( \Password::class, $obj );
+ }
+
+ public function testGetNewPasswordExpiry() {
+ $config = new \HashConfig;
+ $provider = $this->getMockForAbstractClass(
+ AbstractPasswordPrimaryAuthenticationProvider::class
+ );
+ $provider->setConfig( new \MultiConfig( [
+ $config,
+ MediaWikiServices::getInstance()->getMainConfig()
+ ] ) );
+ $provider->setLogger( new \Psr\Log\NullLogger() );
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+
+ $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'ResetPasswordExpiration' => [] ] );
+
+ $config->set( 'PasswordExpirationDays', 0 );
+ $this->assertNull( $providerPriv->getNewPasswordExpiry( 'UTSysop' ) );
+
+ $config->set( 'PasswordExpirationDays', 5 );
+ $this->assertEquals(
+ time() + 5 * 86400,
+ wfTimestamp( TS_UNIX, $providerPriv->getNewPasswordExpiry( 'UTSysop' ) ),
+ '',
+ 2 /* Fuzz */
+ );
+
+ $this->mergeMwGlobalArrayValue( 'wgHooks', [
+ 'ResetPasswordExpiration' => [ function ( $user, &$expires ) {
+ $this->assertSame( 'UTSysop', $user->getName() );
+ $expires = '30001231235959';
+ } ]
+ ] );
+ $this->assertEquals( '30001231235959', $providerPriv->getNewPasswordExpiry( 'UTSysop' ) );
+ }
+
+ public function testCheckPasswordValidity() {
+ $uppCalled = 0;
+ $uppStatus = \Status::newGood();
+ $this->setMwGlobals( [
+ 'wgPasswordPolicy' => [
+ 'policies' => [
+ 'default' => [
+ 'Check' => true,
+ ],
+ ],
+ 'checks' => [
+ 'Check' => function () use ( &$uppCalled, &$uppStatus ) {
+ $uppCalled++;
+ return $uppStatus;
+ },
+ ],
+ ]
+ ] );
+
+ $provider = $this->getMockForAbstractClass(
+ AbstractPasswordPrimaryAuthenticationProvider::class
+ );
+ $provider->setConfig( MediaWikiServices::getInstance()->getMainConfig() );
+ $provider->setLogger( new \Psr\Log\NullLogger() );
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+
+ $this->assertEquals( $uppStatus, $providerPriv->checkPasswordValidity( 'foo', 'bar' ) );
+
+ $uppStatus->fatal( 'arbitrary-warning' );
+ $this->assertEquals( $uppStatus, $providerPriv->checkPasswordValidity( 'foo', 'bar' ) );
+ }
+
+ public function testSetPasswordResetFlag() {
+ $config = new \HashConfig( [
+ 'InvalidPasswordReset' => true,
+ ] );
+
+ $manager = new AuthManager(
+ new \FauxRequest(),
+ MediaWikiServices::getInstance()->getMainConfig()
+ );
+
+ $provider = $this->getMockForAbstractClass(
+ AbstractPasswordPrimaryAuthenticationProvider::class
+ );
+ $provider->setConfig( $config );
+ $provider->setLogger( new \Psr\Log\NullLogger() );
+ $provider->setManager( $manager );
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+
+ $manager->removeAuthenticationSessionData( null );
+ $status = \Status::newGood();
+ $providerPriv->setPasswordResetFlag( 'Foo', $status );
+ $this->assertNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+
+ $manager->removeAuthenticationSessionData( null );
+ $status = \Status::newGood();
+ $status->error( 'testing' );
+ $providerPriv->setPasswordResetFlag( 'Foo', $status );
+ $ret = $manager->getAuthenticationSessionData( 'reset-pass' );
+ $this->assertNotNull( $ret );
+ $this->assertSame( 'resetpass-validity-soft', $ret->msg->getKey() );
+ $this->assertFalse( $ret->hard );
+
+ $config->set( 'InvalidPasswordReset', false );
+ $manager->removeAuthenticationSessionData( null );
+ $providerPriv->setPasswordResetFlag( 'Foo', $status );
+ $ret = $manager->getAuthenticationSessionData( 'reset-pass' );
+ $this->assertNull( $ret );
+ }
+
+ public function testFailResponse() {
+ $provider = $this->getMockForAbstractClass(
+ AbstractPasswordPrimaryAuthenticationProvider::class,
+ [ [ 'authoritative' => false ] ]
+ );
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+
+ $req = new PasswordAuthenticationRequest;
+
+ $ret = $providerPriv->failResponse( $req );
+ $this->assertSame( AuthenticationResponse::ABSTAIN, $ret->status );
+
+ $provider = $this->getMockForAbstractClass(
+ AbstractPasswordPrimaryAuthenticationProvider::class,
+ [ [ 'authoritative' => true ] ]
+ );
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+
+ $req->password = '';
+ $ret = $providerPriv->failResponse( $req );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'wrongpasswordempty', $ret->message->getKey() );
+
+ $req->password = 'X';
+ $ret = $providerPriv->failResponse( $req );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'wrongpassword', $ret->message->getKey() );
+ }
+
+ /**
+ * @dataProvider provideGetAuthenticationRequests
+ * @param string $action
+ * @param array $response
+ */
+ public function testGetAuthenticationRequests( $action, $response ) {
+ $provider = $this->getMockForAbstractClass(
+ AbstractPasswordPrimaryAuthenticationProvider::class
+ );
+
+ $this->assertEquals( $response, $provider->getAuthenticationRequests( $action, [] ) );
+ }
+
+ public static function provideGetAuthenticationRequests() {
+ return [
+ [ AuthManager::ACTION_LOGIN, [ new PasswordAuthenticationRequest() ] ],
+ [ AuthManager::ACTION_CREATE, [ new PasswordAuthenticationRequest() ] ],
+ [ AuthManager::ACTION_LINK, [] ],
+ [ AuthManager::ACTION_CHANGE, [ new PasswordAuthenticationRequest() ] ],
+ [ AuthManager::ACTION_REMOVE, [ new PasswordAuthenticationRequest() ] ],
+ ];
+ }
+
+ public function testProviderRevokeAccessForUser() {
+ $req = new PasswordAuthenticationRequest;
+ $req->action = AuthManager::ACTION_REMOVE;
+ $req->username = 'foo';
+ $req->password = null;
+
+ $provider = $this->getMockForAbstractClass(
+ AbstractPasswordPrimaryAuthenticationProvider::class
+ );
+ $provider->expects( $this->once() )
+ ->method( 'providerChangeAuthenticationData' )
+ ->with( $this->equalTo( $req ) );
+
+ $provider->providerRevokeAccessForUser( 'foo' );
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/AbstractPreAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/AbstractPreAuthenticationProviderTest.php
new file mode 100644
index 00000000..96384518
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/AbstractPreAuthenticationProviderTest.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\AbstractPreAuthenticationProvider
+ */
+class AbstractPreAuthenticationProviderTest extends \MediaWikiTestCase {
+ public function testAbstractPreAuthenticationProvider() {
+ $user = \User::newFromName( 'UTSysop' );
+
+ $provider = $this->getMockForAbstractClass( AbstractPreAuthenticationProvider::class );
+
+ $this->assertEquals(
+ [],
+ $provider->getAuthenticationRequests( AuthManager::ACTION_LOGIN, [] )
+ );
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testForAuthentication( [] )
+ );
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testForAccountCreation( $user, $user, [] )
+ );
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testUserForCreation( $user, AuthManager::AUTOCREATE_SOURCE_SESSION )
+ );
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testUserForCreation( $user, false )
+ );
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testForAccountLink( $user )
+ );
+
+ $res = AuthenticationResponse::newPass();
+ $provider->postAuthentication( $user, $res );
+ $provider->postAccountCreation( $user, $user, $res );
+ $provider->postAccountLink( $user, $res );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/AbstractPrimaryAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/AbstractPrimaryAuthenticationProviderTest.php
new file mode 100644
index 00000000..8d84f4ca
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/AbstractPrimaryAuthenticationProviderTest.php
@@ -0,0 +1,174 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\AbstractPrimaryAuthenticationProvider
+ */
+class AbstractPrimaryAuthenticationProviderTest extends \MediaWikiTestCase {
+ public function testAbstractPrimaryAuthenticationProvider() {
+ $user = \User::newFromName( 'UTSysop' );
+
+ $provider = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
+
+ try {
+ $provider->continuePrimaryAuthentication( [] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \BadMethodCallException $ex ) {
+ }
+
+ try {
+ $provider->continuePrimaryAccountCreation( $user, $user, [] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \BadMethodCallException $ex ) {
+ }
+
+ $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+
+ $this->assertTrue( $provider->providerAllowsPropertyChange( 'foo' ) );
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testForAccountCreation( $user, $user, [] )
+ );
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testUserForCreation( $user, AuthManager::AUTOCREATE_SOURCE_SESSION )
+ );
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testUserForCreation( $user, false )
+ );
+
+ $this->assertNull(
+ $provider->finishAccountCreation( $user, $user, AuthenticationResponse::newPass() )
+ );
+ $provider->autoCreatedAccount( $user, AuthManager::AUTOCREATE_SOURCE_SESSION );
+
+ $res = AuthenticationResponse::newPass();
+ $provider->postAuthentication( $user, $res );
+ $provider->postAccountCreation( $user, $user, $res );
+ $provider->postAccountLink( $user, $res );
+
+ $provider->expects( $this->once() )
+ ->method( 'testUserExists' )
+ ->with( $this->equalTo( 'foo' ) )
+ ->will( $this->returnValue( true ) );
+ $this->assertTrue( $provider->testUserCanAuthenticate( 'foo' ) );
+ }
+
+ public function testProviderRevokeAccessForUser() {
+ $reqs = [];
+ for ( $i = 0; $i < 3; $i++ ) {
+ $reqs[$i] = $this->createMock( AuthenticationRequest::class );
+ $reqs[$i]->done = false;
+ }
+
+ $provider = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
+ $provider->expects( $this->once() )->method( 'getAuthenticationRequests' )
+ ->with(
+ $this->identicalTo( AuthManager::ACTION_REMOVE ),
+ $this->identicalTo( [ 'username' => 'UTSysop' ] )
+ )
+ ->will( $this->returnValue( $reqs ) );
+ $provider->expects( $this->exactly( 3 ) )->method( 'providerChangeAuthenticationData' )
+ ->will( $this->returnCallback( function ( $req ) {
+ $this->assertSame( 'UTSysop', $req->username );
+ $this->assertFalse( $req->done );
+ $req->done = true;
+ } ) );
+
+ $provider->providerRevokeAccessForUser( 'UTSysop' );
+
+ foreach ( $reqs as $i => $req ) {
+ $this->assertTrue( $req->done, "#$i" );
+ }
+ }
+
+ /**
+ * @dataProvider providePrimaryAccountLink
+ * @param string $type PrimaryAuthenticationProvider::TYPE_* constant
+ * @param string $msg Error message from beginPrimaryAccountLink
+ */
+ public function testPrimaryAccountLink( $type, $msg ) {
+ $provider = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
+ $provider->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( $type ) );
+
+ $class = AbstractPrimaryAuthenticationProvider::class;
+ $msg1 = "{$class}::beginPrimaryAccountLink $msg";
+ $msg2 = "{$class}::continuePrimaryAccountLink is not implemented.";
+
+ $user = \User::newFromName( 'Whatever' );
+
+ try {
+ $provider->beginPrimaryAccountLink( $user, [] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \BadMethodCallException $ex ) {
+ $this->assertSame( $msg1, $ex->getMessage() );
+ }
+ try {
+ $provider->continuePrimaryAccountLink( $user, [] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \BadMethodCallException $ex ) {
+ $this->assertSame( $msg2, $ex->getMessage() );
+ }
+ }
+
+ public static function providePrimaryAccountLink() {
+ return [
+ [
+ PrimaryAuthenticationProvider::TYPE_NONE,
+ 'should not be called on a non-link provider.',
+ ],
+ [
+ PrimaryAuthenticationProvider::TYPE_CREATE,
+ 'should not be called on a non-link provider.',
+ ],
+ [
+ PrimaryAuthenticationProvider::TYPE_LINK,
+ 'is not implemented.',
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideProviderNormalizeUsername
+ */
+ public function testProviderNormalizeUsername( $name, $expect ) {
+ // fake interwiki map for the 'Interwiki prefix' testcase
+ $this->mergeMwGlobalArrayValue( 'wgHooks', [
+ 'InterwikiLoadPrefix' => [
+ function ( $prefix, &$iwdata ) {
+ if ( $prefix === 'interwiki' ) {
+ $iwdata = [
+ 'iw_url' => 'http://example.com/',
+ 'iw_local' => 0,
+ 'iw_trans' => 0,
+ ];
+ return false;
+ }
+ },
+ ],
+ ] );
+
+ $provider = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
+ $this->assertSame( $expect, $provider->providerNormalizeUsername( $name ) );
+ }
+
+ public static function provideProviderNormalizeUsername() {
+ return [
+ 'Leading space' => [ ' Leading space', 'Leading space' ],
+ 'Trailing space ' => [ 'Trailing space ', 'Trailing space' ],
+ 'Namespace prefix' => [ 'Talk:Username', null ],
+ 'Interwiki prefix' => [ 'interwiki:Username', null ],
+ 'With hash' => [ 'name with # hash', null ],
+ 'Multi spaces' => [ 'Multi spaces', 'Multi spaces' ],
+ 'Lowercase' => [ 'lowercase', 'Lowercase' ],
+ 'Invalid character' => [ 'in[]valid', null ],
+ 'With slash' => [ 'with / slash', null ],
+ 'Underscores' => [ '___under__scores___', 'Under scores' ],
+ ];
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/AbstractSecondaryAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/AbstractSecondaryAuthenticationProviderTest.php
new file mode 100644
index 00000000..41cf62ea
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/AbstractSecondaryAuthenticationProviderTest.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\AbstractSecondaryAuthenticationProvider
+ */
+class AbstractSecondaryAuthenticationProviderTest extends \MediaWikiTestCase {
+ public function testAbstractSecondaryAuthenticationProvider() {
+ $user = \User::newFromName( 'UTSysop' );
+
+ $provider = $this->getMockForAbstractClass( AbstractSecondaryAuthenticationProvider::class );
+
+ try {
+ $provider->continueSecondaryAuthentication( $user, [] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \BadMethodCallException $ex ) {
+ }
+
+ try {
+ $provider->continueSecondaryAccountCreation( $user, $user, [] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \BadMethodCallException $ex ) {
+ }
+
+ $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+
+ $this->assertTrue( $provider->providerAllowsPropertyChange( 'foo' ) );
+ $this->assertEquals(
+ \StatusValue::newGood( 'ignored' ),
+ $provider->providerAllowsAuthenticationDataChange( $req )
+ );
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testForAccountCreation( $user, $user, [] )
+ );
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testUserForCreation( $user, AuthManager::AUTOCREATE_SOURCE_SESSION )
+ );
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testUserForCreation( $user, false )
+ );
+
+ $provider->providerChangeAuthenticationData( $req );
+ $provider->autoCreatedAccount( $user, AuthManager::AUTOCREATE_SOURCE_SESSION );
+
+ $res = AuthenticationResponse::newPass();
+ $provider->postAuthentication( $user, $res );
+ $provider->postAccountCreation( $user, $user, $res );
+ }
+
+ public function testProviderRevokeAccessForUser() {
+ $reqs = [];
+ for ( $i = 0; $i < 3; $i++ ) {
+ $reqs[$i] = $this->createMock( AuthenticationRequest::class );
+ $reqs[$i]->done = false;
+ }
+
+ $provider = $this->getMockBuilder( AbstractSecondaryAuthenticationProvider::class )
+ ->setMethods( [ 'providerChangeAuthenticationData' ] )
+ ->getMockForAbstractClass();
+ $provider->expects( $this->once() )->method( 'getAuthenticationRequests' )
+ ->with(
+ $this->identicalTo( AuthManager::ACTION_REMOVE ),
+ $this->identicalTo( [ 'username' => 'UTSysop' ] )
+ )
+ ->will( $this->returnValue( $reqs ) );
+ $provider->expects( $this->exactly( 3 ) )->method( 'providerChangeAuthenticationData' )
+ ->will( $this->returnCallback( function ( $req ) {
+ $this->assertSame( 'UTSysop', $req->username );
+ $this->assertFalse( $req->done );
+ $req->done = true;
+ } ) );
+
+ $provider->providerRevokeAccessForUser( 'UTSysop' );
+
+ foreach ( $reqs as $i => $req ) {
+ $this->assertTrue( $req->done, "#$i" );
+ }
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/AuthManagerTest.php b/www/wiki/tests/phpunit/includes/auth/AuthManagerTest.php
new file mode 100644
index 00000000..cc162487
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/AuthManagerTest.php
@@ -0,0 +1,3629 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use Config;
+use MediaWiki\Session\SessionInfo;
+use MediaWiki\Session\UserInfo;
+use Psr\Log\LoggerInterface;
+use Psr\Log\LogLevel;
+use StatusValue;
+use WebRequest;
+use Wikimedia\ScopedCallback;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group AuthManager
+ * @group Database
+ * @covers MediaWiki\Auth\AuthManager
+ */
+class AuthManagerTest extends \MediaWikiTestCase {
+ /** @var WebRequest */
+ protected $request;
+ /** @var Config */
+ protected $config;
+ /** @var LoggerInterface */
+ protected $logger;
+
+ protected $preauthMocks = [];
+ protected $primaryauthMocks = [];
+ protected $secondaryauthMocks = [];
+
+ /** @var AuthManager */
+ protected $manager;
+ /** @var TestingAccessWrapper */
+ protected $managerPriv;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( [ 'wgAuth' => null ] );
+ $this->stashMwGlobals( [ 'wgHooks' ] );
+ }
+
+ /**
+ * Sets a mock on a hook
+ * @param string $hook
+ * @param object $expect From $this->once(), $this->never(), etc.
+ * @return object $mock->expects( $expect )->method( ... ).
+ */
+ protected function hook( $hook, $expect ) {
+ global $wgHooks;
+ $mock = $this->getMockBuilder( __CLASS__ )
+ ->setMethods( [ "on$hook" ] )
+ ->getMock();
+ $wgHooks[$hook] = [ $mock ];
+ return $mock->expects( $expect )->method( "on$hook" );
+ }
+
+ /**
+ * Unsets a hook
+ * @param string $hook
+ */
+ protected function unhook( $hook ) {
+ global $wgHooks;
+ $wgHooks[$hook] = [];
+ }
+
+ /**
+ * Ensure a value is a clean Message object
+ * @param string|Message $key
+ * @param array $params
+ * @return Message
+ */
+ protected function message( $key, $params = [] ) {
+ if ( $key === null ) {
+ return null;
+ }
+ if ( $key instanceof \MessageSpecifier ) {
+ $params = $key->getParams();
+ $key = $key->getKey();
+ }
+ return new \Message( $key, $params, \Language::factory( 'en' ) );
+ }
+
+ /**
+ * Initialize the AuthManagerConfig variable in $this->config
+ *
+ * Uses data from the various 'mocks' fields.
+ */
+ protected function initializeConfig() {
+ $config = [
+ 'preauth' => [
+ ],
+ 'primaryauth' => [
+ ],
+ 'secondaryauth' => [
+ ],
+ ];
+
+ foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
+ $key = $type . 'Mocks';
+ foreach ( $this->$key as $mock ) {
+ $config[$type][$mock->getUniqueId()] = [ 'factory' => function () use ( $mock ) {
+ return $mock;
+ } ];
+ }
+ }
+
+ $this->config->set( 'AuthManagerConfig', $config );
+ $this->config->set( 'LanguageCode', 'en' );
+ $this->config->set( 'NewUserLog', false );
+ }
+
+ /**
+ * Initialize $this->manager
+ * @param bool $regen Force a call to $this->initializeConfig()
+ */
+ protected function initializeManager( $regen = false ) {
+ if ( $regen || !$this->config ) {
+ $this->config = new \HashConfig();
+ }
+ if ( $regen || !$this->request ) {
+ $this->request = new \FauxRequest();
+ }
+ if ( !$this->logger ) {
+ $this->logger = new \TestLogger();
+ }
+
+ if ( $regen || !$this->config->has( 'AuthManagerConfig' ) ) {
+ $this->initializeConfig();
+ }
+ $this->manager = new AuthManager( $this->request, $this->config );
+ $this->manager->setLogger( $this->logger );
+ $this->managerPriv = TestingAccessWrapper::newFromObject( $this->manager );
+ }
+
+ /**
+ * Setup SessionManager with a mock session provider
+ * @param bool|null $canChangeUser If non-null, canChangeUser will be mocked to return this
+ * @param array $methods Additional methods to mock
+ * @return array (MediaWiki\Session\SessionProvider, ScopedCallback)
+ */
+ protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
+ if ( !$this->config ) {
+ $this->config = new \HashConfig();
+ $this->initializeConfig();
+ }
+ $this->config->set( 'ObjectCacheSessionExpiry', 100 );
+
+ $methods[] = '__toString';
+ $methods[] = 'describe';
+ if ( $canChangeUser !== null ) {
+ $methods[] = 'canChangeUser';
+ }
+ $provider = $this->getMockBuilder( \DummySessionProvider::class )
+ ->setMethods( $methods )
+ ->getMock();
+ $provider->expects( $this->any() )->method( '__toString' )
+ ->will( $this->returnValue( 'MockSessionProvider' ) );
+ $provider->expects( $this->any() )->method( 'describe' )
+ ->will( $this->returnValue( 'MockSessionProvider sessions' ) );
+ if ( $canChangeUser !== null ) {
+ $provider->expects( $this->any() )->method( 'canChangeUser' )
+ ->will( $this->returnValue( $canChangeUser ) );
+ }
+ $this->config->set( 'SessionProviders', [
+ [ 'factory' => function () use ( $provider ) {
+ return $provider;
+ } ],
+ ] );
+
+ $manager = new \MediaWiki\Session\SessionManager( [
+ 'config' => $this->config,
+ 'logger' => new \Psr\Log\NullLogger(),
+ 'store' => new \HashBagOStuff(),
+ ] );
+ TestingAccessWrapper::newFromObject( $manager )->getProvider( (string)$provider );
+
+ $reset = \MediaWiki\Session\TestUtils::setSessionManagerSingleton( $manager );
+
+ if ( $this->request ) {
+ $manager->getSessionForRequest( $this->request );
+ }
+
+ return [ $provider, $reset ];
+ }
+
+ public function testSingleton() {
+ // Temporarily clear out the global singleton, if any, to test creating
+ // one.
+ $rProp = new \ReflectionProperty( AuthManager::class, 'instance' );
+ $rProp->setAccessible( true );
+ $old = $rProp->getValue();
+ $cb = new ScopedCallback( [ $rProp, 'setValue' ], [ $old ] );
+ $rProp->setValue( null );
+
+ $singleton = AuthManager::singleton();
+ $this->assertInstanceOf( AuthManager::class, AuthManager::singleton() );
+ $this->assertSame( $singleton, AuthManager::singleton() );
+ $this->assertSame( \RequestContext::getMain()->getRequest(), $singleton->getRequest() );
+ $this->assertSame(
+ \RequestContext::getMain()->getConfig(),
+ TestingAccessWrapper::newFromObject( $singleton )->config
+ );
+ }
+
+ public function testCanAuthenticateNow() {
+ $this->initializeManager();
+
+ list( $provider, $reset ) = $this->getMockSessionProvider( false );
+ $this->assertFalse( $this->manager->canAuthenticateNow() );
+ ScopedCallback::consume( $reset );
+
+ list( $provider, $reset ) = $this->getMockSessionProvider( true );
+ $this->assertTrue( $this->manager->canAuthenticateNow() );
+ ScopedCallback::consume( $reset );
+ }
+
+ public function testNormalizeUsername() {
+ $mocks = [
+ $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
+ $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
+ $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
+ $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
+ ];
+ foreach ( $mocks as $key => $mock ) {
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
+ }
+ $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
+ ->with( $this->identicalTo( 'XYZ' ) )
+ ->willReturn( 'Foo' );
+ $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
+ ->with( $this->identicalTo( 'XYZ' ) )
+ ->willReturn( 'Foo' );
+ $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
+ ->with( $this->identicalTo( 'XYZ' ) )
+ ->willReturn( null );
+ $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
+ ->with( $this->identicalTo( 'XYZ' ) )
+ ->willReturn( 'Bar!' );
+
+ $this->primaryauthMocks = $mocks;
+
+ $this->initializeManager();
+
+ $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager->normalizeUsername( 'XYZ' ) );
+ }
+
+ /**
+ * @dataProvider provideSecuritySensitiveOperationStatus
+ * @param bool $mutableSession
+ */
+ public function testSecuritySensitiveOperationStatus( $mutableSession ) {
+ $this->logger = new \Psr\Log\NullLogger();
+ $user = \User::newFromName( 'UTSysop' );
+ $provideUser = null;
+ $reauth = $mutableSession ? AuthManager::SEC_REAUTH : AuthManager::SEC_FAIL;
+
+ list( $provider, $reset ) = $this->getMockSessionProvider(
+ $mutableSession, [ 'provideSessionInfo' ]
+ );
+ $provider->expects( $this->any() )->method( 'provideSessionInfo' )
+ ->will( $this->returnCallback( function () use ( $provider, &$provideUser ) {
+ return new SessionInfo( SessionInfo::MIN_PRIORITY, [
+ 'provider' => $provider,
+ 'id' => \DummySessionProvider::ID,
+ 'persisted' => true,
+ 'userInfo' => UserInfo::newFromUser( $provideUser, true )
+ ] );
+ } ) );
+ $this->initializeManager();
+
+ $this->config->set( 'ReauthenticateTime', [] );
+ $this->config->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [] );
+ $provideUser = new \User;
+ $session = $provider->getManager()->getSessionForRequest( $this->request );
+ $this->assertSame( 0, $session->getUser()->getId(), 'sanity check' );
+
+ // Anonymous user => reauth
+ $session->set( 'AuthManager:lastAuthId', 0 );
+ $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
+ $this->assertSame( $reauth, $this->manager->securitySensitiveOperationStatus( 'foo' ) );
+
+ $provideUser = $user;
+ $session = $provider->getManager()->getSessionForRequest( $this->request );
+ $this->assertSame( $user->getId(), $session->getUser()->getId(), 'sanity check' );
+
+ // Error for no default (only gets thrown for non-anonymous user)
+ $session->set( 'AuthManager:lastAuthId', $user->getId() + 1 );
+ $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
+ try {
+ $this->manager->securitySensitiveOperationStatus( 'foo' );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame(
+ $mutableSession
+ ? '$wgReauthenticateTime lacks a default'
+ : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
+ $ex->getMessage()
+ );
+ }
+
+ if ( $mutableSession ) {
+ $this->config->set( 'ReauthenticateTime', [
+ 'test' => 100,
+ 'test2' => -1,
+ 'default' => 10,
+ ] );
+
+ // Mismatched user ID
+ $session->set( 'AuthManager:lastAuthId', $user->getId() + 1 );
+ $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
+ $this->assertSame(
+ AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
+ );
+ $this->assertSame(
+ AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
+ );
+ $this->assertSame(
+ AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
+ );
+
+ // Missing time
+ $session->set( 'AuthManager:lastAuthId', $user->getId() );
+ $session->set( 'AuthManager:lastAuthTimestamp', null );
+ $this->assertSame(
+ AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
+ );
+ $this->assertSame(
+ AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
+ );
+ $this->assertSame(
+ AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
+ );
+
+ // Recent enough to pass
+ $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
+ $this->assertSame(
+ AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
+ );
+
+ // Not recent enough to pass
+ $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
+ $this->assertSame(
+ AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
+ );
+ // But recent enough for the 'test' operation
+ $this->assertSame(
+ AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test' )
+ );
+ } else {
+ $this->config->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [
+ 'test' => false,
+ 'default' => true,
+ ] );
+
+ $this->assertEquals(
+ AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
+ );
+
+ $this->assertEquals(
+ AuthManager::SEC_FAIL, $this->manager->securitySensitiveOperationStatus( 'test' )
+ );
+ }
+
+ // Test hook, all three possible values
+ foreach ( [
+ AuthManager::SEC_OK => AuthManager::SEC_OK,
+ AuthManager::SEC_REAUTH => $reauth,
+ AuthManager::SEC_FAIL => AuthManager::SEC_FAIL,
+ ] as $hook => $expect ) {
+ $this->hook( 'SecuritySensitiveOperationStatus', $this->exactly( 2 ) )
+ ->with(
+ $this->anything(),
+ $this->anything(),
+ $this->callback( function ( $s ) use ( $session ) {
+ return $s->getId() === $session->getId();
+ } ),
+ $mutableSession ? $this->equalTo( 500, 1 ) : $this->equalTo( -1 )
+ )
+ ->will( $this->returnCallback( function ( &$v ) use ( $hook ) {
+ $v = $hook;
+ return true;
+ } ) );
+ $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
+ $this->assertEquals(
+ $expect, $this->manager->securitySensitiveOperationStatus( 'test' ), "hook $hook"
+ );
+ $this->assertEquals(
+ $expect, $this->manager->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
+ );
+ $this->unhook( 'SecuritySensitiveOperationStatus' );
+ }
+
+ ScopedCallback::consume( $reset );
+ }
+
+ public function onSecuritySensitiveOperationStatus( &$status, $operation, $session, $time ) {
+ }
+
+ public static function provideSecuritySensitiveOperationStatus() {
+ return [
+ [ true ],
+ [ false ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideUserCanAuthenticate
+ * @param bool $primary1Can
+ * @param bool $primary2Can
+ * @param bool $expect
+ */
+ public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
+ $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock1->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( 'primary1' ) );
+ $mock1->expects( $this->any() )->method( 'testUserCanAuthenticate' )
+ ->with( $this->equalTo( 'UTSysop' ) )
+ ->will( $this->returnValue( $primary1Can ) );
+ $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock2->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( 'primary2' ) );
+ $mock2->expects( $this->any() )->method( 'testUserCanAuthenticate' )
+ ->with( $this->equalTo( 'UTSysop' ) )
+ ->will( $this->returnValue( $primary2Can ) );
+ $this->primaryauthMocks = [ $mock1, $mock2 ];
+
+ $this->initializeManager( true );
+ $this->assertSame( $expect, $this->manager->userCanAuthenticate( 'UTSysop' ) );
+ }
+
+ public static function provideUserCanAuthenticate() {
+ return [
+ [ false, false, false ],
+ [ true, false, true ],
+ [ false, true, true ],
+ [ true, true, true ],
+ ];
+ }
+
+ public function testRevokeAccessForUser() {
+ $this->initializeManager();
+
+ $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( 'primary' ) );
+ $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
+ ->with( $this->equalTo( 'UTSysop' ) );
+ $this->primaryauthMocks = [ $mock ];
+
+ $this->initializeManager( true );
+ $this->logger->setCollect( true );
+
+ $this->manager->revokeAccessForUser( 'UTSysop' );
+
+ $this->assertSame( [
+ [ LogLevel::INFO, 'Revoking access for {user}' ],
+ ], $this->logger->getBuffer() );
+ }
+
+ public function testProviderCreation() {
+ $mocks = [
+ 'pre' => $this->getMockForAbstractClass( PreAuthenticationProvider::class ),
+ 'primary' => $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
+ 'secondary' => $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class ),
+ ];
+ foreach ( $mocks as $key => $mock ) {
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
+ $mock->expects( $this->once() )->method( 'setLogger' );
+ $mock->expects( $this->once() )->method( 'setManager' );
+ $mock->expects( $this->once() )->method( 'setConfig' );
+ }
+ $this->preauthMocks = [ $mocks['pre'] ];
+ $this->primaryauthMocks = [ $mocks['primary'] ];
+ $this->secondaryauthMocks = [ $mocks['secondary'] ];
+
+ // Normal operation
+ $this->initializeManager();
+ $this->assertSame(
+ $mocks['primary'],
+ $this->managerPriv->getAuthenticationProvider( 'primary' )
+ );
+ $this->assertSame(
+ $mocks['secondary'],
+ $this->managerPriv->getAuthenticationProvider( 'secondary' )
+ );
+ $this->assertSame(
+ $mocks['pre'],
+ $this->managerPriv->getAuthenticationProvider( 'pre' )
+ );
+ $this->assertSame(
+ [ 'pre' => $mocks['pre'] ],
+ $this->managerPriv->getPreAuthenticationProviders()
+ );
+ $this->assertSame(
+ [ 'primary' => $mocks['primary'] ],
+ $this->managerPriv->getPrimaryAuthenticationProviders()
+ );
+ $this->assertSame(
+ [ 'secondary' => $mocks['secondary'] ],
+ $this->managerPriv->getSecondaryAuthenticationProviders()
+ );
+
+ // Duplicate IDs
+ $mock1 = $this->getMockForAbstractClass( PreAuthenticationProvider::class );
+ $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+ $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+ $this->preauthMocks = [ $mock1 ];
+ $this->primaryauthMocks = [ $mock2 ];
+ $this->secondaryauthMocks = [];
+ $this->initializeManager( true );
+ try {
+ $this->managerPriv->getAuthenticationProvider( 'Y' );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \RuntimeException $ex ) {
+ $class1 = get_class( $mock1 );
+ $class2 = get_class( $mock2 );
+ $this->assertSame(
+ "Duplicate specifications for id X (classes $class1 and $class2)", $ex->getMessage()
+ );
+ }
+
+ // Wrong classes
+ $mock = $this->getMockForAbstractClass( AuthenticationProvider::class );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+ $class = get_class( $mock );
+ $this->preauthMocks = [ $mock ];
+ $this->primaryauthMocks = [ $mock ];
+ $this->secondaryauthMocks = [ $mock ];
+ $this->initializeManager( true );
+ try {
+ $this->managerPriv->getPreAuthenticationProviders();
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \RuntimeException $ex ) {
+ $this->assertSame(
+ "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
+ $ex->getMessage()
+ );
+ }
+ try {
+ $this->managerPriv->getPrimaryAuthenticationProviders();
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \RuntimeException $ex ) {
+ $this->assertSame(
+ "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
+ $ex->getMessage()
+ );
+ }
+ try {
+ $this->managerPriv->getSecondaryAuthenticationProviders();
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \RuntimeException $ex ) {
+ $this->assertSame(
+ "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
+ $ex->getMessage()
+ );
+ }
+
+ // Sorting
+ $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock3 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
+ $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
+ $mock3->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'C' ) );
+ $this->preauthMocks = [];
+ $this->primaryauthMocks = [ $mock1, $mock2, $mock3 ];
+ $this->secondaryauthMocks = [];
+ $this->initializeConfig();
+ $config = $this->config->get( 'AuthManagerConfig' );
+
+ $this->initializeManager( false );
+ $this->assertSame(
+ [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
+ $this->managerPriv->getPrimaryAuthenticationProviders(),
+ 'sanity check'
+ );
+
+ $config['primaryauth']['A']['sort'] = 100;
+ $config['primaryauth']['C']['sort'] = -1;
+ $this->config->set( 'AuthManagerConfig', $config );
+ $this->initializeManager( false );
+ $this->assertSame(
+ [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
+ $this->managerPriv->getPrimaryAuthenticationProviders()
+ );
+ }
+
+ public function testSetDefaultUserOptions() {
+ $this->initializeManager();
+
+ $context = \RequestContext::getMain();
+ $reset = new ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
+ $context->setLanguage( 'de' );
+ $this->setMwGlobals( 'wgContLang', \Language::factory( 'zh' ) );
+
+ $user = \User::newFromName( self::usernameForCreation() );
+ $user->addToDatabase();
+ $oldToken = $user->getToken();
+ $this->managerPriv->setDefaultUserOptions( $user, false );
+ $user->saveSettings();
+ $this->assertNotEquals( $oldToken, $user->getToken() );
+ $this->assertSame( 'zh', $user->getOption( 'language' ) );
+ $this->assertSame( 'zh', $user->getOption( 'variant' ) );
+
+ $user = \User::newFromName( self::usernameForCreation() );
+ $user->addToDatabase();
+ $oldToken = $user->getToken();
+ $this->managerPriv->setDefaultUserOptions( $user, true );
+ $user->saveSettings();
+ $this->assertNotEquals( $oldToken, $user->getToken() );
+ $this->assertSame( 'de', $user->getOption( 'language' ) );
+ $this->assertSame( 'zh', $user->getOption( 'variant' ) );
+
+ $this->setMwGlobals( 'wgContLang', \Language::factory( 'fr' ) );
+
+ $user = \User::newFromName( self::usernameForCreation() );
+ $user->addToDatabase();
+ $oldToken = $user->getToken();
+ $this->managerPriv->setDefaultUserOptions( $user, true );
+ $user->saveSettings();
+ $this->assertNotEquals( $oldToken, $user->getToken() );
+ $this->assertSame( 'de', $user->getOption( 'language' ) );
+ $this->assertSame( null, $user->getOption( 'variant' ) );
+ }
+
+ public function testForcePrimaryAuthenticationProviders() {
+ $mockA = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mockB = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mockB2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mockA->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
+ $mockB->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
+ $mockB2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
+ $this->primaryauthMocks = [ $mockA ];
+
+ $this->logger = new \TestLogger( true );
+
+ // Test without first initializing the configured providers
+ $this->initializeManager();
+ $this->manager->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
+ $this->assertSame(
+ [ 'B' => $mockB ], $this->managerPriv->getPrimaryAuthenticationProviders()
+ );
+ $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'A' ) );
+ $this->assertSame( $mockB, $this->managerPriv->getAuthenticationProvider( 'B' ) );
+ $this->assertSame( [
+ [ LogLevel::WARNING, 'Overriding AuthManager primary authn because testing' ],
+ ], $this->logger->getBuffer() );
+ $this->logger->clearBuffer();
+
+ // Test with first initializing the configured providers
+ $this->initializeManager();
+ $this->assertSame( $mockA, $this->managerPriv->getAuthenticationProvider( 'A' ) );
+ $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'B' ) );
+ $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
+ $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
+ $this->manager->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
+ $this->assertSame(
+ [ 'B' => $mockB ], $this->managerPriv->getPrimaryAuthenticationProviders()
+ );
+ $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'A' ) );
+ $this->assertSame( $mockB, $this->managerPriv->getAuthenticationProvider( 'B' ) );
+ $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
+ $this->assertNull(
+ $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
+ );
+ $this->assertSame( [
+ [ LogLevel::WARNING, 'Overriding AuthManager primary authn because testing' ],
+ [
+ LogLevel::WARNING,
+ 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
+ ],
+ ], $this->logger->getBuffer() );
+ $this->logger->clearBuffer();
+
+ // Test duplicate IDs
+ $this->initializeManager();
+ try {
+ $this->manager->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \RuntimeException $ex ) {
+ $class1 = get_class( $mockB );
+ $class2 = get_class( $mockB2 );
+ $this->assertSame(
+ "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
+ );
+ }
+
+ // Wrong classes
+ $mock = $this->getMockForAbstractClass( AuthenticationProvider::class );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+ $class = get_class( $mock );
+ try {
+ $this->manager->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \RuntimeException $ex ) {
+ $this->assertSame(
+ "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
+ $ex->getMessage()
+ );
+ }
+ }
+
+ public function testBeginAuthentication() {
+ $this->initializeManager();
+
+ // Immutable session
+ list( $provider, $reset ) = $this->getMockSessionProvider( false );
+ $this->hook( 'UserLoggedIn', $this->never() );
+ $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
+ try {
+ $this->manager->beginAuthentication( [], 'http://localhost/' );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \LogicException $ex ) {
+ $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
+ }
+ $this->unhook( 'UserLoggedIn' );
+ $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
+ ScopedCallback::consume( $reset );
+ $this->initializeManager( true );
+
+ // CreatedAccountAuthenticationRequest
+ $user = \User::newFromName( 'UTSysop' );
+ $reqs = [
+ new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
+ ];
+ $this->hook( 'UserLoggedIn', $this->never() );
+ try {
+ $this->manager->beginAuthentication( $reqs, 'http://localhost/' );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \LogicException $ex ) {
+ $this->assertSame(
+ 'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
+ 'that created the account',
+ $ex->getMessage()
+ );
+ }
+ $this->unhook( 'UserLoggedIn' );
+
+ $this->request->getSession()->clear();
+ $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
+ $this->managerPriv->createdAccountAuthenticationRequests = [ $reqs[0] ];
+ $this->hook( 'UserLoggedIn', $this->once() )
+ ->with( $this->callback( function ( $u ) use ( $user ) {
+ return $user->getId() === $u->getId() && $user->getName() === $u->getName();
+ } ) );
+ $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
+ $this->logger->setCollect( true );
+ $ret = $this->manager->beginAuthentication( $reqs, 'http://localhost/' );
+ $this->logger->setCollect( false );
+ $this->unhook( 'UserLoggedIn' );
+ $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
+ $this->assertSame( AuthenticationResponse::PASS, $ret->status );
+ $this->assertSame( $user->getName(), $ret->username );
+ $this->assertSame( $user->getId(), $this->request->getSessionData( 'AuthManager:lastAuthId' ) );
+ $this->assertEquals(
+ time(), $this->request->getSessionData( 'AuthManager:lastAuthTimestamp' ),
+ 'timestamp ±1', 1
+ );
+ $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
+ $this->assertSame( $user->getId(), $this->request->getSession()->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::INFO, 'Logging in {user} after account creation' ],
+ ], $this->logger->getBuffer() );
+ }
+
+ public function testCreateFromLogin() {
+ $user = \User::newFromName( 'UTSysop' );
+ $req1 = $this->createMock( AuthenticationRequest::class );
+ $req2 = $this->createMock( AuthenticationRequest::class );
+ $req3 = $this->createMock( AuthenticationRequest::class );
+ $userReq = new UsernameAuthenticationRequest;
+ $userReq->username = 'UTDummy';
+
+ $req1->returnToUrl = 'http://localhost/';
+ $req2->returnToUrl = 'http://localhost/';
+ $req3->returnToUrl = 'http://localhost/';
+ $req3->username = 'UTDummy';
+ $userReq->returnToUrl = 'http://localhost/';
+
+ // Passing one into beginAuthentication(), and an immediate FAIL
+ $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
+ $this->primaryauthMocks = [ $primary ];
+ $this->initializeManager( true );
+ $res = AuthenticationResponse::newFail( wfMessage( 'foo' ) );
+ $res->createRequest = $req1;
+ $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
+ ->will( $this->returnValue( $res ) );
+ $createReq = new CreateFromLoginAuthenticationRequest(
+ null, [ $req2->getUniqueId() => $req2 ]
+ );
+ $this->logger->setCollect( true );
+ $ret = $this->manager->beginAuthentication( [ $createReq ], 'http://localhost/' );
+ $this->logger->setCollect( false );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->createRequest );
+ $this->assertSame( $req1, $ret->createRequest->createRequest );
+ $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest->maybeLink );
+
+ // UI, then FAIL in beginAuthentication()
+ $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider::class )
+ ->setMethods( [ 'continuePrimaryAuthentication' ] )
+ ->getMockForAbstractClass();
+ $this->primaryauthMocks = [ $primary ];
+ $this->initializeManager( true );
+ $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
+ ->will( $this->returnValue(
+ AuthenticationResponse::newUI( [ $req1 ], wfMessage( 'foo' ) )
+ ) );
+ $res = AuthenticationResponse::newFail( wfMessage( 'foo' ) );
+ $res->createRequest = $req2;
+ $primary->expects( $this->any() )->method( 'continuePrimaryAuthentication' )
+ ->will( $this->returnValue( $res ) );
+ $this->logger->setCollect( true );
+ $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
+ $this->assertSame( AuthenticationResponse::UI, $ret->status, 'sanity check' );
+ $ret = $this->manager->continueAuthentication( [] );
+ $this->logger->setCollect( false );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->createRequest );
+ $this->assertSame( $req2, $ret->createRequest->createRequest );
+ $this->assertEquals( [], $ret->createRequest->maybeLink );
+
+ // Pass into beginAccountCreation(), see that maybeLink and createRequest get copied
+ $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
+ $this->primaryauthMocks = [ $primary ];
+ $this->initializeManager( true );
+ $createReq = new CreateFromLoginAuthenticationRequest( $req3, [ $req2 ] );
+ $createReq->returnToUrl = 'http://localhost/';
+ $createReq->username = 'UTDummy';
+ $res = AuthenticationResponse::newUI( [ $req1 ], wfMessage( 'foo' ) );
+ $primary->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
+ ->with( $this->anything(), $this->anything(), [ $userReq, $createReq, $req3 ] )
+ ->will( $this->returnValue( $res ) );
+ $primary->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+ $this->logger->setCollect( true );
+ $ret = $this->manager->beginAccountCreation(
+ $user, [ $userReq, $createReq ], 'http://localhost/'
+ );
+ $this->logger->setCollect( false );
+ $this->assertSame( AuthenticationResponse::UI, $ret->status );
+ $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
+ $this->assertNotNull( $state );
+ $this->assertEquals( [ $userReq, $createReq, $req3 ], $state['reqs'] );
+ $this->assertEquals( [ $req2 ], $state['maybeLink'] );
+ }
+
+ /**
+ * @dataProvider provideAuthentication
+ * @param StatusValue $preResponse
+ * @param array $primaryResponses
+ * @param array $secondaryResponses
+ * @param array $managerResponses
+ * @param bool $link Whether the primary authentication provider is a "link" provider
+ */
+ public function testAuthentication(
+ StatusValue $preResponse, array $primaryResponses, array $secondaryResponses,
+ array $managerResponses, $link = false
+ ) {
+ $this->initializeManager();
+ $user = \User::newFromName( 'UTSysop' );
+ $id = $user->getId();
+ $name = $user->getName();
+
+ // Set up lots of mocks...
+ $req = new RememberMeAuthenticationRequest;
+ $req->rememberMe = (bool)rand( 0, 1 );
+ $req->pre = $preResponse;
+ $req->primary = $primaryResponses;
+ $req->secondary = $secondaryResponses;
+ $mocks = [];
+ foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
+ $class = ucfirst( $key ) . 'AuthenticationProvider';
+ $mocks[$key] = $this->getMockForAbstractClass(
+ "MediaWiki\\Auth\\$class", [], "Mock$class"
+ );
+ $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( $key ) );
+ $mocks[$key . '2'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
+ $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( $key . '2' ) );
+ $mocks[$key . '3'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
+ $mocks[$key . '3']->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( $key . '3' ) );
+ }
+ foreach ( $mocks as $mock ) {
+ $mock->expects( $this->any() )->method( 'getAuthenticationRequests' )
+ ->will( $this->returnValue( [] ) );
+ }
+
+ $mocks['pre']->expects( $this->once() )->method( 'testForAuthentication' )
+ ->will( $this->returnCallback( function ( $reqs ) use ( $req ) {
+ $this->assertContains( $req, $reqs );
+ return $req->pre;
+ } ) );
+
+ $ct = count( $req->primary );
+ $callback = $this->returnCallback( function ( $reqs ) use ( $req ) {
+ $this->assertContains( $req, $reqs );
+ return array_shift( $req->primary );
+ } );
+ $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
+ ->method( 'beginPrimaryAuthentication' )
+ ->will( $callback );
+ $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
+ ->method( 'continuePrimaryAuthentication' )
+ ->will( $callback );
+ if ( $link ) {
+ $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
+ }
+
+ $ct = count( $req->secondary );
+ $callback = $this->returnCallback( function ( $user, $reqs ) use ( $id, $name, $req ) {
+ $this->assertSame( $id, $user->getId() );
+ $this->assertSame( $name, $user->getName() );
+ $this->assertContains( $req, $reqs );
+ return array_shift( $req->secondary );
+ } );
+ $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
+ ->method( 'beginSecondaryAuthentication' )
+ ->will( $callback );
+ $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
+ ->method( 'continueSecondaryAuthentication' )
+ ->will( $callback );
+
+ $abstain = AuthenticationResponse::newAbstain();
+ $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAuthentication' )
+ ->will( $this->returnValue( StatusValue::newGood() ) );
+ $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAuthentication' )
+ ->will( $this->returnValue( $abstain ) );
+ $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAuthentication' );
+ $mocks['secondary2']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
+ ->will( $this->returnValue( $abstain ) );
+ $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
+ $mocks['secondary3']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
+ ->will( $this->returnValue( $abstain ) );
+ $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
+
+ $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
+ $this->primaryauthMocks = [ $mocks['primary'], $mocks['primary2'] ];
+ $this->secondaryauthMocks = [
+ $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2'],
+ // So linking happens
+ new ConfirmLinkSecondaryAuthenticationProvider,
+ ];
+ $this->initializeManager( true );
+ $this->logger->setCollect( true );
+
+ $constraint = \PHPUnit_Framework_Assert::logicalOr(
+ $this->equalTo( AuthenticationResponse::PASS ),
+ $this->equalTo( AuthenticationResponse::FAIL )
+ );
+ $providers = array_filter(
+ array_merge(
+ $this->preauthMocks, $this->primaryauthMocks, $this->secondaryauthMocks
+ ),
+ function ( $p ) {
+ return is_callable( [ $p, 'expects' ] );
+ }
+ );
+ foreach ( $providers as $p ) {
+ $p->postCalled = false;
+ $p->expects( $this->atMost( 1 ) )->method( 'postAuthentication' )
+ ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
+ if ( $user !== null ) {
+ $this->assertInstanceOf( \User::class, $user );
+ $this->assertSame( 'UTSysop', $user->getName() );
+ }
+ $this->assertInstanceOf( AuthenticationResponse::class, $response );
+ $this->assertThat( $response->status, $constraint );
+ $p->postCalled = $response->status;
+ } );
+ }
+
+ $session = $this->request->getSession();
+ $session->setRememberUser( !$req->rememberMe );
+
+ foreach ( $managerResponses as $i => $response ) {
+ $success = $response instanceof AuthenticationResponse &&
+ $response->status === AuthenticationResponse::PASS;
+ if ( $success ) {
+ $this->hook( 'UserLoggedIn', $this->once() )
+ ->with( $this->callback( function ( $user ) use ( $id, $name ) {
+ return $user->getId() === $id && $user->getName() === $name;
+ } ) );
+ } else {
+ $this->hook( 'UserLoggedIn', $this->never() );
+ }
+ if ( $success || (
+ $response instanceof AuthenticationResponse &&
+ $response->status === AuthenticationResponse::FAIL &&
+ $response->message->getKey() !== 'authmanager-authn-not-in-progress' &&
+ $response->message->getKey() !== 'authmanager-authn-no-primary'
+ )
+ ) {
+ $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
+ } else {
+ $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->never() );
+ }
+
+ $ex = null;
+ try {
+ if ( !$i ) {
+ $ret = $this->manager->beginAuthentication( [ $req ], 'http://localhost/' );
+ } else {
+ $ret = $this->manager->continueAuthentication( [ $req ] );
+ }
+ if ( $response instanceof \Exception ) {
+ $this->fail( 'Expected exception not thrown', "Response $i" );
+ }
+ } catch ( \Exception $ex ) {
+ if ( !$response instanceof \Exception ) {
+ throw $ex;
+ }
+ $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
+ $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
+ "Response $i, exception, session state" );
+ $this->unhook( 'UserLoggedIn' );
+ $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
+ return;
+ }
+
+ $this->unhook( 'UserLoggedIn' );
+ $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
+
+ $this->assertSame( 'http://localhost/', $req->returnToUrl );
+
+ $ret->message = $this->message( $ret->message );
+ $this->assertEquals( $response, $ret, "Response $i, response" );
+ if ( $success ) {
+ $this->assertSame( $id, $session->getUser()->getId(),
+ "Response $i, authn" );
+ } else {
+ $this->assertSame( 0, $session->getUser()->getId(),
+ "Response $i, authn" );
+ }
+ if ( $success || $response->status === AuthenticationResponse::FAIL ) {
+ $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
+ "Response $i, session state" );
+ foreach ( $providers as $p ) {
+ $this->assertSame( $response->status, $p->postCalled,
+ "Response $i, post-auth callback called" );
+ }
+ } else {
+ $this->assertNotNull( $session->getSecret( 'AuthManager::authnState' ),
+ "Response $i, session state" );
+ foreach ( $ret->neededRequests as $neededReq ) {
+ $this->assertEquals( AuthManager::ACTION_LOGIN, $neededReq->action,
+ "Response $i, neededRequest action" );
+ }
+ $this->assertEquals(
+ $ret->neededRequests,
+ $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN_CONTINUE ),
+ "Response $i, continuation check"
+ );
+ foreach ( $providers as $p ) {
+ $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
+ }
+ }
+
+ $state = $session->getSecret( 'AuthManager::authnState' );
+ $maybeLink = isset( $state['maybeLink'] ) ? $state['maybeLink'] : [];
+ if ( $link && $response->status === AuthenticationResponse::RESTART ) {
+ $this->assertEquals(
+ $response->createRequest->maybeLink,
+ $maybeLink,
+ "Response $i, maybeLink"
+ );
+ } else {
+ $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
+ }
+ }
+
+ if ( $success ) {
+ $this->assertSame( $req->rememberMe, $session->shouldRememberUser(),
+ 'rememberMe checkbox had effect' );
+ } else {
+ $this->assertNotSame( $req->rememberMe, $session->shouldRememberUser(),
+ 'rememberMe checkbox wasn\'t applied' );
+ }
+ }
+
+ public function provideAuthentication() {
+ $rememberReq = new RememberMeAuthenticationRequest;
+ $rememberReq->action = AuthManager::ACTION_LOGIN;
+
+ $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+ $req->foobar = 'baz';
+ $restartResponse = AuthenticationResponse::newRestart(
+ $this->message( 'authmanager-authn-no-local-user' )
+ );
+ $restartResponse->neededRequests = [ $rememberReq ];
+
+ $restartResponse2Pass = AuthenticationResponse::newPass( null );
+ $restartResponse2Pass->linkRequest = $req;
+ $restartResponse2 = AuthenticationResponse::newRestart(
+ $this->message( 'authmanager-authn-no-local-user-link' )
+ );
+ $restartResponse2->createRequest = new CreateFromLoginAuthenticationRequest(
+ null, [ $req->getUniqueId() => $req ]
+ );
+ $restartResponse2->createRequest->action = AuthManager::ACTION_LOGIN;
+ $restartResponse2->neededRequests = [ $rememberReq, $restartResponse2->createRequest ];
+
+ $userName = 'UTSysop';
+
+ return [
+ 'Failure in pre-auth' => [
+ StatusValue::newFatal( 'fail-from-pre' ),
+ [],
+ [],
+ [
+ AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
+ AuthenticationResponse::newFail(
+ $this->message( 'authmanager-authn-not-in-progress' )
+ ),
+ ]
+ ],
+ 'Failure in primary' => [
+ StatusValue::newGood(),
+ $tmp = [
+ AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
+ ],
+ [],
+ $tmp
+ ],
+ 'All primary abstain' => [
+ StatusValue::newGood(),
+ [
+ AuthenticationResponse::newAbstain(),
+ ],
+ [],
+ [
+ AuthenticationResponse::newFail( $this->message( 'authmanager-authn-no-primary' ) )
+ ]
+ ],
+ 'Primary UI, then redirect, then fail' => [
+ StatusValue::newGood(),
+ $tmp = [
+ AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+ AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
+ AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
+ ],
+ [],
+ $tmp
+ ],
+ 'Primary redirect, then abstain' => [
+ StatusValue::newGood(),
+ [
+ $tmp = AuthenticationResponse::newRedirect(
+ [ $req ], '/foo.html', [ 'foo' => 'bar' ]
+ ),
+ AuthenticationResponse::newAbstain(),
+ ],
+ [],
+ [
+ $tmp,
+ new \DomainException(
+ 'MockPrimaryAuthenticationProvider::continuePrimaryAuthentication() returned ABSTAIN'
+ )
+ ]
+ ],
+ 'Primary UI, then pass with no local user' => [
+ StatusValue::newGood(),
+ [
+ $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+ AuthenticationResponse::newPass( null ),
+ ],
+ [],
+ [
+ $tmp,
+ $restartResponse,
+ ]
+ ],
+ 'Primary UI, then pass with no local user (link type)' => [
+ StatusValue::newGood(),
+ [
+ $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+ $restartResponse2Pass,
+ ],
+ [],
+ [
+ $tmp,
+ $restartResponse2,
+ ],
+ true
+ ],
+ 'Primary pass with invalid username' => [
+ StatusValue::newGood(),
+ [
+ AuthenticationResponse::newPass( '<>' ),
+ ],
+ [],
+ [
+ new \DomainException( 'MockPrimaryAuthenticationProvider returned an invalid username: <>' ),
+ ]
+ ],
+ 'Secondary fail' => [
+ StatusValue::newGood(),
+ [
+ AuthenticationResponse::newPass( $userName ),
+ ],
+ $tmp = [
+ AuthenticationResponse::newFail( $this->message( 'fail-in-secondary' ) ),
+ ],
+ $tmp
+ ],
+ 'Secondary UI, then abstain' => [
+ StatusValue::newGood(),
+ [
+ AuthenticationResponse::newPass( $userName ),
+ ],
+ [
+ $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+ AuthenticationResponse::newAbstain()
+ ],
+ [
+ $tmp,
+ AuthenticationResponse::newPass( $userName ),
+ ]
+ ],
+ 'Secondary pass' => [
+ StatusValue::newGood(),
+ [
+ AuthenticationResponse::newPass( $userName ),
+ ],
+ [
+ AuthenticationResponse::newPass()
+ ],
+ [
+ AuthenticationResponse::newPass( $userName ),
+ ]
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideUserExists
+ * @param bool $primary1Exists
+ * @param bool $primary2Exists
+ * @param bool $expect
+ */
+ public function testUserExists( $primary1Exists, $primary2Exists, $expect ) {
+ $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock1->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( 'primary1' ) );
+ $mock1->expects( $this->any() )->method( 'testUserExists' )
+ ->with( $this->equalTo( 'UTSysop' ) )
+ ->will( $this->returnValue( $primary1Exists ) );
+ $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock2->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( 'primary2' ) );
+ $mock2->expects( $this->any() )->method( 'testUserExists' )
+ ->with( $this->equalTo( 'UTSysop' ) )
+ ->will( $this->returnValue( $primary2Exists ) );
+ $this->primaryauthMocks = [ $mock1, $mock2 ];
+
+ $this->initializeManager( true );
+ $this->assertSame( $expect, $this->manager->userExists( 'UTSysop' ) );
+ }
+
+ public static function provideUserExists() {
+ return [
+ [ false, false, false ],
+ [ true, false, true ],
+ [ false, true, true ],
+ [ true, true, true ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideAllowsAuthenticationDataChange
+ * @param StatusValue $primaryReturn
+ * @param StatusValue $secondaryReturn
+ * @param Status $expect
+ */
+ public function testAllowsAuthenticationDataChange( $primaryReturn, $secondaryReturn, $expect ) {
+ $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+
+ $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
+ $mock1->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
+ ->with( $this->equalTo( $req ) )
+ ->will( $this->returnValue( $primaryReturn ) );
+ $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
+ $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
+ $mock2->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
+ ->with( $this->equalTo( $req ) )
+ ->will( $this->returnValue( $secondaryReturn ) );
+
+ $this->primaryauthMocks = [ $mock1 ];
+ $this->secondaryauthMocks = [ $mock2 ];
+ $this->initializeManager( true );
+ $this->assertEquals( $expect, $this->manager->allowsAuthenticationDataChange( $req ) );
+ }
+
+ public static function provideAllowsAuthenticationDataChange() {
+ $ignored = \Status::newGood( 'ignored' );
+ $ignored->warning( 'authmanager-change-not-supported' );
+
+ $okFromPrimary = StatusValue::newGood();
+ $okFromPrimary->warning( 'warning-from-primary' );
+ $okFromSecondary = StatusValue::newGood();
+ $okFromSecondary->warning( 'warning-from-secondary' );
+
+ return [
+ [
+ StatusValue::newGood(),
+ StatusValue::newGood(),
+ \Status::newGood(),
+ ],
+ [
+ StatusValue::newGood(),
+ StatusValue::newGood( 'ignore' ),
+ \Status::newGood(),
+ ],
+ [
+ StatusValue::newGood( 'ignored' ),
+ StatusValue::newGood(),
+ \Status::newGood(),
+ ],
+ [
+ StatusValue::newGood( 'ignored' ),
+ StatusValue::newGood( 'ignored' ),
+ $ignored,
+ ],
+ [
+ StatusValue::newFatal( 'fail from primary' ),
+ StatusValue::newGood(),
+ \Status::newFatal( 'fail from primary' ),
+ ],
+ [
+ $okFromPrimary,
+ StatusValue::newGood(),
+ \Status::wrap( $okFromPrimary ),
+ ],
+ [
+ StatusValue::newGood(),
+ StatusValue::newFatal( 'fail from secondary' ),
+ \Status::newFatal( 'fail from secondary' ),
+ ],
+ [
+ StatusValue::newGood(),
+ $okFromSecondary,
+ \Status::wrap( $okFromSecondary ),
+ ],
+ ];
+ }
+
+ public function testChangeAuthenticationData() {
+ $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+ $req->username = 'UTSysop';
+
+ $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
+ $mock1->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
+ ->with( $this->equalTo( $req ) );
+ $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
+ $mock2->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
+ ->with( $this->equalTo( $req ) );
+
+ $this->primaryauthMocks = [ $mock1, $mock2 ];
+ $this->initializeManager( true );
+ $this->logger->setCollect( true );
+ $this->manager->changeAuthenticationData( $req );
+ $this->assertSame( [
+ [ LogLevel::INFO, 'Changing authentication data for {user} class {what}' ],
+ ], $this->logger->getBuffer() );
+ }
+
+ public function testCanCreateAccounts() {
+ $types = [
+ PrimaryAuthenticationProvider::TYPE_CREATE => true,
+ PrimaryAuthenticationProvider::TYPE_LINK => true,
+ PrimaryAuthenticationProvider::TYPE_NONE => false,
+ ];
+
+ foreach ( $types as $type => $can ) {
+ $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
+ $mock->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( $type ) );
+ $this->primaryauthMocks = [ $mock ];
+ $this->initializeManager( true );
+ $this->assertSame( $can, $this->manager->canCreateAccounts(), $type );
+ }
+ }
+
+ public function testCheckAccountCreatePermissions() {
+ global $wgGroupPermissions;
+
+ $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
+
+ $this->initializeManager( true );
+
+ $wgGroupPermissions['*']['createaccount'] = true;
+ $this->assertEquals(
+ \Status::newGood(),
+ $this->manager->checkAccountCreatePermissions( new \User )
+ );
+
+ $readOnlyMode = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
+ $readOnlyMode->setReason( 'Because' );
+ $this->assertEquals(
+ \Status::newFatal( wfMessage( 'readonlytext', 'Because' ) ),
+ $this->manager->checkAccountCreatePermissions( new \User )
+ );
+ $readOnlyMode->setReason( false );
+
+ $wgGroupPermissions['*']['createaccount'] = false;
+ $status = $this->manager->checkAccountCreatePermissions( new \User );
+ $this->assertFalse( $status->isOK() );
+ $this->assertTrue( $status->hasMessage( 'badaccess-groups' ) );
+ $wgGroupPermissions['*']['createaccount'] = true;
+
+ $user = \User::newFromName( 'UTBlockee' );
+ if ( $user->getID() == 0 ) {
+ $user->addToDatabase();
+ \TestUser::setPasswordForUser( $user, 'UTBlockeePassword' );
+ $user->saveSettings();
+ }
+ $oldBlock = \Block::newFromTarget( 'UTBlockee' );
+ if ( $oldBlock ) {
+ // An old block will prevent our new one from saving.
+ $oldBlock->delete();
+ }
+ $blockOptions = [
+ 'address' => 'UTBlockee',
+ 'user' => $user->getID(),
+ 'by' => $this->getTestSysop()->getUser()->getId(),
+ 'reason' => __METHOD__,
+ 'expiry' => time() + 100500,
+ 'createAccount' => true,
+ ];
+ $block = new \Block( $blockOptions );
+ $block->insert();
+ $status = $this->manager->checkAccountCreatePermissions( $user );
+ $this->assertFalse( $status->isOK() );
+ $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
+
+ $blockOptions = [
+ 'address' => '127.0.0.0/24',
+ 'by' => $this->getTestSysop()->getUser()->getId(),
+ 'reason' => __METHOD__,
+ 'expiry' => time() + 100500,
+ 'createAccount' => true,
+ ];
+ $block = new \Block( $blockOptions );
+ $block->insert();
+ $scopeVariable = new ScopedCallback( [ $block, 'delete' ] );
+ $status = $this->manager->checkAccountCreatePermissions( new \User );
+ $this->assertFalse( $status->isOK() );
+ $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
+ ScopedCallback::consume( $scopeVariable );
+
+ $this->setMwGlobals( [
+ 'wgEnableDnsBlacklist' => true,
+ 'wgDnsBlacklistUrls' => [
+ 'local.wmftest.net', // This will resolve for every subdomain, which works to test "listed?"
+ ],
+ 'wgProxyWhitelist' => [],
+ ] );
+ $status = $this->manager->checkAccountCreatePermissions( new \User );
+ $this->assertFalse( $status->isOK() );
+ $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
+ $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
+ $status = $this->manager->checkAccountCreatePermissions( new \User );
+ $this->assertTrue( $status->isGood() );
+ }
+
+ /**
+ * @param string $uniq
+ * @return string
+ */
+ private static function usernameForCreation( $uniq = '' ) {
+ $i = 0;
+ do {
+ $username = "UTAuthManagerTestAccountCreation" . $uniq . ++$i;
+ } while ( \User::newFromName( $username )->getId() !== 0 );
+ return $username;
+ }
+
+ public function testCanCreateAccount() {
+ $username = self::usernameForCreation();
+ $this->initializeManager();
+
+ $this->assertEquals(
+ \Status::newFatal( 'authmanager-create-disabled' ),
+ $this->manager->canCreateAccount( $username )
+ );
+
+ $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+ $mock->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+ $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
+ $mock->expects( $this->any() )->method( 'testUserForCreation' )
+ ->will( $this->returnValue( StatusValue::newGood() ) );
+ $this->primaryauthMocks = [ $mock ];
+ $this->initializeManager( true );
+
+ $this->assertEquals(
+ \Status::newFatal( 'userexists' ),
+ $this->manager->canCreateAccount( $username )
+ );
+
+ $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+ $mock->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+ $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
+ $mock->expects( $this->any() )->method( 'testUserForCreation' )
+ ->will( $this->returnValue( StatusValue::newGood() ) );
+ $this->primaryauthMocks = [ $mock ];
+ $this->initializeManager( true );
+
+ $this->assertEquals(
+ \Status::newFatal( 'noname' ),
+ $this->manager->canCreateAccount( $username . '<>' )
+ );
+
+ $this->assertEquals(
+ \Status::newFatal( 'userexists' ),
+ $this->manager->canCreateAccount( 'UTSysop' )
+ );
+
+ $this->assertEquals(
+ \Status::newGood(),
+ $this->manager->canCreateAccount( $username )
+ );
+
+ $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+ $mock->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+ $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
+ $mock->expects( $this->any() )->method( 'testUserForCreation' )
+ ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
+ $this->primaryauthMocks = [ $mock ];
+ $this->initializeManager( true );
+
+ $this->assertEquals(
+ \Status::newFatal( 'fail' ),
+ $this->manager->canCreateAccount( $username )
+ );
+ }
+
+ public function testBeginAccountCreation() {
+ $creator = \User::newFromName( 'UTSysop' );
+ $userReq = new UsernameAuthenticationRequest;
+ $this->logger = new \TestLogger( false, function ( $message, $level ) {
+ return $level === LogLevel::DEBUG ? null : $message;
+ } );
+ $this->initializeManager();
+
+ $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ try {
+ $this->manager->beginAccountCreation(
+ $creator, [], 'http://localhost/'
+ );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \LogicException $ex ) {
+ $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
+ }
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertNull(
+ $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
+ );
+
+ $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+ $mock->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+ $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
+ $mock->expects( $this->any() )->method( 'testUserForCreation' )
+ ->will( $this->returnValue( StatusValue::newGood() ) );
+ $this->primaryauthMocks = [ $mock ];
+ $this->initializeManager( true );
+
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->beginAccountCreation( $creator, [], 'http://localhost/' );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'noname', $ret->message->getKey() );
+
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $userReq->username = self::usernameForCreation();
+ $userReq2 = new UsernameAuthenticationRequest;
+ $userReq2->username = $userReq->username . 'X';
+ $ret = $this->manager->beginAccountCreation(
+ $creator, [ $userReq, $userReq2 ], 'http://localhost/'
+ );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'noname', $ret->message->getKey() );
+
+ $readOnlyMode = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
+ $readOnlyMode->setReason( 'Because' );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $userReq->username = self::usernameForCreation();
+ $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'readonlytext', $ret->message->getKey() );
+ $this->assertSame( [ 'Because' ], $ret->message->getParams() );
+ $readOnlyMode->setReason( false );
+
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $userReq->username = self::usernameForCreation();
+ $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'userexists', $ret->message->getKey() );
+
+ $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+ $mock->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+ $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
+ $mock->expects( $this->any() )->method( 'testUserForCreation' )
+ ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
+ $this->primaryauthMocks = [ $mock ];
+ $this->initializeManager( true );
+
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $userReq->username = self::usernameForCreation();
+ $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'fail', $ret->message->getKey() );
+
+ $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+ $mock->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+ $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
+ $mock->expects( $this->any() )->method( 'testUserForCreation' )
+ ->will( $this->returnValue( StatusValue::newGood() ) );
+ $this->primaryauthMocks = [ $mock ];
+ $this->initializeManager( true );
+
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $userReq->username = self::usernameForCreation() . '<>';
+ $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'noname', $ret->message->getKey() );
+
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $userReq->username = $creator->getName();
+ $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'userexists', $ret->message->getKey() );
+
+ $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+ $mock->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+ $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
+ $mock->expects( $this->any() )->method( 'testUserForCreation' )
+ ->will( $this->returnValue( StatusValue::newGood() ) );
+ $mock->expects( $this->any() )->method( 'testForAccountCreation' )
+ ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
+ $this->primaryauthMocks = [ $mock ];
+ $this->initializeManager( true );
+
+ $req = $this->getMockBuilder( UserDataAuthenticationRequest::class )
+ ->setMethods( [ 'populateUser' ] )
+ ->getMock();
+ $req->expects( $this->any() )->method( 'populateUser' )
+ ->willReturn( \StatusValue::newFatal( 'populatefail' ) );
+ $userReq->username = self::usernameForCreation();
+ $ret = $this->manager->beginAccountCreation(
+ $creator, [ $userReq, $req ], 'http://localhost/'
+ );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'populatefail', $ret->message->getKey() );
+
+ $req = new UserDataAuthenticationRequest;
+ $userReq->username = self::usernameForCreation();
+
+ $ret = $this->manager->beginAccountCreation(
+ $creator, [ $userReq, $req ], 'http://localhost/'
+ );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'fail', $ret->message->getKey() );
+
+ $this->manager->beginAccountCreation(
+ \User::newFromName( $userReq->username ), [ $userReq, $req ], 'http://localhost/'
+ );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'fail', $ret->message->getKey() );
+ }
+
+ public function testContinueAccountCreation() {
+ $creator = \User::newFromName( 'UTSysop' );
+ $username = self::usernameForCreation();
+ $this->logger = new \TestLogger( false, function ( $message, $level ) {
+ return $level === LogLevel::DEBUG ? null : $message;
+ } );
+ $this->initializeManager();
+
+ $session = [
+ 'userid' => 0,
+ 'username' => $username,
+ 'creatorid' => 0,
+ 'creatorname' => $username,
+ 'reqs' => [],
+ 'primary' => null,
+ 'primaryResponse' => null,
+ 'secondary' => [],
+ 'ranPreTests' => true,
+ ];
+
+ $this->hook( 'LocalUserCreated', $this->never() );
+ try {
+ $this->manager->continueAccountCreation( [] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \LogicException $ex ) {
+ $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
+ }
+ $this->unhook( 'LocalUserCreated' );
+
+ $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+ $mock->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+ $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
+ $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )->will(
+ $this->returnValue( AuthenticationResponse::newFail( $this->message( 'fail' ) ) )
+ );
+ $this->primaryauthMocks = [ $mock ];
+ $this->initializeManager( true );
+
+ $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', null );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->continueAccountCreation( [] );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'authmanager-create-not-in-progress', $ret->message->getKey() );
+
+ $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
+ [ 'username' => "$username<>" ] + $session );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->continueAccountCreation( [] );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'noname', $ret->message->getKey() );
+ $this->assertNull(
+ $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
+ );
+
+ $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', $session );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $cache = \ObjectCache::getLocalClusterInstance();
+ $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
+ $ret = $this->manager->continueAccountCreation( [] );
+ unset( $lock );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'usernameinprogress', $ret->message->getKey() );
+ // This error shouldn't remove the existing session, because the
+ // raced-with process "owns" it.
+ $this->assertSame(
+ $session, $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
+ );
+
+ $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
+ [ 'username' => $creator->getName() ] + $session );
+ $readOnlyMode = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
+ $readOnlyMode->setReason( 'Because' );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->continueAccountCreation( [] );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'readonlytext', $ret->message->getKey() );
+ $this->assertSame( [ 'Because' ], $ret->message->getParams() );
+ $readOnlyMode->setReason( false );
+
+ $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
+ [ 'username' => $creator->getName() ] + $session );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->continueAccountCreation( [] );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'userexists', $ret->message->getKey() );
+ $this->assertNull(
+ $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
+ );
+
+ $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
+ [ 'userid' => $creator->getId() ] + $session );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ try {
+ $ret = $this->manager->continueAccountCreation( [] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
+ }
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertNull(
+ $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
+ );
+
+ $id = $creator->getId();
+ $name = $creator->getName();
+ $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
+ [ 'username' => $name, 'userid' => $id + 1 ] + $session );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ try {
+ $ret = $this->manager->continueAccountCreation( [] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertEquals(
+ "User \"{$name}\" exists, but ID $id != " . ( $id + 1 ) . '!', $ex->getMessage()
+ );
+ }
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertNull(
+ $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
+ );
+
+ $req = $this->getMockBuilder( UserDataAuthenticationRequest::class )
+ ->setMethods( [ 'populateUser' ] )
+ ->getMock();
+ $req->expects( $this->any() )->method( 'populateUser' )
+ ->willReturn( \StatusValue::newFatal( 'populatefail' ) );
+ $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
+ [ 'reqs' => [ $req ] ] + $session );
+ $ret = $this->manager->continueAccountCreation( [] );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'populatefail', $ret->message->getKey() );
+ $this->assertNull(
+ $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
+ );
+ }
+
+ /**
+ * @dataProvider provideAccountCreation
+ * @param StatusValue $preTest
+ * @param StatusValue $primaryTest
+ * @param StatusValue $secondaryTest
+ * @param array $primaryResponses
+ * @param array $secondaryResponses
+ * @param array $managerResponses
+ */
+ public function testAccountCreation(
+ StatusValue $preTest, $primaryTest, $secondaryTest,
+ array $primaryResponses, array $secondaryResponses, array $managerResponses
+ ) {
+ $creator = \User::newFromName( 'UTSysop' );
+ $username = self::usernameForCreation();
+
+ $this->initializeManager();
+
+ // Set up lots of mocks...
+ $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+ $req->preTest = $preTest;
+ $req->primaryTest = $primaryTest;
+ $req->secondaryTest = $secondaryTest;
+ $req->primary = $primaryResponses;
+ $req->secondary = $secondaryResponses;
+ $mocks = [];
+ foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
+ $class = ucfirst( $key ) . 'AuthenticationProvider';
+ $mocks[$key] = $this->getMockForAbstractClass(
+ "MediaWiki\\Auth\\$class", [], "Mock$class"
+ );
+ $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( $key ) );
+ $mocks[$key]->expects( $this->any() )->method( 'testUserForCreation' )
+ ->will( $this->returnValue( StatusValue::newGood() ) );
+ $mocks[$key]->expects( $this->any() )->method( 'testForAccountCreation' )
+ ->will( $this->returnCallback(
+ function ( $user, $creatorIn, $reqs )
+ use ( $username, $creator, $req, $key )
+ {
+ $this->assertSame( $username, $user->getName() );
+ $this->assertSame( $creator->getId(), $creatorIn->getId() );
+ $this->assertSame( $creator->getName(), $creatorIn->getName() );
+ $foundReq = false;
+ foreach ( $reqs as $r ) {
+ $this->assertSame( $username, $r->username );
+ $foundReq = $foundReq || get_class( $r ) === get_class( $req );
+ }
+ $this->assertTrue( $foundReq, '$reqs contains $req' );
+ $k = $key . 'Test';
+ return $req->$k;
+ }
+ ) );
+
+ for ( $i = 2; $i <= 3; $i++ ) {
+ $mocks[$key . $i] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
+ $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( $key . $i ) );
+ $mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' )
+ ->will( $this->returnValue( StatusValue::newGood() ) );
+ $mocks[$key . $i]->expects( $this->atMost( 1 ) )->method( 'testForAccountCreation' )
+ ->will( $this->returnValue( StatusValue::newGood() ) );
+ }
+ }
+
+ $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+ $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
+ ->will( $this->returnValue( false ) );
+ $ct = count( $req->primary );
+ $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
+ $this->assertSame( $username, $user->getName() );
+ $this->assertSame( 'UTSysop', $creator->getName() );
+ $foundReq = false;
+ foreach ( $reqs as $r ) {
+ $this->assertSame( $username, $r->username );
+ $foundReq = $foundReq || get_class( $r ) === get_class( $req );
+ }
+ $this->assertTrue( $foundReq, '$reqs contains $req' );
+ return array_shift( $req->primary );
+ } );
+ $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
+ ->method( 'beginPrimaryAccountCreation' )
+ ->will( $callback );
+ $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
+ ->method( 'continuePrimaryAccountCreation' )
+ ->will( $callback );
+
+ $ct = count( $req->secondary );
+ $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
+ $this->assertSame( $username, $user->getName() );
+ $this->assertSame( 'UTSysop', $creator->getName() );
+ $foundReq = false;
+ foreach ( $reqs as $r ) {
+ $this->assertSame( $username, $r->username );
+ $foundReq = $foundReq || get_class( $r ) === get_class( $req );
+ }
+ $this->assertTrue( $foundReq, '$reqs contains $req' );
+ return array_shift( $req->secondary );
+ } );
+ $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
+ ->method( 'beginSecondaryAccountCreation' )
+ ->will( $callback );
+ $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
+ ->method( 'continueSecondaryAccountCreation' )
+ ->will( $callback );
+
+ $abstain = AuthenticationResponse::newAbstain();
+ $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
+ $mocks['primary2']->expects( $this->any() )->method( 'testUserExists' )
+ ->will( $this->returnValue( false ) );
+ $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountCreation' )
+ ->will( $this->returnValue( $abstain ) );
+ $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
+ $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_NONE ) );
+ $mocks['primary3']->expects( $this->any() )->method( 'testUserExists' )
+ ->will( $this->returnValue( false ) );
+ $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountCreation' );
+ $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
+ $mocks['secondary2']->expects( $this->atMost( 1 ) )
+ ->method( 'beginSecondaryAccountCreation' )
+ ->will( $this->returnValue( $abstain ) );
+ $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
+ $mocks['secondary3']->expects( $this->atMost( 1 ) )
+ ->method( 'beginSecondaryAccountCreation' )
+ ->will( $this->returnValue( $abstain ) );
+ $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
+
+ $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
+ $this->primaryauthMocks = [ $mocks['primary3'], $mocks['primary'], $mocks['primary2'] ];
+ $this->secondaryauthMocks = [
+ $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2']
+ ];
+
+ $this->logger = new \TestLogger( true, function ( $message, $level ) {
+ return $level === LogLevel::DEBUG ? null : $message;
+ } );
+ $expectLog = [];
+ $this->initializeManager( true );
+
+ $constraint = \PHPUnit_Framework_Assert::logicalOr(
+ $this->equalTo( AuthenticationResponse::PASS ),
+ $this->equalTo( AuthenticationResponse::FAIL )
+ );
+ $providers = array_merge(
+ $this->preauthMocks, $this->primaryauthMocks, $this->secondaryauthMocks
+ );
+ foreach ( $providers as $p ) {
+ $p->postCalled = false;
+ $p->expects( $this->atMost( 1 ) )->method( 'postAccountCreation' )
+ ->willReturnCallback( function ( $user, $creator, $response )
+ use ( $constraint, $p, $username )
+ {
+ $this->assertInstanceOf( \User::class, $user );
+ $this->assertSame( $username, $user->getName() );
+ $this->assertSame( 'UTSysop', $creator->getName() );
+ $this->assertInstanceOf( AuthenticationResponse::class, $response );
+ $this->assertThat( $response->status, $constraint );
+ $p->postCalled = $response->status;
+ } );
+ }
+
+ // We're testing with $wgNewUserLog = false, so assert that it worked
+ $dbw = wfGetDB( DB_MASTER );
+ $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
+
+ $first = true;
+ $created = false;
+ foreach ( $managerResponses as $i => $response ) {
+ $success = $response instanceof AuthenticationResponse &&
+ $response->status === AuthenticationResponse::PASS;
+ if ( $i === 'created' ) {
+ $created = true;
+ $this->hook( 'LocalUserCreated', $this->once() )
+ ->with(
+ $this->callback( function ( $user ) use ( $username ) {
+ return $user->getName() === $username;
+ } ),
+ $this->equalTo( false )
+ );
+ $expectLog[] = [ LogLevel::INFO, "Creating user {user} during account creation" ];
+ } else {
+ $this->hook( 'LocalUserCreated', $this->never() );
+ }
+
+ $ex = null;
+ try {
+ if ( $first ) {
+ $userReq = new UsernameAuthenticationRequest;
+ $userReq->username = $username;
+ $ret = $this->manager->beginAccountCreation(
+ $creator, [ $userReq, $req ], 'http://localhost/'
+ );
+ } else {
+ $ret = $this->manager->continueAccountCreation( [ $req ] );
+ }
+ if ( $response instanceof \Exception ) {
+ $this->fail( 'Expected exception not thrown', "Response $i" );
+ }
+ } catch ( \Exception $ex ) {
+ if ( !$response instanceof \Exception ) {
+ throw $ex;
+ }
+ $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
+ $this->assertNull(
+ $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
+ "Response $i, exception, session state"
+ );
+ $this->unhook( 'LocalUserCreated' );
+ return;
+ }
+
+ $this->unhook( 'LocalUserCreated' );
+
+ $this->assertSame( 'http://localhost/', $req->returnToUrl );
+
+ if ( $success ) {
+ $this->assertNotNull( $ret->loginRequest, "Response $i, login marker" );
+ $this->assertContains(
+ $ret->loginRequest, $this->managerPriv->createdAccountAuthenticationRequests,
+ "Response $i, login marker"
+ );
+
+ $expectLog[] = [
+ LogLevel::INFO,
+ "MediaWiki\Auth\AuthManager::continueAccountCreation: Account creation succeeded for {user}"
+ ];
+
+ // Set some fields in the expected $response that we couldn't
+ // know in provideAccountCreation().
+ $response->username = $username;
+ $response->loginRequest = $ret->loginRequest;
+ } else {
+ $this->assertNull( $ret->loginRequest, "Response $i, login marker" );
+ $this->assertSame( [], $this->managerPriv->createdAccountAuthenticationRequests,
+ "Response $i, login marker" );
+ }
+ $ret->message = $this->message( $ret->message );
+ $this->assertEquals( $response, $ret, "Response $i, response" );
+ if ( $success || $response->status === AuthenticationResponse::FAIL ) {
+ $this->assertNull(
+ $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
+ "Response $i, session state"
+ );
+ foreach ( $providers as $p ) {
+ $this->assertSame( $response->status, $p->postCalled,
+ "Response $i, post-auth callback called" );
+ }
+ } else {
+ $this->assertNotNull(
+ $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
+ "Response $i, session state"
+ );
+ foreach ( $ret->neededRequests as $neededReq ) {
+ $this->assertEquals( AuthManager::ACTION_CREATE, $neededReq->action,
+ "Response $i, neededRequest action" );
+ }
+ $this->assertEquals(
+ $ret->neededRequests,
+ $this->manager->getAuthenticationRequests( AuthManager::ACTION_CREATE_CONTINUE ),
+ "Response $i, continuation check"
+ );
+ foreach ( $providers as $p ) {
+ $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
+ }
+ }
+
+ if ( $created ) {
+ $this->assertNotEquals( 0, \User::idFromName( $username ) );
+ } else {
+ $this->assertEquals( 0, \User::idFromName( $username ) );
+ }
+
+ $first = false;
+ }
+
+ $this->assertSame( $expectLog, $this->logger->getBuffer() );
+
+ $this->assertSame(
+ $maxLogId,
+ $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
+ );
+ }
+
+ public function provideAccountCreation() {
+ $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+ $good = StatusValue::newGood();
+
+ return [
+ 'Pre-creation test fail in pre' => [
+ StatusValue::newFatal( 'fail-from-pre' ), $good, $good,
+ [],
+ [],
+ [
+ AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
+ ]
+ ],
+ 'Pre-creation test fail in primary' => [
+ $good, StatusValue::newFatal( 'fail-from-primary' ), $good,
+ [],
+ [],
+ [
+ AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
+ ]
+ ],
+ 'Pre-creation test fail in secondary' => [
+ $good, $good, StatusValue::newFatal( 'fail-from-secondary' ),
+ [],
+ [],
+ [
+ AuthenticationResponse::newFail( $this->message( 'fail-from-secondary' ) ),
+ ]
+ ],
+ 'Failure in primary' => [
+ $good, $good, $good,
+ $tmp = [
+ AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
+ ],
+ [],
+ $tmp
+ ],
+ 'All primary abstain' => [
+ $good, $good, $good,
+ [
+ AuthenticationResponse::newAbstain(),
+ ],
+ [],
+ [
+ AuthenticationResponse::newFail( $this->message( 'authmanager-create-no-primary' ) )
+ ]
+ ],
+ 'Primary UI, then redirect, then fail' => [
+ $good, $good, $good,
+ $tmp = [
+ AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+ AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
+ AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
+ ],
+ [],
+ $tmp
+ ],
+ 'Primary redirect, then abstain' => [
+ $good, $good, $good,
+ [
+ $tmp = AuthenticationResponse::newRedirect(
+ [ $req ], '/foo.html', [ 'foo' => 'bar' ]
+ ),
+ AuthenticationResponse::newAbstain(),
+ ],
+ [],
+ [
+ $tmp,
+ new \DomainException(
+ 'MockPrimaryAuthenticationProvider::continuePrimaryAccountCreation() returned ABSTAIN'
+ )
+ ]
+ ],
+ 'Primary UI, then pass; secondary abstain' => [
+ $good, $good, $good,
+ [
+ $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+ AuthenticationResponse::newPass(),
+ ],
+ [
+ AuthenticationResponse::newAbstain(),
+ ],
+ [
+ $tmp1,
+ 'created' => AuthenticationResponse::newPass( '' ),
+ ]
+ ],
+ 'Primary pass; secondary UI then pass' => [
+ $good, $good, $good,
+ [
+ AuthenticationResponse::newPass( '' ),
+ ],
+ [
+ $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+ AuthenticationResponse::newPass( '' ),
+ ],
+ [
+ 'created' => $tmp1,
+ AuthenticationResponse::newPass( '' ),
+ ]
+ ],
+ 'Primary pass; secondary fail' => [
+ $good, $good, $good,
+ [
+ AuthenticationResponse::newPass(),
+ ],
+ [
+ AuthenticationResponse::newFail( $this->message( '...' ) ),
+ ],
+ [
+ 'created' => new \DomainException(
+ 'MockSecondaryAuthenticationProvider::beginSecondaryAccountCreation() returned FAIL. ' .
+ 'Secondary providers are not allowed to fail account creation, ' .
+ 'that should have been done via testForAccountCreation().'
+ )
+ ]
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideAccountCreationLogging
+ * @param bool $isAnon
+ * @param string|null $logSubtype
+ */
+ public function testAccountCreationLogging( $isAnon, $logSubtype ) {
+ $creator = $isAnon ? new \User : \User::newFromName( 'UTSysop' );
+ $username = self::usernameForCreation();
+
+ $this->initializeManager();
+
+ // Set up lots of mocks...
+ $mock = $this->getMockForAbstractClass(
+ \MediaWiki\Auth\PrimaryAuthenticationProvider::class, []
+ );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( 'primary' ) );
+ $mock->expects( $this->any() )->method( 'testUserForCreation' )
+ ->will( $this->returnValue( StatusValue::newGood() ) );
+ $mock->expects( $this->any() )->method( 'testForAccountCreation' )
+ ->will( $this->returnValue( StatusValue::newGood() ) );
+ $mock->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+ $mock->expects( $this->any() )->method( 'testUserExists' )
+ ->will( $this->returnValue( false ) );
+ $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
+ ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
+ $mock->expects( $this->any() )->method( 'finishAccountCreation' )
+ ->will( $this->returnValue( $logSubtype ) );
+
+ $this->primaryauthMocks = [ $mock ];
+ $this->initializeManager( true );
+ $this->logger->setCollect( true );
+
+ $this->config->set( 'NewUserLog', true );
+
+ $dbw = wfGetDB( DB_MASTER );
+ $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
+
+ $userReq = new UsernameAuthenticationRequest;
+ $userReq->username = $username;
+ $reasonReq = new CreationReasonAuthenticationRequest;
+ $reasonReq->reason = $this->toString();
+ $ret = $this->manager->beginAccountCreation(
+ $creator, [ $userReq, $reasonReq ], 'http://localhost/'
+ );
+
+ $this->assertSame( AuthenticationResponse::PASS, $ret->status );
+
+ $user = \User::newFromName( $username );
+ $this->assertNotEquals( 0, $user->getId(), 'sanity check' );
+ $this->assertNotEquals( $creator->getId(), $user->getId(), 'sanity check' );
+
+ $data = \DatabaseLogEntry::getSelectQueryData();
+ $rows = iterator_to_array( $dbw->select(
+ $data['tables'],
+ $data['fields'],
+ [
+ 'log_id > ' . (int)$maxLogId,
+ 'log_type' => 'newusers'
+ ] + $data['conds'],
+ __METHOD__,
+ $data['options'],
+ $data['join_conds']
+ ) );
+ $this->assertCount( 1, $rows );
+ $entry = \DatabaseLogEntry::newFromRow( reset( $rows ) );
+
+ $this->assertSame( $logSubtype ?: ( $isAnon ? 'create' : 'create2' ), $entry->getSubtype() );
+ $this->assertSame(
+ $isAnon ? $user->getId() : $creator->getId(),
+ $entry->getPerformer()->getId()
+ );
+ $this->assertSame(
+ $isAnon ? $user->getName() : $creator->getName(),
+ $entry->getPerformer()->getName()
+ );
+ $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
+ $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
+ $this->assertSame( $this->toString(), $entry->getComment() );
+ }
+
+ public static function provideAccountCreationLogging() {
+ return [
+ [ true, null ],
+ [ true, 'foobar' ],
+ [ false, null ],
+ [ false, 'byemail' ],
+ ];
+ }
+
+ public function testAutoAccountCreation() {
+ global $wgGroupPermissions, $wgHooks;
+
+ // PHPUnit seems to have a bug where it will call the ->with()
+ // callbacks for our hooks again after the test is run (WTF?), which
+ // breaks here because $username no longer matches $user by the end of
+ // the testing.
+ $workaroundPHPUnitBug = false;
+
+ $username = self::usernameForCreation();
+ $this->initializeManager();
+
+ $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
+ $wgGroupPermissions['*']['createaccount'] = true;
+ $wgGroupPermissions['*']['autocreateaccount'] = false;
+
+ \ObjectCache::$instances[__METHOD__] = new \HashBagOStuff();
+ $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__ ] );
+
+ // Set up lots of mocks...
+ $mocks = [];
+ foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
+ $class = ucfirst( $key ) . 'AuthenticationProvider';
+ $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
+ $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( $key ) );
+ }
+
+ $good = StatusValue::newGood();
+ $callback = $this->callback( function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
+ return $workaroundPHPUnitBug || $user->getName() === $username;
+ } );
+
+ $mocks['pre']->expects( $this->exactly( 12 ) )->method( 'testUserForCreation' )
+ ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
+ ->will( $this->onConsecutiveCalls(
+ StatusValue::newFatal( 'ok' ), StatusValue::newFatal( 'ok' ), // For testing permissions
+ StatusValue::newFatal( 'fail-in-pre' ), $good, $good,
+ $good, // backoff test
+ $good, // addToDatabase fails test
+ $good, // addToDatabase throws test
+ $good, // addToDatabase exists test
+ $good, $good, $good // success
+ ) );
+
+ $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+ $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
+ ->will( $this->returnValue( true ) );
+ $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
+ ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
+ ->will( $this->onConsecutiveCalls(
+ StatusValue::newFatal( 'fail-in-primary' ), $good,
+ $good, // backoff test
+ $good, // addToDatabase fails test
+ $good, // addToDatabase throws test
+ $good, // addToDatabase exists test
+ $good, $good, $good
+ ) );
+ $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
+ ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) );
+
+ $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
+ ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
+ ->will( $this->onConsecutiveCalls(
+ StatusValue::newFatal( 'fail-in-secondary' ),
+ $good, // backoff test
+ $good, // addToDatabase fails test
+ $good, // addToDatabase throws test
+ $good, // addToDatabase exists test
+ $good, $good, $good
+ ) );
+ $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
+ ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) );
+
+ $this->preauthMocks = [ $mocks['pre'] ];
+ $this->primaryauthMocks = [ $mocks['primary'] ];
+ $this->secondaryauthMocks = [ $mocks['secondary'] ];
+ $this->initializeManager( true );
+ $session = $this->request->getSession();
+
+ $logger = new \TestLogger( true, function ( $m ) {
+ $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
+ return $m;
+ } );
+ $this->manager->setLogger( $logger );
+
+ try {
+ $user = \User::newFromName( 'UTSysop' );
+ $this->manager->autoCreateUser( $user, 'InvalidSource', true );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \InvalidArgumentException $ex ) {
+ $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
+ }
+
+ // First, check an existing user
+ $session->clear();
+ $user = \User::newFromName( 'UTSysop' );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+ $this->unhook( 'LocalUserCreated' );
+ $expect = \Status::newGood();
+ $expect->warning( 'userexists' );
+ $this->assertEquals( $expect, $ret );
+ $this->assertNotEquals( 0, $user->getId() );
+ $this->assertSame( 'UTSysop', $user->getName() );
+ $this->assertEquals( $user->getId(), $session->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::DEBUG, '{username} already exists locally' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ $session->clear();
+ $user = \User::newFromName( 'UTSysop' );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
+ $this->unhook( 'LocalUserCreated' );
+ $expect = \Status::newGood();
+ $expect->warning( 'userexists' );
+ $this->assertEquals( $expect, $ret );
+ $this->assertNotEquals( 0, $user->getId() );
+ $this->assertSame( 'UTSysop', $user->getName() );
+ $this->assertEquals( 0, $session->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::DEBUG, '{username} already exists locally' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Wiki is read-only
+ $session->clear();
+ $readOnlyMode = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
+ $readOnlyMode->setReason( 'Because' );
+ $user = \User::newFromName( $username );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertEquals( \Status::newFatal( wfMessage( 'readonlytext', 'Because' ) ), $ret );
+ $this->assertEquals( 0, $user->getId() );
+ $this->assertNotEquals( $username, $user->getName() );
+ $this->assertEquals( 0, $session->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::DEBUG, 'denied by wfReadOnly(): {reason}' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
+ $readOnlyMode->setReason( false );
+
+ // Session blacklisted
+ $session->clear();
+ $session->set( 'AuthManager::AutoCreateBlacklist', 'test' );
+ $user = \User::newFromName( $username );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertEquals( \Status::newFatal( 'test' ), $ret );
+ $this->assertEquals( 0, $user->getId() );
+ $this->assertNotEquals( $username, $user->getName() );
+ $this->assertEquals( 0, $session->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::DEBUG, 'blacklisted in session {sessionid}' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ $session->clear();
+ $session->set( 'AuthManager::AutoCreateBlacklist', StatusValue::newFatal( 'test2' ) );
+ $user = \User::newFromName( $username );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertEquals( \Status::newFatal( 'test2' ), $ret );
+ $this->assertEquals( 0, $user->getId() );
+ $this->assertNotEquals( $username, $user->getName() );
+ $this->assertEquals( 0, $session->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::DEBUG, 'blacklisted in session {sessionid}' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Uncreatable name
+ $session->clear();
+ $user = \User::newFromName( $username . '@' );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertEquals( \Status::newFatal( 'noname' ), $ret );
+ $this->assertEquals( 0, $user->getId() );
+ $this->assertNotEquals( $username . '@', $user->getId() );
+ $this->assertEquals( 0, $session->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::DEBUG, 'name "{username}" is not creatable' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
+ $this->assertSame( 'noname', $session->get( 'AuthManager::AutoCreateBlacklist' ) );
+
+ // IP unable to create accounts
+ $wgGroupPermissions['*']['createaccount'] = false;
+ $wgGroupPermissions['*']['autocreateaccount'] = false;
+ $session->clear();
+ $user = \User::newFromName( $username );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertEquals( \Status::newFatal( 'authmanager-autocreate-noperm' ), $ret );
+ $this->assertEquals( 0, $user->getId() );
+ $this->assertNotEquals( $username, $user->getName() );
+ $this->assertEquals( 0, $session->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::DEBUG, 'IP lacks the ability to create or autocreate accounts' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
+ $this->assertSame(
+ 'authmanager-autocreate-noperm', $session->get( 'AuthManager::AutoCreateBlacklist' )
+ );
+
+ // Test that both permutations of permissions are allowed
+ // (this hits the two "ok" entries in $mocks['pre'])
+ $wgGroupPermissions['*']['createaccount'] = false;
+ $wgGroupPermissions['*']['autocreateaccount'] = true;
+ $session->clear();
+ $user = \User::newFromName( $username );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertEquals( \Status::newFatal( 'ok' ), $ret );
+
+ $wgGroupPermissions['*']['createaccount'] = true;
+ $wgGroupPermissions['*']['autocreateaccount'] = false;
+ $session->clear();
+ $user = \User::newFromName( $username );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertEquals( \Status::newFatal( 'ok' ), $ret );
+ $logger->clearBuffer();
+
+ // Test lock fail
+ $session->clear();
+ $user = \User::newFromName( $username );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $cache = \ObjectCache::getLocalClusterInstance();
+ $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+ unset( $lock );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertEquals( \Status::newFatal( 'usernameinprogress' ), $ret );
+ $this->assertEquals( 0, $user->getId() );
+ $this->assertNotEquals( $username, $user->getName() );
+ $this->assertEquals( 0, $session->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::DEBUG, 'Could not acquire account creation lock' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Test pre-authentication provider fail
+ $session->clear();
+ $user = \User::newFromName( $username );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertEquals( \Status::newFatal( 'fail-in-pre' ), $ret );
+ $this->assertEquals( 0, $user->getId() );
+ $this->assertNotEquals( $username, $user->getName() );
+ $this->assertEquals( 0, $session->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
+ $this->assertEquals(
+ StatusValue::newFatal( 'fail-in-pre' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
+ );
+
+ $session->clear();
+ $user = \User::newFromName( $username );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertEquals( \Status::newFatal( 'fail-in-primary' ), $ret );
+ $this->assertEquals( 0, $user->getId() );
+ $this->assertNotEquals( $username, $user->getName() );
+ $this->assertEquals( 0, $session->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
+ $this->assertEquals(
+ StatusValue::newFatal( 'fail-in-primary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
+ );
+
+ $session->clear();
+ $user = \User::newFromName( $username );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertEquals( \Status::newFatal( 'fail-in-secondary' ), $ret );
+ $this->assertEquals( 0, $user->getId() );
+ $this->assertNotEquals( $username, $user->getName() );
+ $this->assertEquals( 0, $session->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
+ $this->assertEquals(
+ StatusValue::newFatal( 'fail-in-secondary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
+ );
+
+ // Test backoff
+ $cache = \ObjectCache::getLocalClusterInstance();
+ $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
+ $cache->set( $backoffKey, true );
+ $session->clear();
+ $user = \User::newFromName( $username );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertEquals( \Status::newFatal( 'authmanager-autocreate-exception' ), $ret );
+ $this->assertEquals( 0, $user->getId() );
+ $this->assertNotEquals( $username, $user->getName() );
+ $this->assertEquals( 0, $session->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::DEBUG, '{username} denied by prior creation attempt failures' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
+ $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
+ $cache->delete( $backoffKey );
+
+ // Test addToDatabase fails
+ $session->clear();
+ $user = $this->getMockBuilder( \User::class )
+ ->setMethods( [ 'addToDatabase' ] )->getMock();
+ $user->expects( $this->once() )->method( 'addToDatabase' )
+ ->will( $this->returnValue( \Status::newFatal( 'because' ) ) );
+ $user->setName( $username );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+ $this->assertEquals( \Status::newFatal( 'because' ), $ret );
+ $this->assertEquals( 0, $user->getId() );
+ $this->assertNotEquals( $username, $user->getName() );
+ $this->assertEquals( 0, $session->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
+ [ LogLevel::ERROR, '{username} failed with message {msg}' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
+ $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
+
+ // Test addToDatabase throws an exception
+ $cache = \ObjectCache::getLocalClusterInstance();
+ $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
+ $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
+ $session->clear();
+ $user = $this->getMockBuilder( \User::class )
+ ->setMethods( [ 'addToDatabase' ] )->getMock();
+ $user->expects( $this->once() )->method( 'addToDatabase' )
+ ->will( $this->throwException( new \Exception( 'Excepted' ) ) );
+ $user->setName( $username );
+ try {
+ $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \Exception $ex ) {
+ $this->assertSame( 'Excepted', $ex->getMessage() );
+ }
+ $this->assertEquals( 0, $user->getId() );
+ $this->assertEquals( 0, $session->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
+ [ LogLevel::ERROR, '{username} failed with exception {exception}' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
+ $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
+ $this->assertNotEquals( false, $cache->get( $backoffKey ) );
+ $cache->delete( $backoffKey );
+
+ // Test addToDatabase fails because the user already exists.
+ $session->clear();
+ $user = $this->getMockBuilder( \User::class )
+ ->setMethods( [ 'addToDatabase' ] )->getMock();
+ $user->expects( $this->once() )->method( 'addToDatabase' )
+ ->will( $this->returnCallback( function () use ( $username, &$user ) {
+ $oldUser = \User::newFromName( $username );
+ $status = $oldUser->addToDatabase();
+ $this->assertTrue( $status->isOK(), 'sanity check' );
+ $user->setId( $oldUser->getId() );
+ return \Status::newFatal( 'userexists' );
+ } ) );
+ $user->setName( $username );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+ $expect = \Status::newGood();
+ $expect->warning( 'userexists' );
+ $this->assertEquals( $expect, $ret );
+ $this->assertNotEquals( 0, $user->getId() );
+ $this->assertEquals( $username, $user->getName() );
+ $this->assertEquals( $user->getId(), $session->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
+ [ LogLevel::INFO, '{username} already exists locally (race)' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
+ $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
+
+ // Success!
+ $session->clear();
+ $username = self::usernameForCreation();
+ $user = \User::newFromName( $username );
+ $this->hook( 'AuthPluginAutoCreate', $this->once() )
+ ->with( $callback );
+ $this->hideDeprecated( 'AuthPluginAutoCreate hook (used in ' .
+ get_class( $wgHooks['AuthPluginAutoCreate'][0] ) . '::onAuthPluginAutoCreate)' );
+ $this->hook( 'LocalUserCreated', $this->once() )
+ ->with( $callback, $this->equalTo( true ) );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
+ $this->unhook( 'LocalUserCreated' );
+ $this->unhook( 'AuthPluginAutoCreate' );
+ $this->assertEquals( \Status::newGood(), $ret );
+ $this->assertNotEquals( 0, $user->getId() );
+ $this->assertEquals( $username, $user->getName() );
+ $this->assertEquals( $user->getId(), $session->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ $dbw = wfGetDB( DB_MASTER );
+ $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
+ $session->clear();
+ $username = self::usernameForCreation();
+ $user = \User::newFromName( $username );
+ $this->hook( 'LocalUserCreated', $this->once() )
+ ->with( $callback, $this->equalTo( true ) );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
+ $this->unhook( 'LocalUserCreated' );
+ $this->assertEquals( \Status::newGood(), $ret );
+ $this->assertNotEquals( 0, $user->getId() );
+ $this->assertEquals( $username, $user->getName() );
+ $this->assertEquals( 0, $session->getUser()->getId() );
+ $this->assertSame( [
+ [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
+ $this->assertSame(
+ $maxLogId,
+ $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
+ );
+
+ $this->config->set( 'NewUserLog', true );
+ $session->clear();
+ $username = self::usernameForCreation();
+ $user = \User::newFromName( $username );
+ $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
+ $this->assertEquals( \Status::newGood(), $ret );
+ $logger->clearBuffer();
+
+ $data = \DatabaseLogEntry::getSelectQueryData();
+ $rows = iterator_to_array( $dbw->select(
+ $data['tables'],
+ $data['fields'],
+ [
+ 'log_id > ' . (int)$maxLogId,
+ 'log_type' => 'newusers'
+ ] + $data['conds'],
+ __METHOD__,
+ $data['options'],
+ $data['join_conds']
+ ) );
+ $this->assertCount( 1, $rows );
+ $entry = \DatabaseLogEntry::newFromRow( reset( $rows ) );
+
+ $this->assertSame( 'autocreate', $entry->getSubtype() );
+ $this->assertSame( $user->getId(), $entry->getPerformer()->getId() );
+ $this->assertSame( $user->getName(), $entry->getPerformer()->getName() );
+ $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
+ $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
+
+ $workaroundPHPUnitBug = true;
+ }
+
+ /**
+ * @dataProvider provideGetAuthenticationRequests
+ * @param string $action
+ * @param array $expect
+ * @param array $state
+ */
+ public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
+ $makeReq = function ( $key ) use ( $action ) {
+ $req = $this->createMock( AuthenticationRequest::class );
+ $req->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( $key ) );
+ $req->action = $action === AuthManager::ACTION_UNLINK ? AuthManager::ACTION_REMOVE : $action;
+ $req->key = $key;
+ return $req;
+ };
+ $cmpReqs = function ( $a, $b ) {
+ $ret = strcmp( get_class( $a ), get_class( $b ) );
+ if ( !$ret ) {
+ $ret = strcmp( $a->key, $b->key );
+ }
+ return $ret;
+ };
+
+ $good = StatusValue::newGood();
+
+ $mocks = [];
+ foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
+ $class = ucfirst( $key ) . 'AuthenticationProvider';
+ $mocks[$key] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" )
+ ->setMethods( [
+ 'getUniqueId', 'getAuthenticationRequests', 'providerAllowsAuthenticationDataChange',
+ ] )
+ ->getMockForAbstractClass();
+ $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( $key ) );
+ $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
+ ->will( $this->returnCallback( function ( $action ) use ( $key, $makeReq ) {
+ return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
+ } ) );
+ $mocks[$key]->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
+ ->will( $this->returnValue( $good ) );
+ }
+
+ $primaries = [];
+ foreach ( [
+ PrimaryAuthenticationProvider::TYPE_NONE,
+ PrimaryAuthenticationProvider::TYPE_CREATE,
+ PrimaryAuthenticationProvider::TYPE_LINK
+ ] as $type ) {
+ $class = 'PrimaryAuthenticationProvider';
+ $mocks["primary-$type"] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" )
+ ->setMethods( [
+ 'getUniqueId', 'accountCreationType', 'getAuthenticationRequests',
+ 'providerAllowsAuthenticationDataChange',
+ ] )
+ ->getMockForAbstractClass();
+ $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( "primary-$type" ) );
+ $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( $type ) );
+ $mocks["primary-$type"]->expects( $this->any() )->method( 'getAuthenticationRequests' )
+ ->will( $this->returnCallback( function ( $action ) use ( $type, $makeReq ) {
+ return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
+ } ) );
+ $mocks["primary-$type"]->expects( $this->any() )
+ ->method( 'providerAllowsAuthenticationDataChange' )
+ ->will( $this->returnValue( $good ) );
+ $this->primaryauthMocks[] = $mocks["primary-$type"];
+ }
+
+ $mocks['primary2'] = $this->getMockBuilder( PrimaryAuthenticationProvider::class )
+ ->setMethods( [
+ 'getUniqueId', 'accountCreationType', 'getAuthenticationRequests',
+ 'providerAllowsAuthenticationDataChange',
+ ] )
+ ->getMockForAbstractClass();
+ $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( 'primary2' ) );
+ $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
+ $mocks['primary2']->expects( $this->any() )->method( 'getAuthenticationRequests' )
+ ->will( $this->returnValue( [] ) );
+ $mocks['primary2']->expects( $this->any() )
+ ->method( 'providerAllowsAuthenticationDataChange' )
+ ->will( $this->returnCallback( function ( $req ) use ( $good ) {
+ return $req->key === 'generic' ? StatusValue::newFatal( 'no' ) : $good;
+ } ) );
+ $this->primaryauthMocks[] = $mocks['primary2'];
+
+ $this->preauthMocks = [ $mocks['pre'] ];
+ $this->secondaryauthMocks = [ $mocks['secondary'] ];
+ $this->initializeManager( true );
+
+ if ( $state ) {
+ if ( isset( $state['continueRequests'] ) ) {
+ $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
+ }
+ if ( $action === AuthManager::ACTION_LOGIN_CONTINUE ) {
+ $this->request->getSession()->setSecret( 'AuthManager::authnState', $state );
+ } elseif ( $action === AuthManager::ACTION_CREATE_CONTINUE ) {
+ $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', $state );
+ } elseif ( $action === AuthManager::ACTION_LINK_CONTINUE ) {
+ $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', $state );
+ }
+ }
+
+ $expectReqs = array_map( $makeReq, $expect );
+ if ( $action === AuthManager::ACTION_LOGIN ) {
+ $req = new RememberMeAuthenticationRequest;
+ $req->action = $action;
+ $req->required = AuthenticationRequest::REQUIRED;
+ $expectReqs[] = $req;
+ } elseif ( $action === AuthManager::ACTION_CREATE ) {
+ $req = new UsernameAuthenticationRequest;
+ $req->action = $action;
+ $expectReqs[] = $req;
+ $req = new UserDataAuthenticationRequest;
+ $req->action = $action;
+ $req->required = AuthenticationRequest::REQUIRED;
+ $expectReqs[] = $req;
+ }
+ usort( $expectReqs, $cmpReqs );
+
+ $actual = $this->manager->getAuthenticationRequests( $action );
+ foreach ( $actual as $req ) {
+ // Don't test this here.
+ $req->required = AuthenticationRequest::REQUIRED;
+ }
+ usort( $actual, $cmpReqs );
+
+ $this->assertEquals( $expectReqs, $actual );
+
+ // Test CreationReasonAuthenticationRequest gets returned
+ if ( $action === AuthManager::ACTION_CREATE ) {
+ $req = new CreationReasonAuthenticationRequest;
+ $req->action = $action;
+ $req->required = AuthenticationRequest::REQUIRED;
+ $expectReqs[] = $req;
+ usort( $expectReqs, $cmpReqs );
+
+ $actual = $this->manager->getAuthenticationRequests( $action, \User::newFromName( 'UTSysop' ) );
+ foreach ( $actual as $req ) {
+ // Don't test this here.
+ $req->required = AuthenticationRequest::REQUIRED;
+ }
+ usort( $actual, $cmpReqs );
+
+ $this->assertEquals( $expectReqs, $actual );
+ }
+ }
+
+ public static function provideGetAuthenticationRequests() {
+ return [
+ [
+ AuthManager::ACTION_LOGIN,
+ [ 'pre-login', 'primary-none-login', 'primary-create-login',
+ 'primary-link-login', 'secondary-login', 'generic' ],
+ ],
+ [
+ AuthManager::ACTION_CREATE,
+ [ 'pre-create', 'primary-none-create', 'primary-create-create',
+ 'primary-link-create', 'secondary-create', 'generic' ],
+ ],
+ [
+ AuthManager::ACTION_LINK,
+ [ 'primary-link-link', 'generic' ],
+ ],
+ [
+ AuthManager::ACTION_CHANGE,
+ [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
+ 'secondary-change' ],
+ ],
+ [
+ AuthManager::ACTION_REMOVE,
+ [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
+ 'secondary-remove' ],
+ ],
+ [
+ AuthManager::ACTION_UNLINK,
+ [ 'primary-link-remove' ],
+ ],
+ [
+ AuthManager::ACTION_LOGIN_CONTINUE,
+ [],
+ ],
+ [
+ AuthManager::ACTION_LOGIN_CONTINUE,
+ $reqs = [ 'continue-login', 'foo', 'bar' ],
+ [
+ 'continueRequests' => $reqs,
+ ],
+ ],
+ [
+ AuthManager::ACTION_CREATE_CONTINUE,
+ [],
+ ],
+ [
+ AuthManager::ACTION_CREATE_CONTINUE,
+ $reqs = [ 'continue-create', 'foo', 'bar' ],
+ [
+ 'continueRequests' => $reqs,
+ ],
+ ],
+ [
+ AuthManager::ACTION_LINK_CONTINUE,
+ [],
+ ],
+ [
+ AuthManager::ACTION_LINK_CONTINUE,
+ $reqs = [ 'continue-link', 'foo', 'bar' ],
+ [
+ 'continueRequests' => $reqs,
+ ],
+ ],
+ ];
+ }
+
+ public function testGetAuthenticationRequestsRequired() {
+ $makeReq = function ( $key, $required ) {
+ $req = $this->createMock( AuthenticationRequest::class );
+ $req->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( $key ) );
+ $req->action = AuthManager::ACTION_LOGIN;
+ $req->key = $key;
+ $req->required = $required;
+ return $req;
+ };
+ $cmpReqs = function ( $a, $b ) {
+ $ret = strcmp( get_class( $a ), get_class( $b ) );
+ if ( !$ret ) {
+ $ret = strcmp( $a->key, $b->key );
+ }
+ return $ret;
+ };
+
+ $good = StatusValue::newGood();
+
+ $primary1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $primary1->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( 'primary1' ) );
+ $primary1->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+ $primary1->expects( $this->any() )->method( 'getAuthenticationRequests' )
+ ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
+ return [
+ $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
+ $makeReq( "required", AuthenticationRequest::REQUIRED ),
+ $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
+ $makeReq( "foo", AuthenticationRequest::REQUIRED ),
+ $makeReq( "bar", AuthenticationRequest::REQUIRED ),
+ $makeReq( "baz", AuthenticationRequest::OPTIONAL ),
+ ];
+ } ) );
+
+ $primary2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $primary2->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( 'primary2' ) );
+ $primary2->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+ $primary2->expects( $this->any() )->method( 'getAuthenticationRequests' )
+ ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
+ return [
+ $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
+ $makeReq( "required2", AuthenticationRequest::REQUIRED ),
+ $makeReq( "optional2", AuthenticationRequest::OPTIONAL ),
+ ];
+ } ) );
+
+ $secondary = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
+ $secondary->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( 'secondary' ) );
+ $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
+ ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
+ return [
+ $makeReq( "foo", AuthenticationRequest::OPTIONAL ),
+ $makeReq( "bar", AuthenticationRequest::REQUIRED ),
+ $makeReq( "baz", AuthenticationRequest::REQUIRED ),
+ ];
+ } ) );
+
+ $rememberReq = new RememberMeAuthenticationRequest;
+ $rememberReq->action = AuthManager::ACTION_LOGIN;
+
+ $this->primaryauthMocks = [ $primary1, $primary2 ];
+ $this->secondaryauthMocks = [ $secondary ];
+ $this->initializeManager( true );
+
+ $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
+ $expected = [
+ $rememberReq,
+ $makeReq( "primary-shared", AuthenticationRequest::PRIMARY_REQUIRED ),
+ $makeReq( "required", AuthenticationRequest::PRIMARY_REQUIRED ),
+ $makeReq( "required2", AuthenticationRequest::PRIMARY_REQUIRED ),
+ $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
+ $makeReq( "optional2", AuthenticationRequest::OPTIONAL ),
+ $makeReq( "foo", AuthenticationRequest::PRIMARY_REQUIRED ),
+ $makeReq( "bar", AuthenticationRequest::REQUIRED ),
+ $makeReq( "baz", AuthenticationRequest::REQUIRED ),
+ ];
+ usort( $actual, $cmpReqs );
+ usort( $expected, $cmpReqs );
+ $this->assertEquals( $expected, $actual );
+
+ $this->primaryauthMocks = [ $primary1 ];
+ $this->secondaryauthMocks = [ $secondary ];
+ $this->initializeManager( true );
+
+ $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
+ $expected = [
+ $rememberReq,
+ $makeReq( "primary-shared", AuthenticationRequest::PRIMARY_REQUIRED ),
+ $makeReq( "required", AuthenticationRequest::PRIMARY_REQUIRED ),
+ $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
+ $makeReq( "foo", AuthenticationRequest::PRIMARY_REQUIRED ),
+ $makeReq( "bar", AuthenticationRequest::REQUIRED ),
+ $makeReq( "baz", AuthenticationRequest::REQUIRED ),
+ ];
+ usort( $actual, $cmpReqs );
+ usort( $expected, $cmpReqs );
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public function testAllowsPropertyChange() {
+ $mocks = [];
+ foreach ( [ 'primary', 'secondary' ] as $key ) {
+ $class = ucfirst( $key ) . 'AuthenticationProvider';
+ $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
+ $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( $key ) );
+ $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
+ ->will( $this->returnCallback( function ( $prop ) use ( $key ) {
+ return $prop !== $key;
+ } ) );
+ }
+
+ $this->primaryauthMocks = [ $mocks['primary'] ];
+ $this->secondaryauthMocks = [ $mocks['secondary'] ];
+ $this->initializeManager( true );
+
+ $this->assertTrue( $this->manager->allowsPropertyChange( 'foo' ) );
+ $this->assertFalse( $this->manager->allowsPropertyChange( 'primary' ) );
+ $this->assertFalse( $this->manager->allowsPropertyChange( 'secondary' ) );
+ }
+
+ public function testAutoCreateOnLogin() {
+ $username = self::usernameForCreation();
+
+ $req = $this->createMock( AuthenticationRequest::class );
+
+ $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
+ $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
+ ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
+ $mock->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+ $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
+ $mock->expects( $this->any() )->method( 'testUserForCreation' )
+ ->will( $this->returnValue( StatusValue::newGood() ) );
+
+ $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
+ $mock2->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( 'secondary' ) );
+ $mock2->expects( $this->any() )->method( 'beginSecondaryAuthentication' )->will(
+ $this->returnValue(
+ AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) )
+ )
+ );
+ $mock2->expects( $this->any() )->method( 'continueSecondaryAuthentication' )
+ ->will( $this->returnValue( AuthenticationResponse::newAbstain() ) );
+ $mock2->expects( $this->any() )->method( 'testUserForCreation' )
+ ->will( $this->returnValue( StatusValue::newGood() ) );
+
+ $this->primaryauthMocks = [ $mock ];
+ $this->secondaryauthMocks = [ $mock2 ];
+ $this->initializeManager( true );
+ $this->manager->setLogger( new \Psr\Log\NullLogger() );
+ $session = $this->request->getSession();
+ $session->clear();
+
+ $this->assertSame( 0, \User::newFromName( $username )->getId(),
+ 'sanity check' );
+
+ $callback = $this->callback( function ( $user ) use ( $username ) {
+ return $user->getName() === $username;
+ } );
+
+ $this->hook( 'UserLoggedIn', $this->never() );
+ $this->hook( 'LocalUserCreated', $this->once() )->with( $callback, $this->equalTo( true ) );
+ $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
+ $this->unhook( 'LocalUserCreated' );
+ $this->unhook( 'UserLoggedIn' );
+ $this->assertSame( AuthenticationResponse::UI, $ret->status );
+
+ $id = (int)\User::newFromName( $username )->getId();
+ $this->assertNotSame( 0, \User::newFromName( $username )->getId() );
+ $this->assertSame( 0, $session->getUser()->getId() );
+
+ $this->hook( 'UserLoggedIn', $this->once() )->with( $callback );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->continueAuthentication( [] );
+ $this->unhook( 'LocalUserCreated' );
+ $this->unhook( 'UserLoggedIn' );
+ $this->assertSame( AuthenticationResponse::PASS, $ret->status );
+ $this->assertSame( $username, $ret->username );
+ $this->assertSame( $id, $session->getUser()->getId() );
+ }
+
+ public function testAutoCreateFailOnLogin() {
+ $username = self::usernameForCreation();
+
+ $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
+ $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
+ ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
+ $mock->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+ $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
+ $mock->expects( $this->any() )->method( 'testUserForCreation' )
+ ->will( $this->returnValue( StatusValue::newFatal( 'fail-from-primary' ) ) );
+
+ $this->primaryauthMocks = [ $mock ];
+ $this->initializeManager( true );
+ $this->manager->setLogger( new \Psr\Log\NullLogger() );
+ $session = $this->request->getSession();
+ $session->clear();
+
+ $this->assertSame( 0, $session->getUser()->getId(),
+ 'sanity check' );
+ $this->assertSame( 0, \User::newFromName( $username )->getId(),
+ 'sanity check' );
+
+ $this->hook( 'UserLoggedIn', $this->never() );
+ $this->hook( 'LocalUserCreated', $this->never() );
+ $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
+ $this->unhook( 'LocalUserCreated' );
+ $this->unhook( 'UserLoggedIn' );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message->getKey() );
+
+ $this->assertSame( 0, \User::newFromName( $username )->getId() );
+ $this->assertSame( 0, $session->getUser()->getId() );
+ }
+
+ public function testAuthenticationSessionData() {
+ $this->initializeManager( true );
+
+ $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
+ $this->manager->setAuthenticationSessionData( 'foo', 'foo!' );
+ $this->manager->setAuthenticationSessionData( 'bar', 'bar!' );
+ $this->assertSame( 'foo!', $this->manager->getAuthenticationSessionData( 'foo' ) );
+ $this->assertSame( 'bar!', $this->manager->getAuthenticationSessionData( 'bar' ) );
+ $this->manager->removeAuthenticationSessionData( 'foo' );
+ $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
+ $this->assertSame( 'bar!', $this->manager->getAuthenticationSessionData( 'bar' ) );
+ $this->manager->removeAuthenticationSessionData( 'bar' );
+ $this->assertNull( $this->manager->getAuthenticationSessionData( 'bar' ) );
+
+ $this->manager->setAuthenticationSessionData( 'foo', 'foo!' );
+ $this->manager->setAuthenticationSessionData( 'bar', 'bar!' );
+ $this->manager->removeAuthenticationSessionData( null );
+ $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
+ $this->assertNull( $this->manager->getAuthenticationSessionData( 'bar' ) );
+ }
+
+ public function testCanLinkAccounts() {
+ $types = [
+ PrimaryAuthenticationProvider::TYPE_CREATE => true,
+ PrimaryAuthenticationProvider::TYPE_LINK => true,
+ PrimaryAuthenticationProvider::TYPE_NONE => false,
+ ];
+
+ foreach ( $types as $type => $can ) {
+ $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
+ $mock->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( $type ) );
+ $this->primaryauthMocks = [ $mock ];
+ $this->initializeManager( true );
+ $this->assertSame( $can, $this->manager->canCreateAccounts(), $type );
+ }
+ }
+
+ public function testBeginAccountLink() {
+ $user = \User::newFromName( 'UTSysop' );
+ $this->initializeManager();
+
+ $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', 'test' );
+ try {
+ $this->manager->beginAccountLink( $user, [], 'http://localhost/' );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \LogicException $ex ) {
+ $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
+ }
+ $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
+
+ $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+ $mock->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
+ $this->primaryauthMocks = [ $mock ];
+ $this->initializeManager( true );
+
+ $ret = $this->manager->beginAccountLink( new \User, [], 'http://localhost/' );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'noname', $ret->message->getKey() );
+
+ $ret = $this->manager->beginAccountLink(
+ \User::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
+ );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'authmanager-userdoesnotexist', $ret->message->getKey() );
+ }
+
+ public function testContinueAccountLink() {
+ $user = \User::newFromName( 'UTSysop' );
+ $this->initializeManager();
+
+ $session = [
+ 'userid' => $user->getId(),
+ 'username' => $user->getName(),
+ 'primary' => 'X',
+ ];
+
+ try {
+ $this->manager->continueAccountLink( [] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \LogicException $ex ) {
+ $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
+ }
+
+ $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
+ $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
+ $mock->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
+ $mock->expects( $this->any() )->method( 'beginPrimaryAccountLink' )->will(
+ $this->returnValue( AuthenticationResponse::newFail( $this->message( 'fail' ) ) )
+ );
+ $this->primaryauthMocks = [ $mock ];
+ $this->initializeManager( true );
+
+ $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', null );
+ $ret = $this->manager->continueAccountLink( [] );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'authmanager-link-not-in-progress', $ret->message->getKey() );
+
+ $this->request->getSession()->setSecret( 'AuthManager::accountLinkState',
+ [ 'username' => $user->getName() . '<>' ] + $session );
+ $ret = $this->manager->continueAccountLink( [] );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'noname', $ret->message->getKey() );
+ $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
+
+ $id = $user->getId();
+ $this->request->getSession()->setSecret( 'AuthManager::accountLinkState',
+ [ 'userid' => $id + 1 ] + $session );
+ try {
+ $ret = $this->manager->continueAccountLink( [] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertEquals(
+ "User \"{$user->getName()}\" is valid, but ID $id != " . ( $id + 1 ) . '!',
+ $ex->getMessage()
+ );
+ }
+ $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
+ }
+
+ /**
+ * @dataProvider provideAccountLink
+ * @param StatusValue $preTest
+ * @param array $primaryResponses
+ * @param array $managerResponses
+ */
+ public function testAccountLink(
+ StatusValue $preTest, array $primaryResponses, array $managerResponses
+ ) {
+ $user = \User::newFromName( 'UTSysop' );
+
+ $this->initializeManager();
+
+ // Set up lots of mocks...
+ $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+ $req->primary = $primaryResponses;
+ $mocks = [];
+
+ foreach ( [ 'pre', 'primary' ] as $key ) {
+ $class = ucfirst( $key ) . 'AuthenticationProvider';
+ $mocks[$key] = $this->getMockForAbstractClass(
+ "MediaWiki\\Auth\\$class", [], "Mock$class"
+ );
+ $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( $key ) );
+
+ for ( $i = 2; $i <= 3; $i++ ) {
+ $mocks[$key . $i] = $this->getMockForAbstractClass(
+ "MediaWiki\\Auth\\$class", [], "Mock$class"
+ );
+ $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( $key . $i ) );
+ }
+ }
+
+ $mocks['pre']->expects( $this->any() )->method( 'testForAccountLink' )
+ ->will( $this->returnCallback(
+ function ( $u )
+ use ( $user, $preTest )
+ {
+ $this->assertSame( $user->getId(), $u->getId() );
+ $this->assertSame( $user->getName(), $u->getName() );
+ return $preTest;
+ }
+ ) );
+
+ $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
+ ->will( $this->returnValue( StatusValue::newGood() ) );
+
+ $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
+ $ct = count( $req->primary );
+ $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req ) {
+ $this->assertSame( $user->getId(), $u->getId() );
+ $this->assertSame( $user->getName(), $u->getName() );
+ $foundReq = false;
+ foreach ( $reqs as $r ) {
+ $this->assertSame( $user->getName(), $r->username );
+ $foundReq = $foundReq || get_class( $r ) === get_class( $req );
+ }
+ $this->assertTrue( $foundReq, '$reqs contains $req' );
+ return array_shift( $req->primary );
+ } );
+ $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
+ ->method( 'beginPrimaryAccountLink' )
+ ->will( $callback );
+ $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
+ ->method( 'continuePrimaryAccountLink' )
+ ->will( $callback );
+
+ $abstain = AuthenticationResponse::newAbstain();
+ $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
+ $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
+ ->will( $this->returnValue( $abstain ) );
+ $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
+ $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
+ ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
+ $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
+ $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
+
+ $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
+ $this->primaryauthMocks = [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
+ $this->logger = new \TestLogger( true, function ( $message, $level ) {
+ return $level === LogLevel::DEBUG ? null : $message;
+ } );
+ $this->initializeManager( true );
+
+ $constraint = \PHPUnit_Framework_Assert::logicalOr(
+ $this->equalTo( AuthenticationResponse::PASS ),
+ $this->equalTo( AuthenticationResponse::FAIL )
+ );
+ $providers = array_merge( $this->preauthMocks, $this->primaryauthMocks );
+ foreach ( $providers as $p ) {
+ $p->postCalled = false;
+ $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
+ ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
+ $this->assertInstanceOf( \User::class, $user );
+ $this->assertSame( 'UTSysop', $user->getName() );
+ $this->assertInstanceOf( AuthenticationResponse::class, $response );
+ $this->assertThat( $response->status, $constraint );
+ $p->postCalled = $response->status;
+ } );
+ }
+
+ $first = true;
+ $created = false;
+ $expectLog = [];
+ foreach ( $managerResponses as $i => $response ) {
+ if ( $response instanceof AuthenticationResponse &&
+ $response->status === AuthenticationResponse::PASS
+ ) {
+ $expectLog[] = [ LogLevel::INFO, 'Account linked to {user} by primary' ];
+ }
+
+ $ex = null;
+ try {
+ if ( $first ) {
+ $ret = $this->manager->beginAccountLink( $user, [ $req ], 'http://localhost/' );
+ } else {
+ $ret = $this->manager->continueAccountLink( [ $req ] );
+ }
+ if ( $response instanceof \Exception ) {
+ $this->fail( 'Expected exception not thrown', "Response $i" );
+ }
+ } catch ( \Exception $ex ) {
+ if ( !$response instanceof \Exception ) {
+ throw $ex;
+ }
+ $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
+ $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
+ "Response $i, exception, session state" );
+ return;
+ }
+
+ $this->assertSame( 'http://localhost/', $req->returnToUrl );
+
+ $ret->message = $this->message( $ret->message );
+ $this->assertEquals( $response, $ret, "Response $i, response" );
+ if ( $response->status === AuthenticationResponse::PASS ||
+ $response->status === AuthenticationResponse::FAIL
+ ) {
+ $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
+ "Response $i, session state" );
+ foreach ( $providers as $p ) {
+ $this->assertSame( $response->status, $p->postCalled,
+ "Response $i, post-auth callback called" );
+ }
+ } else {
+ $this->assertNotNull(
+ $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
+ "Response $i, session state"
+ );
+ foreach ( $ret->neededRequests as $neededReq ) {
+ $this->assertEquals( AuthManager::ACTION_LINK, $neededReq->action,
+ "Response $i, neededRequest action" );
+ }
+ $this->assertEquals(
+ $ret->neededRequests,
+ $this->manager->getAuthenticationRequests( AuthManager::ACTION_LINK_CONTINUE ),
+ "Response $i, continuation check"
+ );
+ foreach ( $providers as $p ) {
+ $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
+ }
+ }
+
+ $first = false;
+ }
+
+ $this->assertSame( $expectLog, $this->logger->getBuffer() );
+ }
+
+ public function provideAccountLink() {
+ $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+ $good = StatusValue::newGood();
+
+ return [
+ 'Pre-link test fail in pre' => [
+ StatusValue::newFatal( 'fail-from-pre' ),
+ [],
+ [
+ AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
+ ]
+ ],
+ 'Failure in primary' => [
+ $good,
+ $tmp = [
+ AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
+ ],
+ $tmp
+ ],
+ 'All primary abstain' => [
+ $good,
+ [
+ AuthenticationResponse::newAbstain(),
+ ],
+ [
+ AuthenticationResponse::newFail( $this->message( 'authmanager-link-no-primary' ) )
+ ]
+ ],
+ 'Primary UI, then redirect, then fail' => [
+ $good,
+ $tmp = [
+ AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+ AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
+ AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
+ ],
+ $tmp
+ ],
+ 'Primary redirect, then abstain' => [
+ $good,
+ [
+ $tmp = AuthenticationResponse::newRedirect(
+ [ $req ], '/foo.html', [ 'foo' => 'bar' ]
+ ),
+ AuthenticationResponse::newAbstain(),
+ ],
+ [
+ $tmp,
+ new \DomainException(
+ 'MockPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
+ )
+ ]
+ ],
+ 'Primary UI, then pass' => [
+ $good,
+ [
+ $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
+ AuthenticationResponse::newPass(),
+ ],
+ [
+ $tmp1,
+ AuthenticationResponse::newPass( '' ),
+ ]
+ ],
+ 'Primary pass' => [
+ $good,
+ [
+ AuthenticationResponse::newPass( '' ),
+ ],
+ [
+ AuthenticationResponse::newPass( '' ),
+ ]
+ ],
+ ];
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/AuthPluginPrimaryAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/AuthPluginPrimaryAuthenticationProviderTest.php
new file mode 100644
index 00000000..57c3e7eb
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/AuthPluginPrimaryAuthenticationProviderTest.php
@@ -0,0 +1,716 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\AuthPluginPrimaryAuthenticationProvider
+ */
+class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase {
+ public function testConstruction() {
+ $plugin = new AuthManagerAuthPlugin();
+ try {
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \InvalidArgumentException $ex ) {
+ $this->assertSame(
+ 'Trying to wrap AuthManagerAuthPlugin in AuthPluginPrimaryAuthenticationProvider ' .
+ 'makes no sense.',
+ $ex->getMessage()
+ );
+ }
+
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $this->assertEquals(
+ [ new PasswordAuthenticationRequest ],
+ $provider->getAuthenticationRequests( AuthManager::ACTION_LOGIN, [] )
+ );
+
+ $req = $this->createMock( PasswordAuthenticationRequest::class );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin, get_class( $req ) );
+ $this->assertEquals(
+ [ $req ],
+ $provider->getAuthenticationRequests( AuthManager::ACTION_LOGIN, [] )
+ );
+
+ $reqType = get_class( $this->createMock( AuthenticationRequest::class ) );
+ try {
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin, $reqType );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \InvalidArgumentException $ex ) {
+ $this->assertSame(
+ "$reqType is not a MediaWiki\\Auth\\PasswordAuthenticationRequest",
+ $ex->getMessage()
+ );
+ }
+ }
+
+ public function testOnUserSaveSettings() {
+ $user = \User::newFromName( 'UTSysop' );
+
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->once() )->method( 'updateExternalDB' )
+ ->with( $this->identicalTo( $user ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+ \Hooks::run( 'UserSaveSettings', [ $user ] );
+ }
+
+ public function testOnUserGroupsChanged() {
+ $user = \User::newFromName( 'UTSysop' );
+
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->once() )->method( 'updateExternalDBGroups' )
+ ->with(
+ $this->identicalTo( $user ),
+ $this->identicalTo( [ 'added' ] ),
+ $this->identicalTo( [ 'removed' ] )
+ );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+ \Hooks::run( 'UserGroupsChanged', [ $user, [ 'added' ], [ 'removed' ], false, false, [], [] ] );
+ }
+
+ public function testOnUserLoggedIn() {
+ $user = \User::newFromName( 'UTSysop' );
+
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->exactly( 2 ) )->method( 'updateUser' )
+ ->with( $this->identicalTo( $user ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ \Hooks::run( 'UserLoggedIn', [ $user ] );
+
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->once() )->method( 'updateUser' )
+ ->will( $this->returnCallback( function ( &$user ) {
+ $user = \User::newFromName( 'UTSysop' );
+ } ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ try {
+ \Hooks::run( 'UserLoggedIn', [ $user ] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame(
+ get_class( $plugin ) . '::updateUser() tried to replace $user!',
+ $ex->getMessage()
+ );
+ }
+ }
+
+ public function testOnLocalUserCreated() {
+ $user = \User::newFromName( 'UTSysop' );
+
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->exactly( 2 ) )->method( 'initUser' )
+ ->with( $this->identicalTo( $user ), $this->identicalTo( false ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ \Hooks::run( 'LocalUserCreated', [ $user, false ] );
+
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->once() )->method( 'initUser' )
+ ->will( $this->returnCallback( function ( &$user ) {
+ $user = \User::newFromName( 'UTSysop' );
+ } ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ try {
+ \Hooks::run( 'LocalUserCreated', [ $user, false ] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame(
+ get_class( $plugin ) . '::initUser() tried to replace $user!',
+ $ex->getMessage()
+ );
+ }
+ }
+
+ public function testGetUniqueId() {
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $this->assertSame(
+ 'MediaWiki\\Auth\\AuthPluginPrimaryAuthenticationProvider:' . get_class( $plugin ),
+ $provider->getUniqueId()
+ );
+ }
+
+ /**
+ * @dataProvider provideGetAuthenticationRequests
+ * @param string $action
+ * @param array $response
+ * @param bool $allowPasswordChange
+ */
+ public function testGetAuthenticationRequests( $action, $response, $allowPasswordChange ) {
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->any() )->method( 'allowPasswordChange' )
+ ->will( $this->returnValue( $allowPasswordChange ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $this->assertEquals( $response, $provider->getAuthenticationRequests( $action, [] ) );
+ }
+
+ public static function provideGetAuthenticationRequests() {
+ $arr = [ new PasswordAuthenticationRequest() ];
+ return [
+ [ AuthManager::ACTION_LOGIN, $arr, true ],
+ [ AuthManager::ACTION_LOGIN, $arr, false ],
+ [ AuthManager::ACTION_CREATE, $arr, true ],
+ [ AuthManager::ACTION_CREATE, $arr, false ],
+ [ AuthManager::ACTION_LINK, [], true ],
+ [ AuthManager::ACTION_LINK, [], false ],
+ [ AuthManager::ACTION_CHANGE, $arr, true ],
+ [ AuthManager::ACTION_CHANGE, [], false ],
+ [ AuthManager::ACTION_REMOVE, $arr, true ],
+ [ AuthManager::ACTION_REMOVE, [], false ],
+ ];
+ }
+
+ public function testAuthentication() {
+ $req = new PasswordAuthenticationRequest();
+ $req->action = AuthManager::ACTION_LOGIN;
+ $reqs = [ PasswordAuthenticationRequest::class => $req ];
+
+ $plugin = $this->getMockBuilder( \AuthPlugin::class )
+ ->setMethods( [ 'authenticate' ] )
+ ->getMock();
+ $plugin->expects( $this->never() )->method( 'authenticate' );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAuthentication( [] )
+ );
+
+ $req->username = 'foo';
+ $req->password = null;
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+
+ $req->username = null;
+ $req->password = 'bar';
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+
+ $req->username = 'foo';
+ $req->password = 'bar';
+
+ $plugin = $this->getMockBuilder( \AuthPlugin::class )
+ ->setMethods( [ 'userExists', 'authenticate' ] )
+ ->getMock();
+ $plugin->expects( $this->once() )->method( 'userExists' )
+ ->will( $this->returnValue( true ) );
+ $plugin->expects( $this->once() )->method( 'authenticate' )
+ ->with( $this->equalTo( 'Foo' ), $this->equalTo( 'bar' ) )
+ ->will( $this->returnValue( true ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $this->assertEquals(
+ AuthenticationResponse::newPass( 'Foo', $req ),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+
+ $plugin = $this->getMockBuilder( \AuthPlugin::class )
+ ->setMethods( [ 'userExists', 'authenticate' ] )
+ ->getMock();
+ $plugin->expects( $this->once() )->method( 'userExists' )
+ ->will( $this->returnValue( false ) );
+ $plugin->expects( $this->never() )->method( 'authenticate' );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+
+ $pluginUser = $this->getMockBuilder( \AuthPluginUser::class )
+ ->setMethods( [ 'isLocked' ] )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $pluginUser->expects( $this->once() )->method( 'isLocked' )
+ ->will( $this->returnValue( true ) );
+ $plugin = $this->getMockBuilder( \AuthPlugin::class )
+ ->setMethods( [ 'userExists', 'getUserInstance', 'authenticate' ] )
+ ->getMock();
+ $plugin->expects( $this->once() )->method( 'userExists' )
+ ->will( $this->returnValue( true ) );
+ $plugin->expects( $this->once() )->method( 'getUserInstance' )
+ ->will( $this->returnValue( $pluginUser ) );
+ $plugin->expects( $this->never() )->method( 'authenticate' );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+
+ $plugin = $this->getMockBuilder( \AuthPlugin::class )
+ ->setMethods( [ 'userExists', 'authenticate' ] )
+ ->getMock();
+ $plugin->expects( $this->once() )->method( 'userExists' )
+ ->will( $this->returnValue( true ) );
+ $plugin->expects( $this->once() )->method( 'authenticate' )
+ ->with( $this->equalTo( 'Foo' ), $this->equalTo( 'bar' ) )
+ ->will( $this->returnValue( false ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+
+ $plugin = $this->getMockBuilder( \AuthPlugin::class )
+ ->setMethods( [ 'userExists', 'authenticate', 'strict' ] )
+ ->getMock();
+ $plugin->expects( $this->once() )->method( 'userExists' )
+ ->will( $this->returnValue( true ) );
+ $plugin->expects( $this->once() )->method( 'authenticate' )
+ ->with( $this->equalTo( 'Foo' ), $this->equalTo( 'bar' ) )
+ ->will( $this->returnValue( false ) );
+ $plugin->expects( $this->any() )->method( 'strict' )->will( $this->returnValue( true ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $ret = $provider->beginPrimaryAuthentication( $reqs );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'wrongpassword', $ret->message->getKey() );
+
+ $plugin = $this->getMockBuilder( \AuthPlugin::class )
+ ->setMethods( [ 'userExists', 'authenticate', 'strictUserAuth' ] )
+ ->getMock();
+ $plugin->expects( $this->once() )->method( 'userExists' )
+ ->will( $this->returnValue( true ) );
+ $plugin->expects( $this->once() )->method( 'authenticate' )
+ ->with( $this->equalTo( 'Foo' ), $this->equalTo( 'bar' ) )
+ ->will( $this->returnValue( false ) );
+ $plugin->expects( $this->any() )->method( 'strictUserAuth' )
+ ->with( $this->equalTo( 'Foo' ) )
+ ->will( $this->returnValue( true ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $ret = $provider->beginPrimaryAuthentication( $reqs );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'wrongpassword', $ret->message->getKey() );
+
+ $plugin = $this->getMockBuilder( \AuthPlugin::class )
+ ->setMethods( [ 'domainList', 'validDomain', 'setDomain', 'userExists', 'authenticate' ] )
+ ->getMock();
+ $plugin->expects( $this->any() )->method( 'domainList' )
+ ->will( $this->returnValue( [ 'Domain1', 'Domain2' ] ) );
+ $plugin->expects( $this->any() )->method( 'validDomain' )
+ ->will( $this->returnCallback( function ( $domain ) {
+ return in_array( $domain, [ 'Domain1', 'Domain2' ] );
+ } ) );
+ $plugin->expects( $this->once() )->method( 'setDomain' )
+ ->with( $this->equalTo( 'Domain2' ) );
+ $plugin->expects( $this->once() )->method( 'userExists' )
+ ->will( $this->returnValue( true ) );
+ $plugin->expects( $this->once() )->method( 'authenticate' )
+ ->with( $this->equalTo( 'Foo' ), $this->equalTo( 'bar' ) )
+ ->will( $this->returnValue( true ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ list( $req ) = $provider->getAuthenticationRequests( AuthManager::ACTION_LOGIN, [] );
+ $req->username = 'foo';
+ $req->password = 'bar';
+ $req->domain = 'Domain2';
+ $provider->beginPrimaryAuthentication( [ $req ] );
+ }
+
+ public function testTestUserExists() {
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->once() )->method( 'userExists' )
+ ->with( $this->equalTo( 'Foo' ) )
+ ->will( $this->returnValue( true ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+ $this->assertTrue( $provider->testUserExists( 'foo' ) );
+
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->once() )->method( 'userExists' )
+ ->with( $this->equalTo( 'Foo' ) )
+ ->will( $this->returnValue( false ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+ $this->assertFalse( $provider->testUserExists( 'foo' ) );
+ }
+
+ public function testTestUserCanAuthenticate() {
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->once() )->method( 'userExists' )
+ ->with( $this->equalTo( 'Foo' ) )
+ ->will( $this->returnValue( false ) );
+ $plugin->expects( $this->never() )->method( 'getUserInstance' );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $this->assertFalse( $provider->testUserCanAuthenticate( 'foo' ) );
+
+ $pluginUser = $this->getMockBuilder( \AuthPluginUser::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $pluginUser->expects( $this->once() )->method( 'isLocked' )
+ ->will( $this->returnValue( true ) );
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->once() )->method( 'userExists' )
+ ->with( $this->equalTo( 'Foo' ) )
+ ->will( $this->returnValue( true ) );
+ $plugin->expects( $this->once() )->method( 'getUserInstance' )
+ ->with( $this->callback( function ( $user ) {
+ $this->assertInstanceOf( \User::class, $user );
+ $this->assertEquals( 'Foo', $user->getName() );
+ return true;
+ } ) )
+ ->will( $this->returnValue( $pluginUser ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $this->assertFalse( $provider->testUserCanAuthenticate( 'foo' ) );
+
+ $pluginUser = $this->getMockBuilder( \AuthPluginUser::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $pluginUser->expects( $this->once() )->method( 'isLocked' )
+ ->will( $this->returnValue( false ) );
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->once() )->method( 'userExists' )
+ ->with( $this->equalTo( 'Foo' ) )
+ ->will( $this->returnValue( true ) );
+ $plugin->expects( $this->once() )->method( 'getUserInstance' )
+ ->with( $this->callback( function ( $user ) {
+ $this->assertInstanceOf( \User::class, $user );
+ $this->assertEquals( 'Foo', $user->getName() );
+ return true;
+ } ) )
+ ->will( $this->returnValue( $pluginUser ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $this->assertTrue( $provider->testUserCanAuthenticate( 'foo' ) );
+ }
+
+ public function testProviderRevokeAccessForUser() {
+ $plugin = $this->getMockBuilder( \AuthPlugin::class )
+ ->setMethods( [ 'userExists', 'setPassword' ] )
+ ->getMock();
+ $plugin->expects( $this->once() )->method( 'userExists' )->willReturn( true );
+ $plugin->expects( $this->once() )->method( 'setPassword' )
+ ->with( $this->callback( function ( $u ) {
+ return $u instanceof \User && $u->getName() === 'Foo';
+ } ), $this->identicalTo( null ) )
+ ->willReturn( true );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $provider->providerRevokeAccessForUser( 'foo' );
+
+ $plugin = $this->getMockBuilder( \AuthPlugin::class )
+ ->setMethods( [ 'domainList', 'userExists', 'setPassword' ] )
+ ->getMock();
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [ 'D1', 'D2', 'D3' ] );
+ $plugin->expects( $this->exactly( 3 ) )->method( 'userExists' )
+ ->willReturnCallback( function () use ( $plugin ) {
+ return $plugin->getDomain() !== 'D2';
+ } );
+ $plugin->expects( $this->exactly( 2 ) )->method( 'setPassword' )
+ ->with( $this->callback( function ( $u ) {
+ return $u instanceof \User && $u->getName() === 'Foo';
+ } ), $this->identicalTo( null ) )
+ ->willReturnCallback( function () use ( $plugin ) {
+ $this->assertNotEquals( 'D2', $plugin->getDomain() );
+ return $plugin->getDomain() !== 'D1';
+ } );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ try {
+ $provider->providerRevokeAccessForUser( 'foo' );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame(
+ 'AuthPlugin failed to reset password for Foo in the following domains: D1',
+ $ex->getMessage()
+ );
+ }
+ }
+
+ public function testProviderAllowsPropertyChange() {
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->any() )->method( 'allowPropChange' )
+ ->will( $this->returnCallback( function ( $prop ) {
+ return $prop === 'allow';
+ } ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+ $this->assertTrue( $provider->providerAllowsPropertyChange( 'allow' ) );
+ $this->assertFalse( $provider->providerAllowsPropertyChange( 'deny' ) );
+ }
+
+ /**
+ * @dataProvider provideProviderAllowsAuthenticationDataChange
+ * @param string $type
+ * @param bool|null $allow
+ * @param StatusValue $expect
+ */
+ public function testProviderAllowsAuthenticationDataChange( $type, $allow, $expect ) {
+ $domains = $type instanceof PasswordDomainAuthenticationRequest ? [ 'foo', 'bar' ] : [];
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( $domains );
+ $plugin->expects( $allow === null ? $this->never() : $this->once() )
+ ->method( 'allowPasswordChange' )->will( $this->returnValue( $allow ) );
+ $plugin->expects( $this->any() )->method( 'validDomain' )
+ ->willReturnCallback( function ( $d ) use ( $domains ) {
+ return in_array( $d, $domains, true );
+ } );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+ if ( is_object( $type ) ) {
+ $req = $type;
+ } else {
+ $req = $this->createMock( $type );
+ }
+ $req->action = AuthManager::ACTION_CHANGE;
+ $req->username = 'UTSysop';
+ $req->password = 'Pa$$w0Rd!!!';
+ $req->retype = 'Pa$$w0Rd!!!';
+ $this->assertEquals( $expect, $provider->providerAllowsAuthenticationDataChange( $req ) );
+ }
+
+ public static function provideProviderAllowsAuthenticationDataChange() {
+ $domains = [ 'foo', 'bar' ];
+ $reqNoDomain = new PasswordDomainAuthenticationRequest( $domains );
+ $reqValidDomain = new PasswordDomainAuthenticationRequest( $domains );
+ $reqValidDomain->domain = 'foo';
+ $reqInvalidDomain = new PasswordDomainAuthenticationRequest( $domains );
+ $reqInvalidDomain->domain = 'invalid';
+
+ return [
+ [ AuthenticationRequest::class, null, \StatusValue::newGood( 'ignored' ) ],
+ [ new PasswordAuthenticationRequest, true, \StatusValue::newGood() ],
+ [
+ new PasswordAuthenticationRequest,
+ false,
+ \StatusValue::newFatal( 'authmanager-authplugin-setpass-denied' )
+ ],
+ [ $reqNoDomain, true, \StatusValue::newGood( 'ignored' ) ],
+ [ $reqValidDomain, true, \StatusValue::newGood() ],
+ [
+ $reqInvalidDomain,
+ true,
+ \StatusValue::newFatal( 'authmanager-authplugin-setpass-bad-domain' )
+ ],
+ ];
+ }
+
+ public function testProviderChangeAuthenticationData() {
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->never() )->method( 'setPassword' );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $provider->providerChangeAuthenticationData(
+ $this->createMock( AuthenticationRequest::class )
+ );
+
+ $req = new PasswordAuthenticationRequest();
+ $req->action = AuthManager::ACTION_CHANGE;
+ $req->username = 'foo';
+ $req->password = 'bar';
+
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->once() )->method( 'setPassword' )
+ ->with( $this->callback( function ( $u ) {
+ return $u instanceof \User && $u->getName() === 'Foo';
+ } ), $this->equalTo( 'bar' ) )
+ ->will( $this->returnValue( true ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $provider->providerChangeAuthenticationData( $req );
+
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->once() )->method( 'setPassword' )
+ ->with( $this->callback( function ( $u ) {
+ return $u instanceof \User && $u->getName() === 'Foo';
+ } ), $this->equalTo( 'bar' ) )
+ ->will( $this->returnValue( false ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ try {
+ $provider->providerChangeAuthenticationData( $req );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \ErrorPageError $e ) {
+ $this->assertSame( 'authmanager-authplugin-setpass-failed-title', $e->title );
+ $this->assertSame( 'authmanager-authplugin-setpass-failed-message', $e->msg );
+ }
+
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )
+ ->will( $this->returnValue( [ 'Domain1', 'Domain2' ] ) );
+ $plugin->expects( $this->any() )->method( 'validDomain' )
+ ->will( $this->returnCallback( function ( $domain ) {
+ return in_array( $domain, [ 'Domain1', 'Domain2' ] );
+ } ) );
+ $plugin->expects( $this->once() )->method( 'setDomain' )
+ ->with( $this->equalTo( 'Domain2' ) );
+ $plugin->expects( $this->once() )->method( 'setPassword' )
+ ->with( $this->callback( function ( $u ) {
+ return $u instanceof \User && $u->getName() === 'Foo';
+ } ), $this->equalTo( 'bar' ) )
+ ->will( $this->returnValue( true ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ list( $req ) = $provider->getAuthenticationRequests( AuthManager::ACTION_CREATE, [] );
+ $req->username = 'foo';
+ $req->password = 'bar';
+ $req->domain = 'Domain2';
+ $provider->providerChangeAuthenticationData( $req );
+ }
+
+ /**
+ * @dataProvider provideAccountCreationType
+ * @param bool $can
+ * @param string $expect
+ */
+ public function testAccountCreationType( $can, $expect ) {
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->once() )
+ ->method( 'canCreateAccounts' )->will( $this->returnValue( $can ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+ $this->assertSame( $expect, $provider->accountCreationType() );
+ }
+
+ public static function provideAccountCreationType() {
+ return [
+ [ true, PrimaryAuthenticationProvider::TYPE_CREATE ],
+ [ false, PrimaryAuthenticationProvider::TYPE_NONE ],
+ ];
+ }
+
+ public function testTestForAccountCreation() {
+ $user = \User::newFromName( 'foo' );
+
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testForAccountCreation( $user, $user, [] )
+ );
+ }
+
+ public function testAccountCreation() {
+ $user = \User::newFromName( 'foo' );
+ $user->setEmail( 'email' );
+ $user->setRealName( 'realname' );
+
+ $req = new PasswordAuthenticationRequest();
+ $req->action = AuthManager::ACTION_CREATE;
+ $reqs = [ PasswordAuthenticationRequest::class => $req ];
+
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->any() )->method( 'canCreateAccounts' )
+ ->will( $this->returnValue( false ) );
+ $plugin->expects( $this->never() )->method( 'addUser' );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ try {
+ $provider->beginPrimaryAccountCreation( $user, $user, [] );
+ $this->fail( 'Expected exception was not thrown' );
+ } catch ( \BadMethodCallException $ex ) {
+ $this->assertSame(
+ 'Shouldn\'t call this when accountCreationType() is NONE', $ex->getMessage()
+ );
+ }
+
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->any() )->method( 'canCreateAccounts' )
+ ->will( $this->returnValue( true ) );
+ $plugin->expects( $this->never() )->method( 'addUser' );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAccountCreation( $user, $user, [] )
+ );
+
+ $req->username = 'foo';
+ $req->password = null;
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
+ );
+
+ $req->username = null;
+ $req->password = 'bar';
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
+ );
+
+ $req->username = 'foo';
+ $req->password = 'bar';
+
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->any() )->method( 'canCreateAccounts' )
+ ->will( $this->returnValue( true ) );
+ $plugin->expects( $this->once() )->method( 'addUser' )
+ ->with(
+ $this->callback( function ( $u ) {
+ return $u instanceof \User && $u->getName() === 'Foo';
+ } ),
+ $this->equalTo( 'bar' ),
+ $this->equalTo( 'email' ),
+ $this->equalTo( 'realname' )
+ )
+ ->will( $this->returnValue( true ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $this->assertEquals(
+ AuthenticationResponse::newPass(),
+ $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
+ );
+
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] );
+ $plugin->expects( $this->any() )->method( 'canCreateAccounts' )
+ ->will( $this->returnValue( true ) );
+ $plugin->expects( $this->once() )->method( 'addUser' )
+ ->with(
+ $this->callback( function ( $u ) {
+ return $u instanceof \User && $u->getName() === 'Foo';
+ } ),
+ $this->equalTo( 'bar' ),
+ $this->equalTo( 'email' ),
+ $this->equalTo( 'realname' )
+ )
+ ->will( $this->returnValue( false ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ $ret = $provider->beginPrimaryAccountCreation( $user, $user, $reqs );
+ $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
+ $this->assertSame( 'authmanager-authplugin-create-fail', $ret->message->getKey() );
+
+ $plugin = $this->createMock( \AuthPlugin::class );
+ $plugin->expects( $this->any() )->method( 'canCreateAccounts' )
+ ->will( $this->returnValue( true ) );
+ $plugin->expects( $this->any() )->method( 'domainList' )
+ ->will( $this->returnValue( [ 'Domain1', 'Domain2' ] ) );
+ $plugin->expects( $this->any() )->method( 'validDomain' )
+ ->will( $this->returnCallback( function ( $domain ) {
+ return in_array( $domain, [ 'Domain1', 'Domain2' ] );
+ } ) );
+ $plugin->expects( $this->once() )->method( 'setDomain' )
+ ->with( $this->equalTo( 'Domain2' ) );
+ $plugin->expects( $this->once() )->method( 'addUser' )
+ ->with( $this->callback( function ( $u ) {
+ return $u instanceof \User && $u->getName() === 'Foo';
+ } ), $this->equalTo( 'bar' ) )
+ ->will( $this->returnValue( true ) );
+ $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
+ list( $req ) = $provider->getAuthenticationRequests( AuthManager::ACTION_CREATE, [] );
+ $req->username = 'foo';
+ $req->password = 'bar';
+ $req->domain = 'Domain2';
+ $provider->beginPrimaryAccountCreation( $user, $user, [ $req ] );
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTest.php b/www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTest.php
new file mode 100644
index 00000000..1bc0f31f
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTest.php
@@ -0,0 +1,517 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\AuthenticationRequest
+ */
+class AuthenticationRequestTest extends \MediaWikiTestCase {
+ public function testBasics() {
+ $mock = $this->getMockForAbstractClass( AuthenticationRequest::class );
+
+ $this->assertSame( get_class( $mock ), $mock->getUniqueId() );
+
+ $this->assertType( 'array', $mock->getMetadata() );
+
+ $ret = $mock->describeCredentials();
+ $this->assertInternalType( 'array', $ret );
+ $this->assertArrayHasKey( 'provider', $ret );
+ $this->assertInstanceOf( \Message::class, $ret['provider'] );
+ $this->assertArrayHasKey( 'account', $ret );
+ $this->assertInstanceOf( \Message::class, $ret['account'] );
+ }
+
+ public function testLoadRequestsFromSubmission() {
+ $mb = $this->getMockBuilder( AuthenticationRequest::class )
+ ->setMethods( [ 'loadFromSubmission' ] );
+
+ $data = [ 'foo', 'bar' ];
+
+ $req1 = $mb->getMockForAbstractClass();
+ $req1->expects( $this->once() )->method( 'loadFromSubmission' )
+ ->with( $this->identicalTo( $data ) )
+ ->will( $this->returnValue( false ) );
+
+ $req2 = $mb->getMockForAbstractClass();
+ $req2->expects( $this->once() )->method( 'loadFromSubmission' )
+ ->with( $this->identicalTo( $data ) )
+ ->will( $this->returnValue( true ) );
+
+ $this->assertSame(
+ [ $req2 ],
+ AuthenticationRequest::loadRequestsFromSubmission( [ $req1, $req2 ], $data )
+ );
+ }
+
+ public function testGetRequestByClass() {
+ $mb = $this->getMockBuilder(
+ AuthenticationRequest::class, 'AuthenticationRequestTest_AuthenticationRequest2'
+ );
+
+ $reqs = [
+ $this->getMockForAbstractClass(
+ AuthenticationRequest::class, [], 'AuthenticationRequestTest_AuthenticationRequest1'
+ ),
+ $mb->getMockForAbstractClass(),
+ $mb->getMockForAbstractClass(),
+ $this->getMockForAbstractClass(
+ PasswordAuthenticationRequest::class, [],
+ 'AuthenticationRequestTest_PasswordAuthenticationRequest'
+ ),
+ ];
+
+ $this->assertNull( AuthenticationRequest::getRequestByClass(
+ $reqs, 'AuthenticationRequestTest_AuthenticationRequest0'
+ ) );
+ $this->assertSame( $reqs[0], AuthenticationRequest::getRequestByClass(
+ $reqs, 'AuthenticationRequestTest_AuthenticationRequest1'
+ ) );
+ $this->assertNull( AuthenticationRequest::getRequestByClass(
+ $reqs, 'AuthenticationRequestTest_AuthenticationRequest2'
+ ) );
+ $this->assertNull( AuthenticationRequest::getRequestByClass(
+ $reqs, PasswordAuthenticationRequest::class
+ ) );
+ $this->assertNull( AuthenticationRequest::getRequestByClass(
+ $reqs, 'ClassThatDoesNotExist'
+ ) );
+
+ $this->assertNull( AuthenticationRequest::getRequestByClass(
+ $reqs, 'AuthenticationRequestTest_AuthenticationRequest0', true
+ ) );
+ $this->assertSame( $reqs[0], AuthenticationRequest::getRequestByClass(
+ $reqs, 'AuthenticationRequestTest_AuthenticationRequest1', true
+ ) );
+ $this->assertNull( AuthenticationRequest::getRequestByClass(
+ $reqs, 'AuthenticationRequestTest_AuthenticationRequest2', true
+ ) );
+ $this->assertSame( $reqs[3], AuthenticationRequest::getRequestByClass(
+ $reqs, PasswordAuthenticationRequest::class, true
+ ) );
+ $this->assertNull( AuthenticationRequest::getRequestByClass(
+ $reqs, 'ClassThatDoesNotExist', true
+ ) );
+ }
+
+ public function testGetUsernameFromRequests() {
+ $mb = $this->getMockBuilder( AuthenticationRequest::class );
+
+ for ( $i = 0; $i < 3; $i++ ) {
+ $req = $mb->getMockForAbstractClass();
+ $req->expects( $this->any() )->method( 'getFieldInfo' )->will( $this->returnValue( [
+ 'username' => [
+ 'type' => 'string',
+ ],
+ ] ) );
+ $reqs[] = $req;
+ }
+
+ $req = $mb->getMockForAbstractClass();
+ $req->expects( $this->any() )->method( 'getFieldInfo' )->will( $this->returnValue( [] ) );
+ $req->username = 'baz';
+ $reqs[] = $req;
+
+ $this->assertNull( AuthenticationRequest::getUsernameFromRequests( $reqs ) );
+
+ $reqs[1]->username = 'foo';
+ $this->assertSame( 'foo', AuthenticationRequest::getUsernameFromRequests( $reqs ) );
+
+ $reqs[0]->username = 'foo';
+ $reqs[2]->username = 'foo';
+ $this->assertSame( 'foo', AuthenticationRequest::getUsernameFromRequests( $reqs ) );
+
+ $reqs[1]->username = 'bar';
+ try {
+ AuthenticationRequest::getUsernameFromRequests( $reqs );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame(
+ 'Conflicting username fields: "bar" from ' .
+ get_class( $reqs[1] ) . '::$username vs. "foo" from ' .
+ get_class( $reqs[0] ) . '::$username',
+ $ex->getMessage()
+ );
+ }
+ }
+
+ public function testMergeFieldInfo() {
+ $msg = wfMessage( 'foo' );
+
+ $req1 = $this->createMock( AuthenticationRequest::class );
+ $req1->required = AuthenticationRequest::REQUIRED;
+ $req1->expects( $this->any() )->method( 'getFieldInfo' )->will( $this->returnValue( [
+ 'string1' => [
+ 'type' => 'string',
+ 'label' => $msg,
+ 'help' => $msg,
+ ],
+ 'string2' => [
+ 'type' => 'string',
+ 'label' => $msg,
+ 'help' => $msg,
+ ],
+ 'optional' => [
+ 'type' => 'string',
+ 'label' => $msg,
+ 'help' => $msg,
+ 'optional' => true,
+ ],
+ 'select' => [
+ 'type' => 'select',
+ 'options' => [ 'foo' => $msg, 'baz' => $msg ],
+ 'label' => $msg,
+ 'help' => $msg,
+ ],
+ ] ) );
+
+ $req2 = $this->createMock( AuthenticationRequest::class );
+ $req2->required = AuthenticationRequest::REQUIRED;
+ $req2->expects( $this->any() )->method( 'getFieldInfo' )->will( $this->returnValue( [
+ 'string1' => [
+ 'type' => 'string',
+ 'label' => $msg,
+ 'help' => $msg,
+ 'sensitive' => true,
+ ],
+ 'string3' => [
+ 'type' => 'string',
+ 'label' => $msg,
+ 'help' => $msg,
+ ],
+ 'select' => [
+ 'type' => 'select',
+ 'options' => [ 'bar' => $msg, 'baz' => $msg ],
+ 'label' => $msg,
+ 'help' => $msg,
+ ],
+ ] ) );
+
+ $req3 = $this->createMock( AuthenticationRequest::class );
+ $req3->required = AuthenticationRequest::REQUIRED;
+ $req3->expects( $this->any() )->method( 'getFieldInfo' )->will( $this->returnValue( [
+ 'string1' => [
+ 'type' => 'checkbox',
+ 'label' => $msg,
+ 'help' => $msg,
+ ],
+ ] ) );
+
+ $req4 = $this->createMock( AuthenticationRequest::class );
+ $req4->required = AuthenticationRequest::REQUIRED;
+ $req4->expects( $this->any() )->method( 'getFieldInfo' )->will( $this->returnValue( [] ) );
+
+ // Basic combining
+
+ $fields = AuthenticationRequest::mergeFieldInfo( [ $req1 ] );
+ $expect = $req1->getFieldInfo();
+ foreach ( $expect as $name => &$options ) {
+ $options['optional'] = !empty( $options['optional'] );
+ $options['sensitive'] = !empty( $options['sensitive'] );
+ }
+ unset( $options );
+ $this->assertEquals( $expect, $fields );
+
+ $fields = AuthenticationRequest::mergeFieldInfo( [ $req1, $req4 ] );
+ $this->assertEquals( $expect, $fields );
+
+ try {
+ AuthenticationRequest::mergeFieldInfo( [ $req1, $req3 ] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame(
+ 'Field type conflict for "string1", "string" vs "checkbox"',
+ $ex->getMessage()
+ );
+ }
+
+ $fields = AuthenticationRequest::mergeFieldInfo( [ $req1, $req2 ] );
+ $expect += $req2->getFieldInfo();
+ $expect['string1']['sensitive'] = true;
+ $expect['string2']['optional'] = false;
+ $expect['string3']['optional'] = false;
+ $expect['string3']['sensitive'] = false;
+ $expect['select']['options']['bar'] = $msg;
+ $this->assertEquals( $expect, $fields );
+
+ // Combining with something not required
+
+ $req1->required = AuthenticationRequest::PRIMARY_REQUIRED;
+
+ $fields = AuthenticationRequest::mergeFieldInfo( [ $req1, $req2 ] );
+ $expect += $req2->getFieldInfo();
+ $expect['string1']['optional'] = false;
+ $expect['string1']['sensitive'] = true;
+ $expect['string3']['optional'] = false;
+ $expect['select']['optional'] = false;
+ $expect['select']['options']['bar'] = $msg;
+ $this->assertEquals( $expect, $fields );
+
+ $req2->required = AuthenticationRequest::PRIMARY_REQUIRED;
+
+ $fields = AuthenticationRequest::mergeFieldInfo( [ $req1, $req2 ] );
+ $expect = $req1->getFieldInfo() + $req2->getFieldInfo();
+ foreach ( $expect as $name => &$options ) {
+ $options['sensitive'] = !empty( $options['sensitive'] );
+ }
+ $expect['string1']['optional'] = false;
+ $expect['string1']['sensitive'] = true;
+ $expect['string2']['optional'] = true;
+ $expect['string3']['optional'] = true;
+ $expect['select']['optional'] = false;
+ $expect['select']['options']['bar'] = $msg;
+ $this->assertEquals( $expect, $fields );
+ }
+
+ /**
+ * @dataProvider provideLoadFromSubmission
+ * @param array $fieldInfo
+ * @param array $data
+ * @param array|bool $expectState
+ */
+ public function testLoadFromSubmission( $fieldInfo, $data, $expectState ) {
+ $mock = $this->getMockForAbstractClass( AuthenticationRequest::class );
+ $mock->expects( $this->any() )->method( 'getFieldInfo' )
+ ->will( $this->returnValue( $fieldInfo ) );
+
+ $ret = $mock->loadFromSubmission( $data );
+ if ( is_array( $expectState ) ) {
+ $this->assertTrue( $ret );
+ $expect = call_user_func( [ get_class( $mock ), '__set_state' ], $expectState );
+ $this->assertEquals( $expect, $mock );
+ } else {
+ $this->assertFalse( $ret );
+ }
+ }
+
+ public static function provideLoadFromSubmission() {
+ return [
+ 'No fields' => [
+ [],
+ $data = [ 'foo' => 'bar' ],
+ false
+ ],
+
+ 'Simple field' => [
+ [
+ 'field' => [
+ 'type' => 'string',
+ ],
+ ],
+ $data = [ 'field' => 'string!' ],
+ $data
+ ],
+ 'Simple field, not supplied' => [
+ [
+ 'field' => [
+ 'type' => 'string',
+ ],
+ ],
+ [],
+ false
+ ],
+ 'Simple field, empty' => [
+ [
+ 'field' => [
+ 'type' => 'string',
+ ],
+ ],
+ [ 'field' => '' ],
+ false
+ ],
+ 'Simple field, optional, not supplied' => [
+ [
+ 'field' => [
+ 'type' => 'string',
+ 'optional' => true,
+ ],
+ ],
+ [],
+ false
+ ],
+ 'Simple field, optional, empty' => [
+ [
+ 'field' => [
+ 'type' => 'string',
+ 'optional' => true,
+ ],
+ ],
+ $data = [ 'field' => '' ],
+ $data
+ ],
+
+ 'Checkbox, checked' => [
+ [
+ 'check' => [
+ 'type' => 'checkbox',
+ ],
+ ],
+ [ 'check' => '' ],
+ [ 'check' => true ]
+ ],
+ 'Checkbox, unchecked' => [
+ [
+ 'check' => [
+ 'type' => 'checkbox',
+ ],
+ ],
+ [],
+ false
+ ],
+ 'Checkbox, optional, unchecked' => [
+ [
+ 'check' => [
+ 'type' => 'checkbox',
+ 'optional' => true,
+ ],
+ ],
+ [],
+ [ 'check' => false ]
+ ],
+
+ 'Button, used' => [
+ [
+ 'push' => [
+ 'type' => 'button',
+ ],
+ ],
+ [ 'push' => '' ],
+ [ 'push' => true ]
+ ],
+ 'Button, unused' => [
+ [
+ 'push' => [
+ 'type' => 'button',
+ ],
+ ],
+ [],
+ false
+ ],
+ 'Button, optional, unused' => [
+ [
+ 'push' => [
+ 'type' => 'button',
+ 'optional' => true,
+ ],
+ ],
+ [],
+ [ 'push' => false ]
+ ],
+ 'Button, image-style' => [
+ [
+ 'push' => [
+ 'type' => 'button',
+ ],
+ ],
+ [ 'push_x' => 0, 'push_y' => 0 ],
+ [ 'push' => true ]
+ ],
+
+ 'Select' => [
+ [
+ 'choose' => [
+ 'type' => 'select',
+ 'options' => [
+ 'foo' => wfMessage( 'mainpage' ),
+ 'bar' => wfMessage( 'mainpage' ),
+ ],
+ ],
+ ],
+ $data = [ 'choose' => 'foo' ],
+ $data
+ ],
+ 'Select, invalid choice' => [
+ [
+ 'choose' => [
+ 'type' => 'select',
+ 'options' => [
+ 'foo' => wfMessage( 'mainpage' ),
+ 'bar' => wfMessage( 'mainpage' ),
+ ],
+ ],
+ ],
+ $data = [ 'choose' => 'baz' ],
+ false
+ ],
+ 'Multiselect (2)' => [
+ [
+ 'choose' => [
+ 'type' => 'multiselect',
+ 'options' => [
+ 'foo' => wfMessage( 'mainpage' ),
+ 'bar' => wfMessage( 'mainpage' ),
+ ],
+ ],
+ ],
+ $data = [ 'choose' => [ 'foo', 'bar' ] ],
+ $data
+ ],
+ 'Multiselect (1)' => [
+ [
+ 'choose' => [
+ 'type' => 'multiselect',
+ 'options' => [
+ 'foo' => wfMessage( 'mainpage' ),
+ 'bar' => wfMessage( 'mainpage' ),
+ ],
+ ],
+ ],
+ $data = [ 'choose' => [ 'bar' ] ],
+ $data
+ ],
+ 'Multiselect, string for some reason' => [
+ [
+ 'choose' => [
+ 'type' => 'multiselect',
+ 'options' => [
+ 'foo' => wfMessage( 'mainpage' ),
+ 'bar' => wfMessage( 'mainpage' ),
+ ],
+ ],
+ ],
+ [ 'choose' => 'foo' ],
+ [ 'choose' => [ 'foo' ] ]
+ ],
+ 'Multiselect, invalid choice' => [
+ [
+ 'choose' => [
+ 'type' => 'multiselect',
+ 'options' => [
+ 'foo' => wfMessage( 'mainpage' ),
+ 'bar' => wfMessage( 'mainpage' ),
+ ],
+ ],
+ ],
+ [ 'choose' => [ 'foo', 'baz' ] ],
+ false
+ ],
+ 'Multiselect, empty' => [
+ [
+ 'choose' => [
+ 'type' => 'multiselect',
+ 'options' => [
+ 'foo' => wfMessage( 'mainpage' ),
+ 'bar' => wfMessage( 'mainpage' ),
+ ],
+ ],
+ ],
+ [ 'choose' => [] ],
+ false
+ ],
+ 'Multiselect, optional, nothing submitted' => [
+ [
+ 'choose' => [
+ 'type' => 'multiselect',
+ 'options' => [
+ 'foo' => wfMessage( 'mainpage' ),
+ 'bar' => wfMessage( 'mainpage' ),
+ ],
+ 'optional' => true,
+ ],
+ ],
+ [],
+ [ 'choose' => [] ]
+ ],
+ ];
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTestCase.php b/www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTestCase.php
new file mode 100644
index 00000000..f483b9b6
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTestCase.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ */
+abstract class AuthenticationRequestTestCase extends \MediaWikiTestCase {
+ abstract protected function getInstance( array $args = [] );
+
+ /**
+ * @dataProvider provideGetFieldInfo
+ */
+ public function testGetFieldInfo( array $args ) {
+ $info = $this->getInstance( $args )->getFieldInfo();
+ $this->assertType( 'array', $info );
+
+ foreach ( $info as $field => $data ) {
+ $this->assertType( 'array', $data, "Field $field" );
+ $this->assertArrayHasKey( 'type', $data, "Field $field" );
+ $this->assertArrayHasKey( 'label', $data, "Field $field" );
+ $this->assertInstanceOf( \Message::class, $data['label'], "Field $field, label" );
+
+ if ( $data['type'] !== 'null' ) {
+ $this->assertArrayHasKey( 'help', $data, "Field $field" );
+ $this->assertInstanceOf( \Message::class, $data['help'], "Field $field, help" );
+ }
+
+ if ( isset( $data['optional'] ) ) {
+ $this->assertType( 'bool', $data['optional'], "Field $field, optional" );
+ }
+ if ( isset( $data['image'] ) ) {
+ $this->assertType( 'string', $data['image'], "Field $field, image" );
+ }
+ if ( isset( $data['sensitive'] ) ) {
+ $this->assertType( 'bool', $data['sensitive'], "Field $field, sensitive" );
+ }
+ if ( $data['type'] === 'password' ) {
+ $this->assertTrue( !empty( $data['sensitive'] ),
+ "Field $field, password field must be sensitive" );
+ }
+
+ switch ( $data['type'] ) {
+ case 'string':
+ case 'password':
+ case 'hidden':
+ break;
+ case 'select':
+ case 'multiselect':
+ $this->assertArrayHasKey( 'options', $data, "Field $field" );
+ $this->assertType( 'array', $data['options'], "Field $field, options" );
+ foreach ( $data['options'] as $val => $msg ) {
+ $this->assertInstanceOf( \Message::class, $msg, "Field $field, option $val" );
+ }
+ break;
+ case 'checkbox':
+ break;
+ case 'button':
+ break;
+ case 'null':
+ break;
+ default:
+ $this->fail( "Field $field, unknown type " . $data['type'] );
+ break;
+ }
+ }
+ }
+
+ public static function provideGetFieldInfo() {
+ return [
+ [ [] ]
+ ];
+ }
+
+ /**
+ * @dataProvider provideLoadFromSubmission
+ * @param array $args
+ * @param array $data
+ * @param array|bool $expectState
+ */
+ public function testLoadFromSubmission( array $args, array $data, $expectState ) {
+ $instance = $this->getInstance( $args );
+ $ret = $instance->loadFromSubmission( $data );
+ if ( is_array( $expectState ) ) {
+ $this->assertTrue( $ret );
+ $expect = call_user_func( [ get_class( $instance ), '__set_state' ], $expectState );
+ $this->assertEquals( $expect, $instance );
+ } else {
+ $this->assertFalse( $ret );
+ }
+ }
+
+ abstract public function provideLoadFromSubmission();
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/AuthenticationResponseTest.php b/www/wiki/tests/phpunit/includes/auth/AuthenticationResponseTest.php
new file mode 100644
index 00000000..194b49e0
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/AuthenticationResponseTest.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\AuthenticationResponse
+ */
+class AuthenticationResponseTest extends \MediaWikiTestCase {
+ /**
+ * @dataProvider provideConstructors
+ * @param string $constructor
+ * @param array $args
+ * @param array|Exception $expect
+ */
+ public function testConstructors( $constructor, $args, $expect ) {
+ if ( is_array( $expect ) ) {
+ $res = new AuthenticationResponse();
+ $res->messageType = 'warning';
+ foreach ( $expect as $field => $value ) {
+ $res->$field = $value;
+ }
+ $ret = call_user_func_array( "MediaWiki\\Auth\\AuthenticationResponse::$constructor", $args );
+ $this->assertEquals( $res, $ret );
+ } else {
+ try {
+ call_user_func_array( "MediaWiki\\Auth\\AuthenticationResponse::$constructor", $args );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \Exception $ex ) {
+ $this->assertEquals( $expect, $ex );
+ }
+ }
+ }
+
+ public function provideConstructors() {
+ $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+ $msg = new \Message( 'mainpage' );
+
+ return [
+ [ 'newPass', [], [
+ 'status' => AuthenticationResponse::PASS,
+ ] ],
+ [ 'newPass', [ 'name' ], [
+ 'status' => AuthenticationResponse::PASS,
+ 'username' => 'name',
+ ] ],
+ [ 'newPass', [ 'name', null ], [
+ 'status' => AuthenticationResponse::PASS,
+ 'username' => 'name',
+ ] ],
+
+ [ 'newFail', [ $msg ], [
+ 'status' => AuthenticationResponse::FAIL,
+ 'message' => $msg,
+ 'messageType' => 'error',
+ ] ],
+
+ [ 'newRestart', [ $msg ], [
+ 'status' => AuthenticationResponse::RESTART,
+ 'message' => $msg,
+ ] ],
+
+ [ 'newAbstain', [], [
+ 'status' => AuthenticationResponse::ABSTAIN,
+ ] ],
+
+ [ 'newUI', [ [ $req ], $msg ], [
+ 'status' => AuthenticationResponse::UI,
+ 'neededRequests' => [ $req ],
+ 'message' => $msg,
+ 'messageType' => 'warning',
+ ] ],
+
+ [ 'newUI', [ [ $req ], $msg, 'warning' ], [
+ 'status' => AuthenticationResponse::UI,
+ 'neededRequests' => [ $req ],
+ 'message' => $msg,
+ 'messageType' => 'warning',
+ ] ],
+
+ [ 'newUI', [ [ $req ], $msg, 'error' ], [
+ 'status' => AuthenticationResponse::UI,
+ 'neededRequests' => [ $req ],
+ 'message' => $msg,
+ 'messageType' => 'error',
+ ] ],
+ [ 'newUI', [ [], $msg ],
+ new \InvalidArgumentException( '$reqs may not be empty' )
+ ],
+
+ [ 'newRedirect', [ [ $req ], 'http://example.org/redir' ], [
+ 'status' => AuthenticationResponse::REDIRECT,
+ 'neededRequests' => [ $req ],
+ 'redirectTarget' => 'http://example.org/redir',
+ ] ],
+ [
+ 'newRedirect',
+ [ [ $req ], 'http://example.org/redir', [ 'foo' => 'bar' ] ],
+ [
+ 'status' => AuthenticationResponse::REDIRECT,
+ 'neededRequests' => [ $req ],
+ 'redirectTarget' => 'http://example.org/redir',
+ 'redirectApiData' => [ 'foo' => 'bar' ],
+ ]
+ ],
+ [ 'newRedirect', [ [], 'http://example.org/redir' ],
+ new \InvalidArgumentException( '$reqs may not be empty' )
+ ],
+ ];
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/ButtonAuthenticationRequestTest.php b/www/wiki/tests/phpunit/includes/auth/ButtonAuthenticationRequestTest.php
new file mode 100644
index 00000000..3bc077cb
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/ButtonAuthenticationRequestTest.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\ButtonAuthenticationRequest
+ */
+class ButtonAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+ protected function getInstance( array $args = [] ) {
+ $data = array_intersect_key( $args, [ 'name' => 1, 'label' => 1, 'help' => 1 ] );
+ return ButtonAuthenticationRequest::__set_state( $data );
+ }
+
+ public static function provideGetFieldInfo() {
+ return [
+ [ [ 'name' => 'foo', 'label' => 'bar', 'help' => 'baz' ] ]
+ ];
+ }
+
+ public function provideLoadFromSubmission() {
+ return [
+ 'Empty request' => [
+ [ 'name' => 'foo', 'label' => 'bar', 'help' => 'baz' ],
+ [],
+ false
+ ],
+ 'Button present' => [
+ [ 'name' => 'foo', 'label' => 'bar', 'help' => 'baz' ],
+ [ 'foo' => 'Foobar' ],
+ [ 'name' => 'foo', 'label' => 'bar', 'help' => 'baz', 'foo' => true ]
+ ],
+ ];
+ }
+
+ public function testGetUniqueId() {
+ $req = new ButtonAuthenticationRequest( 'foo', wfMessage( 'bar' ), wfMessage( 'baz' ) );
+ $this->assertSame(
+ 'MediaWiki\\Auth\\ButtonAuthenticationRequest:foo', $req->getUniqueId()
+ );
+ }
+
+ public function testGetRequestByName() {
+ $reqs = [];
+ $reqs['testOne'] = new ButtonAuthenticationRequest(
+ 'foo', wfMessage( 'msg' ), wfMessage( 'help' )
+ );
+ $reqs[] = new ButtonAuthenticationRequest( 'bar', wfMessage( 'msg1' ), wfMessage( 'help1' ) );
+ $reqs[] = new ButtonAuthenticationRequest( 'bar', wfMessage( 'msg2' ), wfMessage( 'help2' ) );
+ $reqs['testSub'] = $this->getMockBuilder( ButtonAuthenticationRequest::class )
+ ->setConstructorArgs( [ 'subclass', wfMessage( 'msg3' ), wfMessage( 'help3' ) ] )
+ ->getMock();
+
+ $this->assertNull( ButtonAuthenticationRequest::getRequestByName( $reqs, 'missing' ) );
+ $this->assertSame(
+ $reqs['testOne'], ButtonAuthenticationRequest::getRequestByName( $reqs, 'foo' )
+ );
+ $this->assertNull( ButtonAuthenticationRequest::getRequestByName( $reqs, 'bar' ) );
+ $this->assertSame(
+ $reqs['testSub'], ButtonAuthenticationRequest::getRequestByName( $reqs, 'subclass' )
+ );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/CheckBlocksSecondaryAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/CheckBlocksSecondaryAuthenticationProviderTest.php
new file mode 100644
index 00000000..e8b61c59
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/CheckBlocksSecondaryAuthenticationProviderTest.php
@@ -0,0 +1,191 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group AuthManager
+ * @group Database
+ * @covers MediaWiki\Auth\CheckBlocksSecondaryAuthenticationProvider
+ */
+class CheckBlocksSecondaryAuthenticationProviderTest extends \MediaWikiTestCase {
+ public function testConstructor() {
+ $provider = new CheckBlocksSecondaryAuthenticationProvider();
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+ $config = new \HashConfig( [
+ 'BlockDisablesLogin' => false
+ ] );
+ $provider->setConfig( $config );
+ $this->assertSame( false, $providerPriv->blockDisablesLogin );
+
+ $provider = new CheckBlocksSecondaryAuthenticationProvider(
+ [ 'blockDisablesLogin' => true ]
+ );
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+ $config = new \HashConfig( [
+ 'BlockDisablesLogin' => false
+ ] );
+ $provider->setConfig( $config );
+ $this->assertSame( true, $providerPriv->blockDisablesLogin );
+ }
+
+ public function testBasics() {
+ $provider = new CheckBlocksSecondaryAuthenticationProvider();
+ $user = \User::newFromName( 'UTSysop' );
+
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginSecondaryAccountCreation( $user, $user, [] )
+ );
+ }
+
+ /**
+ * @dataProvider provideGetAuthenticationRequests
+ * @param string $action
+ * @param array $response
+ */
+ public function testGetAuthenticationRequests( $action, $response ) {
+ $provider = new CheckBlocksSecondaryAuthenticationProvider();
+
+ $this->assertEquals( $response, $provider->getAuthenticationRequests( $action, [] ) );
+ }
+
+ public static function provideGetAuthenticationRequests() {
+ return [
+ [ AuthManager::ACTION_LOGIN, [] ],
+ [ AuthManager::ACTION_CREATE, [] ],
+ [ AuthManager::ACTION_LINK, [] ],
+ [ AuthManager::ACTION_CHANGE, [] ],
+ [ AuthManager::ACTION_REMOVE, [] ],
+ ];
+ }
+
+ private function getBlockedUser() {
+ $user = \User::newFromName( 'UTBlockee' );
+ if ( $user->getID() == 0 ) {
+ $user->addToDatabase();
+ \TestUser::setPasswordForUser( $user, 'UTBlockeePassword' );
+ $user->saveSettings();
+ }
+ $oldBlock = \Block::newFromTarget( 'UTBlockee' );
+ if ( $oldBlock ) {
+ // An old block will prevent our new one from saving.
+ $oldBlock->delete();
+ }
+ $blockOptions = [
+ 'address' => 'UTBlockee',
+ 'user' => $user->getID(),
+ 'by' => $this->getTestSysop()->getUser()->getId(),
+ 'reason' => __METHOD__,
+ 'expiry' => time() + 100500,
+ 'createAccount' => true,
+ ];
+ $block = new \Block( $blockOptions );
+ $block->insert();
+ return $user;
+ }
+
+ public function testBeginSecondaryAuthentication() {
+ $unblockedUser = \User::newFromName( 'UTSysop' );
+ $blockedUser = $this->getBlockedUser();
+
+ $provider = new CheckBlocksSecondaryAuthenticationProvider(
+ [ 'blockDisablesLogin' => false ]
+ );
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginSecondaryAuthentication( $unblockedUser, [] )
+ );
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginSecondaryAuthentication( $blockedUser, [] )
+ );
+
+ $provider = new CheckBlocksSecondaryAuthenticationProvider(
+ [ 'blockDisablesLogin' => true ]
+ );
+ $this->assertEquals(
+ AuthenticationResponse::newPass(),
+ $provider->beginSecondaryAuthentication( $unblockedUser, [] )
+ );
+ $ret = $provider->beginSecondaryAuthentication( $blockedUser, [] );
+ $this->assertEquals( AuthenticationResponse::FAIL, $ret->status );
+ }
+
+ public function testTestUserForCreation() {
+ $provider = new CheckBlocksSecondaryAuthenticationProvider(
+ [ 'blockDisablesLogin' => false ]
+ );
+ $provider->setLogger( new \Psr\Log\NullLogger() );
+ $provider->setConfig( new \HashConfig() );
+ $provider->setManager( AuthManager::singleton() );
+
+ $unblockedUser = \User::newFromName( 'UTSysop' );
+ $blockedUser = $this->getBlockedUser();
+
+ $user = \User::newFromName( 'RandomUser' );
+
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testUserForCreation( $unblockedUser, AuthManager::AUTOCREATE_SOURCE_SESSION )
+ );
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testUserForCreation( $unblockedUser, false )
+ );
+
+ $status = $provider->testUserForCreation( $blockedUser, AuthManager::AUTOCREATE_SOURCE_SESSION );
+ $this->assertInstanceOf( \StatusValue::class, $status );
+ $this->assertFalse( $status->isOK() );
+ $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
+
+ $status = $provider->testUserForCreation( $blockedUser, false );
+ $this->assertInstanceOf( \StatusValue::class, $status );
+ $this->assertFalse( $status->isOK() );
+ $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
+ }
+
+ public function testRangeBlock() {
+ $blockOptions = [
+ 'address' => '127.0.0.0/24',
+ 'reason' => __METHOD__,
+ 'by' => $this->getTestSysop()->getUser()->getId(),
+ 'expiry' => time() + 100500,
+ 'createAccount' => true,
+ ];
+ $block = new \Block( $blockOptions );
+ $block->insert();
+ $scopeVariable = new \Wikimedia\ScopedCallback( [ $block, 'delete' ] );
+
+ $user = \User::newFromName( 'UTNormalUser' );
+ if ( $user->getID() == 0 ) {
+ $user->addToDatabase();
+ \TestUser::setPasswordForUser( $user, 'UTNormalUserPassword' );
+ $user->saveSettings();
+ }
+ $this->setMwGlobals( [ 'wgUser' => $user ] );
+ \RequestContext::getMain()->setUser( $user );
+ $newuser = \User::newFromName( 'RandomUser' );
+
+ $provider = new CheckBlocksSecondaryAuthenticationProvider(
+ [ 'blockDisablesLogin' => true ]
+ );
+ $provider->setLogger( new \Psr\Log\NullLogger() );
+ $provider->setConfig( new \HashConfig() );
+ $provider->setManager( AuthManager::singleton() );
+
+ $ret = $provider->beginSecondaryAuthentication( $user, [] );
+ $this->assertEquals( AuthenticationResponse::FAIL, $ret->status );
+
+ $status = $provider->testUserForCreation( $newuser, AuthManager::AUTOCREATE_SOURCE_SESSION );
+ $this->assertInstanceOf( \StatusValue::class, $status );
+ $this->assertFalse( $status->isOK() );
+ $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
+
+ $status = $provider->testUserForCreation( $newuser, false );
+ $this->assertInstanceOf( \StatusValue::class, $status );
+ $this->assertFalse( $status->isOK() );
+ $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/ConfirmLinkAuthenticationRequestTest.php b/www/wiki/tests/phpunit/includes/auth/ConfirmLinkAuthenticationRequestTest.php
new file mode 100644
index 00000000..f208cc4b
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/ConfirmLinkAuthenticationRequestTest.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use InvalidArgumentException;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\ConfirmLinkAuthenticationRequest
+ */
+class ConfirmLinkAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+ protected function getInstance( array $args = [] ) {
+ return new ConfirmLinkAuthenticationRequest( $this->getLinkRequests() );
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ * @expectedExceptionMessage $linkRequests must not be empty
+ */
+ public function testConstructorException() {
+ new ConfirmLinkAuthenticationRequest( [] );
+ }
+
+ /**
+ * Get requests for testing
+ * @return AuthenticationRequest[]
+ */
+ private function getLinkRequests() {
+ $reqs = [];
+
+ $mb = $this->getMockBuilder( AuthenticationRequest::class )
+ ->setMethods( [ 'getUniqueId' ] );
+ for ( $i = 1; $i <= 3; $i++ ) {
+ $req = $mb->getMockForAbstractClass();
+ $req->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( "Request$i" ) );
+ $reqs[$req->getUniqueId()] = $req;
+ }
+
+ return $reqs;
+ }
+
+ public function provideLoadFromSubmission() {
+ $reqs = $this->getLinkRequests();
+
+ return [
+ 'Empty request' => [
+ [],
+ [],
+ [ 'linkRequests' => $reqs ],
+ ],
+ 'Some confirmed' => [
+ [],
+ [ 'confirmedLinkIDs' => [ 'Request1', 'Request3' ] ],
+ [ 'confirmedLinkIDs' => [ 'Request1', 'Request3' ], 'linkRequests' => $reqs ],
+ ],
+ ];
+ }
+
+ public function testGetUniqueId() {
+ $req = new ConfirmLinkAuthenticationRequest( $this->getLinkRequests() );
+ $this->assertSame(
+ get_class( $req ) . ':Request1|Request2|Request3',
+ $req->getUniqueId()
+ );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/ConfirmLinkSecondaryAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/ConfirmLinkSecondaryAuthenticationProviderTest.php
new file mode 100644
index 00000000..9222843c
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/ConfirmLinkSecondaryAuthenticationProviderTest.php
@@ -0,0 +1,289 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\ConfirmLinkSecondaryAuthenticationProvider
+ */
+class ConfirmLinkSecondaryAuthenticationProviderTest extends \MediaWikiTestCase {
+ /**
+ * @dataProvider provideGetAuthenticationRequests
+ * @param string $action
+ * @param array $response
+ */
+ public function testGetAuthenticationRequests( $action, $response ) {
+ $provider = new ConfirmLinkSecondaryAuthenticationProvider();
+
+ $this->assertEquals( $response, $provider->getAuthenticationRequests( $action, [] ) );
+ }
+
+ public static function provideGetAuthenticationRequests() {
+ return [
+ [ AuthManager::ACTION_LOGIN, [] ],
+ [ AuthManager::ACTION_CREATE, [] ],
+ [ AuthManager::ACTION_LINK, [] ],
+ [ AuthManager::ACTION_CHANGE, [] ],
+ [ AuthManager::ACTION_REMOVE, [] ],
+ ];
+ }
+
+ public function testBeginSecondaryAuthentication() {
+ $user = \User::newFromName( 'UTSysop' );
+ $obj = new \stdClass;
+
+ $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+ ->setMethods( [ 'beginLinkAttempt', 'continueLinkAttempt' ] )
+ ->getMock();
+ $mock->expects( $this->once() )->method( 'beginLinkAttempt' )
+ ->with( $this->identicalTo( $user ), $this->identicalTo( 'AuthManager::authnState' ) )
+ ->will( $this->returnValue( $obj ) );
+ $mock->expects( $this->never() )->method( 'continueLinkAttempt' );
+
+ $this->assertSame( $obj, $mock->beginSecondaryAuthentication( $user, [] ) );
+ }
+
+ public function testContinueSecondaryAuthentication() {
+ $user = \User::newFromName( 'UTSysop' );
+ $obj = new \stdClass;
+ $reqs = [ new \stdClass ];
+
+ $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+ ->setMethods( [ 'beginLinkAttempt', 'continueLinkAttempt' ] )
+ ->getMock();
+ $mock->expects( $this->never() )->method( 'beginLinkAttempt' );
+ $mock->expects( $this->once() )->method( 'continueLinkAttempt' )
+ ->with(
+ $this->identicalTo( $user ),
+ $this->identicalTo( 'AuthManager::authnState' ),
+ $this->identicalTo( $reqs )
+ )
+ ->will( $this->returnValue( $obj ) );
+
+ $this->assertSame( $obj, $mock->continueSecondaryAuthentication( $user, $reqs ) );
+ }
+
+ public function testBeginSecondaryAccountCreation() {
+ $user = \User::newFromName( 'UTSysop' );
+ $obj = new \stdClass;
+
+ $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+ ->setMethods( [ 'beginLinkAttempt', 'continueLinkAttempt' ] )
+ ->getMock();
+ $mock->expects( $this->once() )->method( 'beginLinkAttempt' )
+ ->with( $this->identicalTo( $user ), $this->identicalTo( 'AuthManager::accountCreationState' ) )
+ ->will( $this->returnValue( $obj ) );
+ $mock->expects( $this->never() )->method( 'continueLinkAttempt' );
+
+ $this->assertSame( $obj, $mock->beginSecondaryAccountCreation( $user, $user, [] ) );
+ }
+
+ public function testContinueSecondaryAccountCreation() {
+ $user = \User::newFromName( 'UTSysop' );
+ $obj = new \stdClass;
+ $reqs = [ new \stdClass ];
+
+ $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+ ->setMethods( [ 'beginLinkAttempt', 'continueLinkAttempt' ] )
+ ->getMock();
+ $mock->expects( $this->never() )->method( 'beginLinkAttempt' );
+ $mock->expects( $this->once() )->method( 'continueLinkAttempt' )
+ ->with(
+ $this->identicalTo( $user ),
+ $this->identicalTo( 'AuthManager::accountCreationState' ),
+ $this->identicalTo( $reqs )
+ )
+ ->will( $this->returnValue( $obj ) );
+
+ $this->assertSame( $obj, $mock->continueSecondaryAccountCreation( $user, $user, $reqs ) );
+ }
+
+ /**
+ * Get requests for testing
+ * @return AuthenticationRequest[]
+ */
+ private function getLinkRequests() {
+ $reqs = [];
+
+ $mb = $this->getMockBuilder( AuthenticationRequest::class )
+ ->setMethods( [ 'getUniqueId' ] );
+ for ( $i = 1; $i <= 3; $i++ ) {
+ $req = $mb->getMockForAbstractClass();
+ $req->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( "Request$i" ) );
+ $req->id = $i - 1;
+ $reqs[$req->getUniqueId()] = $req;
+ }
+
+ return $reqs;
+ }
+
+ public function testBeginLinkAttempt() {
+ $badReq = $this->getMockBuilder( AuthenticationRequest::class )
+ ->setMethods( [ 'getUniqueId' ] )
+ ->getMockForAbstractClass();
+ $badReq->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( "BadReq" ) );
+
+ $user = \User::newFromName( 'UTSysop' );
+ $provider = TestingAccessWrapper::newFromObject(
+ new ConfirmLinkSecondaryAuthenticationProvider
+ );
+ $request = new \FauxRequest();
+ $manager = $this->getMockBuilder( AuthManager::class )
+ ->setMethods( [ 'allowsAuthenticationDataChange' ] )
+ ->setConstructorArgs( [ $request, \RequestContext::getMain()->getConfig() ] )
+ ->getMock();
+ $manager->expects( $this->any() )->method( 'allowsAuthenticationDataChange' )
+ ->will( $this->returnCallback( function ( $req ) {
+ return $req->getUniqueId() !== 'BadReq'
+ ? \StatusValue::newGood()
+ : \StatusValue::newFatal( 'no' );
+ } ) );
+ $provider->setManager( $manager );
+
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginLinkAttempt( $user, 'state' )
+ );
+
+ $request->getSession()->setSecret( 'state', [
+ 'maybeLink' => [],
+ ] );
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginLinkAttempt( $user, 'state' )
+ );
+
+ $reqs = $this->getLinkRequests();
+ $request->getSession()->setSecret( 'state', [
+ 'maybeLink' => $reqs + [ 'BadReq' => $badReq ]
+ ] );
+ $res = $provider->beginLinkAttempt( $user, 'state' );
+ $this->assertInstanceOf( AuthenticationResponse::class, $res );
+ $this->assertSame( AuthenticationResponse::UI, $res->status );
+ $this->assertSame( 'authprovider-confirmlink-message', $res->message->getKey() );
+ $this->assertCount( 1, $res->neededRequests );
+ $req = $res->neededRequests[0];
+ $this->assertInstanceOf( ConfirmLinkAuthenticationRequest::class, $req );
+ $expectReqs = $this->getLinkRequests();
+ foreach ( $expectReqs as $r ) {
+ $r->action = AuthManager::ACTION_CHANGE;
+ $r->username = $user->getName();
+ }
+ $this->assertEquals( $expectReqs, TestingAccessWrapper::newFromObject( $req )->linkRequests );
+ }
+
+ public function testContinueLinkAttempt() {
+ $user = \User::newFromName( 'UTSysop' );
+ $obj = new \stdClass;
+ $reqs = $this->getLinkRequests();
+
+ $done = [ false, false, false ];
+
+ // First, test the pass-through for not containing the ConfirmLinkAuthenticationRequest
+ $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+ ->setMethods( [ 'beginLinkAttempt' ] )
+ ->getMock();
+ $mock->expects( $this->once() )->method( 'beginLinkAttempt' )
+ ->with( $this->identicalTo( $user ), $this->identicalTo( 'state' ) )
+ ->will( $this->returnValue( $obj ) );
+ $this->assertSame(
+ $obj,
+ TestingAccessWrapper::newFromObject( $mock )->continueLinkAttempt( $user, 'state', $reqs )
+ );
+
+ // Now test the actual functioning
+ $provider = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+ ->setMethods( [
+ 'beginLinkAttempt', 'providerAllowsAuthenticationDataChange',
+ 'providerChangeAuthenticationData'
+ ] )
+ ->getMock();
+ $provider->expects( $this->never() )->method( 'beginLinkAttempt' );
+ $provider->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
+ ->will( $this->returnCallback( function ( $req ) use ( $reqs ) {
+ return $req->getUniqueId() === 'Request3'
+ ? \StatusValue::newFatal( 'foo' ) : \StatusValue::newGood();
+ } ) );
+ $provider->expects( $this->any() )->method( 'providerChangeAuthenticationData' )
+ ->will( $this->returnCallback( function ( $req ) use ( &$done ) {
+ $done[$req->id] = true;
+ } ) );
+ $config = new \HashConfig( [
+ 'AuthManagerConfig' => [
+ 'preauth' => [],
+ 'primaryauth' => [],
+ 'secondaryauth' => [
+ [ 'factory' => function () use ( $provider ) {
+ return $provider;
+ } ],
+ ],
+ ],
+ ] );
+ $request = new \FauxRequest();
+ $manager = new AuthManager( $request, $config );
+ $provider->setManager( $manager );
+ $provider = TestingAccessWrapper::newFromObject( $provider );
+
+ $req = new ConfirmLinkAuthenticationRequest( $reqs );
+
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->continueLinkAttempt( $user, 'state', [ $req ] )
+ );
+
+ $request->getSession()->setSecret( 'state', [
+ 'maybeLink' => [],
+ ] );
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->continueLinkAttempt( $user, 'state', [ $req ] )
+ );
+
+ $request->getSession()->setSecret( 'state', [
+ 'maybeLink' => $reqs
+ ] );
+ $this->assertEquals(
+ AuthenticationResponse::newPass(),
+ $res = $provider->continueLinkAttempt( $user, 'state', [ $req ] )
+ );
+ $this->assertSame( [ false, false, false ], $done );
+
+ $request->getSession()->setSecret( 'state', [
+ 'maybeLink' => [ $reqs['Request2'] ],
+ ] );
+ $req->confirmedLinkIDs = [ 'Request1', 'Request2' ];
+ $res = $provider->continueLinkAttempt( $user, 'state', [ $req ] );
+ $this->assertEquals( AuthenticationResponse::newPass(), $res );
+ $this->assertSame( [ false, true, false ], $done );
+ $done = [ false, false, false ];
+
+ $request->getSession()->setSecret( 'state', [
+ 'maybeLink' => $reqs,
+ ] );
+ $req->confirmedLinkIDs = [ 'Request1', 'Request2' ];
+ $res = $provider->continueLinkAttempt( $user, 'state', [ $req ] );
+ $this->assertEquals( AuthenticationResponse::newPass(), $res );
+ $this->assertSame( [ true, true, false ], $done );
+ $done = [ false, false, false ];
+
+ $request->getSession()->setSecret( 'state', [
+ 'maybeLink' => $reqs,
+ ] );
+ $req->confirmedLinkIDs = [ 'Request1', 'Request3' ];
+ $res = $provider->continueLinkAttempt( $user, 'state', [ $req ] );
+ $this->assertEquals( AuthenticationResponse::UI, $res->status );
+ $this->assertCount( 1, $res->neededRequests );
+ $this->assertInstanceOf( ButtonAuthenticationRequest::class, $res->neededRequests[0] );
+ $this->assertSame( [ true, false, false ], $done );
+ $done = [ false, false, false ];
+
+ $res = $provider->continueLinkAttempt( $user, 'state', [ $res->neededRequests[0] ] );
+ $this->assertEquals( AuthenticationResponse::newPass(), $res );
+ $this->assertSame( [ false, false, false ], $done );
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/CreateFromLoginAuthenticationRequestTest.php b/www/wiki/tests/phpunit/includes/auth/CreateFromLoginAuthenticationRequestTest.php
new file mode 100644
index 00000000..d166caa6
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/CreateFromLoginAuthenticationRequestTest.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\CreateFromLoginAuthenticationRequest
+ */
+class CreateFromLoginAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+ protected function getInstance( array $args = [] ) {
+ return new CreateFromLoginAuthenticationRequest(
+ null, []
+ );
+ }
+
+ public function provideLoadFromSubmission() {
+ return [
+ 'Empty request' => [
+ [],
+ [],
+ [],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideState
+ */
+ public function testState(
+ $createReq, $maybeLink, $username, $loginState, $createState, $createPrimaryState
+ ) {
+ $req = new CreateFromLoginAuthenticationRequest( $createReq, $maybeLink );
+ $this->assertSame( $username, $req->username );
+ $this->assertSame( $loginState, $req->hasStateForAction( AuthManager::ACTION_LOGIN ) );
+ $this->assertSame( $createState, $req->hasStateForAction( AuthManager::ACTION_CREATE ) );
+ $this->assertFalse( $req->hasStateForAction( AuthManager::ACTION_LINK ) );
+ $this->assertFalse( $req->hasPrimaryStateForAction( AuthManager::ACTION_LOGIN ) );
+ $this->assertSame( $createPrimaryState,
+ $req->hasPrimaryStateForAction( AuthManager::ACTION_CREATE ) );
+ }
+
+ public static function provideState() {
+ $req1 = new UsernameAuthenticationRequest;
+ $req2 = new UsernameAuthenticationRequest;
+ $req2->username = 'Bob';
+
+ return [
+ 'Nothing' => [ null, [], null, false, false, false ],
+ 'Link, no create' => [ null, [ $req2 ], null, true, true, false ],
+ 'No link, create but no name' => [ $req1, [], null, false, true, true ],
+ 'Link and create but no name' => [ $req1, [ $req2 ], null, true, true, true ],
+ 'No link, create with name' => [ $req2, [], 'Bob', false, true, true ],
+ 'Link and create with name' => [ $req2, [ $req2 ], 'Bob', true, true, true ],
+ ];
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/CreatedAccountAuthenticationRequestTest.php b/www/wiki/tests/phpunit/includes/auth/CreatedAccountAuthenticationRequestTest.php
new file mode 100644
index 00000000..fc1e6f15
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/CreatedAccountAuthenticationRequestTest.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\CreatedAccountAuthenticationRequest
+ */
+class CreatedAccountAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+ protected function getInstance( array $args = [] ) {
+ return new CreatedAccountAuthenticationRequest( 42, 'Test' );
+ }
+
+ public function testConstructor() {
+ $ret = new CreatedAccountAuthenticationRequest( 42, 'Test' );
+ $this->assertSame( 42, $ret->id );
+ $this->assertSame( 'Test', $ret->username );
+ }
+
+ public function provideLoadFromSubmission() {
+ return [
+ 'Empty request' => [
+ [],
+ [],
+ false
+ ],
+ ];
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/CreationReasonAuthenticationRequestTest.php b/www/wiki/tests/phpunit/includes/auth/CreationReasonAuthenticationRequestTest.php
new file mode 100644
index 00000000..cce1e8cd
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/CreationReasonAuthenticationRequestTest.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\CreationReasonAuthenticationRequest
+ */
+class CreationReasonAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+ protected function getInstance( array $args = [] ) {
+ return new CreationReasonAuthenticationRequest();
+ }
+
+ public function provideLoadFromSubmission() {
+ return [
+ 'Empty request' => [
+ [],
+ [],
+ false
+ ],
+ 'Reason given' => [
+ [],
+ $data = [ 'reason' => 'Because' ],
+ $data,
+ ],
+ 'Reason empty' => [
+ [],
+ [ 'reason' => '' ],
+ false
+ ],
+ ];
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php
new file mode 100644
index 00000000..1a7ed12d
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php
@@ -0,0 +1,109 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use Psr\Log\LoggerInterface;
+use Wikimedia\TestingAccessWrapper;
+
+class EmailNotificationSecondaryAuthenticationProviderTest extends \PHPUnit\Framework\TestCase {
+ public function testConstructor() {
+ $config = new \HashConfig( [
+ 'EnableEmail' => true,
+ 'EmailAuthentication' => true,
+ ] );
+
+ $provider = new EmailNotificationSecondaryAuthenticationProvider();
+ $provider->setConfig( $config );
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+ $this->assertTrue( $providerPriv->sendConfirmationEmail );
+
+ $provider = new EmailNotificationSecondaryAuthenticationProvider( [
+ 'sendConfirmationEmail' => false,
+ ] );
+ $provider->setConfig( $config );
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+ $this->assertFalse( $providerPriv->sendConfirmationEmail );
+ }
+
+ /**
+ * @dataProvider provideGetAuthenticationRequests
+ * @param string $action
+ * @param AuthenticationRequest[] $expected
+ */
+ public function testGetAuthenticationRequests( $action, $expected ) {
+ $provider = new EmailNotificationSecondaryAuthenticationProvider( [
+ 'sendConfirmationEmail' => true,
+ ] );
+ $this->assertSame( $expected, $provider->getAuthenticationRequests( $action, [] ) );
+ }
+
+ public function provideGetAuthenticationRequests() {
+ return [
+ [ AuthManager::ACTION_LOGIN, [] ],
+ [ AuthManager::ACTION_CREATE, [] ],
+ [ AuthManager::ACTION_LINK, [] ],
+ [ AuthManager::ACTION_CHANGE, [] ],
+ [ AuthManager::ACTION_REMOVE, [] ],
+ ];
+ }
+
+ public function testBeginSecondaryAuthentication() {
+ $provider = new EmailNotificationSecondaryAuthenticationProvider( [
+ 'sendConfirmationEmail' => true,
+ ] );
+ $this->assertEquals( AuthenticationResponse::newAbstain(),
+ $provider->beginSecondaryAuthentication( \User::newFromName( 'Foo' ), [] ) );
+ }
+
+ public function testBeginSecondaryAccountCreation() {
+ $authManager = new AuthManager( new \FauxRequest(), new \HashConfig() );
+
+ $creator = $this->getMockBuilder( \User::class )->getMock();
+ $userWithoutEmail = $this->getMockBuilder( \User::class )->getMock();
+ $userWithoutEmail->expects( $this->any() )->method( 'getEmail' )->willReturn( '' );
+ $userWithoutEmail->expects( $this->any() )->method( 'getInstanceForUpdate' )->willReturnSelf();
+ $userWithoutEmail->expects( $this->never() )->method( 'sendConfirmationMail' );
+ $userWithEmailError = $this->getMockBuilder( \User::class )->getMock();
+ $userWithEmailError->expects( $this->any() )->method( 'getEmail' )->willReturn( 'foo@bar.baz' );
+ $userWithEmailError->expects( $this->any() )->method( 'getInstanceForUpdate' )->willReturnSelf();
+ $userWithEmailError->expects( $this->any() )->method( 'sendConfirmationMail' )
+ ->willReturn( \Status::newFatal( 'fail' ) );
+ $userExpectsConfirmation = $this->getMockBuilder( \User::class )->getMock();
+ $userExpectsConfirmation->expects( $this->any() )->method( 'getEmail' )
+ ->willReturn( 'foo@bar.baz' );
+ $userExpectsConfirmation->expects( $this->any() )->method( 'getInstanceForUpdate' )
+ ->willReturnSelf();
+ $userExpectsConfirmation->expects( $this->once() )->method( 'sendConfirmationMail' )
+ ->willReturn( \Status::newGood() );
+ $userNotExpectsConfirmation = $this->getMockBuilder( \User::class )->getMock();
+ $userNotExpectsConfirmation->expects( $this->any() )->method( 'getEmail' )
+ ->willReturn( 'foo@bar.baz' );
+ $userNotExpectsConfirmation->expects( $this->any() )->method( 'getInstanceForUpdate' )
+ ->willReturnSelf();
+ $userNotExpectsConfirmation->expects( $this->never() )->method( 'sendConfirmationMail' );
+
+ $provider = new EmailNotificationSecondaryAuthenticationProvider( [
+ 'sendConfirmationEmail' => false,
+ ] );
+ $provider->setManager( $authManager );
+ $provider->beginSecondaryAccountCreation( $userNotExpectsConfirmation, $creator, [] );
+
+ $provider = new EmailNotificationSecondaryAuthenticationProvider( [
+ 'sendConfirmationEmail' => true,
+ ] );
+ $provider->setManager( $authManager );
+ $provider->beginSecondaryAccountCreation( $userWithoutEmail, $creator, [] );
+ $provider->beginSecondaryAccountCreation( $userExpectsConfirmation, $creator, [] );
+
+ // test logging of email errors
+ $logger = $this->getMockForAbstractClass( LoggerInterface::class );
+ $logger->expects( $this->once() )->method( 'warning' );
+ $provider->setLogger( $logger );
+ $provider->beginSecondaryAccountCreation( $userWithEmailError, $creator, [] );
+
+ // test disable flag used by other providers
+ $authManager->setAuthenticationSessionData( 'no-email', true );
+ $provider->setManager( $authManager );
+ $provider->beginSecondaryAccountCreation( $userNotExpectsConfirmation, $creator, [] );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/LegacyHookPreAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/LegacyHookPreAuthenticationProviderTest.php
new file mode 100644
index 00000000..38ccb8a3
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/LegacyHookPreAuthenticationProviderTest.php
@@ -0,0 +1,373 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use MediaWiki\MediaWikiServices;
+
+/**
+ * @group AuthManager
+ * @group Database
+ * @covers MediaWiki\Auth\LegacyHookPreAuthenticationProvider
+ */
+class LegacyHookPreAuthenticationProviderTest extends \MediaWikiTestCase {
+ /**
+ * Get an instance of the provider
+ * @return LegacyHookPreAuthenticationProvider
+ */
+ protected function getProvider() {
+ $request = $this->getMockBuilder( \FauxRequest::class )
+ ->setMethods( [ 'getIP' ] )->getMock();
+ $request->expects( $this->any() )->method( 'getIP' )->will( $this->returnValue( '127.0.0.42' ) );
+
+ $manager = new AuthManager(
+ $request,
+ MediaWikiServices::getInstance()->getMainConfig()
+ );
+
+ $provider = new LegacyHookPreAuthenticationProvider();
+ $provider->setManager( $manager );
+ $provider->setLogger( new \Psr\Log\NullLogger() );
+ $provider->setConfig( new \HashConfig( [
+ 'PasswordAttemptThrottle' => [ 'count' => 23, 'seconds' => 42 ],
+ ] ) );
+ return $provider;
+ }
+
+ /**
+ * Sets a mock on a hook
+ * @param string $hook
+ * @param object $expect From $this->once(), $this->never(), etc.
+ * @return object $mock->expects( $expect )->method( ... ).
+ */
+ protected function hook( $hook, $expect ) {
+ $mock = $this->getMockBuilder( __CLASS__ )->setMethods( [ "on$hook" ] )->getMock();
+ $this->mergeMwGlobalArrayValue( 'wgHooks', [
+ $hook => [ $mock ],
+ ] );
+ return $mock->expects( $expect )->method( "on$hook" );
+ }
+
+ /**
+ * Unsets a hook
+ * @param string $hook
+ */
+ protected function unhook( $hook ) {
+ $this->mergeMwGlobalArrayValue( 'wgHooks', [
+ $hook => [],
+ ] );
+ }
+
+ // Stubs for hooks taking reference parameters
+ public function onLoginUserMigrated( $user, &$msg ) {
+ }
+ public function onAbortLogin( $user, $password, &$abort, &$msg ) {
+ }
+ public function onAbortNewAccount( $user, &$abortError, &$abortStatus ) {
+ }
+ public function onAbortAutoAccount( $user, &$abortError ) {
+ }
+
+ /**
+ * @dataProvider provideTestForAuthentication
+ * @param string|null $username
+ * @param string|null $password
+ * @param string|null $msgForLoginUserMigrated
+ * @param int|null $abortForAbortLogin
+ * @param string|null $msgForAbortLogin
+ * @param string|null $failMsg
+ * @param array $failParams
+ */
+ public function testTestForAuthentication(
+ $username, $password,
+ $msgForLoginUserMigrated, $abortForAbortLogin, $msgForAbortLogin,
+ $failMsg, $failParams = []
+ ) {
+ $reqs = [];
+ if ( $username === null ) {
+ $this->hook( 'LoginUserMigrated', $this->never() );
+ $this->hook( 'AbortLogin', $this->never() );
+ } else {
+ if ( $password === null ) {
+ $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+ } else {
+ $req = new PasswordAuthenticationRequest();
+ $req->action = AuthManager::ACTION_LOGIN;
+ $req->password = $password;
+ }
+ $req->username = $username;
+ $reqs[get_class( $req )] = $req;
+
+ $h = $this->hook( 'LoginUserMigrated', $this->once() );
+ if ( $msgForLoginUserMigrated !== null ) {
+ $h->will( $this->returnCallback(
+ function ( $user, &$msg ) use ( $username, $msgForLoginUserMigrated ) {
+ $this->assertInstanceOf( \User::class, $user );
+ $this->assertSame( $username, $user->getName() );
+ $msg = $msgForLoginUserMigrated;
+ return false;
+ }
+ ) );
+ $this->hook( 'AbortLogin', $this->never() );
+ } else {
+ $h->will( $this->returnCallback(
+ function ( $user, &$msg ) use ( $username ) {
+ $this->assertInstanceOf( \User::class, $user );
+ $this->assertSame( $username, $user->getName() );
+ return true;
+ }
+ ) );
+ $h2 = $this->hook( 'AbortLogin', $this->once() );
+ if ( $abortForAbortLogin !== null ) {
+ $h2->will( $this->returnCallback(
+ function ( $user, $pass, &$abort, &$msg )
+ use ( $username, $password, $abortForAbortLogin, $msgForAbortLogin )
+ {
+ $this->assertInstanceOf( \User::class, $user );
+ $this->assertSame( $username, $user->getName() );
+ if ( $password !== null ) {
+ $this->assertSame( $password, $pass );
+ } else {
+ $this->assertInternalType( 'string', $pass );
+ }
+ $abort = $abortForAbortLogin;
+ $msg = $msgForAbortLogin;
+ return false;
+ }
+ ) );
+ } else {
+ $h2->will( $this->returnCallback(
+ function ( $user, $pass, &$abort, &$msg ) use ( $username, $password ) {
+ $this->assertInstanceOf( \User::class, $user );
+ $this->assertSame( $username, $user->getName() );
+ if ( $password !== null ) {
+ $this->assertSame( $password, $pass );
+ } else {
+ $this->assertInternalType( 'string', $pass );
+ }
+ return true;
+ }
+ ) );
+ }
+ }
+ }
+ unset( $h, $h2 );
+
+ $status = $this->getProvider()->testForAuthentication( $reqs );
+
+ $this->unhook( 'LoginUserMigrated' );
+ $this->unhook( 'AbortLogin' );
+
+ if ( $failMsg === null ) {
+ $this->assertEquals( \StatusValue::newGood(), $status, 'should succeed' );
+ } else {
+ $this->assertInstanceOf( \StatusValue::class, $status, 'should fail (type)' );
+ $this->assertFalse( $status->isOk(), 'should fail (ok)' );
+ $errors = $status->getErrors();
+ $this->assertEquals( $failMsg, $errors[0]['message'], 'should fail (message)' );
+ $this->assertEquals( $failParams, $errors[0]['params'], 'should fail (params)' );
+ }
+ }
+
+ public static function provideTestForAuthentication() {
+ return [
+ 'No valid requests' => [
+ null, null, null, null, null, null
+ ],
+ 'No hook errors' => [
+ 'User', 'PaSsWoRd', null, null, null, null
+ ],
+ 'No hook errors, no password' => [
+ 'User', null, null, null, null, null
+ ],
+ 'LoginUserMigrated no message' => [
+ 'User', 'PaSsWoRd', false, null, null, 'login-migrated-generic'
+ ],
+ 'LoginUserMigrated with message' => [
+ 'User', 'PaSsWoRd', 'LUM-abort', null, null, 'LUM-abort'
+ ],
+ 'LoginUserMigrated with message and params' => [
+ 'User', 'PaSsWoRd', [ 'LUM-abort', 'foo' ], null, null, 'LUM-abort', [ 'foo' ]
+ ],
+ 'AbortLogin, SUCCESS' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::SUCCESS, null, null
+ ],
+ 'AbortLogin, NEED_TOKEN, no message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::NEED_TOKEN, null, 'nocookiesforlogin'
+ ],
+ 'AbortLogin, NEED_TOKEN, with message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::NEED_TOKEN, 'needtoken', 'needtoken'
+ ],
+ 'AbortLogin, WRONG_TOKEN, no message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::WRONG_TOKEN, null, 'sessionfailure'
+ ],
+ 'AbortLogin, WRONG_TOKEN, with message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::WRONG_TOKEN, 'wrongtoken', 'wrongtoken'
+ ],
+ 'AbortLogin, ILLEGAL, no message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::ILLEGAL, null, 'noname'
+ ],
+ 'AbortLogin, ILLEGAL, with message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::ILLEGAL, 'badname', 'badname'
+ ],
+ 'AbortLogin, NO_NAME, no message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::NO_NAME, null, 'noname'
+ ],
+ 'AbortLogin, NO_NAME, with message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::NO_NAME, 'badname', 'badname'
+ ],
+ 'AbortLogin, WRONG_PASS, no message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::WRONG_PASS, null, 'wrongpassword'
+ ],
+ 'AbortLogin, WRONG_PASS, with message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::WRONG_PASS, 'badpass', 'badpass'
+ ],
+ 'AbortLogin, WRONG_PLUGIN_PASS, no message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::WRONG_PLUGIN_PASS, null, 'wrongpassword'
+ ],
+ 'AbortLogin, WRONG_PLUGIN_PASS, with message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::WRONG_PLUGIN_PASS, 'badpass', 'badpass'
+ ],
+ 'AbortLogin, NOT_EXISTS, no message' => [
+ "User'", 'A', null, \LoginForm::NOT_EXISTS, null, 'nosuchusershort', [ 'User&#39;' ]
+ ],
+ 'AbortLogin, NOT_EXISTS, with message' => [
+ "User'", 'A', null, \LoginForm::NOT_EXISTS, 'badname', 'badname', [ 'User&#39;' ]
+ ],
+ 'AbortLogin, EMPTY_PASS, no message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::EMPTY_PASS, null, 'wrongpasswordempty'
+ ],
+ 'AbortLogin, EMPTY_PASS, with message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::EMPTY_PASS, 'badpass', 'badpass'
+ ],
+ 'AbortLogin, RESET_PASS, no message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::RESET_PASS, null, 'resetpass_announce'
+ ],
+ 'AbortLogin, RESET_PASS, with message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::RESET_PASS, 'resetpass', 'resetpass'
+ ],
+ 'AbortLogin, THROTTLED, no message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::THROTTLED, null, 'login-throttled',
+ [ \Message::durationParam( 42 ) ]
+ ],
+ 'AbortLogin, THROTTLED, with message' => [
+ 'User', 'PaSsWoRd', null, \LoginForm::THROTTLED, 't', 't',
+ [ \Message::durationParam( 42 ) ]
+ ],
+ 'AbortLogin, USER_BLOCKED, no message' => [
+ "User'", 'P', null, \LoginForm::USER_BLOCKED, null, 'login-userblocked', [ 'User&#39;' ]
+ ],
+ 'AbortLogin, USER_BLOCKED, with message' => [
+ "User'", 'P', null, \LoginForm::USER_BLOCKED, 'blocked', 'blocked', [ 'User&#39;' ]
+ ],
+ 'AbortLogin, ABORTED, no message' => [
+ "User'", 'P', null, \LoginForm::ABORTED, null, 'login-abort-generic', [ 'User&#39;' ]
+ ],
+ 'AbortLogin, ABORTED, with message' => [
+ "User'", 'P', null, \LoginForm::ABORTED, 'aborted', 'aborted', [ 'User&#39;' ]
+ ],
+ 'AbortLogin, USER_MIGRATED, no message' => [
+ 'User', 'P', null, \LoginForm::USER_MIGRATED, null, 'login-migrated-generic'
+ ],
+ 'AbortLogin, USER_MIGRATED, with message' => [
+ 'User', 'P', null, \LoginForm::USER_MIGRATED, 'migrated', 'migrated'
+ ],
+ 'AbortLogin, USER_MIGRATED, with message and params' => [
+ 'User', 'P', null, \LoginForm::USER_MIGRATED, [ 'migrated', 'foo' ],
+ 'migrated', [ 'foo' ]
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideTestForAccountCreation
+ * @param string $msg
+ * @param Status|null $status
+ * @param StatusValue $result
+ */
+ public function testTestForAccountCreation( $msg, $status, $result ) {
+ $this->hook( 'AbortNewAccount', $this->once() )
+ ->will( $this->returnCallback( function ( $user, &$error, &$abortStatus )
+ use ( $msg, $status )
+ {
+ $this->assertInstanceOf( \User::class, $user );
+ $this->assertSame( 'User', $user->getName() );
+ $error = $msg;
+ $abortStatus = $status;
+ return $error === null && $status === null;
+ } ) );
+
+ $user = \User::newFromName( 'User' );
+ $creator = \User::newFromName( 'UTSysop' );
+ $ret = $this->getProvider()->testForAccountCreation( $user, $creator, [] );
+
+ $this->unhook( 'AbortNewAccount' );
+
+ $this->assertEquals( $result, $ret );
+ }
+
+ public static function provideTestForAccountCreation() {
+ return [
+ 'No hook errors' => [
+ null, null, \StatusValue::newGood()
+ ],
+ 'AbortNewAccount, old style' => [
+ 'foobar', null, \StatusValue::newFatal(
+ \Message::newFromKey( 'createaccount-hook-aborted' )->rawParams( 'foobar' )
+ )
+ ],
+ 'AbortNewAccount, new style' => [
+ 'foobar',
+ \Status::newFatal( 'aborted!', 'param' ),
+ \StatusValue::newFatal( 'aborted!', 'param' )
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideTestUserForCreation
+ * @param string|null $error
+ * @param string|null $failMsg
+ */
+ public function testTestUserForCreation( $error, $failMsg ) {
+ $testUser = self::getTestUser()->getUser();
+ $provider = $this->getProvider();
+ $options = [ 'flags' => \User::READ_LOCKING, 'creating' => true ];
+
+ $this->hook( 'AbortNewAccount', $this->never() );
+ $this->hook( 'AbortAutoAccount', $this->once() )
+ ->will( $this->returnCallback( function ( $user, &$abortError ) use ( $testUser, $error ) {
+ $this->assertInstanceOf( \User::class, $user );
+ $this->assertSame( $testUser->getName(), $user->getName() );
+ $abortError = $error;
+ return $error === null;
+ } ) );
+ $status = $provider->testUserForCreation(
+ $testUser, AuthManager::AUTOCREATE_SOURCE_SESSION, $options
+ );
+ $this->unhook( 'AbortNewAccount' );
+ $this->unhook( 'AbortAutoAccount' );
+ if ( $failMsg === null ) {
+ $this->assertEquals( \StatusValue::newGood(), $status, 'should succeed' );
+ } else {
+ $this->assertInstanceOf( \StatusValue::class, $status, 'should fail (type)' );
+ $this->assertFalse( $status->isOk(), 'should fail (ok)' );
+ $errors = $status->getErrors();
+ $this->assertEquals( $failMsg, $errors[0]['message'], 'should fail (message)' );
+ }
+
+ $this->hook( 'AbortAutoAccount', $this->never() );
+ $this->hook( 'AbortNewAccount', $this->never() );
+ $status = $provider->testUserForCreation( $testUser, false, $options );
+ $this->unhook( 'AbortNewAccount' );
+ $this->unhook( 'AbortAutoAccount' );
+ $this->assertEquals( \StatusValue::newGood(), $status, 'should succeed' );
+ }
+
+ public static function provideTestUserForCreation() {
+ return [
+ 'Success' => [ null, null ],
+ 'Fail, no message' => [ false, 'login-abort-generic' ],
+ 'Fail, with message' => [ 'fail', 'fail' ],
+ ];
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php
new file mode 100644
index 00000000..5f370785
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php
@@ -0,0 +1,658 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use MediaWiki\MediaWikiServices;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group AuthManager
+ * @group Database
+ * @covers MediaWiki\Auth\LocalPasswordPrimaryAuthenticationProvider
+ */
+class LocalPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestCase {
+
+ private $manager = null;
+ private $config = null;
+ private $validity = null;
+
+ /**
+ * Get an instance of the provider
+ *
+ * $provider->checkPasswordValidity is mocked to return $this->validity,
+ * because we don't need to test that here.
+ *
+ * @param bool $loginOnly
+ * @return LocalPasswordPrimaryAuthenticationProvider
+ */
+ protected function getProvider( $loginOnly = false ) {
+ if ( !$this->config ) {
+ $this->config = new \HashConfig();
+ }
+ $config = new \MultiConfig( [
+ $this->config,
+ MediaWikiServices::getInstance()->getMainConfig()
+ ] );
+
+ if ( !$this->manager ) {
+ $this->manager = new AuthManager( new \FauxRequest(), $config );
+ }
+ $this->validity = \Status::newGood();
+
+ $provider = $this->getMockBuilder( LocalPasswordPrimaryAuthenticationProvider::class )
+ ->setMethods( [ 'checkPasswordValidity' ] )
+ ->setConstructorArgs( [ [ 'loginOnly' => $loginOnly ] ] )
+ ->getMock();
+ $provider->expects( $this->any() )->method( 'checkPasswordValidity' )
+ ->will( $this->returnCallback( function () {
+ return $this->validity;
+ } ) );
+ $provider->setConfig( $config );
+ $provider->setLogger( new \Psr\Log\NullLogger() );
+ $provider->setManager( $this->manager );
+
+ return $provider;
+ }
+
+ public function testBasics() {
+ $user = $this->getMutableTestUser()->getUser();
+ $userName = $user->getName();
+ $lowerInitialUserName = mb_strtolower( $userName[0] ) . substr( $userName, 1 );
+
+ $provider = new LocalPasswordPrimaryAuthenticationProvider();
+
+ $this->assertSame(
+ PrimaryAuthenticationProvider::TYPE_CREATE,
+ $provider->accountCreationType()
+ );
+
+ $this->assertTrue( $provider->testUserExists( $userName ) );
+ $this->assertTrue( $provider->testUserExists( $lowerInitialUserName ) );
+ $this->assertFalse( $provider->testUserExists( 'DoesNotExist' ) );
+ $this->assertFalse( $provider->testUserExists( '<invalid>' ) );
+
+ $provider = new LocalPasswordPrimaryAuthenticationProvider( [ 'loginOnly' => true ] );
+
+ $this->assertSame(
+ PrimaryAuthenticationProvider::TYPE_NONE,
+ $provider->accountCreationType()
+ );
+
+ $this->assertTrue( $provider->testUserExists( $userName ) );
+ $this->assertFalse( $provider->testUserExists( 'DoesNotExist' ) );
+
+ $req = new PasswordAuthenticationRequest;
+ $req->action = AuthManager::ACTION_CHANGE;
+ $req->username = '<invalid>';
+ $provider->providerChangeAuthenticationData( $req );
+ }
+
+ public function testTestUserCanAuthenticate() {
+ $user = $this->getMutableTestUser()->getUser();
+ $userName = $user->getName();
+ $dbw = wfGetDB( DB_MASTER );
+
+ $provider = $this->getProvider();
+
+ $this->assertFalse( $provider->testUserCanAuthenticate( '<invalid>' ) );
+
+ $this->assertFalse( $provider->testUserCanAuthenticate( 'DoesNotExist' ) );
+
+ $this->assertTrue( $provider->testUserCanAuthenticate( $userName ) );
+ $lowerInitialUserName = mb_strtolower( $userName[0] ) . substr( $userName, 1 );
+ $this->assertTrue( $provider->testUserCanAuthenticate( $lowerInitialUserName ) );
+
+ $dbw->update(
+ 'user',
+ [ 'user_password' => \PasswordFactory::newInvalidPassword()->toString() ],
+ [ 'user_name' => $userName ]
+ );
+ $this->assertFalse( $provider->testUserCanAuthenticate( $userName ) );
+
+ // Really old format
+ $dbw->update(
+ 'user',
+ [ 'user_password' => '0123456789abcdef0123456789abcdef' ],
+ [ 'user_name' => $userName ]
+ );
+ $this->assertTrue( $provider->testUserCanAuthenticate( $userName ) );
+ }
+
+ public function testSetPasswordResetFlag() {
+ // Set instance vars
+ $this->getProvider();
+
+ /// @todo: Because we're currently using User, which uses the global config...
+ $this->setMwGlobals( [ 'wgPasswordExpireGrace' => 100 ] );
+
+ $this->config->set( 'PasswordExpireGrace', 100 );
+ $this->config->set( 'InvalidPasswordReset', true );
+
+ $provider = new LocalPasswordPrimaryAuthenticationProvider();
+ $provider->setConfig( $this->config );
+ $provider->setLogger( new \Psr\Log\NullLogger() );
+ $provider->setManager( $this->manager );
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+
+ $user = $this->getMutableTestUser()->getUser();
+ $userName = $user->getName();
+ $dbw = wfGetDB( DB_MASTER );
+ $row = $dbw->selectRow(
+ 'user',
+ '*',
+ [ 'user_name' => $userName ],
+ __METHOD__
+ );
+
+ $this->manager->removeAuthenticationSessionData( null );
+ $row->user_password_expires = wfTimestamp( TS_MW, time() + 200 );
+ $providerPriv->setPasswordResetFlag( $userName, \Status::newGood(), $row );
+ $this->assertNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
+
+ $this->manager->removeAuthenticationSessionData( null );
+ $row->user_password_expires = wfTimestamp( TS_MW, time() - 200 );
+ $providerPriv->setPasswordResetFlag( $userName, \Status::newGood(), $row );
+ $ret = $this->manager->getAuthenticationSessionData( 'reset-pass' );
+ $this->assertNotNull( $ret );
+ $this->assertSame( 'resetpass-expired', $ret->msg->getKey() );
+ $this->assertTrue( $ret->hard );
+
+ $this->manager->removeAuthenticationSessionData( null );
+ $row->user_password_expires = wfTimestamp( TS_MW, time() - 1 );
+ $providerPriv->setPasswordResetFlag( $userName, \Status::newGood(), $row );
+ $ret = $this->manager->getAuthenticationSessionData( 'reset-pass' );
+ $this->assertNotNull( $ret );
+ $this->assertSame( 'resetpass-expired-soft', $ret->msg->getKey() );
+ $this->assertFalse( $ret->hard );
+
+ $this->manager->removeAuthenticationSessionData( null );
+ $row->user_password_expires = null;
+ $status = \Status::newGood();
+ $status->error( 'testing' );
+ $providerPriv->setPasswordResetFlag( $userName, $status, $row );
+ $ret = $this->manager->getAuthenticationSessionData( 'reset-pass' );
+ $this->assertNotNull( $ret );
+ $this->assertSame( 'resetpass-validity-soft', $ret->msg->getKey() );
+ $this->assertFalse( $ret->hard );
+ }
+
+ public function testAuthentication() {
+ $testUser = $this->getMutableTestUser();
+ $userName = $testUser->getUser()->getName();
+
+ $dbw = wfGetDB( DB_MASTER );
+ $id = \User::idFromName( $userName );
+
+ $req = new PasswordAuthenticationRequest();
+ $req->action = AuthManager::ACTION_LOGIN;
+ $reqs = [ PasswordAuthenticationRequest::class => $req ];
+
+ $provider = $this->getProvider();
+
+ // General failures
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAuthentication( [] )
+ );
+
+ $req->username = 'foo';
+ $req->password = null;
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+
+ $req->username = null;
+ $req->password = 'bar';
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+
+ $req->username = '<invalid>';
+ $req->password = 'WhoCares';
+ $ret = $provider->beginPrimaryAuthentication( $reqs );
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+
+ $req->username = 'DoesNotExist';
+ $req->password = 'DoesNotExist';
+ $ret = $provider->beginPrimaryAuthentication( $reqs );
+ $this->assertEquals(
+ AuthenticationResponse::FAIL,
+ $ret->status
+ );
+ $this->assertEquals(
+ 'wrongpassword',
+ $ret->message->getKey()
+ );
+
+ // Validation failure
+ $req->username = $userName;
+ $req->password = $testUser->getPassword();
+ $this->validity = \Status::newFatal( 'arbitrary-failure' );
+ $ret = $provider->beginPrimaryAuthentication( $reqs );
+ $this->assertEquals(
+ AuthenticationResponse::FAIL,
+ $ret->status
+ );
+ $this->assertEquals(
+ 'arbitrary-failure',
+ $ret->message->getKey()
+ );
+
+ // Successful auth
+ $this->manager->removeAuthenticationSessionData( null );
+ $this->validity = \Status::newGood();
+ $this->assertEquals(
+ AuthenticationResponse::newPass( $userName ),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+ $this->assertNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
+
+ // Successful auth after normalizing name
+ $this->manager->removeAuthenticationSessionData( null );
+ $this->validity = \Status::newGood();
+ $req->username = mb_strtolower( $userName[0] ) . substr( $userName, 1 );
+ $this->assertEquals(
+ AuthenticationResponse::newPass( $userName ),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+ $this->assertNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
+ $req->username = $userName;
+
+ // Successful auth with reset
+ $this->manager->removeAuthenticationSessionData( null );
+ $this->validity->error( 'arbitrary-warning' );
+ $this->assertEquals(
+ AuthenticationResponse::newPass( $userName ),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+ $this->assertNotNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
+
+ // Wrong password
+ $this->validity = \Status::newGood();
+ $req->password = 'Wrong';
+ $ret = $provider->beginPrimaryAuthentication( $reqs );
+ $this->assertEquals(
+ AuthenticationResponse::FAIL,
+ $ret->status
+ );
+ $this->assertEquals(
+ 'wrongpassword',
+ $ret->message->getKey()
+ );
+
+ // Correct handling of legacy encodings
+ $password = ':B:salt:' . md5( 'salt-' . md5( "\xe1\xe9\xed\xf3\xfa" ) );
+ $dbw->update( 'user', [ 'user_password' => $password ], [ 'user_name' => $userName ] );
+ $req->password = 'áéíóú';
+ $ret = $provider->beginPrimaryAuthentication( $reqs );
+ $this->assertEquals(
+ AuthenticationResponse::FAIL,
+ $ret->status
+ );
+ $this->assertEquals(
+ 'wrongpassword',
+ $ret->message->getKey()
+ );
+
+ $this->config->set( 'LegacyEncoding', true );
+ $this->assertEquals(
+ AuthenticationResponse::newPass( $userName ),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+
+ $req->password = 'áéíóú Wrong';
+ $ret = $provider->beginPrimaryAuthentication( $reqs );
+ $this->assertEquals(
+ AuthenticationResponse::FAIL,
+ $ret->status
+ );
+ $this->assertEquals(
+ 'wrongpassword',
+ $ret->message->getKey()
+ );
+
+ // Correct handling of really old password hashes
+ $this->config->set( 'PasswordSalt', false );
+ $password = md5( 'FooBar' );
+ $dbw->update( 'user', [ 'user_password' => $password ], [ 'user_name' => $userName ] );
+ $req->password = 'FooBar';
+ $this->assertEquals(
+ AuthenticationResponse::newPass( $userName ),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+
+ $this->config->set( 'PasswordSalt', true );
+ $password = md5( "$id-" . md5( 'FooBar' ) );
+ $dbw->update( 'user', [ 'user_password' => $password ], [ 'user_name' => $userName ] );
+ $req->password = 'FooBar';
+ $this->assertEquals(
+ AuthenticationResponse::newPass( $userName ),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+ }
+
+ /**
+ * @dataProvider provideProviderAllowsAuthenticationDataChange
+ * @param string $type
+ * @param string $user
+ * @param \Status $validity Result of the password validity check
+ * @param \StatusValue $expect1 Expected result with $checkData = false
+ * @param \StatusValue $expect2 Expected result with $checkData = true
+ */
+ public function testProviderAllowsAuthenticationDataChange( $type, $user, \Status $validity,
+ \StatusValue $expect1, \StatusValue $expect2
+ ) {
+ if ( $type === PasswordAuthenticationRequest::class ) {
+ $req = new $type();
+ } elseif ( $type === PasswordDomainAuthenticationRequest::class ) {
+ $req = new $type( [] );
+ } else {
+ $req = $this->createMock( $type );
+ }
+ $req->action = AuthManager::ACTION_CHANGE;
+ $req->username = $user;
+ $req->password = 'NewPassword';
+ $req->retype = 'NewPassword';
+
+ $provider = $this->getProvider();
+ $this->validity = $validity;
+ $this->assertEquals( $expect1, $provider->providerAllowsAuthenticationDataChange( $req, false ) );
+ $this->assertEquals( $expect2, $provider->providerAllowsAuthenticationDataChange( $req, true ) );
+
+ $req->retype = 'BadRetype';
+ $this->assertEquals(
+ $expect1,
+ $provider->providerAllowsAuthenticationDataChange( $req, false )
+ );
+ $this->assertEquals(
+ $expect2->getValue() === 'ignored' ? $expect2 : \StatusValue::newFatal( 'badretype' ),
+ $provider->providerAllowsAuthenticationDataChange( $req, true )
+ );
+
+ $provider = $this->getProvider( true );
+ $this->assertEquals(
+ \StatusValue::newGood( 'ignored' ),
+ $provider->providerAllowsAuthenticationDataChange( $req, true ),
+ 'loginOnly mode should claim to ignore all changes'
+ );
+ }
+
+ public static function provideProviderAllowsAuthenticationDataChange() {
+ $err = \StatusValue::newGood();
+ $err->error( 'arbitrary-warning' );
+
+ return [
+ [ AuthenticationRequest::class, 'UTSysop', \Status::newGood(),
+ \StatusValue::newGood( 'ignored' ), \StatusValue::newGood( 'ignored' ) ],
+ [ PasswordAuthenticationRequest::class, 'UTSysop', \Status::newGood(),
+ \StatusValue::newGood(), \StatusValue::newGood() ],
+ [ PasswordAuthenticationRequest::class, 'uTSysop', \Status::newGood(),
+ \StatusValue::newGood(), \StatusValue::newGood() ],
+ [ PasswordAuthenticationRequest::class, 'UTSysop', \Status::wrap( $err ),
+ \StatusValue::newGood(), $err ],
+ [ PasswordAuthenticationRequest::class, 'UTSysop', \Status::newFatal( 'arbitrary-error' ),
+ \StatusValue::newGood(), \StatusValue::newFatal( 'arbitrary-error' ) ],
+ [ PasswordAuthenticationRequest::class, 'DoesNotExist', \Status::newGood(),
+ \StatusValue::newGood(), \StatusValue::newGood( 'ignored' ) ],
+ [ PasswordDomainAuthenticationRequest::class, 'UTSysop', \Status::newGood(),
+ \StatusValue::newGood( 'ignored' ), \StatusValue::newGood( 'ignored' ) ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideProviderChangeAuthenticationData
+ * @param callable|bool $usernameTransform
+ * @param string $type
+ * @param bool $loginOnly
+ * @param bool $changed
+ */
+ public function testProviderChangeAuthenticationData(
+ $usernameTransform, $type, $loginOnly, $changed ) {
+ $testUser = $this->getMutableTestUser();
+ $user = $testUser->getUser()->getName();
+ if ( is_callable( $usernameTransform ) ) {
+ $user = call_user_func( $usernameTransform, $user );
+ }
+ $cuser = ucfirst( $user );
+ $oldpass = $testUser->getPassword();
+ $newpass = 'NewPassword';
+
+ $dbw = wfGetDB( DB_MASTER );
+ $oldExpiry = $dbw->selectField( 'user', 'user_password_expires', [ 'user_name' => $cuser ] );
+
+ $this->mergeMwGlobalArrayValue( 'wgHooks', [
+ 'ResetPasswordExpiration' => [ function ( $user, &$expires ) {
+ $expires = '30001231235959';
+ } ]
+ ] );
+
+ $provider = $this->getProvider( $loginOnly );
+
+ // Sanity check
+ $loginReq = new PasswordAuthenticationRequest();
+ $loginReq->action = AuthManager::ACTION_LOGIN;
+ $loginReq->username = $user;
+ $loginReq->password = $oldpass;
+ $loginReqs = [ PasswordAuthenticationRequest::class => $loginReq ];
+ $this->assertEquals(
+ AuthenticationResponse::newPass( $cuser ),
+ $provider->beginPrimaryAuthentication( $loginReqs ),
+ 'Sanity check'
+ );
+
+ if ( $type === PasswordAuthenticationRequest::class ) {
+ $changeReq = new $type();
+ } else {
+ $changeReq = $this->createMock( $type );
+ }
+ $changeReq->action = AuthManager::ACTION_CHANGE;
+ $changeReq->username = $user;
+ $changeReq->password = $newpass;
+ $provider->providerChangeAuthenticationData( $changeReq );
+
+ if ( $loginOnly && $changed ) {
+ $old = 'fail';
+ $new = 'fail';
+ $expectExpiry = null;
+ } elseif ( $changed ) {
+ $old = 'fail';
+ $new = 'pass';
+ $expectExpiry = '30001231235959';
+ } else {
+ $old = 'pass';
+ $new = 'fail';
+ $expectExpiry = $oldExpiry;
+ }
+
+ $loginReq->password = $oldpass;
+ $ret = $provider->beginPrimaryAuthentication( $loginReqs );
+ if ( $old === 'pass' ) {
+ $this->assertEquals(
+ AuthenticationResponse::newPass( $cuser ),
+ $ret,
+ 'old password should pass'
+ );
+ } else {
+ $this->assertEquals(
+ AuthenticationResponse::FAIL,
+ $ret->status,
+ 'old password should fail'
+ );
+ $this->assertEquals(
+ 'wrongpassword',
+ $ret->message->getKey(),
+ 'old password should fail'
+ );
+ }
+
+ $loginReq->password = $newpass;
+ $ret = $provider->beginPrimaryAuthentication( $loginReqs );
+ if ( $new === 'pass' ) {
+ $this->assertEquals(
+ AuthenticationResponse::newPass( $cuser ),
+ $ret,
+ 'new password should pass'
+ );
+ } else {
+ $this->assertEquals(
+ AuthenticationResponse::FAIL,
+ $ret->status,
+ 'new password should fail'
+ );
+ $this->assertEquals(
+ 'wrongpassword',
+ $ret->message->getKey(),
+ 'new password should fail'
+ );
+ }
+
+ $this->assertSame(
+ $expectExpiry,
+ wfTimestampOrNull(
+ TS_MW,
+ $dbw->selectField( 'user', 'user_password_expires', [ 'user_name' => $cuser ] )
+ )
+ );
+ }
+
+ public static function provideProviderChangeAuthenticationData() {
+ return [
+ [ false, AuthenticationRequest::class, false, false ],
+ [ false, PasswordAuthenticationRequest::class, false, true ],
+ [ false, AuthenticationRequest::class, true, false ],
+ [ false, PasswordAuthenticationRequest::class, true, true ],
+ [ 'ucfirst', PasswordAuthenticationRequest::class, false, true ],
+ [ 'ucfirst', PasswordAuthenticationRequest::class, true, true ],
+ ];
+ }
+
+ public function testTestForAccountCreation() {
+ $user = \User::newFromName( 'foo' );
+ $req = new PasswordAuthenticationRequest();
+ $req->action = AuthManager::ACTION_CREATE;
+ $req->username = 'Foo';
+ $req->password = 'Bar';
+ $req->retype = 'Bar';
+ $reqs = [ PasswordAuthenticationRequest::class => $req ];
+
+ $provider = $this->getProvider();
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testForAccountCreation( $user, $user, [] ),
+ 'No password request'
+ );
+
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testForAccountCreation( $user, $user, $reqs ),
+ 'Password request, validated'
+ );
+
+ $req->retype = 'Baz';
+ $this->assertEquals(
+ \StatusValue::newFatal( 'badretype' ),
+ $provider->testForAccountCreation( $user, $user, $reqs ),
+ 'Password request, bad retype'
+ );
+ $req->retype = 'Bar';
+
+ $this->validity->error( 'arbitrary warning' );
+ $expect = \StatusValue::newGood();
+ $expect->error( 'arbitrary warning' );
+ $this->assertEquals(
+ $expect,
+ $provider->testForAccountCreation( $user, $user, $reqs ),
+ 'Password request, not validated'
+ );
+
+ $provider = $this->getProvider( true );
+ $this->validity->error( 'arbitrary warning' );
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testForAccountCreation( $user, $user, $reqs ),
+ 'Password request, not validated, loginOnly'
+ );
+ }
+
+ public function testAccountCreation() {
+ $user = \User::newFromName( 'Foo' );
+
+ $req = new PasswordAuthenticationRequest();
+ $req->action = AuthManager::ACTION_CREATE;
+ $reqs = [ PasswordAuthenticationRequest::class => $req ];
+
+ $provider = $this->getProvider( true );
+ try {
+ $provider->beginPrimaryAccountCreation( $user, $user, [] );
+ $this->fail( 'Expected exception was not thrown' );
+ } catch ( \BadMethodCallException $ex ) {
+ $this->assertSame(
+ 'Shouldn\'t call this when accountCreationType() is NONE', $ex->getMessage()
+ );
+ }
+
+ try {
+ $provider->finishAccountCreation( $user, $user, AuthenticationResponse::newPass() );
+ $this->fail( 'Expected exception was not thrown' );
+ } catch ( \BadMethodCallException $ex ) {
+ $this->assertSame(
+ 'Shouldn\'t call this when accountCreationType() is NONE', $ex->getMessage()
+ );
+ }
+
+ $provider = $this->getProvider( false );
+
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAccountCreation( $user, $user, [] )
+ );
+
+ $req->username = 'foo';
+ $req->password = null;
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
+ );
+
+ $req->username = null;
+ $req->password = 'bar';
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
+ );
+
+ $req->username = 'foo';
+ $req->password = 'bar';
+
+ $expect = AuthenticationResponse::newPass( 'Foo' );
+ $expect->createRequest = clone $req;
+ $expect->createRequest->username = 'Foo';
+ $this->assertEquals( $expect, $provider->beginPrimaryAccountCreation( $user, $user, $reqs ) );
+
+ // We have to cheat a bit to avoid having to add a new user to
+ // the database to test the actual setting of the password works right
+ $dbw = wfGetDB( DB_MASTER );
+
+ $user = \User::newFromName( 'UTSysop' );
+ $req->username = $user->getName();
+ $req->password = 'NewPassword';
+ $expect = AuthenticationResponse::newPass( 'UTSysop' );
+ $expect->createRequest = $req;
+
+ $res2 = $provider->beginPrimaryAccountCreation( $user, $user, $reqs );
+ $this->assertEquals( $expect, $res2, 'Sanity check' );
+
+ $ret = $provider->beginPrimaryAuthentication( $reqs );
+ $this->assertEquals( AuthenticationResponse::FAIL, $ret->status, 'sanity check' );
+
+ $this->assertNull( $provider->finishAccountCreation( $user, $user, $res2 ) );
+ $ret = $provider->beginPrimaryAuthentication( $reqs );
+ $this->assertEquals( AuthenticationResponse::PASS, $ret->status, 'new password is set' );
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/PasswordAuthenticationRequestTest.php b/www/wiki/tests/phpunit/includes/auth/PasswordAuthenticationRequestTest.php
new file mode 100644
index 00000000..1ef675b6
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/PasswordAuthenticationRequestTest.php
@@ -0,0 +1,138 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\PasswordAuthenticationRequest
+ */
+class PasswordAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+ protected function getInstance( array $args = [] ) {
+ $ret = new PasswordAuthenticationRequest();
+ $ret->action = $args[0];
+ return $ret;
+ }
+
+ public static function provideGetFieldInfo() {
+ return [
+ [ [ AuthManager::ACTION_LOGIN ] ],
+ [ [ AuthManager::ACTION_CREATE ] ],
+ [ [ AuthManager::ACTION_CHANGE ] ],
+ [ [ AuthManager::ACTION_REMOVE ] ],
+ ];
+ }
+
+ public function testGetFieldInfo2() {
+ $info = [];
+ foreach ( [
+ AuthManager::ACTION_LOGIN,
+ AuthManager::ACTION_CREATE,
+ AuthManager::ACTION_CHANGE,
+ AuthManager::ACTION_REMOVE,
+ ] as $action ) {
+ $req = new PasswordAuthenticationRequest();
+ $req->action = $action;
+ $info[$action] = $req->getFieldInfo();
+ }
+
+ $this->assertSame( [], $info[AuthManager::ACTION_REMOVE], 'No data needed to remove' );
+
+ $this->assertArrayNotHasKey( 'retype', $info[AuthManager::ACTION_LOGIN],
+ 'No need to retype password on login' );
+ $this->assertArrayHasKey( 'retype', $info[AuthManager::ACTION_CREATE],
+ 'Need to retype when creating new password' );
+ $this->assertArrayHasKey( 'retype', $info[AuthManager::ACTION_CHANGE],
+ 'Need to retype when changing password' );
+
+ $this->assertNotEquals(
+ $info[AuthManager::ACTION_LOGIN]['password']['label'],
+ $info[AuthManager::ACTION_CHANGE]['password']['label'],
+ 'Password field for change is differentiated from login'
+ );
+ $this->assertNotEquals(
+ $info[AuthManager::ACTION_CREATE]['password']['label'],
+ $info[AuthManager::ACTION_CHANGE]['password']['label'],
+ 'Password field for change is differentiated from create'
+ );
+ $this->assertNotEquals(
+ $info[AuthManager::ACTION_CREATE]['retype']['label'],
+ $info[AuthManager::ACTION_CHANGE]['retype']['label'],
+ 'Retype field for change is differentiated from create'
+ );
+ }
+
+ public function provideLoadFromSubmission() {
+ return [
+ 'Empty request, login' => [
+ [ AuthManager::ACTION_LOGIN ],
+ [],
+ false,
+ ],
+ 'Empty request, change' => [
+ [ AuthManager::ACTION_CHANGE ],
+ [],
+ false,
+ ],
+ 'Empty request, remove' => [
+ [ AuthManager::ACTION_REMOVE ],
+ [],
+ false,
+ ],
+ 'Username + password, login' => [
+ [ AuthManager::ACTION_LOGIN ],
+ $data = [ 'username' => 'User', 'password' => 'Bar' ],
+ $data + [ 'action' => AuthManager::ACTION_LOGIN ],
+ ],
+ 'Username + password, change' => [
+ [ AuthManager::ACTION_CHANGE ],
+ [ 'username' => 'User', 'password' => 'Bar' ],
+ false,
+ ],
+ 'Username + password + retype' => [
+ [ AuthManager::ACTION_CHANGE ],
+ [ 'username' => 'User', 'password' => 'Bar', 'retype' => 'baz' ],
+ [ 'password' => 'Bar', 'retype' => 'baz', 'action' => AuthManager::ACTION_CHANGE ],
+ ],
+ 'Username empty, login' => [
+ [ AuthManager::ACTION_LOGIN ],
+ [ 'username' => '', 'password' => 'Bar' ],
+ false,
+ ],
+ 'Username empty, change' => [
+ [ AuthManager::ACTION_CHANGE ],
+ [ 'username' => '', 'password' => 'Bar', 'retype' => 'baz' ],
+ [ 'password' => 'Bar', 'retype' => 'baz', 'action' => AuthManager::ACTION_CHANGE ],
+ ],
+ 'Password empty, login' => [
+ [ AuthManager::ACTION_LOGIN ],
+ [ 'username' => 'User', 'password' => '' ],
+ false,
+ ],
+ 'Password empty, login, with retype' => [
+ [ AuthManager::ACTION_LOGIN ],
+ [ 'username' => 'User', 'password' => '', 'retype' => 'baz' ],
+ false,
+ ],
+ 'Retype empty' => [
+ [ AuthManager::ACTION_CHANGE ],
+ [ 'username' => 'User', 'password' => 'Bar', 'retype' => '' ],
+ false,
+ ],
+ ];
+ }
+
+ public function testDescribeCredentials() {
+ $req = new PasswordAuthenticationRequest;
+ $req->action = AuthManager::ACTION_LOGIN;
+ $req->username = 'UTSysop';
+ $ret = $req->describeCredentials();
+ $this->assertInternalType( 'array', $ret );
+ $this->assertArrayHasKey( 'provider', $ret );
+ $this->assertInstanceOf( \Message::class, $ret['provider'] );
+ $this->assertSame( 'authmanager-provider-password', $ret['provider']->getKey() );
+ $this->assertArrayHasKey( 'account', $ret );
+ $this->assertInstanceOf( \Message::class, $ret['account'] );
+ $this->assertSame( [ 'UTSysop' ], $ret['account']->getParams() );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/PasswordDomainAuthenticationRequestTest.php b/www/wiki/tests/phpunit/includes/auth/PasswordDomainAuthenticationRequestTest.php
new file mode 100644
index 00000000..36be4243
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/PasswordDomainAuthenticationRequestTest.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\PasswordDomainAuthenticationRequest
+ */
+class PasswordDomainAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+ protected function getInstance( array $args = [] ) {
+ $ret = new PasswordDomainAuthenticationRequest( [ 'd1', 'd2' ] );
+ $ret->action = $args[0];
+ return $ret;
+ }
+
+ public static function provideGetFieldInfo() {
+ return [
+ [ [ AuthManager::ACTION_LOGIN ] ],
+ [ [ AuthManager::ACTION_CREATE ] ],
+ [ [ AuthManager::ACTION_CHANGE ] ],
+ [ [ AuthManager::ACTION_REMOVE ] ],
+ ];
+ }
+
+ public function testGetFieldInfo2() {
+ $info = [];
+ foreach ( [
+ AuthManager::ACTION_LOGIN,
+ AuthManager::ACTION_CREATE,
+ AuthManager::ACTION_CHANGE,
+ AuthManager::ACTION_REMOVE,
+ ] as $action ) {
+ $req = new PasswordDomainAuthenticationRequest( [ 'd1', 'd2' ] );
+ $req->action = $action;
+ $info[$action] = $req->getFieldInfo();
+ }
+
+ $this->assertSame( [], $info[AuthManager::ACTION_REMOVE], 'No data needed to remove' );
+
+ $this->assertArrayNotHasKey( 'retype', $info[AuthManager::ACTION_LOGIN],
+ 'No need to retype password on login' );
+ $this->assertArrayHasKey( 'domain', $info[AuthManager::ACTION_LOGIN],
+ 'Domain needed on login' );
+ $this->assertArrayHasKey( 'retype', $info[AuthManager::ACTION_CREATE],
+ 'Need to retype when creating new password' );
+ $this->assertArrayHasKey( 'domain', $info[AuthManager::ACTION_CREATE],
+ 'Domain needed on account creation' );
+ $this->assertArrayHasKey( 'retype', $info[AuthManager::ACTION_CHANGE],
+ 'Need to retype when changing password' );
+ $this->assertArrayNotHasKey( 'domain', $info[AuthManager::ACTION_CHANGE],
+ 'Domain not needed on account creation' );
+
+ $this->assertNotEquals(
+ $info[AuthManager::ACTION_LOGIN]['password']['label'],
+ $info[AuthManager::ACTION_CHANGE]['password']['label'],
+ 'Password field for change is differentiated from login'
+ );
+ $this->assertNotEquals(
+ $info[AuthManager::ACTION_CREATE]['password']['label'],
+ $info[AuthManager::ACTION_CHANGE]['password']['label'],
+ 'Password field for change is differentiated from create'
+ );
+ $this->assertNotEquals(
+ $info[AuthManager::ACTION_CREATE]['retype']['label'],
+ $info[AuthManager::ACTION_CHANGE]['retype']['label'],
+ 'Retype field for change is differentiated from create'
+ );
+ }
+
+ public function provideLoadFromSubmission() {
+ $domainList = [ 'domainList' => [ 'd1', 'd2' ] ];
+ return [
+ 'Empty request, login' => [
+ [ AuthManager::ACTION_LOGIN ],
+ [],
+ false,
+ ],
+ 'Empty request, change' => [
+ [ AuthManager::ACTION_CHANGE ],
+ [],
+ false,
+ ],
+ 'Empty request, remove' => [
+ [ AuthManager::ACTION_REMOVE ],
+ [],
+ false,
+ ],
+ 'Username + password, login' => [
+ [ AuthManager::ACTION_LOGIN ],
+ $data = [ 'username' => 'User', 'password' => 'Bar' ],
+ false,
+ ],
+ 'Username + password + domain, login' => [
+ [ AuthManager::ACTION_LOGIN ],
+ $data = [ 'username' => 'User', 'password' => 'Bar', 'domain' => 'd1' ],
+ $data + [ 'action' => AuthManager::ACTION_LOGIN ] + $domainList,
+ ],
+ 'Username + password + bad domain, login' => [
+ [ AuthManager::ACTION_LOGIN ],
+ $data = [ 'username' => 'User', 'password' => 'Bar', 'domain' => 'd5' ],
+ false,
+ ],
+ 'Username + password + domain, change' => [
+ [ AuthManager::ACTION_CHANGE ],
+ [ 'username' => 'User', 'password' => 'Bar', 'domain' => 'd1' ],
+ false,
+ ],
+ 'Username + password + domain + retype' => [
+ [ AuthManager::ACTION_CHANGE ],
+ [ 'username' => 'User', 'password' => 'Bar', 'retype' => 'baz', 'domain' => 'd1' ],
+ [ 'password' => 'Bar', 'retype' => 'baz', 'action' => AuthManager::ACTION_CHANGE ] +
+ $domainList,
+ ],
+ 'Username empty, login' => [
+ [ AuthManager::ACTION_LOGIN ],
+ [ 'username' => '', 'password' => 'Bar', 'domain' => 'd1' ],
+ false,
+ ],
+ 'Username empty, change' => [
+ [ AuthManager::ACTION_CHANGE ],
+ [ 'username' => '', 'password' => 'Bar', 'retype' => 'baz', 'domain' => 'd1' ],
+ [ 'password' => 'Bar', 'retype' => 'baz', 'action' => AuthManager::ACTION_CHANGE ] +
+ $domainList,
+ ],
+ 'Password empty, login' => [
+ [ AuthManager::ACTION_LOGIN ],
+ [ 'username' => 'User', 'password' => '', 'domain' => 'd1' ],
+ false,
+ ],
+ 'Password empty, login, with retype' => [
+ [ AuthManager::ACTION_LOGIN ],
+ [ 'username' => 'User', 'password' => '', 'retype' => 'baz', 'domain' => 'd1' ],
+ false,
+ ],
+ 'Retype empty' => [
+ [ AuthManager::ACTION_CHANGE ],
+ [ 'username' => 'User', 'password' => 'Bar', 'retype' => '', 'domain' => 'd1' ],
+ false,
+ ],
+ ];
+ }
+
+ public function testDescribeCredentials() {
+ $req = new PasswordDomainAuthenticationRequest( [ 'd1', 'd2' ] );
+ $req->action = AuthManager::ACTION_LOGIN;
+ $req->username = 'UTSysop';
+ $req->domain = 'd2';
+ $ret = $req->describeCredentials();
+ $this->assertInternalType( 'array', $ret );
+ $this->assertArrayHasKey( 'provider', $ret );
+ $this->assertInstanceOf( \Message::class, $ret['provider'] );
+ $this->assertSame( 'authmanager-provider-password-domain', $ret['provider']->getKey() );
+ $this->assertArrayHasKey( 'account', $ret );
+ $this->assertInstanceOf( \Message::class, $ret['account'] );
+ $this->assertSame( 'authmanager-account-password-domain', $ret['account']->getKey() );
+ $this->assertSame( [ 'UTSysop', 'd2' ], $ret['account']->getParams() );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/RememberMeAuthenticationRequestTest.php b/www/wiki/tests/phpunit/includes/auth/RememberMeAuthenticationRequestTest.php
new file mode 100644
index 00000000..9bcab777
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/RememberMeAuthenticationRequestTest.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\RememberMeAuthenticationRequest
+ */
+class RememberMeAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+ public static function provideGetFieldInfo() {
+ return [
+ [ [ 1 ] ],
+ [ [ null ] ],
+ ];
+ }
+
+ public function testGetFieldInfo_2() {
+ $req = new RememberMeAuthenticationRequest();
+ $reqWrapper = TestingAccessWrapper::newFromObject( $req );
+
+ $reqWrapper->expiration = 30 * 24 * 3600;
+ $this->assertNotEmpty( $req->getFieldInfo() );
+
+ $reqWrapper->expiration = null;
+ $this->assertEmpty( $req->getFieldInfo() );
+ }
+
+ protected function getInstance( array $args = [] ) {
+ $req = new RememberMeAuthenticationRequest();
+ $reqWrapper = TestingAccessWrapper::newFromObject( $req );
+ $reqWrapper->expiration = $args[0];
+ return $req;
+ }
+
+ public function provideLoadFromSubmission() {
+ return [
+ 'Empty request' => [
+ [ 30 * 24 * 3600 ],
+ [],
+ [ 'expiration' => 30 * 24 * 3600, 'rememberMe' => false ]
+ ],
+ 'RememberMe present' => [
+ [ 30 * 24 * 3600 ],
+ [ 'rememberMe' => '' ],
+ [ 'expiration' => 30 * 24 * 3600, 'rememberMe' => true ]
+ ],
+ 'RememberMe present but session provider cannot remember' => [
+ [ null ],
+ [ 'rememberMe' => '' ],
+ false
+ ],
+ ];
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/ResetPasswordSecondaryAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/ResetPasswordSecondaryAuthenticationProviderTest.php
new file mode 100644
index 00000000..f454a96a
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/ResetPasswordSecondaryAuthenticationProviderTest.php
@@ -0,0 +1,310 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\ResetPasswordSecondaryAuthenticationProvider
+ */
+class ResetPasswordSecondaryAuthenticationProviderTest extends \MediaWikiTestCase {
+ /**
+ * @dataProvider provideGetAuthenticationRequests
+ * @param string $action
+ * @param array $response
+ */
+ public function testGetAuthenticationRequests( $action, $response ) {
+ $provider = new ResetPasswordSecondaryAuthenticationProvider();
+
+ $this->assertEquals( $response, $provider->getAuthenticationRequests( $action, [] ) );
+ }
+
+ public static function provideGetAuthenticationRequests() {
+ return [
+ [ AuthManager::ACTION_LOGIN, [] ],
+ [ AuthManager::ACTION_CREATE, [] ],
+ [ AuthManager::ACTION_LINK, [] ],
+ [ AuthManager::ACTION_CHANGE, [] ],
+ [ AuthManager::ACTION_REMOVE, [] ],
+ ];
+ }
+
+ public function testBasics() {
+ $user = \User::newFromName( 'UTSysop' );
+ $user2 = new \User;
+ $obj = new \stdClass;
+ $reqs = [ new \stdClass ];
+
+ $mb = $this->getMockBuilder( ResetPasswordSecondaryAuthenticationProvider::class )
+ ->setMethods( [ 'tryReset' ] );
+
+ $methods = [
+ 'beginSecondaryAuthentication' => [ $user, $reqs ],
+ 'continueSecondaryAuthentication' => [ $user, $reqs ],
+ 'beginSecondaryAccountCreation' => [ $user, $user2, $reqs ],
+ 'continueSecondaryAccountCreation' => [ $user, $user2, $reqs ],
+ ];
+ foreach ( $methods as $method => $args ) {
+ $mock = $mb->getMock();
+ $mock->expects( $this->once() )->method( 'tryReset' )
+ ->with( $this->identicalTo( $user ), $this->identicalTo( $reqs ) )
+ ->will( $this->returnValue( $obj ) );
+ $this->assertSame( $obj, call_user_func_array( [ $mock, $method ], $args ) );
+ }
+ }
+
+ public function testTryReset() {
+ $user = \User::newFromName( 'UTSysop' );
+
+ $provider = $this->getMockBuilder(
+ ResetPasswordSecondaryAuthenticationProvider::class
+ )
+ ->setMethods( [
+ 'providerAllowsAuthenticationDataChange', 'providerChangeAuthenticationData'
+ ] )
+ ->getMock();
+ $provider->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
+ ->will( $this->returnCallback( function ( $req ) {
+ $this->assertSame( 'UTSysop', $req->username );
+ return $req->allow;
+ } ) );
+ $provider->expects( $this->any() )->method( 'providerChangeAuthenticationData' )
+ ->will( $this->returnCallback( function ( $req ) {
+ $this->assertSame( 'UTSysop', $req->username );
+ $req->done = true;
+ } ) );
+ $config = new \HashConfig( [
+ 'AuthManagerConfig' => [
+ 'preauth' => [],
+ 'primaryauth' => [],
+ 'secondaryauth' => [
+ [ 'factory' => function () use ( $provider ) {
+ return $provider;
+ } ],
+ ],
+ ],
+ ] );
+ $manager = new AuthManager( new \FauxRequest, $config );
+ $provider->setManager( $manager );
+ $provider = TestingAccessWrapper::newFromObject( $provider );
+
+ $msg = wfMessage( 'foo' );
+ $skipReq = new ButtonAuthenticationRequest(
+ 'skipReset',
+ wfMessage( 'authprovider-resetpass-skip-label' ),
+ wfMessage( 'authprovider-resetpass-skip-help' )
+ );
+ $passReq = new PasswordAuthenticationRequest();
+ $passReq->action = AuthManager::ACTION_CHANGE;
+ $passReq->password = 'Foo';
+ $passReq->retype = 'Bar';
+ $passReq->allow = \StatusValue::newGood();
+ $passReq->done = false;
+
+ $passReq2 = $this->getMockBuilder( PasswordAuthenticationRequest::class )
+ ->enableProxyingToOriginalMethods()
+ ->getMock();
+ $passReq2->action = AuthManager::ACTION_CHANGE;
+ $passReq2->password = 'Foo';
+ $passReq2->retype = 'Foo';
+ $passReq2->allow = \StatusValue::newGood();
+ $passReq2->done = false;
+
+ $passReq3 = new PasswordAuthenticationRequest();
+ $passReq3->action = AuthManager::ACTION_LOGIN;
+ $passReq3->password = 'Foo';
+ $passReq3->retype = 'Foo';
+ $passReq3->allow = \StatusValue::newGood();
+ $passReq3->done = false;
+
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->tryReset( $user, [] )
+ );
+
+ $manager->setAuthenticationSessionData( 'reset-pass', 'foo' );
+ try {
+ $provider->tryReset( $user, [] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame( 'reset-pass is not valid', $ex->getMessage() );
+ }
+
+ $manager->setAuthenticationSessionData( 'reset-pass', (object)[] );
+ try {
+ $provider->tryReset( $user, [] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame( 'reset-pass msg is missing', $ex->getMessage() );
+ }
+
+ $manager->setAuthenticationSessionData( 'reset-pass', [
+ 'msg' => 'foo',
+ ] );
+ try {
+ $provider->tryReset( $user, [] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame( 'reset-pass msg is not valid', $ex->getMessage() );
+ }
+
+ $manager->setAuthenticationSessionData( 'reset-pass', [
+ 'msg' => $msg,
+ ] );
+ try {
+ $provider->tryReset( $user, [] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame( 'reset-pass hard is missing', $ex->getMessage() );
+ }
+
+ $manager->setAuthenticationSessionData( 'reset-pass', [
+ 'msg' => $msg,
+ 'hard' => true,
+ 'req' => 'foo',
+ ] );
+ try {
+ $provider->tryReset( $user, [] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame( 'reset-pass req is not valid', $ex->getMessage() );
+ }
+
+ $manager->setAuthenticationSessionData( 'reset-pass', [
+ 'msg' => $msg,
+ 'hard' => false,
+ 'req' => $passReq3,
+ ] );
+ try {
+ $provider->tryReset( $user, [ $passReq ] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame( 'reset-pass req is not valid', $ex->getMessage() );
+ }
+
+ $manager->setAuthenticationSessionData( 'reset-pass', [
+ 'msg' => $msg,
+ 'hard' => true,
+ ] );
+ $res = $provider->tryReset( $user, [] );
+ $this->assertInstanceOf( AuthenticationResponse::class, $res );
+ $this->assertSame( AuthenticationResponse::UI, $res->status );
+ $this->assertEquals( $msg, $res->message );
+ $this->assertCount( 1, $res->neededRequests );
+ $this->assertInstanceOf(
+ PasswordAuthenticationRequest::class,
+ $res->neededRequests[0]
+ );
+ $this->assertNotNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+ $this->assertFalse( $passReq->done );
+
+ $manager->setAuthenticationSessionData( 'reset-pass', [
+ 'msg' => $msg,
+ 'hard' => false,
+ 'req' => $passReq,
+ ] );
+ $res = $provider->tryReset( $user, [] );
+ $this->assertInstanceOf( AuthenticationResponse::class, $res );
+ $this->assertSame( AuthenticationResponse::UI, $res->status );
+ $this->assertEquals( $msg, $res->message );
+ $this->assertCount( 2, $res->neededRequests );
+ $expectedPassReq = clone $passReq;
+ $expectedPassReq->required = AuthenticationRequest::OPTIONAL;
+ $this->assertEquals( $expectedPassReq, $res->neededRequests[0] );
+ $this->assertEquals( $skipReq, $res->neededRequests[1] );
+ $this->assertNotNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+ $this->assertFalse( $passReq->done );
+
+ $passReq->retype = 'Bad';
+ $manager->setAuthenticationSessionData( 'reset-pass', [
+ 'msg' => $msg,
+ 'hard' => false,
+ 'req' => $passReq,
+ ] );
+ $res = $provider->tryReset( $user, [ $skipReq, $passReq ] );
+ $this->assertEquals( AuthenticationResponse::newPass(), $res );
+ $this->assertNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+ $this->assertFalse( $passReq->done );
+
+ $passReq->retype = 'Bad';
+ $manager->setAuthenticationSessionData( 'reset-pass', [
+ 'msg' => $msg,
+ 'hard' => true,
+ ] );
+ $res = $provider->tryReset( $user, [ $skipReq, $passReq ] );
+ $this->assertSame( AuthenticationResponse::UI, $res->status );
+ $this->assertSame( 'badretype', $res->message->getKey() );
+ $this->assertCount( 1, $res->neededRequests );
+ $this->assertInstanceOf(
+ PasswordAuthenticationRequest::class,
+ $res->neededRequests[0]
+ );
+ $this->assertNotNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+ $this->assertFalse( $passReq->done );
+
+ $manager->setAuthenticationSessionData( 'reset-pass', [
+ 'msg' => $msg,
+ 'hard' => true,
+ ] );
+ $res = $provider->tryReset( $user, [ $skipReq, $passReq3 ] );
+ $this->assertSame( AuthenticationResponse::UI, $res->status );
+ $this->assertEquals( $msg, $res->message );
+ $this->assertCount( 1, $res->neededRequests );
+ $this->assertInstanceOf(
+ PasswordAuthenticationRequest::class,
+ $res->neededRequests[0]
+ );
+ $this->assertNotNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+ $this->assertFalse( $passReq->done );
+
+ $passReq->retype = $passReq->password;
+ $passReq->allow = \StatusValue::newFatal( 'arbitrary-fail' );
+ $res = $provider->tryReset( $user, [ $skipReq, $passReq ] );
+ $this->assertSame( AuthenticationResponse::UI, $res->status );
+ $this->assertSame( 'arbitrary-fail', $res->message->getKey() );
+ $this->assertCount( 1, $res->neededRequests );
+ $this->assertInstanceOf(
+ PasswordAuthenticationRequest::class,
+ $res->neededRequests[0]
+ );
+ $this->assertNotNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+ $this->assertFalse( $passReq->done );
+
+ $passReq->allow = \StatusValue::newGood();
+ $res = $provider->tryReset( $user, [ $skipReq, $passReq ] );
+ $this->assertEquals( AuthenticationResponse::newPass(), $res );
+ $this->assertNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+ $this->assertTrue( $passReq->done );
+
+ $manager->setAuthenticationSessionData( 'reset-pass', [
+ 'msg' => $msg,
+ 'hard' => false,
+ 'req' => $passReq2,
+ ] );
+ $res = $provider->tryReset( $user, [ $passReq2 ] );
+ $this->assertEquals( AuthenticationResponse::newPass(), $res );
+ $this->assertNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+ $this->assertTrue( $passReq2->done );
+
+ $passReq->done = false;
+ $passReq2->done = false;
+ $manager->setAuthenticationSessionData( 'reset-pass', [
+ 'msg' => $msg,
+ 'hard' => false,
+ 'req' => $passReq2,
+ ] );
+ $res = $provider->tryReset( $user, [ $passReq ] );
+ $this->assertInstanceOf( AuthenticationResponse::class, $res );
+ $this->assertSame( AuthenticationResponse::UI, $res->status );
+ $this->assertEquals( $msg, $res->message );
+ $this->assertCount( 2, $res->neededRequests );
+ $expectedPassReq = clone $passReq2;
+ $expectedPassReq->required = AuthenticationRequest::OPTIONAL;
+ $this->assertEquals( $expectedPassReq, $res->neededRequests[0] );
+ $this->assertEquals( $skipReq, $res->neededRequests[1] );
+ $this->assertNotNull( $manager->getAuthenticationSessionData( 'reset-pass' ) );
+ $this->assertFalse( $passReq->done );
+ $this->assertFalse( $passReq2->done );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/TemporaryPasswordAuthenticationRequestTest.php b/www/wiki/tests/phpunit/includes/auth/TemporaryPasswordAuthenticationRequestTest.php
new file mode 100644
index 00000000..ab4a174e
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/TemporaryPasswordAuthenticationRequestTest.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\TemporaryPasswordAuthenticationRequest
+ */
+class TemporaryPasswordAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+ protected function getInstance( array $args = [] ) {
+ $ret = new TemporaryPasswordAuthenticationRequest;
+ $ret->action = $args[0];
+ return $ret;
+ }
+
+ public static function provideGetFieldInfo() {
+ return [
+ [ [ AuthManager::ACTION_CREATE ] ],
+ [ [ AuthManager::ACTION_CHANGE ] ],
+ [ [ AuthManager::ACTION_REMOVE ] ],
+ ];
+ }
+
+ public function testNewRandom() {
+ global $wgPasswordPolicy;
+
+ $this->stashMwGlobals( 'wgPasswordPolicy' );
+ $wgPasswordPolicy['policies']['default'] += [
+ 'MinimalPasswordLength' => 1,
+ 'MinimalPasswordLengthToLogin' => 1,
+ ];
+
+ $ret1 = TemporaryPasswordAuthenticationRequest::newRandom();
+ $ret2 = TemporaryPasswordAuthenticationRequest::newRandom();
+ $this->assertNotSame( '', $ret1->password );
+ $this->assertNotSame( '', $ret2->password );
+ $this->assertNotSame( $ret1->password, $ret2->password );
+ }
+
+ public function testNewInvalid() {
+ $ret = TemporaryPasswordAuthenticationRequest::newInvalid();
+ $this->assertNull( $ret->password );
+ }
+
+ public function provideLoadFromSubmission() {
+ return [
+ 'Empty request' => [
+ [ AuthManager::ACTION_REMOVE ],
+ [],
+ false,
+ ],
+ 'Create, empty request' => [
+ [ AuthManager::ACTION_CREATE ],
+ [],
+ false,
+ ],
+ 'Create, mailpassword set' => [
+ [ AuthManager::ACTION_CREATE ],
+ [ 'mailpassword' => 1 ],
+ [ 'mailpassword' => true, 'action' => AuthManager::ACTION_CREATE ],
+ ],
+ ];
+ }
+
+ public function testDescribeCredentials() {
+ $req = new TemporaryPasswordAuthenticationRequest;
+ $req->action = AuthManager::ACTION_LOGIN;
+ $req->username = 'UTSysop';
+ $ret = $req->describeCredentials();
+ $this->assertInternalType( 'array', $ret );
+ $this->assertArrayHasKey( 'provider', $ret );
+ $this->assertInstanceOf( \Message::class, $ret['provider'] );
+ $this->assertSame( 'authmanager-provider-temporarypassword', $ret['provider']->getKey() );
+ $this->assertArrayHasKey( 'account', $ret );
+ $this->assertInstanceOf( \Message::class, $ret['account'] );
+ $this->assertSame( [ 'UTSysop' ], $ret['account']->getParams() );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/TemporaryPasswordPrimaryAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/TemporaryPasswordPrimaryAuthenticationProviderTest.php
new file mode 100644
index 00000000..1708f1c0
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/TemporaryPasswordPrimaryAuthenticationProviderTest.php
@@ -0,0 +1,720 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use MediaWiki\MediaWikiServices;
+use Wikimedia\ScopedCallback;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group AuthManager
+ * @group Database
+ * @covers MediaWiki\Auth\TemporaryPasswordPrimaryAuthenticationProvider
+ */
+class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestCase {
+
+ private $manager = null;
+ private $config = null;
+ private $validity = null;
+
+ /**
+ * Get an instance of the provider
+ *
+ * $provider->checkPasswordValidity is mocked to return $this->validity,
+ * because we don't need to test that here.
+ *
+ * @param array $params
+ * @return TemporaryPasswordPrimaryAuthenticationProvider
+ */
+ protected function getProvider( $params = [] ) {
+ if ( !$this->config ) {
+ $this->config = new \HashConfig( [
+ 'EmailEnabled' => true,
+ ] );
+ }
+ $config = new \MultiConfig( [
+ $this->config,
+ MediaWikiServices::getInstance()->getMainConfig()
+ ] );
+
+ if ( !$this->manager ) {
+ $this->manager = new AuthManager( new \FauxRequest(), $config );
+ }
+ $this->validity = \Status::newGood();
+
+ $mockedMethods[] = 'checkPasswordValidity';
+ $provider = $this->getMockBuilder( TemporaryPasswordPrimaryAuthenticationProvider::class )
+ ->setMethods( $mockedMethods )
+ ->setConstructorArgs( [ $params ] )
+ ->getMock();
+ $provider->expects( $this->any() )->method( 'checkPasswordValidity' )
+ ->will( $this->returnCallback( function () {
+ return $this->validity;
+ } ) );
+ $provider->setConfig( $config );
+ $provider->setLogger( new \Psr\Log\NullLogger() );
+ $provider->setManager( $this->manager );
+
+ return $provider;
+ }
+
+ protected function hookMailer( $func = null ) {
+ \Hooks::clear( 'AlternateUserMailer' );
+ if ( $func ) {
+ \Hooks::register( 'AlternateUserMailer', $func );
+ // Safety
+ \Hooks::register( 'AlternateUserMailer', function () {
+ return false;
+ } );
+ } else {
+ \Hooks::register( 'AlternateUserMailer', function () {
+ $this->fail( 'AlternateUserMailer hook called unexpectedly' );
+ return false;
+ } );
+ }
+
+ return new ScopedCallback( function () {
+ \Hooks::clear( 'AlternateUserMailer' );
+ \Hooks::register( 'AlternateUserMailer', function () {
+ return false;
+ } );
+ } );
+ }
+
+ public function testBasics() {
+ $provider = new TemporaryPasswordPrimaryAuthenticationProvider();
+
+ $this->assertSame(
+ PrimaryAuthenticationProvider::TYPE_CREATE,
+ $provider->accountCreationType()
+ );
+
+ $this->assertTrue( $provider->testUserExists( 'UTSysop' ) );
+ $this->assertTrue( $provider->testUserExists( 'uTSysop' ) );
+ $this->assertFalse( $provider->testUserExists( 'DoesNotExist' ) );
+ $this->assertFalse( $provider->testUserExists( '<invalid>' ) );
+
+ $req = new PasswordAuthenticationRequest;
+ $req->action = AuthManager::ACTION_CHANGE;
+ $req->username = '<invalid>';
+ $provider->providerChangeAuthenticationData( $req );
+ }
+
+ public function testConfig() {
+ $config = new \HashConfig( [
+ 'EnableEmail' => false,
+ 'NewPasswordExpiry' => 100,
+ 'PasswordReminderResendTime' => 101,
+ ] );
+
+ $p = TestingAccessWrapper::newFromObject( new TemporaryPasswordPrimaryAuthenticationProvider() );
+ $p->setConfig( $config );
+ $this->assertSame( false, $p->emailEnabled );
+ $this->assertSame( 100, $p->newPasswordExpiry );
+ $this->assertSame( 101, $p->passwordReminderResendTime );
+
+ $p = TestingAccessWrapper::newFromObject( new TemporaryPasswordPrimaryAuthenticationProvider( [
+ 'emailEnabled' => true,
+ 'newPasswordExpiry' => 42,
+ 'passwordReminderResendTime' => 43,
+ ] ) );
+ $p->setConfig( $config );
+ $this->assertSame( true, $p->emailEnabled );
+ $this->assertSame( 42, $p->newPasswordExpiry );
+ $this->assertSame( 43, $p->passwordReminderResendTime );
+ }
+
+ public function testTestUserCanAuthenticate() {
+ $user = self::getMutableTestUser()->getUser();
+
+ $dbw = wfGetDB( DB_MASTER );
+
+ $passwordFactory = new \PasswordFactory();
+ $passwordFactory->init( \RequestContext::getMain()->getConfig() );
+ // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
+ $passwordFactory->setDefaultType( 'A' );
+ $pwhash = $passwordFactory->newFromPlaintext( 'password' )->toString();
+
+ $provider = $this->getProvider();
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+
+ $this->assertFalse( $provider->testUserCanAuthenticate( '<invalid>' ) );
+ $this->assertFalse( $provider->testUserCanAuthenticate( 'DoesNotExist' ) );
+
+ $dbw->update(
+ 'user',
+ [
+ 'user_newpassword' => \PasswordFactory::newInvalidPassword()->toString(),
+ 'user_newpass_time' => null,
+ ],
+ [ 'user_id' => $user->getId() ]
+ );
+ $this->assertFalse( $provider->testUserCanAuthenticate( $user->getName() ) );
+
+ $dbw->update(
+ 'user',
+ [
+ 'user_newpassword' => $pwhash,
+ 'user_newpass_time' => null,
+ ],
+ [ 'user_id' => $user->getId() ]
+ );
+ $this->assertTrue( $provider->testUserCanAuthenticate( $user->getName() ) );
+ $this->assertTrue( $provider->testUserCanAuthenticate( lcfirst( $user->getName() ) ) );
+
+ $dbw->update(
+ 'user',
+ [
+ 'user_newpassword' => $pwhash,
+ 'user_newpass_time' => $dbw->timestamp( time() - 10 ),
+ ],
+ [ 'user_id' => $user->getId() ]
+ );
+ $providerPriv->newPasswordExpiry = 100;
+ $this->assertTrue( $provider->testUserCanAuthenticate( $user->getName() ) );
+ $providerPriv->newPasswordExpiry = 1;
+ $this->assertFalse( $provider->testUserCanAuthenticate( $user->getName() ) );
+
+ $dbw->update(
+ 'user',
+ [
+ 'user_newpassword' => \PasswordFactory::newInvalidPassword()->toString(),
+ 'user_newpass_time' => null,
+ ],
+ [ 'user_id' => $user->getId() ]
+ );
+ }
+
+ /**
+ * @dataProvider provideGetAuthenticationRequests
+ * @param string $action
+ * @param array $options
+ * @param array $expected
+ */
+ public function testGetAuthenticationRequests( $action, $options, $expected ) {
+ $actual = $this->getProvider()->getAuthenticationRequests( $action, $options );
+ foreach ( $actual as $req ) {
+ if ( $req instanceof TemporaryPasswordAuthenticationRequest && $req->password !== null ) {
+ $req->password = 'random';
+ }
+ }
+ $this->assertEquals( $expected, $actual );
+ }
+
+ public static function provideGetAuthenticationRequests() {
+ $anon = [ 'username' => null ];
+ $loggedIn = [ 'username' => 'UTSysop' ];
+
+ return [
+ [ AuthManager::ACTION_LOGIN, $anon, [
+ new PasswordAuthenticationRequest
+ ] ],
+ [ AuthManager::ACTION_LOGIN, $loggedIn, [
+ new PasswordAuthenticationRequest
+ ] ],
+ [ AuthManager::ACTION_CREATE, $anon, [] ],
+ [ AuthManager::ACTION_CREATE, $loggedIn, [
+ new TemporaryPasswordAuthenticationRequest( 'random' )
+ ] ],
+ [ AuthManager::ACTION_LINK, $anon, [] ],
+ [ AuthManager::ACTION_LINK, $loggedIn, [] ],
+ [ AuthManager::ACTION_CHANGE, $anon, [
+ new TemporaryPasswordAuthenticationRequest( 'random' )
+ ] ],
+ [ AuthManager::ACTION_CHANGE, $loggedIn, [
+ new TemporaryPasswordAuthenticationRequest( 'random' )
+ ] ],
+ [ AuthManager::ACTION_REMOVE, $anon, [
+ new TemporaryPasswordAuthenticationRequest
+ ] ],
+ [ AuthManager::ACTION_REMOVE, $loggedIn, [
+ new TemporaryPasswordAuthenticationRequest
+ ] ],
+ ];
+ }
+
+ public function testAuthentication() {
+ $user = self::getMutableTestUser()->getUser();
+
+ $password = 'TemporaryPassword';
+ $hash = ':A:' . md5( $password );
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->update(
+ 'user',
+ [ 'user_newpassword' => $hash, 'user_newpass_time' => $dbw->timestamp( time() - 10 ) ],
+ [ 'user_id' => $user->getId() ]
+ );
+
+ $req = new PasswordAuthenticationRequest();
+ $req->action = AuthManager::ACTION_LOGIN;
+ $reqs = [ PasswordAuthenticationRequest::class => $req ];
+
+ $provider = $this->getProvider();
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+
+ $providerPriv->newPasswordExpiry = 100;
+
+ // General failures
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAuthentication( [] )
+ );
+
+ $req->username = 'foo';
+ $req->password = null;
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+
+ $req->username = null;
+ $req->password = 'bar';
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+
+ $req->username = '<invalid>';
+ $req->password = 'WhoCares';
+ $ret = $provider->beginPrimaryAuthentication( $reqs );
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+
+ $req->username = 'DoesNotExist';
+ $req->password = 'DoesNotExist';
+ $ret = $provider->beginPrimaryAuthentication( $reqs );
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+
+ // Validation failure
+ $req->username = $user->getName();
+ $req->password = $password;
+ $this->validity = \Status::newFatal( 'arbitrary-failure' );
+ $ret = $provider->beginPrimaryAuthentication( $reqs );
+ $this->assertEquals(
+ AuthenticationResponse::FAIL,
+ $ret->status
+ );
+ $this->assertEquals(
+ 'arbitrary-failure',
+ $ret->message->getKey()
+ );
+
+ // Successful auth
+ $this->manager->removeAuthenticationSessionData( null );
+ $this->validity = \Status::newGood();
+ $this->assertEquals(
+ AuthenticationResponse::newPass( $user->getName() ),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+ $this->assertNotNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
+
+ $this->manager->removeAuthenticationSessionData( null );
+ $this->validity = \Status::newGood();
+ $req->username = lcfirst( $user->getName() );
+ $this->assertEquals(
+ AuthenticationResponse::newPass( $user->getName() ),
+ $provider->beginPrimaryAuthentication( $reqs )
+ );
+ $this->assertNotNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
+ $req->username = $user->getName();
+
+ // Expired password
+ $providerPriv->newPasswordExpiry = 1;
+ $ret = $provider->beginPrimaryAuthentication( $reqs );
+ $this->assertEquals(
+ AuthenticationResponse::FAIL,
+ $ret->status
+ );
+ $this->assertEquals(
+ 'wrongpassword',
+ $ret->message->getKey()
+ );
+
+ // Bad password
+ $providerPriv->newPasswordExpiry = 100;
+ $this->validity = \Status::newGood();
+ $req->password = 'Wrong';
+ $ret = $provider->beginPrimaryAuthentication( $reqs );
+ $this->assertEquals(
+ AuthenticationResponse::FAIL,
+ $ret->status
+ );
+ $this->assertEquals(
+ 'wrongpassword',
+ $ret->message->getKey()
+ );
+ }
+
+ /**
+ * @dataProvider provideProviderAllowsAuthenticationDataChange
+ * @param string $type
+ * @param string $user
+ * @param \Status $validity Result of the password validity check
+ * @param \StatusValue $expect1 Expected result with $checkData = false
+ * @param \StatusValue $expect2 Expected result with $checkData = true
+ */
+ public function testProviderAllowsAuthenticationDataChange( $type, $user, \Status $validity,
+ \StatusValue $expect1, \StatusValue $expect2
+ ) {
+ if ( $type === PasswordAuthenticationRequest::class ||
+ $type === TemporaryPasswordAuthenticationRequest::class
+ ) {
+ $req = new $type();
+ } else {
+ $req = $this->createMock( $type );
+ }
+ $req->action = AuthManager::ACTION_CHANGE;
+ $req->username = $user;
+ $req->password = 'NewPassword';
+
+ $provider = $this->getProvider();
+ $this->validity = $validity;
+ $this->assertEquals( $expect1, $provider->providerAllowsAuthenticationDataChange( $req, false ) );
+ $this->assertEquals( $expect2, $provider->providerAllowsAuthenticationDataChange( $req, true ) );
+ }
+
+ public static function provideProviderAllowsAuthenticationDataChange() {
+ $err = \StatusValue::newGood();
+ $err->error( 'arbitrary-warning' );
+
+ return [
+ [ AuthenticationRequest::class, 'UTSysop', \Status::newGood(),
+ \StatusValue::newGood( 'ignored' ), \StatusValue::newGood( 'ignored' ) ],
+ [ PasswordAuthenticationRequest::class, 'UTSysop', \Status::newGood(),
+ \StatusValue::newGood( 'ignored' ), \StatusValue::newGood( 'ignored' ) ],
+ [ TemporaryPasswordAuthenticationRequest::class, 'UTSysop', \Status::newGood(),
+ \StatusValue::newGood(), \StatusValue::newGood() ],
+ [ TemporaryPasswordAuthenticationRequest::class, 'uTSysop', \Status::newGood(),
+ \StatusValue::newGood(), \StatusValue::newGood() ],
+ [ TemporaryPasswordAuthenticationRequest::class, 'UTSysop', \Status::wrap( $err ),
+ \StatusValue::newGood(), $err ],
+ [ TemporaryPasswordAuthenticationRequest::class, 'UTSysop',
+ \Status::newFatal( 'arbitrary-error' ), \StatusValue::newGood(),
+ \StatusValue::newFatal( 'arbitrary-error' ) ],
+ [ TemporaryPasswordAuthenticationRequest::class, 'DoesNotExist', \Status::newGood(),
+ \StatusValue::newGood(), \StatusValue::newGood( 'ignored' ) ],
+ [ TemporaryPasswordAuthenticationRequest::class, '<invalid>', \Status::newGood(),
+ \StatusValue::newGood(), \StatusValue::newGood( 'ignored' ) ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideProviderChangeAuthenticationData
+ * @param string $user
+ * @param string $type
+ * @param bool $changed
+ */
+ public function testProviderChangeAuthenticationData( $user, $type, $changed ) {
+ $cuser = ucfirst( $user );
+ $oldpass = 'OldTempPassword';
+ $newpass = 'NewTempPassword';
+
+ $dbw = wfGetDB( DB_MASTER );
+ $oldHash = $dbw->selectField( 'user', 'user_newpassword', [ 'user_name' => $cuser ] );
+ $cb = new ScopedCallback( function () use ( $dbw, $cuser, $oldHash ) {
+ $dbw->update( 'user', [ 'user_newpassword' => $oldHash ], [ 'user_name' => $cuser ] );
+ } );
+
+ $hash = ':A:' . md5( $oldpass );
+ $dbw->update(
+ 'user',
+ [ 'user_newpassword' => $hash, 'user_newpass_time' => $dbw->timestamp( time() + 10 ) ],
+ [ 'user_name' => $cuser ]
+ );
+
+ $provider = $this->getProvider();
+
+ // Sanity check
+ $loginReq = new PasswordAuthenticationRequest();
+ $loginReq->action = AuthManager::ACTION_CHANGE;
+ $loginReq->username = $user;
+ $loginReq->password = $oldpass;
+ $loginReqs = [ PasswordAuthenticationRequest::class => $loginReq ];
+ $this->assertEquals(
+ AuthenticationResponse::newPass( $cuser ),
+ $provider->beginPrimaryAuthentication( $loginReqs ),
+ 'Sanity check'
+ );
+
+ if ( $type === PasswordAuthenticationRequest::class ||
+ $type === TemporaryPasswordAuthenticationRequest::class
+ ) {
+ $changeReq = new $type();
+ } else {
+ $changeReq = $this->createMock( $type );
+ }
+ $changeReq->action = AuthManager::ACTION_CHANGE;
+ $changeReq->username = $user;
+ $changeReq->password = $newpass;
+ $resetMailer = $this->hookMailer();
+ $provider->providerChangeAuthenticationData( $changeReq );
+ ScopedCallback::consume( $resetMailer );
+
+ $loginReq->password = $oldpass;
+ $ret = $provider->beginPrimaryAuthentication( $loginReqs );
+ $this->assertEquals(
+ AuthenticationResponse::FAIL,
+ $ret->status,
+ 'old password should fail'
+ );
+ $this->assertEquals(
+ 'wrongpassword',
+ $ret->message->getKey(),
+ 'old password should fail'
+ );
+
+ $loginReq->password = $newpass;
+ $ret = $provider->beginPrimaryAuthentication( $loginReqs );
+ if ( $changed ) {
+ $this->assertEquals(
+ AuthenticationResponse::newPass( $cuser ),
+ $ret,
+ 'new password should pass'
+ );
+ $this->assertNotNull(
+ $dbw->selectField( 'user', 'user_newpass_time', [ 'user_name' => $cuser ] )
+ );
+ } else {
+ $this->assertEquals(
+ AuthenticationResponse::FAIL,
+ $ret->status,
+ 'new password should fail'
+ );
+ $this->assertEquals(
+ 'wrongpassword',
+ $ret->message->getKey(),
+ 'new password should fail'
+ );
+ $this->assertNull(
+ $dbw->selectField( 'user', 'user_newpass_time', [ 'user_name' => $cuser ] )
+ );
+ }
+ }
+
+ public static function provideProviderChangeAuthenticationData() {
+ return [
+ [ 'UTSysop', AuthenticationRequest::class, false ],
+ [ 'UTSysop', PasswordAuthenticationRequest::class, false ],
+ [ 'UTSysop', TemporaryPasswordAuthenticationRequest::class, true ],
+ ];
+ }
+
+ public function testProviderChangeAuthenticationDataEmail() {
+ $user = self::getMutableTestUser()->getUser();
+
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->update(
+ 'user',
+ [ 'user_newpass_time' => $dbw->timestamp( time() - 5 * 3600 ) ],
+ [ 'user_id' => $user->getId() ]
+ );
+
+ $req = TemporaryPasswordAuthenticationRequest::newRandom();
+ $req->username = $user->getName();
+ $req->mailpassword = true;
+
+ $provider = $this->getProvider( [ 'emailEnabled' => false ] );
+ $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+ $this->assertEquals( \StatusValue::newFatal( 'passwordreset-emaildisabled' ), $status );
+
+ $provider = $this->getProvider( [ 'passwordReminderResendTime' => 10 ] );
+ $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+ $this->assertEquals( \StatusValue::newFatal( 'throttled-mailpassword', 10 ), $status );
+
+ $provider = $this->getProvider( [ 'passwordReminderResendTime' => 3 ] );
+ $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+ $this->assertFalse( $status->hasMessage( 'throttled-mailpassword' ) );
+
+ $dbw->update(
+ 'user',
+ [ 'user_newpass_time' => $dbw->timestamp( time() + 5 * 3600 ) ],
+ [ 'user_id' => $user->getId() ]
+ );
+ $provider = $this->getProvider( [ 'passwordReminderResendTime' => 0 ] );
+ $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+ $this->assertFalse( $status->hasMessage( 'throttled-mailpassword' ) );
+
+ $req->caller = null;
+ $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+ $this->assertEquals( \StatusValue::newFatal( 'passwordreset-nocaller' ), $status );
+
+ $req->caller = '127.0.0.256';
+ $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+ $this->assertEquals( \StatusValue::newFatal( 'passwordreset-nosuchcaller', '127.0.0.256' ),
+ $status );
+
+ $req->caller = '<Invalid>';
+ $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+ $this->assertEquals( \StatusValue::newFatal( 'passwordreset-nosuchcaller', '<Invalid>' ),
+ $status );
+
+ $req->caller = '127.0.0.1';
+ $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+ $this->assertEquals( \StatusValue::newGood(), $status );
+
+ $req->caller = $user->getName();
+ $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
+ $this->assertEquals( \StatusValue::newGood(), $status );
+
+ $mailed = false;
+ $resetMailer = $this->hookMailer( function ( $headers, $to, $from, $subject, $body )
+ use ( &$mailed, $req, $user )
+ {
+ $mailed = true;
+ $this->assertSame( $user->getEmail(), $to[0]->address );
+ $this->assertContains( $req->password, $body );
+ return false;
+ } );
+ $provider->providerChangeAuthenticationData( $req );
+ ScopedCallback::consume( $resetMailer );
+ $this->assertTrue( $mailed );
+
+ $priv = TestingAccessWrapper::newFromObject( $provider );
+ $req->username = '<invalid>';
+ $status = $priv->sendPasswordResetEmail( $req );
+ $this->assertEquals( \Status::newFatal( 'noname' ), $status );
+ }
+
+ public function testTestForAccountCreation() {
+ $user = \User::newFromName( 'foo' );
+ $req = new TemporaryPasswordAuthenticationRequest();
+ $req->username = 'Foo';
+ $req->password = 'Bar';
+ $reqs = [ TemporaryPasswordAuthenticationRequest::class => $req ];
+
+ $provider = $this->getProvider();
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testForAccountCreation( $user, $user, [] ),
+ 'No password request'
+ );
+
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testForAccountCreation( $user, $user, $reqs ),
+ 'Password request, validated'
+ );
+
+ $this->validity->error( 'arbitrary warning' );
+ $expect = \StatusValue::newGood();
+ $expect->error( 'arbitrary warning' );
+ $this->assertEquals(
+ $expect,
+ $provider->testForAccountCreation( $user, $user, $reqs ),
+ 'Password request, not validated'
+ );
+ }
+
+ public function testAccountCreation() {
+ $resetMailer = $this->hookMailer();
+
+ $user = \User::newFromName( 'Foo' );
+
+ $req = new TemporaryPasswordAuthenticationRequest();
+ $reqs = [ TemporaryPasswordAuthenticationRequest::class => $req ];
+
+ $authreq = new PasswordAuthenticationRequest();
+ $authreq->action = AuthManager::ACTION_CREATE;
+ $authreqs = [ PasswordAuthenticationRequest::class => $authreq ];
+
+ $provider = $this->getProvider();
+
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAccountCreation( $user, $user, [] )
+ );
+
+ $req->username = 'foo';
+ $req->password = null;
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
+ );
+
+ $req->username = null;
+ $req->password = 'bar';
+ $this->assertEquals(
+ AuthenticationResponse::newAbstain(),
+ $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
+ );
+
+ $req->username = 'foo';
+ $req->password = 'bar';
+
+ $expect = AuthenticationResponse::newPass( 'Foo' );
+ $expect->createRequest = clone $req;
+ $expect->createRequest->username = 'Foo';
+ $this->assertEquals( $expect, $provider->beginPrimaryAccountCreation( $user, $user, $reqs ) );
+ $this->assertNull( $this->manager->getAuthenticationSessionData( 'no-email' ) );
+
+ $user = self::getMutableTestUser()->getUser();
+ $req->username = $authreq->username = $user->getName();
+ $req->password = $authreq->password = 'NewPassword';
+ $expect = AuthenticationResponse::newPass( $user->getName() );
+ $expect->createRequest = $req;
+
+ $res2 = $provider->beginPrimaryAccountCreation( $user, $user, $reqs );
+ $this->assertEquals( $expect, $res2, 'Sanity check' );
+
+ $ret = $provider->beginPrimaryAuthentication( $authreqs );
+ $this->assertEquals( AuthenticationResponse::FAIL, $ret->status, 'sanity check' );
+
+ $this->assertSame( null, $provider->finishAccountCreation( $user, $user, $res2 ) );
+
+ $ret = $provider->beginPrimaryAuthentication( $authreqs );
+ $this->assertEquals( AuthenticationResponse::PASS, $ret->status, 'new password is set' );
+ }
+
+ public function testAccountCreationEmail() {
+ $creator = \User::newFromName( 'Foo' );
+
+ $user = self::getMutableTestUser()->getUser();
+ $user->setEmail( null );
+
+ $req = TemporaryPasswordAuthenticationRequest::newRandom();
+ $req->username = $user->getName();
+ $req->mailpassword = true;
+
+ $provider = $this->getProvider( [ 'emailEnabled' => false ] );
+ $status = $provider->testForAccountCreation( $user, $creator, [ $req ] );
+ $this->assertEquals( \StatusValue::newFatal( 'emaildisabled' ), $status );
+
+ $provider = $this->getProvider( [ 'emailEnabled' => true ] );
+ $status = $provider->testForAccountCreation( $user, $creator, [ $req ] );
+ $this->assertEquals( \StatusValue::newFatal( 'noemailcreate' ), $status );
+
+ $user->setEmail( 'test@localhost.localdomain' );
+ $status = $provider->testForAccountCreation( $user, $creator, [ $req ] );
+ $this->assertEquals( \StatusValue::newGood(), $status );
+
+ $mailed = false;
+ $resetMailer = $this->hookMailer( function ( $headers, $to, $from, $subject, $body )
+ use ( &$mailed, $req )
+ {
+ $mailed = true;
+ $this->assertSame( 'test@localhost.localdomain', $to[0]->address );
+ $this->assertContains( $req->password, $body );
+ return false;
+ } );
+
+ $expect = AuthenticationResponse::newPass( $user->getName() );
+ $expect->createRequest = clone $req;
+ $expect->createRequest->username = $user->getName();
+ $res = $provider->beginPrimaryAccountCreation( $user, $creator, [ $req ] );
+ $this->assertEquals( $expect, $res );
+ $this->assertTrue( $this->manager->getAuthenticationSessionData( 'no-email' ) );
+ $this->assertFalse( $mailed );
+
+ $this->assertSame( 'byemail', $provider->finishAccountCreation( $user, $creator, $res ) );
+ $this->assertTrue( $mailed );
+
+ ScopedCallback::consume( $resetMailer );
+ $this->assertTrue( $mailed );
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/ThrottlePreAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/ThrottlePreAuthenticationProviderTest.php
new file mode 100644
index 00000000..d03b1515
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/ThrottlePreAuthenticationProviderTest.php
@@ -0,0 +1,236 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group AuthManager
+ * @group Database
+ * @covers MediaWiki\Auth\ThrottlePreAuthenticationProvider
+ */
+class ThrottlePreAuthenticationProviderTest extends \MediaWikiTestCase {
+ public function testConstructor() {
+ $provider = new ThrottlePreAuthenticationProvider();
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+ $config = new \HashConfig( [
+ 'AccountCreationThrottle' => [ [
+ 'count' => 123,
+ 'seconds' => 86400,
+ ] ],
+ 'PasswordAttemptThrottle' => [ [
+ 'count' => 5,
+ 'seconds' => 300,
+ ] ],
+ ] );
+ $provider->setConfig( $config );
+ $this->assertSame( [
+ 'accountCreationThrottle' => [ [ 'count' => 123, 'seconds' => 86400 ] ],
+ 'passwordAttemptThrottle' => [ [ 'count' => 5, 'seconds' => 300 ] ]
+ ], $providerPriv->throttleSettings );
+ $accountCreationThrottle = TestingAccessWrapper::newFromObject(
+ $providerPriv->accountCreationThrottle );
+ $this->assertSame( [ [ 'count' => 123, 'seconds' => 86400 ] ],
+ $accountCreationThrottle->conditions );
+ $passwordAttemptThrottle = TestingAccessWrapper::newFromObject(
+ $providerPriv->passwordAttemptThrottle );
+ $this->assertSame( [ [ 'count' => 5, 'seconds' => 300 ] ],
+ $passwordAttemptThrottle->conditions );
+
+ $provider = new ThrottlePreAuthenticationProvider( [
+ 'accountCreationThrottle' => [ [ 'count' => 43, 'seconds' => 10000 ] ],
+ 'passwordAttemptThrottle' => [ [ 'count' => 11, 'seconds' => 100 ] ],
+ ] );
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+ $config = new \HashConfig( [
+ 'AccountCreationThrottle' => [ [
+ 'count' => 123,
+ 'seconds' => 86400,
+ ] ],
+ 'PasswordAttemptThrottle' => [ [
+ 'count' => 5,
+ 'seconds' => 300,
+ ] ],
+ ] );
+ $provider->setConfig( $config );
+ $this->assertSame( [
+ 'accountCreationThrottle' => [ [ 'count' => 43, 'seconds' => 10000 ] ],
+ 'passwordAttemptThrottle' => [ [ 'count' => 11, 'seconds' => 100 ] ],
+ ], $providerPriv->throttleSettings );
+
+ $cache = new \HashBagOStuff();
+ $provider = new ThrottlePreAuthenticationProvider( [ 'cache' => $cache ] );
+ $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+ $provider->setConfig( new \HashConfig( [
+ 'AccountCreationThrottle' => [ [ 'count' => 1, 'seconds' => 1 ] ],
+ 'PasswordAttemptThrottle' => [ [ 'count' => 1, 'seconds' => 1 ] ],
+ ] ) );
+ $accountCreationThrottle = TestingAccessWrapper::newFromObject(
+ $providerPriv->accountCreationThrottle );
+ $this->assertSame( $cache, $accountCreationThrottle->cache );
+ $passwordAttemptThrottle = TestingAccessWrapper::newFromObject(
+ $providerPriv->passwordAttemptThrottle );
+ $this->assertSame( $cache, $passwordAttemptThrottle->cache );
+ }
+
+ public function testDisabled() {
+ $provider = new ThrottlePreAuthenticationProvider( [
+ 'accountCreationThrottle' => [],
+ 'passwordAttemptThrottle' => [],
+ 'cache' => new \HashBagOStuff(),
+ ] );
+ $provider->setLogger( new \Psr\Log\NullLogger() );
+ $provider->setConfig( new \HashConfig( [
+ 'AccountCreationThrottle' => null,
+ 'PasswordAttemptThrottle' => null,
+ ] ) );
+ $provider->setManager( AuthManager::singleton() );
+
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testForAccountCreation(
+ \User::newFromName( 'Created' ),
+ \User::newFromName( 'Creator' ),
+ []
+ )
+ );
+ $this->assertEquals(
+ \StatusValue::newGood(),
+ $provider->testForAuthentication( [] )
+ );
+ }
+
+ /**
+ * @dataProvider provideTestForAccountCreation
+ * @param string $creatorname
+ * @param bool $succeed
+ * @param bool $hook
+ */
+ public function testTestForAccountCreation( $creatorname, $succeed, $hook ) {
+ $provider = new ThrottlePreAuthenticationProvider( [
+ 'accountCreationThrottle' => [ [ 'count' => 2, 'seconds' => 86400 ] ],
+ 'cache' => new \HashBagOStuff(),
+ ] );
+ $provider->setLogger( new \Psr\Log\NullLogger() );
+ $provider->setConfig( new \HashConfig( [
+ 'AccountCreationThrottle' => null,
+ 'PasswordAttemptThrottle' => null,
+ ] ) );
+ $provider->setManager( AuthManager::singleton() );
+
+ $user = \User::newFromName( 'RandomUser' );
+ $creator = \User::newFromName( $creatorname );
+ if ( $hook ) {
+ $mock = $this->getMockBuilder( stdClass::class )
+ ->setMethods( [ 'onExemptFromAccountCreationThrottle' ] )
+ ->getMock();
+ $mock->expects( $this->any() )->method( 'onExemptFromAccountCreationThrottle' )
+ ->will( $this->returnValue( false ) );
+ $this->mergeMwGlobalArrayValue( 'wgHooks', [
+ 'ExemptFromAccountCreationThrottle' => [ $mock ],
+ ] );
+ }
+
+ $this->assertEquals(
+ true,
+ $provider->testForAccountCreation( $user, $creator, [] )->isOK(),
+ 'attempt #1'
+ );
+ $this->assertEquals(
+ true,
+ $provider->testForAccountCreation( $user, $creator, [] )->isOK(),
+ 'attempt #2'
+ );
+ $this->assertEquals(
+ $succeed ? true : false,
+ $provider->testForAccountCreation( $user, $creator, [] )->isOK(),
+ 'attempt #3'
+ );
+ }
+
+ public static function provideTestForAccountCreation() {
+ return [
+ 'Normal user' => [ 'NormalUser', false, false ],
+ 'Sysop' => [ 'UTSysop', true, false ],
+ 'Normal user with hook' => [ 'NormalUser', true, true ],
+ ];
+ }
+
+ public function testTestForAuthentication() {
+ $provider = new ThrottlePreAuthenticationProvider( [
+ 'passwordAttemptThrottle' => [ [ 'count' => 2, 'seconds' => 86400 ] ],
+ 'cache' => new \HashBagOStuff(),
+ ] );
+ $provider->setLogger( new \Psr\Log\NullLogger() );
+ $provider->setConfig( new \HashConfig( [
+ 'AccountCreationThrottle' => null,
+ 'PasswordAttemptThrottle' => null,
+ ] ) );
+ $provider->setManager( AuthManager::singleton() );
+
+ $req = new UsernameAuthenticationRequest;
+ $req->username = 'SomeUser';
+ for ( $i = 1; $i <= 3; $i++ ) {
+ $status = $provider->testForAuthentication( [ $req ] );
+ $this->assertEquals( $i < 3, $status->isGood(), "attempt #$i" );
+ }
+ $this->assertCount( 1, $status->getErrors() );
+ $msg = new \Message( $status->getErrors()[0]['message'], $status->getErrors()[0]['params'] );
+ $this->assertEquals( 'login-throttled', $msg->getKey() );
+
+ $provider->postAuthentication( \User::newFromName( 'SomeUser' ),
+ AuthenticationResponse::newFail( wfMessage( 'foo' ) ) );
+ $this->assertFalse( $provider->testForAuthentication( [ $req ] )->isGood(), 'after FAIL' );
+
+ $provider->postAuthentication( \User::newFromName( 'SomeUser' ),
+ AuthenticationResponse::newPass() );
+ $this->assertTrue( $provider->testForAuthentication( [ $req ] )->isGood(), 'after PASS' );
+
+ $req1 = new UsernameAuthenticationRequest;
+ $req1->username = 'foo';
+ $req2 = new UsernameAuthenticationRequest;
+ $req2->username = 'bar';
+ $this->assertTrue( $provider->testForAuthentication( [ $req1, $req2 ] )->isGood() );
+
+ $req = new UsernameAuthenticationRequest;
+ $req->username = 'Some user';
+ $provider->testForAuthentication( [ $req ] );
+ $req->username = 'Some_user';
+ $provider->testForAuthentication( [ $req ] );
+ $req->username = 'some user';
+ $status = $provider->testForAuthentication( [ $req ] );
+ $this->assertFalse( $status->isGood(), 'denormalized usernames are normalized' );
+ }
+
+ public function testPostAuthentication() {
+ $provider = new ThrottlePreAuthenticationProvider( [
+ 'passwordAttemptThrottle' => [],
+ 'cache' => new \HashBagOStuff(),
+ ] );
+ $provider->setLogger( new \TestLogger );
+ $provider->setConfig( new \HashConfig( [
+ 'AccountCreationThrottle' => null,
+ 'PasswordAttemptThrottle' => null,
+ ] ) );
+ $provider->setManager( AuthManager::singleton() );
+ $provider->postAuthentication( \User::newFromName( 'SomeUser' ),
+ AuthenticationResponse::newPass() );
+
+ $provider = new ThrottlePreAuthenticationProvider( [
+ 'passwordAttemptThrottle' => [ [ 'count' => 2, 'seconds' => 86400 ] ],
+ 'cache' => new \HashBagOStuff(),
+ ] );
+ $logger = new \TestLogger( true );
+ $provider->setLogger( $logger );
+ $provider->setConfig( new \HashConfig( [
+ 'AccountCreationThrottle' => null,
+ 'PasswordAttemptThrottle' => null,
+ ] ) );
+ $provider->setManager( AuthManager::singleton() );
+ $provider->postAuthentication( \User::newFromName( 'SomeUser' ),
+ AuthenticationResponse::newPass() );
+ $this->assertSame( [
+ [ \Psr\Log\LogLevel::INFO, 'throttler data not found for {user}' ],
+ ], $logger->getBuffer() );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/ThrottlerTest.php b/www/wiki/tests/phpunit/includes/auth/ThrottlerTest.php
new file mode 100644
index 00000000..f963ad9c
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/ThrottlerTest.php
@@ -0,0 +1,238 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use BagOStuff;
+use HashBagOStuff;
+use Psr\Log\AbstractLogger;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\Throttler
+ */
+class ThrottlerTest extends \MediaWikiTestCase {
+ public function testConstructor() {
+ $cache = new \HashBagOStuff();
+ $logger = $this->getMockBuilder( AbstractLogger::class )
+ ->setMethods( [ 'log' ] )
+ ->getMockForAbstractClass();
+
+ $throttler = new Throttler(
+ [ [ 'count' => 123, 'seconds' => 456 ] ],
+ [ 'type' => 'foo', 'cache' => $cache ]
+ );
+ $throttler->setLogger( $logger );
+ $throttlerPriv = TestingAccessWrapper::newFromObject( $throttler );
+ $this->assertSame( [ [ 'count' => 123, 'seconds' => 456 ] ], $throttlerPriv->conditions );
+ $this->assertSame( 'foo', $throttlerPriv->type );
+ $this->assertSame( $cache, $throttlerPriv->cache );
+ $this->assertSame( $logger, $throttlerPriv->logger );
+
+ $throttler = new Throttler( [ [ 'count' => 123, 'seconds' => 456 ] ] );
+ $throttler->setLogger( new NullLogger() );
+ $throttlerPriv = TestingAccessWrapper::newFromObject( $throttler );
+ $this->assertSame( [ [ 'count' => 123, 'seconds' => 456 ] ], $throttlerPriv->conditions );
+ $this->assertSame( 'custom', $throttlerPriv->type );
+ $this->assertInstanceOf( BagOStuff::class, $throttlerPriv->cache );
+ $this->assertInstanceOf( LoggerInterface::class, $throttlerPriv->logger );
+
+ $this->setMwGlobals( [ 'wgPasswordAttemptThrottle' => [ [ 'count' => 321,
+ 'seconds' => 654 ] ] ] );
+ $throttler = new Throttler();
+ $throttler->setLogger( new NullLogger() );
+ $throttlerPriv = TestingAccessWrapper::newFromObject( $throttler );
+ $this->assertSame( [ [ 'count' => 321, 'seconds' => 654 ] ], $throttlerPriv->conditions );
+ $this->assertSame( 'password', $throttlerPriv->type );
+ $this->assertInstanceOf( BagOStuff::class, $throttlerPriv->cache );
+ $this->assertInstanceOf( LoggerInterface::class, $throttlerPriv->logger );
+
+ try {
+ new Throttler( [], [ 'foo' => 1, 'bar' => 2, 'baz' => 3 ] );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \InvalidArgumentException $ex ) {
+ $this->assertSame( 'unrecognized parameters: foo, bar, baz', $ex->getMessage() );
+ }
+ }
+
+ /**
+ * @dataProvider provideNormalizeThrottleConditions
+ */
+ public function testNormalizeThrottleConditions( $condition, $normalized ) {
+ $throttler = new Throttler( $condition );
+ $throttler->setLogger( new NullLogger() );
+ $throttlerPriv = TestingAccessWrapper::newFromObject( $throttler );
+ $this->assertSame( $normalized, $throttlerPriv->conditions );
+ }
+
+ public function provideNormalizeThrottleConditions() {
+ return [
+ [
+ [],
+ [],
+ ],
+ [
+ [ 'count' => 1, 'seconds' => 2 ],
+ [ [ 'count' => 1, 'seconds' => 2 ] ],
+ ],
+ [
+ [ [ 'count' => 1, 'seconds' => 2 ], [ 'count' => 2, 'seconds' => 3 ] ],
+ [ [ 'count' => 1, 'seconds' => 2 ], [ 'count' => 2, 'seconds' => 3 ] ],
+ ],
+ ];
+ }
+
+ public function testNormalizeThrottleConditions2() {
+ $priv = TestingAccessWrapper::newFromClass( Throttler::class );
+ $this->assertSame( [], $priv->normalizeThrottleConditions( null ) );
+ $this->assertSame( [], $priv->normalizeThrottleConditions( 'bad' ) );
+ }
+
+ public function testIncrease() {
+ $cache = new \HashBagOStuff();
+ $throttler = new Throttler( [
+ [ 'count' => 2, 'seconds' => 10, ],
+ [ 'count' => 4, 'seconds' => 15, 'allIPs' => true ],
+ ], [ 'cache' => $cache ] );
+ $throttler->setLogger( new NullLogger() );
+
+ $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+ $this->assertFalse( $result, 'should not throttle' );
+
+ $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+ $this->assertFalse( $result, 'should not throttle' );
+
+ $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+ $this->assertSame( [ 'throttleIndex' => 0, 'count' => 2, 'wait' => 10 ], $result );
+
+ $result = $throttler->increase( 'OtherUser', '1.2.3.4' );
+ $this->assertFalse( $result, 'should not throttle' );
+
+ $result = $throttler->increase( 'SomeUser', '2.3.4.5' );
+ $this->assertFalse( $result, 'should not throttle' );
+
+ $result = $throttler->increase( 'SomeUser', '3.4.5.6' );
+ $this->assertFalse( $result, 'should not throttle' );
+
+ $result = $throttler->increase( 'SomeUser', '3.4.5.6' );
+ $this->assertSame( [ 'throttleIndex' => 1, 'count' => 4, 'wait' => 15 ], $result );
+ }
+
+ public function testZeroCount() {
+ $cache = new \HashBagOStuff();
+ $throttler = new Throttler( [ [ 'count' => 0, 'seconds' => 10 ] ], [ 'cache' => $cache ] );
+ $throttler->setLogger( new NullLogger() );
+
+ $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+ $this->assertFalse( $result, 'should not throttle, count=0 is ignored' );
+
+ $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+ $this->assertFalse( $result, 'should not throttle, count=0 is ignored' );
+
+ $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+ $this->assertFalse( $result, 'should not throttle, count=0 is ignored' );
+ }
+
+ public function testNamespacing() {
+ $cache = new \HashBagOStuff();
+ $throttler1 = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ],
+ [ 'cache' => $cache, 'type' => 'foo' ] );
+ $throttler2 = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ],
+ [ 'cache' => $cache, 'type' => 'foo' ] );
+ $throttler3 = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ],
+ [ 'cache' => $cache, 'type' => 'bar' ] );
+ $throttler1->setLogger( new NullLogger() );
+ $throttler2->setLogger( new NullLogger() );
+ $throttler3->setLogger( new NullLogger() );
+
+ $throttled = [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ];
+
+ $result = $throttler1->increase( 'SomeUser', '1.2.3.4' );
+ $this->assertFalse( $result, 'should not throttle' );
+
+ $result = $throttler1->increase( 'SomeUser', '1.2.3.4' );
+ $this->assertEquals( $throttled, $result, 'should throttle' );
+
+ $result = $throttler2->increase( 'SomeUser', '1.2.3.4' );
+ $this->assertEquals( $throttled, $result, 'should throttle, same namespace' );
+
+ $result = $throttler3->increase( 'SomeUser', '1.2.3.4' );
+ $this->assertFalse( $result, 'should not throttle, different namespace' );
+ }
+
+ public function testExpiration() {
+ $cache = $this->getMockBuilder( HashBagOStuff::class )
+ ->setMethods( [ 'add' ] )->getMock();
+ $throttler = new Throttler( [ [ 'count' => 3, 'seconds' => 10 ] ], [ 'cache' => $cache ] );
+ $throttler->setLogger( new NullLogger() );
+
+ $cache->expects( $this->once() )->method( 'add' )->with( $this->anything(), 1, 10 );
+ $throttler->increase( 'SomeUser' );
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testException() {
+ $throttler = new Throttler( [ [ 'count' => 3, 'seconds' => 10 ] ] );
+ $throttler->setLogger( new NullLogger() );
+ $throttler->increase();
+ }
+
+ public function testLog() {
+ $cache = new \HashBagOStuff();
+ $throttler = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ], [ 'cache' => $cache ] );
+
+ $logger = $this->getMockBuilder( AbstractLogger::class )
+ ->setMethods( [ 'log' ] )
+ ->getMockForAbstractClass();
+ $logger->expects( $this->never() )->method( 'log' );
+ $throttler->setLogger( $logger );
+ $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+ $this->assertFalse( $result, 'should not throttle' );
+
+ $logger = $this->getMockBuilder( AbstractLogger::class )
+ ->setMethods( [ 'log' ] )
+ ->getMockForAbstractClass();
+ $logger->expects( $this->once() )->method( 'log' )->with( $this->anything(), $this->anything(), [
+ 'throttle' => 'custom',
+ 'index' => 0,
+ 'ip' => '1.2.3.4',
+ 'username' => 'SomeUser',
+ 'count' => 1,
+ 'expiry' => 10,
+ 'method' => 'foo',
+ ] );
+ $throttler->setLogger( $logger );
+ $result = $throttler->increase( 'SomeUser', '1.2.3.4', 'foo' );
+ $this->assertSame( [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ], $result );
+ }
+
+ public function testClear() {
+ $cache = new \HashBagOStuff();
+ $throttler = new Throttler( [ [ 'count' => 1, 'seconds' => 10 ] ], [ 'cache' => $cache ] );
+ $throttler->setLogger( new NullLogger() );
+
+ $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+ $this->assertFalse( $result, 'should not throttle' );
+
+ $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+ $this->assertSame( [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ], $result );
+
+ $result = $throttler->increase( 'OtherUser', '1.2.3.4' );
+ $this->assertFalse( $result, 'should not throttle' );
+
+ $result = $throttler->increase( 'OtherUser', '1.2.3.4' );
+ $this->assertSame( [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ], $result );
+
+ $throttler->clear( 'SomeUser', '1.2.3.4' );
+
+ $result = $throttler->increase( 'SomeUser', '1.2.3.4' );
+ $this->assertFalse( $result, 'should not throttle' );
+
+ $result = $throttler->increase( 'OtherUser', '1.2.3.4' );
+ $this->assertSame( [ 'throttleIndex' => 0, 'count' => 1, 'wait' => 10 ], $result );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/UserDataAuthenticationRequestTest.php b/www/wiki/tests/phpunit/includes/auth/UserDataAuthenticationRequestTest.php
new file mode 100644
index 00000000..7dea123c
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/UserDataAuthenticationRequestTest.php
@@ -0,0 +1,176 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\UserDataAuthenticationRequest
+ */
+class UserDataAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+ protected function getInstance( array $args = [] ) {
+ return new UserDataAuthenticationRequest;
+ }
+
+ protected function setUp() {
+ parent::setUp();
+ $this->setMwGlobals( 'wgHiddenPrefs', [] );
+ }
+
+ /**
+ * @dataProvider providePopulateUser
+ * @param string $email Email to set
+ * @param string $realname Realname to set
+ * @param StatusValue $expect Expected return
+ */
+ public function testPopulateUser( $email, $realname, $expect ) {
+ $user = new \User();
+ $user->setEmail( 'default@example.com' );
+ $user->setRealName( 'Fake Name' );
+
+ $req = new UserDataAuthenticationRequest;
+ $req->email = $email;
+ $req->realname = $realname;
+ $this->assertEquals( $expect, $req->populateUser( $user ) );
+ if ( $expect->isOk() ) {
+ $this->assertSame( $email ?: 'default@example.com', $user->getEmail() );
+ $this->assertSame( $realname ?: 'Fake Name', $user->getRealName() );
+ }
+ }
+
+ public static function providePopulateUser() {
+ $good = \StatusValue::newGood();
+ return [
+ [ 'email@example.com', 'Real Name', $good ],
+ [ 'email@example.com', '', $good ],
+ [ '', 'Real Name', $good ],
+ [ '', '', $good ],
+ [ 'invalid-email', 'Real Name', \StatusValue::newFatal( 'invalidemailaddress' ) ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideLoadFromSubmission
+ */
+ public function testLoadFromSubmission(
+ array $args, array $data, $expectState /* $hiddenPref, $enableEmail */
+ ) {
+ list( $args, $data, $expectState, $hiddenPref, $enableEmail ) = func_get_args();
+ $this->setMwGlobals( 'wgHiddenPrefs', $hiddenPref );
+ $this->setMwGlobals( 'wgEnableEmail', $enableEmail );
+ parent::testLoadFromSubmission( $args, $data, $expectState );
+ }
+
+ public function provideLoadFromSubmission() {
+ $unhidden = [];
+ $hidden = [ 'realname' ];
+
+ return [
+ 'Empty request, unhidden, email enabled' => [
+ [],
+ [],
+ false,
+ $unhidden,
+ true
+ ],
+ 'email + realname, unhidden, email enabled' => [
+ [],
+ $data = [ 'email' => 'Email', 'realname' => 'Name' ],
+ $data,
+ $unhidden,
+ true
+ ],
+ 'email empty, unhidden, email enabled' => [
+ [],
+ $data = [ 'email' => '', 'realname' => 'Name' ],
+ $data,
+ $unhidden,
+ true
+ ],
+ 'email omitted, unhidden, email enabled' => [
+ [],
+ [ 'realname' => 'Name' ],
+ false,
+ $unhidden,
+ true
+ ],
+ 'realname empty, unhidden, email enabled' => [
+ [],
+ $data = [ 'email' => 'Email', 'realname' => '' ],
+ $data,
+ $unhidden,
+ true
+ ],
+ 'realname omitted, unhidden, email enabled' => [
+ [],
+ [ 'email' => 'Email' ],
+ false,
+ $unhidden,
+ true
+ ],
+ 'Empty request, hidden, email enabled' => [
+ [],
+ [],
+ false,
+ $hidden,
+ true
+ ],
+ 'email + realname, hidden, email enabled' => [
+ [],
+ [ 'email' => 'Email', 'realname' => 'Name' ],
+ [ 'email' => 'Email' ],
+ $hidden,
+ true
+ ],
+ 'email empty, hidden, email enabled' => [
+ [],
+ $data = [ 'email' => '', 'realname' => 'Name' ],
+ [ 'email' => '' ],
+ $hidden,
+ true
+ ],
+ 'email omitted, hidden, email enabled' => [
+ [],
+ [ 'realname' => 'Name' ],
+ false,
+ $hidden,
+ true
+ ],
+ 'realname empty, hidden, email enabled' => [
+ [],
+ $data = [ 'email' => 'Email', 'realname' => '' ],
+ [ 'email' => 'Email' ],
+ $hidden,
+ true
+ ],
+ 'realname omitted, hidden, email enabled' => [
+ [],
+ [ 'email' => 'Email' ],
+ [ 'email' => 'Email' ],
+ $hidden,
+ true
+ ],
+ 'email + realname, unhidden, email disabled' => [
+ [],
+ [ 'email' => 'Email', 'realname' => 'Name' ],
+ [ 'realname' => 'Name' ],
+ $unhidden,
+ false
+ ],
+ 'email omitted, unhidden, email disabled' => [
+ [],
+ [ 'realname' => 'Name' ],
+ [ 'realname' => 'Name' ],
+ $unhidden,
+ false
+ ],
+ 'email empty, unhidden, email disabled' => [
+ [],
+ [ 'email' => '', 'realname' => 'Name' ],
+ [ 'realname' => 'Name' ],
+ $unhidden,
+ false
+ ],
+ ];
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/auth/UsernameAuthenticationRequestTest.php b/www/wiki/tests/phpunit/includes/auth/UsernameAuthenticationRequestTest.php
new file mode 100644
index 00000000..63628dd8
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/auth/UsernameAuthenticationRequestTest.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers MediaWiki\Auth\UsernameAuthenticationRequest
+ */
+class UsernameAuthenticationRequestTest extends AuthenticationRequestTestCase {
+
+ protected function getInstance( array $args = [] ) {
+ return new UsernameAuthenticationRequest();
+ }
+
+ public function provideLoadFromSubmission() {
+ return [
+ 'Empty request' => [
+ [],
+ [],
+ false
+ ],
+ 'Username' => [
+ [],
+ $data = [ 'username' => 'User' ],
+ $data,
+ ],
+ 'Username empty' => [
+ [],
+ [ 'username' => '' ],
+ false
+ ],
+ ];
+ }
+}