3 namespace MediaWiki\Auth
;
6 use MediaWiki\Block\DatabaseBlock
;
7 use MediaWiki\Session\SessionInfo
;
8 use MediaWiki\Session\UserInfo
;
9 use Psr\Log\LoggerInterface
;
13 use Wikimedia\ScopedCallback
;
14 use Wikimedia\TestingAccessWrapper
;
19 * @covers \MediaWiki\Auth\AuthManager
21 class AuthManagerTest
extends \MediaWikiTestCase
{
22 /** @var WebRequest */
26 /** @var LoggerInterface */
29 protected $preauthMocks = [];
30 protected $primaryauthMocks = [];
31 protected $secondaryauthMocks = [];
33 /** @var AuthManager */
35 /** @var TestingAccessWrapper */
36 protected $managerPriv;
39 * Sets a mock on a hook
41 * @param object $expect From $this->once(), $this->never(), etc.
42 * @return object $mock->expects( $expect )->method( ... ).
44 protected function hook( $hook, $expect ) {
45 $mock = $this->getMockBuilder( __CLASS__
)
46 ->setMethods( [ "on$hook" ] )
48 $this->setTemporaryHook( $hook, $mock );
49 return $mock->expects( $expect )->method( "on$hook" );
56 protected function unhook( $hook ) {
62 * Ensure a value is a clean Message object
63 * @param string|Message $key
64 * @param array $params
67 protected function message( $key, $params = [] ) {
68 if ( $key === null ) {
71 if ( $key instanceof \MessageSpecifier
) {
72 $params = $key->getParams();
73 $key = $key->getKey();
75 return new \
Message( $key, $params, \Language
::factory( 'en' ) );
79 * Test two AuthenticationResponses for equality. We don't want to use regular assertEquals
80 * because that recursively compares members, which leads to false negatives if e.g. Language
83 * @param AuthenticationResponse $response1
84 * @param AuthenticationResponse $response2
88 private function assertResponseEquals(
89 AuthenticationResponse
$expected, AuthenticationResponse
$actual, $msg = ''
91 foreach ( ( new \
ReflectionClass( $expected ) )->getProperties() as $prop ) {
92 $name = $prop->getName();
93 $usedMsg = ltrim( "$msg ($name)" );
94 if ( $name === 'message' && $expected->message
) {
95 $this->assertSame( $expected->message
->serialize(), $actual->message
->serialize(),
98 $this->assertEquals( $expected->$name, $actual->$name, $usedMsg );
104 * Initialize the AuthManagerConfig variable in $this->config
106 * Uses data from the various 'mocks' fields.
108 protected function initializeConfig() {
118 foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
119 $key = $type . 'Mocks';
120 foreach ( $this->$key as $mock ) {
121 $config[$type][$mock->getUniqueId()] = [ 'factory' => function () use ( $mock ) {
127 $this->config
->set( 'AuthManagerConfig', $config );
128 $this->config
->set( 'LanguageCode', 'en' );
129 $this->config
->set( 'NewUserLog', false );
133 * Initialize $this->manager
134 * @param bool $regen Force a call to $this->initializeConfig()
136 protected function initializeManager( $regen = false ) {
137 if ( $regen ||
!$this->config
) {
138 $this->config
= new \
HashConfig();
140 if ( $regen ||
!$this->request
) {
141 $this->request
= new \
FauxRequest();
143 if ( !$this->logger
) {
144 $this->logger
= new \
TestLogger();
147 if ( $regen ||
!$this->config
->has( 'AuthManagerConfig' ) ) {
148 $this->initializeConfig();
150 $this->manager
= new AuthManager( $this->request
, $this->config
);
151 $this->manager
->setLogger( $this->logger
);
152 $this->managerPriv
= TestingAccessWrapper
::newFromObject( $this->manager
);
156 * Setup SessionManager with a mock session provider
157 * @param bool|null $canChangeUser If non-null, canChangeUser will be mocked to return this
158 * @param array $methods Additional methods to mock
159 * @return array (MediaWiki\Session\SessionProvider, ScopedCallback)
161 protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
162 if ( !$this->config
) {
163 $this->config
= new \
HashConfig();
164 $this->initializeConfig();
166 $this->config
->set( 'ObjectCacheSessionExpiry', 100 );
168 $methods[] = '__toString';
169 $methods[] = 'describe';
170 if ( $canChangeUser !== null ) {
171 $methods[] = 'canChangeUser';
173 $provider = $this->getMockBuilder( \DummySessionProvider
::class )
174 ->setMethods( $methods )
176 $provider->expects( $this->any() )->method( '__toString' )
177 ->will( $this->returnValue( 'MockSessionProvider' ) );
178 $provider->expects( $this->any() )->method( 'describe' )
179 ->will( $this->returnValue( 'MockSessionProvider sessions' ) );
180 if ( $canChangeUser !== null ) {
181 $provider->expects( $this->any() )->method( 'canChangeUser' )
182 ->will( $this->returnValue( $canChangeUser ) );
184 $this->config
->set( 'SessionProviders', [
185 [ 'factory' => function () use ( $provider ) {
190 $manager = new \MediaWiki\Session\
SessionManager( [
191 'config' => $this->config
,
192 'logger' => new \Psr\Log\
NullLogger(),
193 'store' => new \
HashBagOStuff(),
195 TestingAccessWrapper
::newFromObject( $manager )->getProvider( (string)$provider );
197 $reset = \MediaWiki\Session\TestUtils
::setSessionManagerSingleton( $manager );
199 if ( $this->request
) {
200 $manager->getSessionForRequest( $this->request
);
203 return [ $provider, $reset ];
206 public function testSingleton() {
207 // Temporarily clear out the global singleton, if any, to test creating
209 $rProp = new \
ReflectionProperty( AuthManager
::class, 'instance' );
210 $rProp->setAccessible( true );
211 $old = $rProp->getValue();
212 $cb = new ScopedCallback( [ $rProp, 'setValue' ], [ $old ] );
213 $rProp->setValue( null );
215 $singleton = AuthManager
::singleton();
216 $this->assertInstanceOf( AuthManager
::class, AuthManager
::singleton() );
217 $this->assertSame( $singleton, AuthManager
::singleton() );
218 $this->assertSame( \RequestContext
::getMain()->getRequest(), $singleton->getRequest() );
220 \RequestContext
::getMain()->getConfig(),
221 TestingAccessWrapper
::newFromObject( $singleton )->config
225 public function testCanAuthenticateNow() {
226 $this->initializeManager();
228 list( $provider, $reset ) = $this->getMockSessionProvider( false );
229 $this->assertFalse( $this->manager
->canAuthenticateNow() );
230 ScopedCallback
::consume( $reset );
232 list( $provider, $reset ) = $this->getMockSessionProvider( true );
233 $this->assertTrue( $this->manager
->canAuthenticateNow() );
234 ScopedCallback
::consume( $reset );
237 public function testNormalizeUsername() {
239 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
240 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
241 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
242 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
244 foreach ( $mocks as $key => $mock ) {
245 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
247 $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
248 ->with( $this->identicalTo( 'XYZ' ) )
249 ->willReturn( 'Foo' );
250 $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
251 ->with( $this->identicalTo( 'XYZ' ) )
252 ->willReturn( 'Foo' );
253 $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
254 ->with( $this->identicalTo( 'XYZ' ) )
255 ->willReturn( null );
256 $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
257 ->with( $this->identicalTo( 'XYZ' ) )
258 ->willReturn( 'Bar!' );
260 $this->primaryauthMocks
= $mocks;
262 $this->initializeManager();
264 $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager
->normalizeUsername( 'XYZ' ) );
268 * @dataProvider provideSecuritySensitiveOperationStatus
269 * @param bool $mutableSession
271 public function testSecuritySensitiveOperationStatus( $mutableSession ) {
272 $this->logger
= new \Psr\Log\
NullLogger();
273 $user = \User
::newFromName( 'UTSysop' );
275 $reauth = $mutableSession ? AuthManager
::SEC_REAUTH
: AuthManager
::SEC_FAIL
;
277 list( $provider, $reset ) = $this->getMockSessionProvider(
278 $mutableSession, [ 'provideSessionInfo' ]
280 $provider->expects( $this->any() )->method( 'provideSessionInfo' )
281 ->will( $this->returnCallback( function () use ( $provider, &$provideUser ) {
282 return new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
283 'provider' => $provider,
284 'id' => \DummySessionProvider
::ID
,
286 'userInfo' => UserInfo
::newFromUser( $provideUser, true )
289 $this->initializeManager();
291 $this->config
->set( 'ReauthenticateTime', [] );
292 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [] );
293 $provideUser = new \User
;
294 $session = $provider->getManager()->getSessionForRequest( $this->request
);
295 $this->assertSame( 0, $session->getUser()->getId(), 'sanity check' );
297 // Anonymous user => reauth
298 $session->set( 'AuthManager:lastAuthId', 0 );
299 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
300 $this->assertSame( $reauth, $this->manager
->securitySensitiveOperationStatus( 'foo' ) );
302 $provideUser = $user;
303 $session = $provider->getManager()->getSessionForRequest( $this->request
);
304 $this->assertSame( $user->getId(), $session->getUser()->getId(), 'sanity check' );
306 // Error for no default (only gets thrown for non-anonymous user)
307 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
308 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
310 $this->manager
->securitySensitiveOperationStatus( 'foo' );
311 $this->fail( 'Expected exception not thrown' );
312 } catch ( \UnexpectedValueException
$ex ) {
315 ?
'$wgReauthenticateTime lacks a default'
316 : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
321 if ( $mutableSession ) {
322 $this->config
->set( 'ReauthenticateTime', [
328 // Mismatched user ID
329 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
330 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
332 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
335 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
338 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
342 $session->set( 'AuthManager:lastAuthId', $user->getId() );
343 $session->set( 'AuthManager:lastAuthTimestamp', null );
345 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
348 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
351 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
354 // Recent enough to pass
355 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
357 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
360 // Not recent enough to pass
361 $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
363 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
365 // But recent enough for the 'test' operation
367 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test' )
370 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [
376 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
380 AuthManager
::SEC_FAIL
, $this->manager
->securitySensitiveOperationStatus( 'test' )
384 // Test hook, all three possible values
386 AuthManager
::SEC_OK
=> AuthManager
::SEC_OK
,
387 AuthManager
::SEC_REAUTH
=> $reauth,
388 AuthManager
::SEC_FAIL
=> AuthManager
::SEC_FAIL
,
389 ] as $hook => $expect ) {
390 $this->hook( 'SecuritySensitiveOperationStatus', $this->exactly( 2 ) )
394 $this->callback( function ( $s ) use ( $session ) {
395 return $s->getId() === $session->getId();
397 $mutableSession ?
$this->equalTo( 500, 1 ) : $this->equalTo( -1 )
399 ->will( $this->returnCallback( function ( &$v ) use ( $hook ) {
403 $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
405 $expect, $this->manager
->securitySensitiveOperationStatus( 'test' ), "hook $hook"
408 $expect, $this->manager
->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
410 $this->unhook( 'SecuritySensitiveOperationStatus' );
413 ScopedCallback
::consume( $reset );
416 public function onSecuritySensitiveOperationStatus( &$status, $operation, $session, $time ) {
419 public static function provideSecuritySensitiveOperationStatus() {
427 * @dataProvider provideUserCanAuthenticate
428 * @param bool $primary1Can
429 * @param bool $primary2Can
430 * @param bool $expect
432 public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
433 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
434 $mock1->expects( $this->any() )->method( 'getUniqueId' )
435 ->will( $this->returnValue( 'primary1' ) );
436 $mock1->expects( $this->any() )->method( 'testUserCanAuthenticate' )
437 ->with( $this->equalTo( 'UTSysop' ) )
438 ->will( $this->returnValue( $primary1Can ) );
439 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
440 $mock2->expects( $this->any() )->method( 'getUniqueId' )
441 ->will( $this->returnValue( 'primary2' ) );
442 $mock2->expects( $this->any() )->method( 'testUserCanAuthenticate' )
443 ->with( $this->equalTo( 'UTSysop' ) )
444 ->will( $this->returnValue( $primary2Can ) );
445 $this->primaryauthMocks
= [ $mock1, $mock2 ];
447 $this->initializeManager( true );
448 $this->assertSame( $expect, $this->manager
->userCanAuthenticate( 'UTSysop' ) );
451 public static function provideUserCanAuthenticate() {
453 [ false, false, false ],
454 [ true, false, true ],
455 [ false, true, true ],
456 [ true, true, true ],
460 public function testRevokeAccessForUser() {
461 $this->initializeManager();
463 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
464 $mock->expects( $this->any() )->method( 'getUniqueId' )
465 ->will( $this->returnValue( 'primary' ) );
466 $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
467 ->with( $this->equalTo( 'UTSysop' ) );
468 $this->primaryauthMocks
= [ $mock ];
470 $this->initializeManager( true );
471 $this->logger
->setCollect( true );
473 $this->manager
->revokeAccessForUser( 'UTSysop' );
476 [ LogLevel
::INFO
, 'Revoking access for {user}' ],
477 ], $this->logger
->getBuffer() );
480 public function testProviderCreation() {
482 'pre' => $this->getMockForAbstractClass( PreAuthenticationProvider
::class ),
483 'primary' => $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
484 'secondary' => $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class ),
486 foreach ( $mocks as $key => $mock ) {
487 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
488 $mock->expects( $this->once() )->method( 'setLogger' );
489 $mock->expects( $this->once() )->method( 'setManager' );
490 $mock->expects( $this->once() )->method( 'setConfig' );
492 $this->preauthMocks
= [ $mocks['pre'] ];
493 $this->primaryauthMocks
= [ $mocks['primary'] ];
494 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
497 $this->initializeManager();
500 $this->managerPriv
->getAuthenticationProvider( 'primary' )
504 $this->managerPriv
->getAuthenticationProvider( 'secondary' )
508 $this->managerPriv
->getAuthenticationProvider( 'pre' )
511 [ 'pre' => $mocks['pre'] ],
512 $this->managerPriv
->getPreAuthenticationProviders()
515 [ 'primary' => $mocks['primary'] ],
516 $this->managerPriv
->getPrimaryAuthenticationProviders()
519 [ 'secondary' => $mocks['secondary'] ],
520 $this->managerPriv
->getSecondaryAuthenticationProviders()
524 $mock1 = $this->getMockForAbstractClass( PreAuthenticationProvider
::class );
525 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
526 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
527 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
528 $this->preauthMocks
= [ $mock1 ];
529 $this->primaryauthMocks
= [ $mock2 ];
530 $this->secondaryauthMocks
= [];
531 $this->initializeManager( true );
533 $this->managerPriv
->getAuthenticationProvider( 'Y' );
534 $this->fail( 'Expected exception not thrown' );
535 } catch ( \RuntimeException
$ex ) {
536 $class1 = get_class( $mock1 );
537 $class2 = get_class( $mock2 );
539 "Duplicate specifications for id X (classes $class1 and $class2)", $ex->getMessage()
544 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
545 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
546 $class = get_class( $mock );
547 $this->preauthMocks
= [ $mock ];
548 $this->primaryauthMocks
= [ $mock ];
549 $this->secondaryauthMocks
= [ $mock ];
550 $this->initializeManager( true );
552 $this->managerPriv
->getPreAuthenticationProviders();
553 $this->fail( 'Expected exception not thrown' );
554 } catch ( \RuntimeException
$ex ) {
556 "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
561 $this->managerPriv
->getPrimaryAuthenticationProviders();
562 $this->fail( 'Expected exception not thrown' );
563 } catch ( \RuntimeException
$ex ) {
565 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
570 $this->managerPriv
->getSecondaryAuthenticationProviders();
571 $this->fail( 'Expected exception not thrown' );
572 } catch ( \RuntimeException
$ex ) {
574 "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
580 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
581 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
582 $mock3 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
583 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
584 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
585 $mock3->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'C' ) );
586 $this->preauthMocks
= [];
587 $this->primaryauthMocks
= [ $mock1, $mock2, $mock3 ];
588 $this->secondaryauthMocks
= [];
589 $this->initializeConfig();
590 $config = $this->config
->get( 'AuthManagerConfig' );
592 $this->initializeManager( false );
594 [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
595 $this->managerPriv
->getPrimaryAuthenticationProviders(),
599 $config['primaryauth']['A']['sort'] = 100;
600 $config['primaryauth']['C']['sort'] = -1;
601 $this->config
->set( 'AuthManagerConfig', $config );
602 $this->initializeManager( false );
604 [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
605 $this->managerPriv
->getPrimaryAuthenticationProviders()
610 * @dataProvider provideSetDefaultUserOptions
612 public function testSetDefaultUserOptions(
613 $contLang, $useContextLang, $expectedLang, $expectedVariant
615 $this->initializeManager();
617 $this->setContentLang( $contLang );
618 $context = \RequestContext
::getMain();
619 $reset = new ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
620 $context->setLanguage( 'de' );
622 $user = \User
::newFromName( self
::usernameForCreation() );
623 $user->addToDatabase();
624 $oldToken = $user->getToken();
625 $this->managerPriv
->setDefaultUserOptions( $user, $useContextLang );
626 $user->saveSettings();
627 $this->assertNotEquals( $oldToken, $user->getToken() );
628 $this->assertSame( $expectedLang, $user->getOption( 'language' ) );
629 $this->assertSame( $expectedVariant, $user->getOption( 'variant' ) );
632 public function provideSetDefaultUserOptions() {
634 [ 'zh', false, 'zh', 'zh' ],
635 [ 'zh', true, 'de', 'zh' ],
636 [ 'fr', true, 'de', null ],
640 public function testForcePrimaryAuthenticationProviders() {
641 $mockA = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
642 $mockB = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
643 $mockB2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
644 $mockA->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
645 $mockB->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
646 $mockB2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
647 $this->primaryauthMocks
= [ $mockA ];
649 $this->logger
= new \
TestLogger( true );
651 // Test without first initializing the configured providers
652 $this->initializeManager();
653 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
655 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
657 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
658 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
660 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
661 ], $this->logger
->getBuffer() );
662 $this->logger
->clearBuffer();
664 // Test with first initializing the configured providers
665 $this->initializeManager();
666 $this->assertSame( $mockA, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
667 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
668 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
669 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
670 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
672 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
674 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
675 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
676 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
678 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
681 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
684 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
686 ], $this->logger
->getBuffer() );
687 $this->logger
->clearBuffer();
689 // Test duplicate IDs
690 $this->initializeManager();
692 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
693 $this->fail( 'Expected exception not thrown' );
694 } catch ( \RuntimeException
$ex ) {
695 $class1 = get_class( $mockB );
696 $class2 = get_class( $mockB2 );
698 "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
703 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
704 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
705 $class = get_class( $mock );
707 $this->manager
->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
708 $this->fail( 'Expected exception not thrown' );
709 } catch ( \RuntimeException
$ex ) {
711 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
717 public function testBeginAuthentication() {
718 $this->initializeManager();
721 list( $provider, $reset ) = $this->getMockSessionProvider( false );
722 $this->hook( 'UserLoggedIn', $this->never() );
723 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
725 $this->manager
->beginAuthentication( [], 'http://localhost/' );
726 $this->fail( 'Expected exception not thrown' );
727 } catch ( \LogicException
$ex ) {
728 $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
730 $this->unhook( 'UserLoggedIn' );
731 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
732 ScopedCallback
::consume( $reset );
733 $this->initializeManager( true );
735 // CreatedAccountAuthenticationRequest
736 $user = \User
::newFromName( 'UTSysop' );
738 new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
740 $this->hook( 'UserLoggedIn', $this->never() );
742 $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
743 $this->fail( 'Expected exception not thrown' );
744 } catch ( \LogicException
$ex ) {
746 'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
747 'that created the account',
751 $this->unhook( 'UserLoggedIn' );
753 $this->request
->getSession()->clear();
754 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
755 $this->managerPriv
->createdAccountAuthenticationRequests
= [ $reqs[0] ];
756 $this->hook( 'UserLoggedIn', $this->once() )
757 ->with( $this->callback( function ( $u ) use ( $user ) {
758 return $user->getId() === $u->getId() && $user->getName() === $u->getName();
760 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
761 $this->logger
->setCollect( true );
762 $ret = $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
763 $this->logger
->setCollect( false );
764 $this->unhook( 'UserLoggedIn' );
765 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
766 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
767 $this->assertSame( $user->getName(), $ret->username
);
768 $this->assertSame( $user->getId(), $this->request
->getSessionData( 'AuthManager:lastAuthId' ) );
770 time(), $this->request
->getSessionData( 'AuthManager:lastAuthTimestamp' ),
773 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
774 $this->assertSame( $user->getId(), $this->request
->getSession()->getUser()->getId() );
776 [ LogLevel
::INFO
, 'Logging in {user} after account creation' ],
777 ], $this->logger
->getBuffer() );
780 public function testCreateFromLogin() {
781 $user = \User
::newFromName( 'UTSysop' );
782 $req1 = $this->createMock( AuthenticationRequest
::class );
783 $req2 = $this->createMock( AuthenticationRequest
::class );
784 $req3 = $this->createMock( AuthenticationRequest
::class );
785 $userReq = new UsernameAuthenticationRequest
;
786 $userReq->username
= 'UTDummy';
788 $req1->returnToUrl
= 'http://localhost/';
789 $req2->returnToUrl
= 'http://localhost/';
790 $req3->returnToUrl
= 'http://localhost/';
791 $req3->username
= 'UTDummy';
792 $userReq->returnToUrl
= 'http://localhost/';
794 // Passing one into beginAuthentication(), and an immediate FAIL
795 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
796 $this->primaryauthMocks
= [ $primary ];
797 $this->initializeManager( true );
798 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
799 $res->createRequest
= $req1;
800 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
801 ->will( $this->returnValue( $res ) );
802 $createReq = new CreateFromLoginAuthenticationRequest(
803 null, [ $req2->getUniqueId() => $req2 ]
805 $this->logger
->setCollect( true );
806 $ret = $this->manager
->beginAuthentication( [ $createReq ], 'http://localhost/' );
807 $this->logger
->setCollect( false );
808 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
809 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
810 $this->assertSame( $req1, $ret->createRequest
->createRequest
);
811 $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest
->maybeLink
);
813 // UI, then FAIL in beginAuthentication()
814 $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider
::class )
815 ->setMethods( [ 'continuePrimaryAuthentication' ] )
816 ->getMockForAbstractClass();
817 $this->primaryauthMocks
= [ $primary ];
818 $this->initializeManager( true );
819 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
820 ->will( $this->returnValue(
821 AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) )
823 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
824 $res->createRequest
= $req2;
825 $primary->expects( $this->any() )->method( 'continuePrimaryAuthentication' )
826 ->will( $this->returnValue( $res ) );
827 $this->logger
->setCollect( true );
828 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
829 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
, 'sanity check' );
830 $ret = $this->manager
->continueAuthentication( [] );
831 $this->logger
->setCollect( false );
832 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
833 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
834 $this->assertSame( $req2, $ret->createRequest
->createRequest
);
835 $this->assertEquals( [], $ret->createRequest
->maybeLink
);
837 // Pass into beginAccountCreation(), see that maybeLink and createRequest get copied
838 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
839 $this->primaryauthMocks
= [ $primary ];
840 $this->initializeManager( true );
841 $createReq = new CreateFromLoginAuthenticationRequest( $req3, [ $req2 ] );
842 $createReq->returnToUrl
= 'http://localhost/';
843 $createReq->username
= 'UTDummy';
844 $res = AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) );
845 $primary->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
846 ->with( $this->anything(), $this->anything(), [ $userReq, $createReq, $req3 ] )
847 ->will( $this->returnValue( $res ) );
848 $primary->expects( $this->any() )->method( 'accountCreationType' )
849 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
850 $this->logger
->setCollect( true );
851 $ret = $this->manager
->beginAccountCreation(
852 $user, [ $userReq, $createReq ], 'http://localhost/'
854 $this->logger
->setCollect( false );
855 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
856 $state = $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' );
857 $this->assertNotNull( $state );
858 $this->assertEquals( [ $userReq, $createReq, $req3 ], $state['reqs'] );
859 $this->assertEquals( [ $req2 ], $state['maybeLink'] );
863 * @dataProvider provideAuthentication
864 * @param StatusValue $preResponse
865 * @param array $primaryResponses
866 * @param array $secondaryResponses
867 * @param array $managerResponses
868 * @param bool $link Whether the primary authentication provider is a "link" provider
870 public function testAuthentication(
871 StatusValue
$preResponse, array $primaryResponses, array $secondaryResponses,
872 array $managerResponses, $link = false
874 $this->initializeManager();
875 $user = \User
::newFromName( 'UTSysop' );
876 $id = $user->getId();
877 $name = $user->getName();
879 // Set up lots of mocks...
880 $req = new RememberMeAuthenticationRequest
;
881 $req->rememberMe
= (bool)rand( 0, 1 );
882 $req->pre
= $preResponse;
883 $req->primary
= $primaryResponses;
884 $req->secondary
= $secondaryResponses;
886 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
887 $class = ucfirst( $key ) . 'AuthenticationProvider';
888 $mocks[$key] = $this->getMockForAbstractClass(
889 "MediaWiki\\Auth\\$class", [], "Mock$class"
891 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
892 ->will( $this->returnValue( $key ) );
893 $mocks[$key . '2'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
894 $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
895 ->will( $this->returnValue( $key . '2' ) );
896 $mocks[$key . '3'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
897 $mocks[$key . '3']->expects( $this->any() )->method( 'getUniqueId' )
898 ->will( $this->returnValue( $key . '3' ) );
900 foreach ( $mocks as $mock ) {
901 $mock->expects( $this->any() )->method( 'getAuthenticationRequests' )
902 ->will( $this->returnValue( [] ) );
905 $mocks['pre']->expects( $this->once() )->method( 'testForAuthentication' )
906 ->will( $this->returnCallback( function ( $reqs ) use ( $req ) {
907 $this->assertContains( $req, $reqs );
911 $ct = count( $req->primary
);
912 $callback = $this->returnCallback( function ( $reqs ) use ( $req ) {
913 $this->assertContains( $req, $reqs );
914 return array_shift( $req->primary
);
916 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
917 ->method( 'beginPrimaryAuthentication' )
919 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
920 ->method( 'continuePrimaryAuthentication' )
923 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
924 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
927 $ct = count( $req->secondary
);
928 $callback = $this->returnCallback( function ( $user, $reqs ) use ( $id, $name, $req ) {
929 $this->assertSame( $id, $user->getId() );
930 $this->assertSame( $name, $user->getName() );
931 $this->assertContains( $req, $reqs );
932 return array_shift( $req->secondary
);
934 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
935 ->method( 'beginSecondaryAuthentication' )
937 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
938 ->method( 'continueSecondaryAuthentication' )
941 $abstain = AuthenticationResponse
::newAbstain();
942 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAuthentication' )
943 ->will( $this->returnValue( StatusValue
::newGood() ) );
944 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAuthentication' )
945 ->will( $this->returnValue( $abstain ) );
946 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAuthentication' );
947 $mocks['secondary2']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
948 ->will( $this->returnValue( $abstain ) );
949 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
950 $mocks['secondary3']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
951 ->will( $this->returnValue( $abstain ) );
952 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
954 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
955 $this->primaryauthMocks
= [ $mocks['primary'], $mocks['primary2'] ];
956 $this->secondaryauthMocks
= [
957 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2'],
958 // So linking happens
959 new ConfirmLinkSecondaryAuthenticationProvider
,
961 $this->initializeManager( true );
962 $this->logger
->setCollect( true );
964 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
965 $this->equalTo( AuthenticationResponse
::PASS
),
966 $this->equalTo( AuthenticationResponse
::FAIL
)
968 $providers = array_filter(
970 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
973 return is_callable( [ $p, 'expects' ] );
976 foreach ( $providers as $p ) {
977 $p->postCalled
= false;
978 $p->expects( $this->atMost( 1 ) )->method( 'postAuthentication' )
979 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
980 if ( $user !== null ) {
981 $this->assertInstanceOf( \User
::class, $user );
982 $this->assertSame( 'UTSysop', $user->getName() );
984 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
985 $this->assertThat( $response->status
, $constraint );
986 $p->postCalled
= $response->status
;
990 $session = $this->request
->getSession();
991 $session->setRememberUser( !$req->rememberMe
);
993 foreach ( $managerResponses as $i => $response ) {
994 $success = $response instanceof AuthenticationResponse
&&
995 $response->status
=== AuthenticationResponse
::PASS
;
997 $this->hook( 'UserLoggedIn', $this->once() )
998 ->with( $this->callback( function ( $user ) use ( $id, $name ) {
999 return $user->getId() === $id && $user->getName() === $name;
1002 $this->hook( 'UserLoggedIn', $this->never() );
1005 $response instanceof AuthenticationResponse
&&
1006 $response->status
=== AuthenticationResponse
::FAIL
&&
1007 $response->message
->getKey() !== 'authmanager-authn-not-in-progress' &&
1008 $response->message
->getKey() !== 'authmanager-authn-no-primary'
1011 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
1013 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->never() );
1019 $ret = $this->manager
->beginAuthentication( [ $req ], 'http://localhost/' );
1021 $ret = $this->manager
->continueAuthentication( [ $req ] );
1023 if ( $response instanceof \Exception
) {
1024 $this->fail( 'Expected exception not thrown', "Response $i" );
1026 } catch ( \Exception
$ex ) {
1027 if ( !$response instanceof \Exception
) {
1030 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
1031 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1032 "Response $i, exception, session state" );
1033 $this->unhook( 'UserLoggedIn' );
1034 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1038 $this->unhook( 'UserLoggedIn' );
1039 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1041 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
1043 $ret->message
= $this->message( $ret->message
);
1044 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
1046 $this->assertSame( $id, $session->getUser()->getId(),
1047 "Response $i, authn" );
1049 $this->assertSame( 0, $session->getUser()->getId(),
1050 "Response $i, authn" );
1052 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
1053 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1054 "Response $i, session state" );
1055 foreach ( $providers as $p ) {
1056 $this->assertSame( $response->status
, $p->postCalled
,
1057 "Response $i, post-auth callback called" );
1060 $this->assertNotNull( $session->getSecret( 'AuthManager::authnState' ),
1061 "Response $i, session state" );
1062 foreach ( $ret->neededRequests
as $neededReq ) {
1063 $this->assertEquals( AuthManager
::ACTION_LOGIN
, $neededReq->action
,
1064 "Response $i, neededRequest action" );
1066 $this->assertEquals(
1067 $ret->neededRequests
,
1068 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN_CONTINUE
),
1069 "Response $i, continuation check"
1071 foreach ( $providers as $p ) {
1072 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
1076 $state = $session->getSecret( 'AuthManager::authnState' );
1077 $maybeLink = $state['maybeLink'] ??
[];
1078 if ( $link && $response->status
=== AuthenticationResponse
::RESTART
) {
1079 $this->assertEquals(
1080 $response->createRequest
->maybeLink
,
1082 "Response $i, maybeLink"
1085 $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
1090 $this->assertSame( $req->rememberMe
, $session->shouldRememberUser(),
1091 'rememberMe checkbox had effect' );
1093 $this->assertNotSame( $req->rememberMe
, $session->shouldRememberUser(),
1094 'rememberMe checkbox wasn\'t applied' );
1098 public function provideAuthentication() {
1099 $rememberReq = new RememberMeAuthenticationRequest
;
1100 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
1102 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1103 $req->foobar
= 'baz';
1104 $restartResponse = AuthenticationResponse
::newRestart(
1105 $this->message( 'authmanager-authn-no-local-user' )
1107 $restartResponse->neededRequests
= [ $rememberReq ];
1109 $restartResponse2Pass = AuthenticationResponse
::newPass( null );
1110 $restartResponse2Pass->linkRequest
= $req;
1111 $restartResponse2 = AuthenticationResponse
::newRestart(
1112 $this->message( 'authmanager-authn-no-local-user-link' )
1114 $restartResponse2->createRequest
= new CreateFromLoginAuthenticationRequest(
1115 null, [ $req->getUniqueId() => $req ]
1117 $restartResponse2->createRequest
->action
= AuthManager
::ACTION_LOGIN
;
1118 $restartResponse2->neededRequests
= [ $rememberReq, $restartResponse2->createRequest
];
1120 $userName = 'UTSysop';
1123 'Failure in pre-auth' => [
1124 StatusValue
::newFatal( 'fail-from-pre' ),
1128 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
1129 AuthenticationResponse
::newFail(
1130 $this->message( 'authmanager-authn-not-in-progress' )
1134 'Failure in primary' => [
1135 StatusValue
::newGood(),
1137 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
1142 'All primary abstain' => [
1143 StatusValue
::newGood(),
1145 AuthenticationResponse
::newAbstain(),
1149 AuthenticationResponse
::newFail( $this->message( 'authmanager-authn-no-primary' ) )
1152 'Primary UI, then redirect, then fail' => [
1153 StatusValue
::newGood(),
1155 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1156 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
1157 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
1162 'Primary redirect, then abstain' => [
1163 StatusValue
::newGood(),
1165 $tmp = AuthenticationResponse
::newRedirect(
1166 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
1168 AuthenticationResponse
::newAbstain(),
1173 new \
DomainException(
1174 'MockPrimaryAuthenticationProvider::continuePrimaryAuthentication() returned ABSTAIN'
1178 'Primary UI, then pass with no local user' => [
1179 StatusValue
::newGood(),
1181 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1182 AuthenticationResponse
::newPass( null ),
1190 'Primary UI, then pass with no local user (link type)' => [
1191 StatusValue
::newGood(),
1193 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1194 $restartResponse2Pass,
1203 'Primary pass with invalid username' => [
1204 StatusValue
::newGood(),
1206 AuthenticationResponse
::newPass( '<>' ),
1210 new \
DomainException( 'MockPrimaryAuthenticationProvider returned an invalid username: <>' ),
1213 'Secondary fail' => [
1214 StatusValue
::newGood(),
1216 AuthenticationResponse
::newPass( $userName ),
1219 AuthenticationResponse
::newFail( $this->message( 'fail-in-secondary' ) ),
1223 'Secondary UI, then abstain' => [
1224 StatusValue
::newGood(),
1226 AuthenticationResponse
::newPass( $userName ),
1229 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1230 AuthenticationResponse
::newAbstain()
1234 AuthenticationResponse
::newPass( $userName ),
1237 'Secondary pass' => [
1238 StatusValue
::newGood(),
1240 AuthenticationResponse
::newPass( $userName ),
1243 AuthenticationResponse
::newPass()
1246 AuthenticationResponse
::newPass( $userName ),
1253 * @dataProvider provideUserExists
1254 * @param bool $primary1Exists
1255 * @param bool $primary2Exists
1256 * @param bool $expect
1258 public function testUserExists( $primary1Exists, $primary2Exists, $expect ) {
1259 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1260 $mock1->expects( $this->any() )->method( 'getUniqueId' )
1261 ->will( $this->returnValue( 'primary1' ) );
1262 $mock1->expects( $this->any() )->method( 'testUserExists' )
1263 ->with( $this->equalTo( 'UTSysop' ) )
1264 ->will( $this->returnValue( $primary1Exists ) );
1265 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1266 $mock2->expects( $this->any() )->method( 'getUniqueId' )
1267 ->will( $this->returnValue( 'primary2' ) );
1268 $mock2->expects( $this->any() )->method( 'testUserExists' )
1269 ->with( $this->equalTo( 'UTSysop' ) )
1270 ->will( $this->returnValue( $primary2Exists ) );
1271 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1273 $this->initializeManager( true );
1274 $this->assertSame( $expect, $this->manager
->userExists( 'UTSysop' ) );
1277 public static function provideUserExists() {
1279 [ false, false, false ],
1280 [ true, false, true ],
1281 [ false, true, true ],
1282 [ true, true, true ],
1287 * @dataProvider provideAllowsAuthenticationDataChange
1288 * @param StatusValue $primaryReturn
1289 * @param StatusValue $secondaryReturn
1290 * @param Status $expect
1292 public function testAllowsAuthenticationDataChange( $primaryReturn, $secondaryReturn, $expect ) {
1293 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1295 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1296 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1297 $mock1->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1298 ->with( $this->equalTo( $req ) )
1299 ->will( $this->returnValue( $primaryReturn ) );
1300 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
1301 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1302 $mock2->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1303 ->with( $this->equalTo( $req ) )
1304 ->will( $this->returnValue( $secondaryReturn ) );
1306 $this->primaryauthMocks
= [ $mock1 ];
1307 $this->secondaryauthMocks
= [ $mock2 ];
1308 $this->initializeManager( true );
1309 $this->assertEquals( $expect, $this->manager
->allowsAuthenticationDataChange( $req ) );
1312 public static function provideAllowsAuthenticationDataChange() {
1313 $ignored = \Status
::newGood( 'ignored' );
1314 $ignored->warning( 'authmanager-change-not-supported' );
1316 $okFromPrimary = StatusValue
::newGood();
1317 $okFromPrimary->warning( 'warning-from-primary' );
1318 $okFromSecondary = StatusValue
::newGood();
1319 $okFromSecondary->warning( 'warning-from-secondary' );
1323 StatusValue
::newGood(),
1324 StatusValue
::newGood(),
1328 StatusValue
::newGood(),
1329 StatusValue
::newGood( 'ignore' ),
1333 StatusValue
::newGood( 'ignored' ),
1334 StatusValue
::newGood(),
1338 StatusValue
::newGood( 'ignored' ),
1339 StatusValue
::newGood( 'ignored' ),
1343 StatusValue
::newFatal( 'fail from primary' ),
1344 StatusValue
::newGood(),
1345 \Status
::newFatal( 'fail from primary' ),
1349 StatusValue
::newGood(),
1350 \Status
::wrap( $okFromPrimary ),
1353 StatusValue
::newGood(),
1354 StatusValue
::newFatal( 'fail from secondary' ),
1355 \Status
::newFatal( 'fail from secondary' ),
1358 StatusValue
::newGood(),
1360 \Status
::wrap( $okFromSecondary ),
1365 public function testChangeAuthenticationData() {
1366 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1367 $req->username
= 'UTSysop';
1369 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1370 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1371 $mock1->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1372 ->with( $this->equalTo( $req ) );
1373 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1374 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1375 $mock2->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1376 ->with( $this->equalTo( $req ) );
1378 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1379 $this->initializeManager( true );
1380 $this->logger
->setCollect( true );
1381 $this->manager
->changeAuthenticationData( $req );
1382 $this->assertSame( [
1383 [ LogLevel
::INFO
, 'Changing authentication data for {user} class {what}' ],
1384 ], $this->logger
->getBuffer() );
1387 public function testCanCreateAccounts() {
1389 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
1390 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
1391 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
1394 foreach ( $types as $type => $can ) {
1395 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1396 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
1397 $mock->expects( $this->any() )->method( 'accountCreationType' )
1398 ->will( $this->returnValue( $type ) );
1399 $this->primaryauthMocks
= [ $mock ];
1400 $this->initializeManager( true );
1401 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
1405 public function testCheckAccountCreatePermissions() {
1406 $this->initializeManager( true );
1408 $this->setGroupPermissions( '*', 'createaccount', true );
1409 $this->assertEquals(
1411 $this->manager
->checkAccountCreatePermissions( new \User
)
1414 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1415 $readOnlyMode->setReason( 'Because' );
1416 $this->assertEquals(
1417 \Status
::newFatal( wfMessage( 'readonlytext', 'Because' ) ),
1418 $this->manager
->checkAccountCreatePermissions( new \User
)
1420 $readOnlyMode->setReason( false );
1422 $this->setGroupPermissions( '*', 'createaccount', false );
1423 $this->overrideMwServices();
1424 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1425 $this->assertFalse( $status->isOK() );
1426 $this->assertTrue( $status->hasMessage( 'badaccess-groups' ) );
1427 $this->setGroupPermissions( '*', 'createaccount', true );
1429 $user = \User
::newFromName( 'UTBlockee' );
1430 if ( $user->getID() == 0 ) {
1431 $user->addToDatabase();
1432 \TestUser
::setPasswordForUser( $user, 'UTBlockeePassword' );
1433 $user->saveSettings();
1435 $oldBlock = DatabaseBlock
::newFromTarget( 'UTBlockee' );
1437 // An old block will prevent our new one from saving.
1438 $oldBlock->delete();
1441 'address' => 'UTBlockee',
1442 'user' => $user->getID(),
1443 'by' => $this->getTestSysop()->getUser()->getId(),
1444 'reason' => __METHOD__
,
1445 'expiry' => time() +
100500,
1446 'createAccount' => true,
1448 $block = new DatabaseBlock( $blockOptions );
1450 $this->overrideMwServices();
1451 $status = $this->manager
->checkAccountCreatePermissions( $user );
1452 $this->assertFalse( $status->isOK() );
1453 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
1456 'address' => '127.0.0.0/24',
1457 'by' => $this->getTestSysop()->getUser()->getId(),
1458 'reason' => __METHOD__
,
1459 'expiry' => time() +
100500,
1460 'createAccount' => true,
1462 $block = new DatabaseBlock( $blockOptions );
1464 $scopeVariable = new ScopedCallback( [ $block, 'delete' ] );
1465 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1466 $this->assertFalse( $status->isOK() );
1467 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
1468 ScopedCallback
::consume( $scopeVariable );
1470 $this->setMwGlobals( [
1471 'wgEnableDnsBlacklist' => true,
1472 'wgDnsBlacklistUrls' => [
1473 'local.wmftest.net', // This will resolve for every subdomain, which works to test "listed?"
1475 'wgProxyWhitelist' => [],
1477 $this->overrideMwServices();
1478 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1479 $this->assertFalse( $status->isOK() );
1480 $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
1481 $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
1482 $this->overrideMwServices();
1483 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1484 $this->assertTrue( $status->isGood() );
1488 * @param string $uniq
1491 private static function usernameForCreation( $uniq = '' ) {
1494 $username = "UTAuthManagerTestAccountCreation" . $uniq . ++
$i;
1495 } while ( \User
::newFromName( $username )->getId() !== 0 );
1499 public function testCanCreateAccount() {
1500 $username = self
::usernameForCreation();
1501 $this->initializeManager();
1503 $this->assertEquals(
1504 \Status
::newFatal( 'authmanager-create-disabled' ),
1505 $this->manager
->canCreateAccount( $username )
1508 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1509 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1510 $mock->expects( $this->any() )->method( 'accountCreationType' )
1511 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1512 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1513 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1514 ->will( $this->returnValue( StatusValue
::newGood() ) );
1515 $this->primaryauthMocks
= [ $mock ];
1516 $this->initializeManager( true );
1518 $this->assertEquals(
1519 \Status
::newFatal( 'userexists' ),
1520 $this->manager
->canCreateAccount( $username )
1523 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1524 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1525 $mock->expects( $this->any() )->method( 'accountCreationType' )
1526 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1527 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1528 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1529 ->will( $this->returnValue( StatusValue
::newGood() ) );
1530 $this->primaryauthMocks
= [ $mock ];
1531 $this->initializeManager( true );
1533 $this->assertEquals(
1534 \Status
::newFatal( 'noname' ),
1535 $this->manager
->canCreateAccount( $username . '<>' )
1538 $this->assertEquals(
1539 \Status
::newFatal( 'userexists' ),
1540 $this->manager
->canCreateAccount( 'UTSysop' )
1543 $this->assertEquals(
1545 $this->manager
->canCreateAccount( $username )
1548 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1549 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1550 $mock->expects( $this->any() )->method( 'accountCreationType' )
1551 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1552 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1553 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1554 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1555 $this->primaryauthMocks
= [ $mock ];
1556 $this->initializeManager( true );
1558 $this->assertEquals(
1559 \Status
::newFatal( 'fail' ),
1560 $this->manager
->canCreateAccount( $username )
1564 public function testBeginAccountCreation() {
1565 $creator = \User
::newFromName( 'UTSysop' );
1566 $userReq = new UsernameAuthenticationRequest
;
1567 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1568 return $level === LogLevel
::DEBUG ?
null : $message;
1570 $this->initializeManager();
1572 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
1573 $this->hook( 'LocalUserCreated', $this->never() );
1575 $this->manager
->beginAccountCreation(
1576 $creator, [], 'http://localhost/'
1578 $this->fail( 'Expected exception not thrown' );
1579 } catch ( \LogicException
$ex ) {
1580 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1582 $this->unhook( 'LocalUserCreated' );
1584 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1587 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1588 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1589 $mock->expects( $this->any() )->method( 'accountCreationType' )
1590 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1591 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1592 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1593 ->will( $this->returnValue( StatusValue
::newGood() ) );
1594 $this->primaryauthMocks
= [ $mock ];
1595 $this->initializeManager( true );
1597 $this->hook( 'LocalUserCreated', $this->never() );
1598 $ret = $this->manager
->beginAccountCreation( $creator, [], 'http://localhost/' );
1599 $this->unhook( 'LocalUserCreated' );
1600 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1601 $this->assertSame( 'noname', $ret->message
->getKey() );
1603 $this->hook( 'LocalUserCreated', $this->never() );
1604 $userReq->username
= self
::usernameForCreation();
1605 $userReq2 = new UsernameAuthenticationRequest
;
1606 $userReq2->username
= $userReq->username
. 'X';
1607 $ret = $this->manager
->beginAccountCreation(
1608 $creator, [ $userReq, $userReq2 ], 'http://localhost/'
1610 $this->unhook( 'LocalUserCreated' );
1611 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1612 $this->assertSame( 'noname', $ret->message
->getKey() );
1614 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1615 $readOnlyMode->setReason( 'Because' );
1616 $this->hook( 'LocalUserCreated', $this->never() );
1617 $userReq->username
= self
::usernameForCreation();
1618 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1619 $this->unhook( 'LocalUserCreated' );
1620 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1621 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
1622 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1623 $readOnlyMode->setReason( false );
1625 $this->hook( 'LocalUserCreated', $this->never() );
1626 $userReq->username
= self
::usernameForCreation();
1627 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1628 $this->unhook( 'LocalUserCreated' );
1629 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1630 $this->assertSame( 'userexists', $ret->message
->getKey() );
1632 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1633 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1634 $mock->expects( $this->any() )->method( 'accountCreationType' )
1635 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1636 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1637 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1638 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1639 $this->primaryauthMocks
= [ $mock ];
1640 $this->initializeManager( true );
1642 $this->hook( 'LocalUserCreated', $this->never() );
1643 $userReq->username
= self
::usernameForCreation();
1644 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1645 $this->unhook( 'LocalUserCreated' );
1646 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1647 $this->assertSame( 'fail', $ret->message
->getKey() );
1649 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1650 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1651 $mock->expects( $this->any() )->method( 'accountCreationType' )
1652 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1653 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1654 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1655 ->will( $this->returnValue( StatusValue
::newGood() ) );
1656 $this->primaryauthMocks
= [ $mock ];
1657 $this->initializeManager( true );
1659 $this->hook( 'LocalUserCreated', $this->never() );
1660 $userReq->username
= self
::usernameForCreation() . '<>';
1661 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1662 $this->unhook( 'LocalUserCreated' );
1663 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1664 $this->assertSame( 'noname', $ret->message
->getKey() );
1666 $this->hook( 'LocalUserCreated', $this->never() );
1667 $userReq->username
= $creator->getName();
1668 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1669 $this->unhook( 'LocalUserCreated' );
1670 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1671 $this->assertSame( 'userexists', $ret->message
->getKey() );
1673 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1674 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1675 $mock->expects( $this->any() )->method( 'accountCreationType' )
1676 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1677 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1678 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1679 ->will( $this->returnValue( StatusValue
::newGood() ) );
1680 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
1681 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1682 $this->primaryauthMocks
= [ $mock ];
1683 $this->initializeManager( true );
1685 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1686 ->setMethods( [ 'populateUser' ] )
1688 $req->expects( $this->any() )->method( 'populateUser' )
1689 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1690 $userReq->username
= self
::usernameForCreation();
1691 $ret = $this->manager
->beginAccountCreation(
1692 $creator, [ $userReq, $req ], 'http://localhost/'
1694 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1695 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1697 $req = new UserDataAuthenticationRequest
;
1698 $userReq->username
= self
::usernameForCreation();
1700 $ret = $this->manager
->beginAccountCreation(
1701 $creator, [ $userReq, $req ], 'http://localhost/'
1703 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1704 $this->assertSame( 'fail', $ret->message
->getKey() );
1706 $this->manager
->beginAccountCreation(
1707 \User
::newFromName( $userReq->username
), [ $userReq, $req ], 'http://localhost/'
1709 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1710 $this->assertSame( 'fail', $ret->message
->getKey() );
1713 public function testContinueAccountCreation() {
1714 $creator = \User
::newFromName( 'UTSysop' );
1715 $username = self
::usernameForCreation();
1716 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1717 return $level === LogLevel
::DEBUG ?
null : $message;
1719 $this->initializeManager();
1723 'username' => $username,
1725 'creatorname' => $username,
1728 'primaryResponse' => null,
1730 'ranPreTests' => true,
1733 $this->hook( 'LocalUserCreated', $this->never() );
1735 $this->manager
->continueAccountCreation( [] );
1736 $this->fail( 'Expected exception not thrown' );
1737 } catch ( \LogicException
$ex ) {
1738 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1740 $this->unhook( 'LocalUserCreated' );
1742 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1743 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1744 $mock->expects( $this->any() )->method( 'accountCreationType' )
1745 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1746 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1747 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )->will(
1748 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
1750 $this->primaryauthMocks
= [ $mock ];
1751 $this->initializeManager( true );
1753 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', null );
1754 $this->hook( 'LocalUserCreated', $this->never() );
1755 $ret = $this->manager
->continueAccountCreation( [] );
1756 $this->unhook( 'LocalUserCreated' );
1757 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1758 $this->assertSame( 'authmanager-create-not-in-progress', $ret->message
->getKey() );
1760 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1761 [ 'username' => "$username<>" ] +
$session );
1762 $this->hook( 'LocalUserCreated', $this->never() );
1763 $ret = $this->manager
->continueAccountCreation( [] );
1764 $this->unhook( 'LocalUserCreated' );
1765 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1766 $this->assertSame( 'noname', $ret->message
->getKey() );
1768 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1771 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $session );
1772 $this->hook( 'LocalUserCreated', $this->never() );
1773 $cache = \ObjectCache
::getLocalClusterInstance();
1774 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1775 $ret = $this->manager
->continueAccountCreation( [] );
1777 $this->unhook( 'LocalUserCreated' );
1778 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1779 $this->assertSame( 'usernameinprogress', $ret->message
->getKey() );
1780 // This error shouldn't remove the existing session, because the
1781 // raced-with process "owns" it.
1783 $session, $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1786 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1787 [ 'username' => $creator->getName() ] +
$session );
1788 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1789 $readOnlyMode->setReason( 'Because' );
1790 $this->hook( 'LocalUserCreated', $this->never() );
1791 $ret = $this->manager
->continueAccountCreation( [] );
1792 $this->unhook( 'LocalUserCreated' );
1793 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1794 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
1795 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1796 $readOnlyMode->setReason( false );
1798 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1799 [ 'username' => $creator->getName() ] +
$session );
1800 $this->hook( 'LocalUserCreated', $this->never() );
1801 $ret = $this->manager
->continueAccountCreation( [] );
1802 $this->unhook( 'LocalUserCreated' );
1803 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1804 $this->assertSame( 'userexists', $ret->message
->getKey() );
1806 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1809 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1810 [ 'userid' => $creator->getId() ] +
$session );
1811 $this->hook( 'LocalUserCreated', $this->never() );
1813 $ret = $this->manager
->continueAccountCreation( [] );
1814 $this->fail( 'Expected exception not thrown' );
1815 } catch ( \UnexpectedValueException
$ex ) {
1816 $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
1818 $this->unhook( 'LocalUserCreated' );
1820 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1823 $id = $creator->getId();
1824 $name = $creator->getName();
1825 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1826 [ 'username' => $name, 'userid' => $id +
1 ] +
$session );
1827 $this->hook( 'LocalUserCreated', $this->never() );
1829 $ret = $this->manager
->continueAccountCreation( [] );
1830 $this->fail( 'Expected exception not thrown' );
1831 } catch ( \UnexpectedValueException
$ex ) {
1832 $this->assertEquals(
1833 "User \"{$name}\" exists, but ID $id !== " . ( $id +
1 ) . '!', $ex->getMessage()
1836 $this->unhook( 'LocalUserCreated' );
1838 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1841 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1842 ->setMethods( [ 'populateUser' ] )
1844 $req->expects( $this->any() )->method( 'populateUser' )
1845 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1846 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1847 [ 'reqs' => [ $req ] ] +
$session );
1848 $ret = $this->manager
->continueAccountCreation( [] );
1849 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1850 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1852 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1857 * @dataProvider provideAccountCreation
1858 * @param StatusValue $preTest
1859 * @param StatusValue $primaryTest
1860 * @param StatusValue $secondaryTest
1861 * @param array $primaryResponses
1862 * @param array $secondaryResponses
1863 * @param array $managerResponses
1865 public function testAccountCreation(
1866 StatusValue
$preTest, $primaryTest, $secondaryTest,
1867 array $primaryResponses, array $secondaryResponses, array $managerResponses
1869 $creator = \User
::newFromName( 'UTSysop' );
1870 $username = self
::usernameForCreation();
1872 $this->initializeManager();
1874 // Set up lots of mocks...
1875 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1876 $req->preTest
= $preTest;
1877 $req->primaryTest
= $primaryTest;
1878 $req->secondaryTest
= $secondaryTest;
1879 $req->primary
= $primaryResponses;
1880 $req->secondary
= $secondaryResponses;
1882 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
1883 $class = ucfirst( $key ) . 'AuthenticationProvider';
1884 $mocks[$key] = $this->getMockForAbstractClass(
1885 "MediaWiki\\Auth\\$class", [], "Mock$class"
1887 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
1888 ->will( $this->returnValue( $key ) );
1889 $mocks[$key]->expects( $this->any() )->method( 'testUserForCreation' )
1890 ->will( $this->returnValue( StatusValue
::newGood() ) );
1891 $mocks[$key]->expects( $this->any() )->method( 'testForAccountCreation' )
1892 ->will( $this->returnCallback(
1893 function ( $user, $creatorIn, $reqs )
1894 use ( $username, $creator, $req, $key )
1896 $this->assertSame( $username, $user->getName() );
1897 $this->assertSame( $creator->getId(), $creatorIn->getId() );
1898 $this->assertSame( $creator->getName(), $creatorIn->getName() );
1900 foreach ( $reqs as $r ) {
1901 $this->assertSame( $username, $r->username
);
1902 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1904 $this->assertTrue( $foundReq, '$reqs contains $req' );
1910 for ( $i = 2; $i <= 3; $i++
) {
1911 $mocks[$key . $i] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
1912 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
1913 ->will( $this->returnValue( $key . $i ) );
1914 $mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' )
1915 ->will( $this->returnValue( StatusValue
::newGood() ) );
1916 $mocks[$key . $i]->expects( $this->atMost( 1 ) )->method( 'testForAccountCreation' )
1917 ->will( $this->returnValue( StatusValue
::newGood() ) );
1921 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
1922 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1923 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
1924 ->will( $this->returnValue( false ) );
1925 $ct = count( $req->primary
);
1926 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1927 $this->assertSame( $username, $user->getName() );
1928 $this->assertSame( 'UTSysop', $creator->getName() );
1930 foreach ( $reqs as $r ) {
1931 $this->assertSame( $username, $r->username
);
1932 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1934 $this->assertTrue( $foundReq, '$reqs contains $req' );
1935 return array_shift( $req->primary
);
1937 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
1938 ->method( 'beginPrimaryAccountCreation' )
1939 ->will( $callback );
1940 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1941 ->method( 'continuePrimaryAccountCreation' )
1942 ->will( $callback );
1944 $ct = count( $req->secondary
);
1945 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1946 $this->assertSame( $username, $user->getName() );
1947 $this->assertSame( 'UTSysop', $creator->getName() );
1949 foreach ( $reqs as $r ) {
1950 $this->assertSame( $username, $r->username
);
1951 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1953 $this->assertTrue( $foundReq, '$reqs contains $req' );
1954 return array_shift( $req->secondary
);
1956 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
1957 ->method( 'beginSecondaryAccountCreation' )
1958 ->will( $callback );
1959 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1960 ->method( 'continueSecondaryAccountCreation' )
1961 ->will( $callback );
1963 $abstain = AuthenticationResponse
::newAbstain();
1964 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
1965 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
1966 $mocks['primary2']->expects( $this->any() )->method( 'testUserExists' )
1967 ->will( $this->returnValue( false ) );
1968 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountCreation' )
1969 ->will( $this->returnValue( $abstain ) );
1970 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1971 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
1972 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_NONE
) );
1973 $mocks['primary3']->expects( $this->any() )->method( 'testUserExists' )
1974 ->will( $this->returnValue( false ) );
1975 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountCreation' );
1976 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1977 $mocks['secondary2']->expects( $this->atMost( 1 ) )
1978 ->method( 'beginSecondaryAccountCreation' )
1979 ->will( $this->returnValue( $abstain ) );
1980 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1981 $mocks['secondary3']->expects( $this->atMost( 1 ) )
1982 ->method( 'beginSecondaryAccountCreation' )
1983 ->will( $this->returnValue( $abstain ) );
1984 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1986 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
1987 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary'], $mocks['primary2'] ];
1988 $this->secondaryauthMocks
= [
1989 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2']
1992 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
1993 return $level === LogLevel
::DEBUG ?
null : $message;
1996 $this->initializeManager( true );
1998 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
1999 $this->equalTo( AuthenticationResponse
::PASS
),
2000 $this->equalTo( AuthenticationResponse
::FAIL
)
2002 $providers = array_merge(
2003 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
2005 foreach ( $providers as $p ) {
2006 $p->postCalled
= false;
2007 $p->expects( $this->atMost( 1 ) )->method( 'postAccountCreation' )
2008 ->willReturnCallback( function ( $user, $creator, $response )
2009 use ( $constraint, $p, $username )
2011 $this->assertInstanceOf( \User
::class, $user );
2012 $this->assertSame( $username, $user->getName() );
2013 $this->assertSame( 'UTSysop', $creator->getName() );
2014 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
2015 $this->assertThat( $response->status
, $constraint );
2016 $p->postCalled
= $response->status
;
2020 // We're testing with $wgNewUserLog = false, so assert that it worked
2021 $dbw = wfGetDB( DB_MASTER
);
2022 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2026 foreach ( $managerResponses as $i => $response ) {
2027 $success = $response instanceof AuthenticationResponse
&&
2028 $response->status
=== AuthenticationResponse
::PASS
;
2029 if ( $i === 'created' ) {
2031 $this->hook( 'LocalUserCreated', $this->once() )
2033 $this->callback( function ( $user ) use ( $username ) {
2034 return $user->getName() === $username;
2036 $this->equalTo( false )
2038 $expectLog[] = [ LogLevel
::INFO
, "Creating user {user} during account creation" ];
2040 $this->hook( 'LocalUserCreated', $this->never() );
2046 $userReq = new UsernameAuthenticationRequest
;
2047 $userReq->username
= $username;
2048 $ret = $this->manager
->beginAccountCreation(
2049 $creator, [ $userReq, $req ], 'http://localhost/'
2052 $ret = $this->manager
->continueAccountCreation( [ $req ] );
2054 if ( $response instanceof \Exception
) {
2055 $this->fail( 'Expected exception not thrown', "Response $i" );
2057 } catch ( \Exception
$ex ) {
2058 if ( !$response instanceof \Exception
) {
2061 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
2063 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2064 "Response $i, exception, session state"
2066 $this->unhook( 'LocalUserCreated' );
2070 $this->unhook( 'LocalUserCreated' );
2072 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
2075 $this->assertNotNull( $ret->loginRequest
, "Response $i, login marker" );
2076 $this->assertContains(
2077 $ret->loginRequest
, $this->managerPriv
->createdAccountAuthenticationRequests
,
2078 "Response $i, login marker"
2083 "MediaWiki\Auth\AuthManager::continueAccountCreation: Account creation succeeded for {user}"
2086 // Set some fields in the expected $response that we couldn't
2087 // know in provideAccountCreation().
2088 $response->username
= $username;
2089 $response->loginRequest
= $ret->loginRequest
;
2091 $this->assertNull( $ret->loginRequest
, "Response $i, login marker" );
2092 $this->assertSame( [], $this->managerPriv
->createdAccountAuthenticationRequests
,
2093 "Response $i, login marker" );
2095 $ret->message
= $this->message( $ret->message
);
2096 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
2097 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
2099 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2100 "Response $i, session state"
2102 foreach ( $providers as $p ) {
2103 $this->assertSame( $response->status
, $p->postCalled
,
2104 "Response $i, post-auth callback called" );
2107 $this->assertNotNull(
2108 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2109 "Response $i, session state"
2111 foreach ( $ret->neededRequests
as $neededReq ) {
2112 $this->assertEquals( AuthManager
::ACTION_CREATE
, $neededReq->action
,
2113 "Response $i, neededRequest action" );
2115 $this->assertEquals(
2116 $ret->neededRequests
,
2117 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_CREATE_CONTINUE
),
2118 "Response $i, continuation check"
2120 foreach ( $providers as $p ) {
2121 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
2126 $this->assertNotEquals( 0, \User
::idFromName( $username ) );
2128 $this->assertEquals( 0, \User
::idFromName( $username ) );
2134 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
2138 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2142 public function provideAccountCreation() {
2143 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
2144 $good = StatusValue
::newGood();
2147 'Pre-creation test fail in pre' => [
2148 StatusValue
::newFatal( 'fail-from-pre' ), $good, $good,
2152 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
2155 'Pre-creation test fail in primary' => [
2156 $good, StatusValue
::newFatal( 'fail-from-primary' ), $good,
2160 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2163 'Pre-creation test fail in secondary' => [
2164 $good, $good, StatusValue
::newFatal( 'fail-from-secondary' ),
2168 AuthenticationResponse
::newFail( $this->message( 'fail-from-secondary' ) ),
2171 'Failure in primary' => [
2172 $good, $good, $good,
2174 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2179 'All primary abstain' => [
2180 $good, $good, $good,
2182 AuthenticationResponse
::newAbstain(),
2186 AuthenticationResponse
::newFail( $this->message( 'authmanager-create-no-primary' ) )
2189 'Primary UI, then redirect, then fail' => [
2190 $good, $good, $good,
2192 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2193 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
2194 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
2199 'Primary redirect, then abstain' => [
2200 $good, $good, $good,
2202 $tmp = AuthenticationResponse
::newRedirect(
2203 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
2205 AuthenticationResponse
::newAbstain(),
2210 new \
DomainException(
2211 'MockPrimaryAuthenticationProvider::continuePrimaryAccountCreation() returned ABSTAIN'
2215 'Primary UI, then pass; secondary abstain' => [
2216 $good, $good, $good,
2218 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2219 AuthenticationResponse
::newPass(),
2222 AuthenticationResponse
::newAbstain(),
2226 'created' => AuthenticationResponse
::newPass( '' ),
2229 'Primary pass; secondary UI then pass' => [
2230 $good, $good, $good,
2232 AuthenticationResponse
::newPass( '' ),
2235 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2236 AuthenticationResponse
::newPass( '' ),
2240 AuthenticationResponse
::newPass( '' ),
2243 'Primary pass; secondary fail' => [
2244 $good, $good, $good,
2246 AuthenticationResponse
::newPass(),
2249 AuthenticationResponse
::newFail( $this->message( '...' ) ),
2252 'created' => new \
DomainException(
2253 'MockSecondaryAuthenticationProvider::beginSecondaryAccountCreation() returned FAIL. ' .
2254 'Secondary providers are not allowed to fail account creation, ' .
2255 'that should have been done via testForAccountCreation().'
2263 * @dataProvider provideAccountCreationLogging
2264 * @param bool $isAnon
2265 * @param string|null $logSubtype
2267 public function testAccountCreationLogging( $isAnon, $logSubtype ) {
2268 $creator = $isAnon ?
new \User
: \User
::newFromName( 'UTSysop' );
2269 $username = self
::usernameForCreation();
2271 $this->initializeManager();
2273 // Set up lots of mocks...
2274 $mock = $this->getMockForAbstractClass(
2275 \MediaWiki\Auth\PrimaryAuthenticationProvider
::class, []
2277 $mock->expects( $this->any() )->method( 'getUniqueId' )
2278 ->will( $this->returnValue( 'primary' ) );
2279 $mock->expects( $this->any() )->method( 'testUserForCreation' )
2280 ->will( $this->returnValue( StatusValue
::newGood() ) );
2281 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
2282 ->will( $this->returnValue( StatusValue
::newGood() ) );
2283 $mock->expects( $this->any() )->method( 'accountCreationType' )
2284 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2285 $mock->expects( $this->any() )->method( 'testUserExists' )
2286 ->will( $this->returnValue( false ) );
2287 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
2288 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
2289 $mock->expects( $this->any() )->method( 'finishAccountCreation' )
2290 ->will( $this->returnValue( $logSubtype ) );
2292 $this->primaryauthMocks
= [ $mock ];
2293 $this->initializeManager( true );
2294 $this->logger
->setCollect( true );
2296 $this->config
->set( 'NewUserLog', true );
2298 $dbw = wfGetDB( DB_MASTER
);
2299 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2301 $userReq = new UsernameAuthenticationRequest
;
2302 $userReq->username
= $username;
2303 $reasonReq = new CreationReasonAuthenticationRequest
;
2304 $reasonReq->reason
= $this->toString();
2305 $ret = $this->manager
->beginAccountCreation(
2306 $creator, [ $userReq, $reasonReq ], 'http://localhost/'
2309 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
2311 $user = \User
::newFromName( $username );
2312 $this->assertNotEquals( 0, $user->getId(), 'sanity check' );
2313 $this->assertNotEquals( $creator->getId(), $user->getId(), 'sanity check' );
2315 $data = \DatabaseLogEntry
::getSelectQueryData();
2316 $rows = iterator_to_array( $dbw->select(
2320 'log_id > ' . (int)$maxLogId,
2321 'log_type' => 'newusers'
2327 $this->assertCount( 1, $rows );
2328 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2330 $this->assertSame( $logSubtype ?
: ( $isAnon ?
'create' : 'create2' ), $entry->getSubtype() );
2332 $isAnon ?
$user->getId() : $creator->getId(),
2333 $entry->getPerformer()->getId()
2336 $isAnon ?
$user->getName() : $creator->getName(),
2337 $entry->getPerformer()->getName()
2339 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2340 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2341 $this->assertSame( $this->toString(), $entry->getComment() );
2344 public static function provideAccountCreationLogging() {
2349 [ false, 'byemail' ],
2353 public function testAutoAccountCreation() {
2354 // PHPUnit seems to have a bug where it will call the ->with()
2355 // callbacks for our hooks again after the test is run (WTF?), which
2356 // breaks here because $username no longer matches $user by the end of
2358 $workaroundPHPUnitBug = false;
2360 $username = self
::usernameForCreation();
2361 $expectedSource = AuthManager
::AUTOCREATE_SOURCE_SESSION
;
2362 $this->initializeManager();
2364 $this->setGroupPermissions( '*', 'createaccount', true );
2365 $this->setGroupPermissions( '*', 'autocreateaccount', false );
2367 $this->mergeMwGlobalArrayValue( 'wgObjectCaches',
2368 [ __METHOD__
=> [ 'class' => 'HashBagOStuff' ] ] );
2369 $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__
] );
2370 // Supply services with updated globals
2371 $this->overrideMwServices();
2373 // Set up lots of mocks...
2375 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2376 $class = ucfirst( $key ) . 'AuthenticationProvider';
2377 $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
2378 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2379 ->will( $this->returnValue( $key ) );
2382 $good = StatusValue
::newGood();
2383 $ok = StatusValue
::newFatal( 'ok' );
2384 $callback = $this->callback( function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
2385 return $workaroundPHPUnitBug ||
$user->getName() === $username;
2387 $callback2 = $this->callback(
2388 function ( $source ) use ( &$expectedSource, &$workaroundPHPUnitBug ) {
2389 return $workaroundPHPUnitBug ||
$source === $expectedSource;
2393 $mocks['pre']->expects( $this->exactly( 13 ) )->method( 'testUserForCreation' )
2394 ->with( $callback, $callback2 )
2395 ->will( $this->onConsecutiveCalls(
2396 $ok, $ok, $ok, // For testing permissions
2397 StatusValue
::newFatal( 'fail-in-pre' ), $good, $good,
2398 $good, // backoff test
2399 $good, // addToDatabase fails test
2400 $good, // addToDatabase throws test
2401 $good, // addToDatabase exists test
2402 $good, $good, $good // success
2405 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
2406 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2407 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
2408 ->will( $this->returnValue( true ) );
2409 $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
2410 ->with( $callback, $callback2 )
2411 ->will( $this->onConsecutiveCalls(
2412 StatusValue
::newFatal( 'fail-in-primary' ), $good,
2413 $good, // backoff test
2414 $good, // addToDatabase fails test
2415 $good, // addToDatabase throws test
2416 $good, // addToDatabase exists test
2419 $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2420 ->with( $callback, $callback2 );
2422 $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
2423 ->with( $callback, $callback2 )
2424 ->will( $this->onConsecutiveCalls(
2425 StatusValue
::newFatal( 'fail-in-secondary' ),
2426 $good, // backoff test
2427 $good, // addToDatabase fails test
2428 $good, // addToDatabase throws test
2429 $good, // addToDatabase exists test
2432 $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2433 ->with( $callback, $callback2 );
2435 $this->preauthMocks
= [ $mocks['pre'] ];
2436 $this->primaryauthMocks
= [ $mocks['primary'] ];
2437 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2438 $this->initializeManager( true );
2439 $session = $this->request
->getSession();
2441 $logger = new \
TestLogger( true, function ( $m ) {
2442 $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
2445 $this->manager
->setLogger( $logger );
2448 $user = \User
::newFromName( 'UTSysop' );
2449 $this->manager
->autoCreateUser( $user, 'InvalidSource', true );
2450 $this->fail( 'Expected exception not thrown' );
2451 } catch ( \InvalidArgumentException
$ex ) {
2452 $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
2455 // First, check an existing user
2457 $user = \User
::newFromName( 'UTSysop' );
2458 $this->hook( 'LocalUserCreated', $this->never() );
2459 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2460 $this->unhook( 'LocalUserCreated' );
2461 $expect = \Status
::newGood();
2462 $expect->warning( 'userexists' );
2463 $this->assertEquals( $expect, $ret );
2464 $this->assertNotEquals( 0, $user->getId() );
2465 $this->assertSame( 'UTSysop', $user->getName() );
2466 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2467 $this->assertSame( [
2468 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2469 ], $logger->getBuffer() );
2470 $logger->clearBuffer();
2473 $user = \User
::newFromName( 'UTSysop' );
2474 $this->hook( 'LocalUserCreated', $this->never() );
2475 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2476 $this->unhook( 'LocalUserCreated' );
2477 $expect = \Status
::newGood();
2478 $expect->warning( 'userexists' );
2479 $this->assertEquals( $expect, $ret );
2480 $this->assertNotEquals( 0, $user->getId() );
2481 $this->assertSame( 'UTSysop', $user->getName() );
2482 $this->assertEquals( 0, $session->getUser()->getId() );
2483 $this->assertSame( [
2484 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2485 ], $logger->getBuffer() );
2486 $logger->clearBuffer();
2488 // Wiki is read-only
2490 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
2491 $readOnlyMode->setReason( 'Because' );
2492 $user = \User
::newFromName( $username );
2493 $this->hook( 'LocalUserCreated', $this->never() );
2494 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2495 $this->unhook( 'LocalUserCreated' );
2496 $this->assertEquals( \Status
::newFatal( wfMessage( 'readonlytext', 'Because' ) ), $ret );
2497 $this->assertEquals( 0, $user->getId() );
2498 $this->assertNotEquals( $username, $user->getName() );
2499 $this->assertEquals( 0, $session->getUser()->getId() );
2500 $this->assertSame( [
2501 [ LogLevel
::DEBUG
, 'denied by wfReadOnly(): {reason}' ],
2502 ], $logger->getBuffer() );
2503 $logger->clearBuffer();
2504 $readOnlyMode->setReason( false );
2506 // Session blacklisted
2508 $session->set( 'AuthManager::AutoCreateBlacklist', 'test' );
2509 $user = \User
::newFromName( $username );
2510 $this->hook( 'LocalUserCreated', $this->never() );
2511 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2512 $this->unhook( 'LocalUserCreated' );
2513 $this->assertEquals( \Status
::newFatal( 'test' ), $ret );
2514 $this->assertEquals( 0, $user->getId() );
2515 $this->assertNotEquals( $username, $user->getName() );
2516 $this->assertEquals( 0, $session->getUser()->getId() );
2517 $this->assertSame( [
2518 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2519 ], $logger->getBuffer() );
2520 $logger->clearBuffer();
2523 $session->set( 'AuthManager::AutoCreateBlacklist', StatusValue
::newFatal( 'test2' ) );
2524 $user = \User
::newFromName( $username );
2525 $this->hook( 'LocalUserCreated', $this->never() );
2526 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2527 $this->unhook( 'LocalUserCreated' );
2528 $this->assertEquals( \Status
::newFatal( 'test2' ), $ret );
2529 $this->assertEquals( 0, $user->getId() );
2530 $this->assertNotEquals( $username, $user->getName() );
2531 $this->assertEquals( 0, $session->getUser()->getId() );
2532 $this->assertSame( [
2533 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2534 ], $logger->getBuffer() );
2535 $logger->clearBuffer();
2539 $user = \User
::newFromName( $username . '@' );
2540 $this->hook( 'LocalUserCreated', $this->never() );
2541 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2542 $this->unhook( 'LocalUserCreated' );
2543 $this->assertEquals( \Status
::newFatal( 'noname' ), $ret );
2544 $this->assertEquals( 0, $user->getId() );
2545 $this->assertNotEquals( $username . '@', $user->getId() );
2546 $this->assertEquals( 0, $session->getUser()->getId() );
2547 $this->assertSame( [
2548 [ LogLevel
::DEBUG
, 'name "{username}" is not creatable' ],
2549 ], $logger->getBuffer() );
2550 $logger->clearBuffer();
2551 $this->assertSame( 'noname', $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2553 // IP unable to create accounts
2554 $this->setGroupPermissions( '*', 'createaccount', false );
2555 $this->setGroupPermissions( '*', 'autocreateaccount', false );
2556 $this->overrideMwServices();
2558 $user = \User
::newFromName( $username );
2559 $this->hook( 'LocalUserCreated', $this->never() );
2560 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2561 $this->unhook( 'LocalUserCreated' );
2562 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-noperm' ), $ret );
2563 $this->assertEquals( 0, $user->getId() );
2564 $this->assertNotEquals( $username, $user->getName() );
2565 $this->assertEquals( 0, $session->getUser()->getId() );
2566 $this->assertSame( [
2567 [ LogLevel
::DEBUG
, 'IP lacks the ability to create or autocreate accounts' ],
2568 ], $logger->getBuffer() );
2569 $logger->clearBuffer();
2571 'authmanager-autocreate-noperm', $session->get( 'AuthManager::AutoCreateBlacklist' )
2574 // maintenance scripts always work
2575 $expectedSource = AuthManager
::AUTOCREATE_SOURCE_MAINT
;
2576 $this->setGroupPermissions( '*', 'createaccount', false );
2577 $this->setGroupPermissions( '*', 'autocreateaccount', false );
2579 $user = \User
::newFromName( $username );
2580 $this->hook( 'LocalUserCreated', $this->never() );
2581 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_MAINT
, true );
2582 $this->unhook( 'LocalUserCreated' );
2583 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2585 // Test that both permutations of permissions are allowed
2586 // (this hits the two "ok" entries in $mocks['pre'])
2587 $expectedSource = AuthManager
::AUTOCREATE_SOURCE_SESSION
;
2588 $this->setGroupPermissions( '*', 'createaccount', false );
2589 $this->setGroupPermissions( '*', 'autocreateaccount', true );
2591 $user = \User
::newFromName( $username );
2592 $this->hook( 'LocalUserCreated', $this->never() );
2593 $this->overrideMwServices();
2594 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2595 $this->unhook( 'LocalUserCreated' );
2596 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2598 $this->setGroupPermissions( '*', 'createaccount', true );
2599 $this->setGroupPermissions( '*', 'autocreateaccount', false );
2601 $user = \User
::newFromName( $username );
2602 $this->hook( 'LocalUserCreated', $this->never() );
2603 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2604 $this->unhook( 'LocalUserCreated' );
2605 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2606 $logger->clearBuffer();
2610 $user = \User
::newFromName( $username );
2611 $this->hook( 'LocalUserCreated', $this->never() );
2612 $cache = \ObjectCache
::getLocalClusterInstance();
2613 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2614 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2616 $this->unhook( 'LocalUserCreated' );
2617 $this->assertEquals( \Status
::newFatal( 'usernameinprogress' ), $ret );
2618 $this->assertEquals( 0, $user->getId() );
2619 $this->assertNotEquals( $username, $user->getName() );
2620 $this->assertEquals( 0, $session->getUser()->getId() );
2621 $this->assertSame( [
2622 [ LogLevel
::DEBUG
, 'Could not acquire account creation lock' ],
2623 ], $logger->getBuffer() );
2624 $logger->clearBuffer();
2626 // Test pre-authentication provider fail
2628 $user = \User
::newFromName( $username );
2629 $this->hook( 'LocalUserCreated', $this->never() );
2630 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2631 $this->unhook( 'LocalUserCreated' );
2632 $this->assertEquals( \Status
::newFatal( 'fail-in-pre' ), $ret );
2633 $this->assertEquals( 0, $user->getId() );
2634 $this->assertNotEquals( $username, $user->getName() );
2635 $this->assertEquals( 0, $session->getUser()->getId() );
2636 $this->assertSame( [
2637 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2638 ], $logger->getBuffer() );
2639 $logger->clearBuffer();
2640 $this->assertEquals(
2641 StatusValue
::newFatal( 'fail-in-pre' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2645 $user = \User
::newFromName( $username );
2646 $this->hook( 'LocalUserCreated', $this->never() );
2647 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2648 $this->unhook( 'LocalUserCreated' );
2649 $this->assertEquals( \Status
::newFatal( 'fail-in-primary' ), $ret );
2650 $this->assertEquals( 0, $user->getId() );
2651 $this->assertNotEquals( $username, $user->getName() );
2652 $this->assertEquals( 0, $session->getUser()->getId() );
2653 $this->assertSame( [
2654 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2655 ], $logger->getBuffer() );
2656 $logger->clearBuffer();
2657 $this->assertEquals(
2658 StatusValue
::newFatal( 'fail-in-primary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2662 $user = \User
::newFromName( $username );
2663 $this->hook( 'LocalUserCreated', $this->never() );
2664 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2665 $this->unhook( 'LocalUserCreated' );
2666 $this->assertEquals( \Status
::newFatal( 'fail-in-secondary' ), $ret );
2667 $this->assertEquals( 0, $user->getId() );
2668 $this->assertNotEquals( $username, $user->getName() );
2669 $this->assertEquals( 0, $session->getUser()->getId() );
2670 $this->assertSame( [
2671 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2672 ], $logger->getBuffer() );
2673 $logger->clearBuffer();
2674 $this->assertEquals(
2675 StatusValue
::newFatal( 'fail-in-secondary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2679 $cache = \ObjectCache
::getLocalClusterInstance();
2680 $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2681 $cache->set( $backoffKey, true );
2683 $user = \User
::newFromName( $username );
2684 $this->hook( 'LocalUserCreated', $this->never() );
2685 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2686 $this->unhook( 'LocalUserCreated' );
2687 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-exception' ), $ret );
2688 $this->assertEquals( 0, $user->getId() );
2689 $this->assertNotEquals( $username, $user->getName() );
2690 $this->assertEquals( 0, $session->getUser()->getId() );
2691 $this->assertSame( [
2692 [ LogLevel
::DEBUG
, '{username} denied by prior creation attempt failures' ],
2693 ], $logger->getBuffer() );
2694 $logger->clearBuffer();
2695 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2696 $cache->delete( $backoffKey );
2698 // Test addToDatabase fails
2700 $user = $this->getMockBuilder( \User
::class )
2701 ->setMethods( [ 'addToDatabase' ] )->getMock();
2702 $user->expects( $this->once() )->method( 'addToDatabase' )
2703 ->will( $this->returnValue( \Status
::newFatal( 'because' ) ) );
2704 $user->setName( $username );
2705 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2706 $this->assertEquals( \Status
::newFatal( 'because' ), $ret );
2707 $this->assertEquals( 0, $user->getId() );
2708 $this->assertNotEquals( $username, $user->getName() );
2709 $this->assertEquals( 0, $session->getUser()->getId() );
2710 $this->assertSame( [
2711 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2712 [ LogLevel
::ERROR
, '{username} failed with message {msg}' ],
2713 ], $logger->getBuffer() );
2714 $logger->clearBuffer();
2715 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2717 // Test addToDatabase throws an exception
2718 $cache = \ObjectCache
::getLocalClusterInstance();
2719 $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2720 $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
2722 $user = $this->getMockBuilder( \User
::class )
2723 ->setMethods( [ 'addToDatabase' ] )->getMock();
2724 $user->expects( $this->once() )->method( 'addToDatabase' )
2725 ->will( $this->throwException( new \
Exception( 'Excepted' ) ) );
2726 $user->setName( $username );
2728 $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2729 $this->fail( 'Expected exception not thrown' );
2730 } catch ( \Exception
$ex ) {
2731 $this->assertSame( 'Excepted', $ex->getMessage() );
2733 $this->assertEquals( 0, $user->getId() );
2734 $this->assertEquals( 0, $session->getUser()->getId() );
2735 $this->assertSame( [
2736 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2737 [ LogLevel
::ERROR
, '{username} failed with exception {exception}' ],
2738 ], $logger->getBuffer() );
2739 $logger->clearBuffer();
2740 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2741 $this->assertNotEquals( false, $cache->get( $backoffKey ) );
2742 $cache->delete( $backoffKey );
2744 // Test addToDatabase fails because the user already exists.
2746 $user = $this->getMockBuilder( \User
::class )
2747 ->setMethods( [ 'addToDatabase' ] )->getMock();
2748 $user->expects( $this->once() )->method( 'addToDatabase' )
2749 ->will( $this->returnCallback( function () use ( $username, &$user ) {
2750 $oldUser = \User
::newFromName( $username );
2751 $status = $oldUser->addToDatabase();
2752 $this->assertTrue( $status->isOK(), 'sanity check' );
2753 $user->setId( $oldUser->getId() );
2754 return \Status
::newFatal( 'userexists' );
2756 $user->setName( $username );
2757 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2758 $expect = \Status
::newGood();
2759 $expect->warning( 'userexists' );
2760 $this->assertEquals( $expect, $ret );
2761 $this->assertNotEquals( 0, $user->getId() );
2762 $this->assertEquals( $username, $user->getName() );
2763 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2764 $this->assertSame( [
2765 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2766 [ LogLevel
::INFO
, '{username} already exists locally (race)' ],
2767 ], $logger->getBuffer() );
2768 $logger->clearBuffer();
2769 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2773 $username = self
::usernameForCreation();
2774 $user = \User
::newFromName( $username );
2775 $this->hook( 'LocalUserCreated', $this->once() )
2776 ->with( $callback, $this->equalTo( true ) );
2777 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2778 $this->unhook( 'LocalUserCreated' );
2779 $this->assertEquals( \Status
::newGood(), $ret );
2780 $this->assertNotEquals( 0, $user->getId() );
2781 $this->assertEquals( $username, $user->getName() );
2782 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2783 $this->assertSame( [
2784 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2785 ], $logger->getBuffer() );
2786 $logger->clearBuffer();
2788 $dbw = wfGetDB( DB_MASTER
);
2789 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2791 $username = self
::usernameForCreation();
2792 $user = \User
::newFromName( $username );
2793 $this->hook( 'LocalUserCreated', $this->once() )
2794 ->with( $callback, $this->equalTo( true ) );
2795 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2796 $this->unhook( 'LocalUserCreated' );
2797 $this->assertEquals( \Status
::newGood(), $ret );
2798 $this->assertNotEquals( 0, $user->getId() );
2799 $this->assertEquals( $username, $user->getName() );
2800 $this->assertEquals( 0, $session->getUser()->getId() );
2801 $this->assertSame( [
2802 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2803 ], $logger->getBuffer() );
2804 $logger->clearBuffer();
2807 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2810 $this->config
->set( 'NewUserLog', true );
2812 $username = self
::usernameForCreation();
2813 $user = \User
::newFromName( $username );
2814 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2815 $this->assertEquals( \Status
::newGood(), $ret );
2816 $logger->clearBuffer();
2818 $data = \DatabaseLogEntry
::getSelectQueryData();
2819 $rows = iterator_to_array( $dbw->select(
2823 'log_id > ' . (int)$maxLogId,
2824 'log_type' => 'newusers'
2830 $this->assertCount( 1, $rows );
2831 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2833 $this->assertSame( 'autocreate', $entry->getSubtype() );
2834 $this->assertSame( $user->getId(), $entry->getPerformer()->getId() );
2835 $this->assertSame( $user->getName(), $entry->getPerformer()->getName() );
2836 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2837 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2839 $workaroundPHPUnitBug = true;
2843 * @dataProvider provideGetAuthenticationRequests
2844 * @param string $action
2845 * @param array $expect
2846 * @param array $state
2848 public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
2849 $makeReq = function ( $key ) use ( $action ) {
2850 $req = $this->createMock( AuthenticationRequest
::class );
2851 $req->expects( $this->any() )->method( 'getUniqueId' )
2852 ->will( $this->returnValue( $key ) );
2853 $req->action
= $action === AuthManager
::ACTION_UNLINK ? AuthManager
::ACTION_REMOVE
: $action;
2857 $cmpReqs = function ( $a, $b ) {
2858 $ret = strcmp( get_class( $a ), get_class( $b ) );
2860 $ret = strcmp( $a->key
, $b->key
);
2865 $good = StatusValue
::newGood();
2868 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2869 $class = ucfirst( $key ) . 'AuthenticationProvider';
2870 $mocks[$key] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" )
2872 'getUniqueId', 'getAuthenticationRequests', 'providerAllowsAuthenticationDataChange',
2874 ->getMockForAbstractClass();
2875 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2876 ->will( $this->returnValue( $key ) );
2877 $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2878 ->will( $this->returnCallback( function ( $action ) use ( $key, $makeReq ) {
2879 return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
2881 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
2882 ->will( $this->returnValue( $good ) );
2887 PrimaryAuthenticationProvider
::TYPE_NONE
,
2888 PrimaryAuthenticationProvider
::TYPE_CREATE
,
2889 PrimaryAuthenticationProvider
::TYPE_LINK
2891 $class = 'PrimaryAuthenticationProvider';
2892 $mocks["primary-$type"] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" )
2894 'getUniqueId', 'accountCreationType', 'getAuthenticationRequests',
2895 'providerAllowsAuthenticationDataChange',
2897 ->getMockForAbstractClass();
2898 $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
2899 ->will( $this->returnValue( "primary-$type" ) );
2900 $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
2901 ->will( $this->returnValue( $type ) );
2902 $mocks["primary-$type"]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2903 ->will( $this->returnCallback( function ( $action ) use ( $type, $makeReq ) {
2904 return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
2906 $mocks["primary-$type"]->expects( $this->any() )
2907 ->method( 'providerAllowsAuthenticationDataChange' )
2908 ->will( $this->returnValue( $good ) );
2909 $this->primaryauthMocks
[] = $mocks["primary-$type"];
2912 $mocks['primary2'] = $this->getMockBuilder( PrimaryAuthenticationProvider
::class )
2914 'getUniqueId', 'accountCreationType', 'getAuthenticationRequests',
2915 'providerAllowsAuthenticationDataChange',
2917 ->getMockForAbstractClass();
2918 $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
2919 ->will( $this->returnValue( 'primary2' ) );
2920 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
2921 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
2922 $mocks['primary2']->expects( $this->any() )->method( 'getAuthenticationRequests' )
2923 ->will( $this->returnValue( [] ) );
2924 $mocks['primary2']->expects( $this->any() )
2925 ->method( 'providerAllowsAuthenticationDataChange' )
2926 ->will( $this->returnCallback( function ( $req ) use ( $good ) {
2927 return $req->key
=== 'generic' ? StatusValue
::newFatal( 'no' ) : $good;
2929 $this->primaryauthMocks
[] = $mocks['primary2'];
2931 $this->preauthMocks
= [ $mocks['pre'] ];
2932 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2933 $this->initializeManager( true );
2936 if ( isset( $state['continueRequests'] ) ) {
2937 $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
2939 if ( $action === AuthManager
::ACTION_LOGIN_CONTINUE
) {
2940 $this->request
->getSession()->setSecret( 'AuthManager::authnState', $state );
2941 } elseif ( $action === AuthManager
::ACTION_CREATE_CONTINUE
) {
2942 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $state );
2943 } elseif ( $action === AuthManager
::ACTION_LINK_CONTINUE
) {
2944 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', $state );
2948 $expectReqs = array_map( $makeReq, $expect );
2949 if ( $action === AuthManager
::ACTION_LOGIN
) {
2950 $req = new RememberMeAuthenticationRequest
;
2951 $req->action
= $action;
2952 $req->required
= AuthenticationRequest
::REQUIRED
;
2953 $expectReqs[] = $req;
2954 } elseif ( $action === AuthManager
::ACTION_CREATE
) {
2955 $req = new UsernameAuthenticationRequest
;
2956 $req->action
= $action;
2957 $expectReqs[] = $req;
2958 $req = new UserDataAuthenticationRequest
;
2959 $req->action
= $action;
2960 $req->required
= AuthenticationRequest
::REQUIRED
;
2961 $expectReqs[] = $req;
2963 usort( $expectReqs, $cmpReqs );
2965 $actual = $this->manager
->getAuthenticationRequests( $action );
2966 foreach ( $actual as $req ) {
2967 // Don't test this here.
2968 $req->required
= AuthenticationRequest
::REQUIRED
;
2970 usort( $actual, $cmpReqs );
2972 $this->assertEquals( $expectReqs, $actual );
2974 // Test CreationReasonAuthenticationRequest gets returned
2975 if ( $action === AuthManager
::ACTION_CREATE
) {
2976 $req = new CreationReasonAuthenticationRequest
;
2977 $req->action
= $action;
2978 $req->required
= AuthenticationRequest
::REQUIRED
;
2979 $expectReqs[] = $req;
2980 usort( $expectReqs, $cmpReqs );
2982 $actual = $this->manager
->getAuthenticationRequests( $action, \User
::newFromName( 'UTSysop' ) );
2983 foreach ( $actual as $req ) {
2984 // Don't test this here.
2985 $req->required
= AuthenticationRequest
::REQUIRED
;
2987 usort( $actual, $cmpReqs );
2989 $this->assertEquals( $expectReqs, $actual );
2993 public static function provideGetAuthenticationRequests() {
2996 AuthManager
::ACTION_LOGIN
,
2997 [ 'pre-login', 'primary-none-login', 'primary-create-login',
2998 'primary-link-login', 'secondary-login', 'generic' ],
3001 AuthManager
::ACTION_CREATE
,
3002 [ 'pre-create', 'primary-none-create', 'primary-create-create',
3003 'primary-link-create', 'secondary-create', 'generic' ],
3006 AuthManager
::ACTION_LINK
,
3007 [ 'primary-link-link', 'generic' ],
3010 AuthManager
::ACTION_CHANGE
,
3011 [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
3012 'secondary-change' ],
3015 AuthManager
::ACTION_REMOVE
,
3016 [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
3017 'secondary-remove' ],
3020 AuthManager
::ACTION_UNLINK
,
3021 [ 'primary-link-remove' ],
3024 AuthManager
::ACTION_LOGIN_CONTINUE
,
3028 AuthManager
::ACTION_LOGIN_CONTINUE
,
3029 $reqs = [ 'continue-login', 'foo', 'bar' ],
3031 'continueRequests' => $reqs,
3035 AuthManager
::ACTION_CREATE_CONTINUE
,
3039 AuthManager
::ACTION_CREATE_CONTINUE
,
3040 $reqs = [ 'continue-create', 'foo', 'bar' ],
3042 'continueRequests' => $reqs,
3046 AuthManager
::ACTION_LINK_CONTINUE
,
3050 AuthManager
::ACTION_LINK_CONTINUE
,
3051 $reqs = [ 'continue-link', 'foo', 'bar' ],
3053 'continueRequests' => $reqs,
3059 public function testGetAuthenticationRequestsRequired() {
3060 $makeReq = function ( $key, $required ) {
3061 $req = $this->createMock( AuthenticationRequest
::class );
3062 $req->expects( $this->any() )->method( 'getUniqueId' )
3063 ->will( $this->returnValue( $key ) );
3064 $req->action
= AuthManager
::ACTION_LOGIN
;
3066 $req->required
= $required;
3069 $cmpReqs = function ( $a, $b ) {
3070 $ret = strcmp( get_class( $a ), get_class( $b ) );
3072 $ret = strcmp( $a->key
, $b->key
);
3077 $good = StatusValue
::newGood();
3079 $primary1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3080 $primary1->expects( $this->any() )->method( 'getUniqueId' )
3081 ->will( $this->returnValue( 'primary1' ) );
3082 $primary1->expects( $this->any() )->method( 'accountCreationType' )
3083 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3084 $primary1->expects( $this->any() )->method( 'getAuthenticationRequests' )
3085 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3087 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3088 $makeReq( "required", AuthenticationRequest
::REQUIRED
),
3089 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3090 $makeReq( "foo", AuthenticationRequest
::REQUIRED
),
3091 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3092 $makeReq( "baz", AuthenticationRequest
::OPTIONAL
),
3096 $primary2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3097 $primary2->expects( $this->any() )->method( 'getUniqueId' )
3098 ->will( $this->returnValue( 'primary2' ) );
3099 $primary2->expects( $this->any() )->method( 'accountCreationType' )
3100 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3101 $primary2->expects( $this->any() )->method( 'getAuthenticationRequests' )
3102 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3104 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3105 $makeReq( "required2", AuthenticationRequest
::REQUIRED
),
3106 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3110 $secondary = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3111 $secondary->expects( $this->any() )->method( 'getUniqueId' )
3112 ->will( $this->returnValue( 'secondary' ) );
3113 $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
3114 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3116 $makeReq( "foo", AuthenticationRequest
::OPTIONAL
),
3117 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3118 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3122 $rememberReq = new RememberMeAuthenticationRequest
;
3123 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
3125 $this->primaryauthMocks
= [ $primary1, $primary2 ];
3126 $this->secondaryauthMocks
= [ $secondary ];
3127 $this->initializeManager( true );
3129 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3132 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3133 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3134 $makeReq( "required2", AuthenticationRequest
::PRIMARY_REQUIRED
),
3135 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3136 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3137 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3138 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3139 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3141 usort( $actual, $cmpReqs );
3142 usort( $expected, $cmpReqs );
3143 $this->assertEquals( $expected, $actual );
3145 $this->primaryauthMocks
= [ $primary1 ];
3146 $this->secondaryauthMocks
= [ $secondary ];
3147 $this->initializeManager( true );
3149 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3152 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3153 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3154 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3155 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3156 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3157 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3159 usort( $actual, $cmpReqs );
3160 usort( $expected, $cmpReqs );
3161 $this->assertEquals( $expected, $actual );
3164 public function testAllowsPropertyChange() {
3166 foreach ( [ 'primary', 'secondary' ] as $key ) {
3167 $class = ucfirst( $key ) . 'AuthenticationProvider';
3168 $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
3169 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3170 ->will( $this->returnValue( $key ) );
3171 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
3172 ->will( $this->returnCallback( function ( $prop ) use ( $key ) {
3173 return $prop !== $key;
3177 $this->primaryauthMocks
= [ $mocks['primary'] ];
3178 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
3179 $this->initializeManager( true );
3181 $this->assertTrue( $this->manager
->allowsPropertyChange( 'foo' ) );
3182 $this->assertFalse( $this->manager
->allowsPropertyChange( 'primary' ) );
3183 $this->assertFalse( $this->manager
->allowsPropertyChange( 'secondary' ) );
3186 public function testAutoCreateOnLogin() {
3187 $username = self
::usernameForCreation();
3189 $req = $this->createMock( AuthenticationRequest
::class );
3191 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3192 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3193 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3194 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3195 $mock->expects( $this->any() )->method( 'accountCreationType' )
3196 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3197 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3198 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3199 ->will( $this->returnValue( StatusValue
::newGood() ) );
3201 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3202 $mock2->expects( $this->any() )->method( 'getUniqueId' )
3203 ->will( $this->returnValue( 'secondary' ) );
3204 $mock2->expects( $this->any() )->method( 'beginSecondaryAuthentication' )->will(
3206 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) )
3209 $mock2->expects( $this->any() )->method( 'continueSecondaryAuthentication' )
3210 ->will( $this->returnValue( AuthenticationResponse
::newAbstain() ) );
3211 $mock2->expects( $this->any() )->method( 'testUserForCreation' )
3212 ->will( $this->returnValue( StatusValue
::newGood() ) );
3214 $this->primaryauthMocks
= [ $mock ];
3215 $this->secondaryauthMocks
= [ $mock2 ];
3216 $this->initializeManager( true );
3217 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3218 $session = $this->request
->getSession();
3221 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3224 $callback = $this->callback( function ( $user ) use ( $username ) {
3225 return $user->getName() === $username;
3228 $this->hook( 'UserLoggedIn', $this->never() );
3229 $this->hook( 'LocalUserCreated', $this->once() )->with( $callback, $this->equalTo( true ) );
3230 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3231 $this->unhook( 'LocalUserCreated' );
3232 $this->unhook( 'UserLoggedIn' );
3233 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
3235 $id = (int)\User
::newFromName( $username )->getId();
3236 $this->assertNotSame( 0, \User
::newFromName( $username )->getId() );
3237 $this->assertSame( 0, $session->getUser()->getId() );
3239 $this->hook( 'UserLoggedIn', $this->once() )->with( $callback );
3240 $this->hook( 'LocalUserCreated', $this->never() );
3241 $ret = $this->manager
->continueAuthentication( [] );
3242 $this->unhook( 'LocalUserCreated' );
3243 $this->unhook( 'UserLoggedIn' );
3244 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
3245 $this->assertSame( $username, $ret->username
);
3246 $this->assertSame( $id, $session->getUser()->getId() );
3249 public function testAutoCreateFailOnLogin() {
3250 $username = self
::usernameForCreation();
3252 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3253 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3254 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3255 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3256 $mock->expects( $this->any() )->method( 'accountCreationType' )
3257 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3258 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3259 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3260 ->will( $this->returnValue( StatusValue
::newFatal( 'fail-from-primary' ) ) );
3262 $this->primaryauthMocks
= [ $mock ];
3263 $this->initializeManager( true );
3264 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3265 $session = $this->request
->getSession();
3268 $this->assertSame( 0, $session->getUser()->getId(),
3270 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3273 $this->hook( 'UserLoggedIn', $this->never() );
3274 $this->hook( 'LocalUserCreated', $this->never() );
3275 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3276 $this->unhook( 'LocalUserCreated' );
3277 $this->unhook( 'UserLoggedIn' );
3278 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3279 $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message
->getKey() );
3281 $this->assertSame( 0, \User
::newFromName( $username )->getId() );
3282 $this->assertSame( 0, $session->getUser()->getId() );
3285 public function testAuthenticationSessionData() {
3286 $this->initializeManager( true );
3288 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3289 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3290 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3291 $this->assertSame( 'foo!', $this->manager
->getAuthenticationSessionData( 'foo' ) );
3292 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3293 $this->manager
->removeAuthenticationSessionData( 'foo' );
3294 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3295 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3296 $this->manager
->removeAuthenticationSessionData( 'bar' );
3297 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3299 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3300 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3301 $this->manager
->removeAuthenticationSessionData( null );
3302 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3303 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3306 public function testCanLinkAccounts() {
3308 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
3309 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
3310 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
3313 foreach ( $types as $type => $can ) {
3314 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3315 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
3316 $mock->expects( $this->any() )->method( 'accountCreationType' )
3317 ->will( $this->returnValue( $type ) );
3318 $this->primaryauthMocks
= [ $mock ];
3319 $this->initializeManager( true );
3320 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
3324 public function testBeginAccountLink() {
3325 $user = \User
::newFromName( 'UTSysop' );
3326 $this->initializeManager();
3328 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', 'test' );
3330 $this->manager
->beginAccountLink( $user, [], 'http://localhost/' );
3331 $this->fail( 'Expected exception not thrown' );
3332 } catch ( \LogicException
$ex ) {
3333 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3335 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3337 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3338 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3339 $mock->expects( $this->any() )->method( 'accountCreationType' )
3340 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3341 $this->primaryauthMocks
= [ $mock ];
3342 $this->initializeManager( true );
3344 $ret = $this->manager
->beginAccountLink( new \User
, [], 'http://localhost/' );
3345 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3346 $this->assertSame( 'noname', $ret->message
->getKey() );
3348 $ret = $this->manager
->beginAccountLink(
3349 \User
::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
3351 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3352 $this->assertSame( 'authmanager-userdoesnotexist', $ret->message
->getKey() );
3355 public function testContinueAccountLink() {
3356 $user = \User
::newFromName( 'UTSysop' );
3357 $this->initializeManager();
3360 'userid' => $user->getId(),
3361 'username' => $user->getName(),
3366 $this->manager
->continueAccountLink( [] );
3367 $this->fail( 'Expected exception not thrown' );
3368 } catch ( \LogicException
$ex ) {
3369 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3372 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3373 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3374 $mock->expects( $this->any() )->method( 'accountCreationType' )
3375 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3376 $mock->expects( $this->any() )->method( 'beginPrimaryAccountLink' )->will(
3377 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
3379 $this->primaryauthMocks
= [ $mock ];
3380 $this->initializeManager( true );
3382 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', null );
3383 $ret = $this->manager
->continueAccountLink( [] );
3384 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3385 $this->assertSame( 'authmanager-link-not-in-progress', $ret->message
->getKey() );
3387 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3388 [ 'username' => $user->getName() . '<>' ] +
$session );
3389 $ret = $this->manager
->continueAccountLink( [] );
3390 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3391 $this->assertSame( 'noname', $ret->message
->getKey() );
3392 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3394 $id = $user->getId();
3395 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3396 [ 'userid' => $id +
1 ] +
$session );
3398 $ret = $this->manager
->continueAccountLink( [] );
3399 $this->fail( 'Expected exception not thrown' );
3400 } catch ( \UnexpectedValueException
$ex ) {
3401 $this->assertEquals(
3402 "User \"{$user->getName()}\" is valid, but ID $id !== " . ( $id +
1 ) . '!',
3406 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3410 * @dataProvider provideAccountLink
3411 * @param StatusValue $preTest
3412 * @param array $primaryResponses
3413 * @param array $managerResponses
3415 public function testAccountLink(
3416 StatusValue
$preTest, array $primaryResponses, array $managerResponses
3418 $user = \User
::newFromName( 'UTSysop' );
3420 $this->initializeManager();
3422 // Set up lots of mocks...
3423 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3424 $req->primary
= $primaryResponses;
3427 foreach ( [ 'pre', 'primary' ] as $key ) {
3428 $class = ucfirst( $key ) . 'AuthenticationProvider';
3429 $mocks[$key] = $this->getMockForAbstractClass(
3430 "MediaWiki\\Auth\\$class", [], "Mock$class"
3432 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3433 ->will( $this->returnValue( $key ) );
3435 for ( $i = 2; $i <= 3; $i++
) {
3436 $mocks[$key . $i] = $this->getMockForAbstractClass(
3437 "MediaWiki\\Auth\\$class", [], "Mock$class"
3439 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
3440 ->will( $this->returnValue( $key . $i ) );
3444 $mocks['pre']->expects( $this->any() )->method( 'testForAccountLink' )
3445 ->will( $this->returnCallback(
3447 use ( $user, $preTest )
3449 $this->assertSame( $user->getId(), $u->getId() );
3450 $this->assertSame( $user->getName(), $u->getName() );
3455 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
3456 ->will( $this->returnValue( StatusValue
::newGood() ) );
3458 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
3459 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3460 $ct = count( $req->primary
);
3461 $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req ) {
3462 $this->assertSame( $user->getId(), $u->getId() );
3463 $this->assertSame( $user->getName(), $u->getName() );
3465 foreach ( $reqs as $r ) {
3466 $this->assertSame( $user->getName(), $r->username
);
3467 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
3469 $this->assertTrue( $foundReq, '$reqs contains $req' );
3470 return array_shift( $req->primary
);
3472 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
3473 ->method( 'beginPrimaryAccountLink' )
3474 ->will( $callback );
3475 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
3476 ->method( 'continuePrimaryAccountLink' )
3477 ->will( $callback );
3479 $abstain = AuthenticationResponse
::newAbstain();
3480 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
3481 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3482 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
3483 ->will( $this->returnValue( $abstain ) );
3484 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3485 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
3486 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3487 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
3488 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3490 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
3491 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
3492 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
3493 return $level === LogLevel
::DEBUG ?
null : $message;
3495 $this->initializeManager( true );
3497 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
3498 $this->equalTo( AuthenticationResponse
::PASS
),
3499 $this->equalTo( AuthenticationResponse
::FAIL
)
3501 $providers = array_merge( $this->preauthMocks
, $this->primaryauthMocks
);
3502 foreach ( $providers as $p ) {
3503 $p->postCalled
= false;
3504 $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
3505 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
3506 $this->assertInstanceOf( \User
::class, $user );
3507 $this->assertSame( 'UTSysop', $user->getName() );
3508 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
3509 $this->assertThat( $response->status
, $constraint );
3510 $p->postCalled
= $response->status
;
3517 foreach ( $managerResponses as $i => $response ) {
3518 if ( $response instanceof AuthenticationResponse
&&
3519 $response->status
=== AuthenticationResponse
::PASS
3521 $expectLog[] = [ LogLevel
::INFO
, 'Account linked to {user} by primary' ];
3527 $ret = $this->manager
->beginAccountLink( $user, [ $req ], 'http://localhost/' );
3529 $ret = $this->manager
->continueAccountLink( [ $req ] );
3531 if ( $response instanceof \Exception
) {
3532 $this->fail( 'Expected exception not thrown', "Response $i" );
3534 } catch ( \Exception
$ex ) {
3535 if ( !$response instanceof \Exception
) {
3538 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
3539 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3540 "Response $i, exception, session state" );
3544 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
3546 $ret->message
= $this->message( $ret->message
);
3547 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
3548 if ( $response->status
=== AuthenticationResponse
::PASS ||
3549 $response->status
=== AuthenticationResponse
::FAIL
3551 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3552 "Response $i, session state" );
3553 foreach ( $providers as $p ) {
3554 $this->assertSame( $response->status
, $p->postCalled
,
3555 "Response $i, post-auth callback called" );
3558 $this->assertNotNull(
3559 $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3560 "Response $i, session state"
3562 foreach ( $ret->neededRequests
as $neededReq ) {
3563 $this->assertEquals( AuthManager
::ACTION_LINK
, $neededReq->action
,
3564 "Response $i, neededRequest action" );
3566 $this->assertEquals(
3567 $ret->neededRequests
,
3568 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LINK_CONTINUE
),
3569 "Response $i, continuation check"
3571 foreach ( $providers as $p ) {
3572 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
3579 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
3582 public function provideAccountLink() {
3583 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3584 $good = StatusValue
::newGood();
3587 'Pre-link test fail in pre' => [
3588 StatusValue
::newFatal( 'fail-from-pre' ),
3591 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
3594 'Failure in primary' => [
3597 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
3601 'All primary abstain' => [
3604 AuthenticationResponse
::newAbstain(),
3607 AuthenticationResponse
::newFail( $this->message( 'authmanager-link-no-primary' ) )
3610 'Primary UI, then redirect, then fail' => [
3613 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3614 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
3615 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
3619 'Primary redirect, then abstain' => [
3622 $tmp = AuthenticationResponse
::newRedirect(
3623 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
3625 AuthenticationResponse
::newAbstain(),
3629 new \
DomainException(
3630 'MockPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
3634 'Primary UI, then pass' => [
3637 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3638 AuthenticationResponse
::newPass(),
3642 AuthenticationResponse
::newPass( '' ),
3648 AuthenticationResponse
::newPass( '' ),
3651 AuthenticationResponse
::newPass( '' ),