3 namespace MediaWiki\Auth
;
5 use MediaWiki\Session\SessionInfo
;
6 use MediaWiki\Session\UserInfo
;
13 * @covers MediaWiki\Auth\AuthManager
15 class AuthManagerTest
extends \MediaWikiTestCase
{
16 /** @var WebRequest */
20 /** @var \\Psr\\Log\\LoggerInterface */
23 protected $preauthMocks = [];
24 protected $primaryauthMocks = [];
25 protected $secondaryauthMocks = [];
27 /** @var AuthManager */
29 /** @var TestingAccessWrapper */
30 protected $managerPriv;
32 protected function setUp() {
33 global $wgDisableAuthManager;
36 if ( $wgDisableAuthManager ) {
37 $this->markTestSkipped( '$wgDisableAuthManager is set' );
40 $this->setMwGlobals( [ 'wgAuth' => null ] );
41 $this->stashMwGlobals( [ 'wgHooks' ] );
45 * Sets a mock on a hook
47 * @param object $expect From $this->once(), $this->never(), etc.
48 * @return object $mock->expects( $expect )->method( ... ).
50 protected function hook( $hook, $expect ) {
52 $mock = $this->getMock( __CLASS__
, [ "on$hook" ] );
53 $wgHooks[$hook] = [ $mock ];
54 return $mock->expects( $expect )->method( "on$hook" );
61 protected function unhook( $hook ) {
67 * Ensure a value is a clean Message object
68 * @param string|Message $key
69 * @param array $params
72 protected function message( $key, $params = [] ) {
73 if ( $key === null ) {
76 if ( $key instanceof \MessageSpecifier
) {
77 $params = $key->getParams();
78 $key = $key->getKey();
80 return new \
Message( $key, $params, \Language
::factory( 'en' ) );
84 * Initialize the AuthManagerConfig variable in $this->config
86 * Uses data from the various 'mocks' fields.
88 protected function initializeConfig() {
98 foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
99 $key = $type . 'Mocks';
100 foreach ( $this->$key as $mock ) {
101 $config[$type][$mock->getUniqueId()] = [ 'factory' => function () use ( $mock ) {
107 $this->config
->set( 'AuthManagerConfig', $config );
108 $this->config
->set( 'LanguageCode', 'en' );
109 $this->config
->set( 'NewUserLog', false );
113 * Initialize $this->manager
114 * @param bool $regen Force a call to $this->initializeConfig()
116 protected function initializeManager( $regen = false ) {
117 if ( $regen ||
!$this->config
) {
118 $this->config
= new \
HashConfig();
120 if ( $regen ||
!$this->request
) {
121 $this->request
= new \
FauxRequest();
123 if ( !$this->logger
) {
124 $this->logger
= new \
TestLogger();
127 if ( $regen ||
!$this->config
->has( 'AuthManagerConfig' ) ) {
128 $this->initializeConfig();
130 $this->manager
= new AuthManager( $this->request
, $this->config
);
131 $this->manager
->setLogger( $this->logger
);
132 $this->managerPriv
= \TestingAccessWrapper
::newFromObject( $this->manager
);
136 * Setup SessionManager with a mock session provider
137 * @param bool|null $canChangeUser If non-null, canChangeUser will be mocked to return this
138 * @param array $methods Additional methods to mock
139 * @return array (MediaWiki\Session\SessionProvider, ScopedCallback)
141 protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
142 if ( !$this->config
) {
143 $this->config
= new \
HashConfig();
144 $this->initializeConfig();
146 $this->config
->set( 'ObjectCacheSessionExpiry', 100 );
148 $methods[] = '__toString';
149 $methods[] = 'describe';
150 if ( $canChangeUser !== null ) {
151 $methods[] = 'canChangeUser';
153 $provider = $this->getMockBuilder( 'DummySessionProvider' )
154 ->setMethods( $methods )
156 $provider->expects( $this->any() )->method( '__toString' )
157 ->will( $this->returnValue( 'MockSessionProvider' ) );
158 $provider->expects( $this->any() )->method( 'describe' )
159 ->will( $this->returnValue( 'MockSessionProvider sessions' ) );
160 if ( $canChangeUser !== null ) {
161 $provider->expects( $this->any() )->method( 'canChangeUser' )
162 ->will( $this->returnValue( $canChangeUser ) );
164 $this->config
->set( 'SessionProviders', [
165 [ 'factory' => function () use ( $provider ) {
170 $manager = new \MediaWiki\Session\
SessionManager( [
171 'config' => $this->config
,
172 'logger' => new \Psr\Log\
NullLogger(),
173 'store' => new \
HashBagOStuff(),
175 \TestingAccessWrapper
::newFromObject( $manager )->getProvider( (string)$provider );
177 $reset = \MediaWiki\Session\TestUtils
::setSessionManagerSingleton( $manager );
179 if ( $this->request
) {
180 $manager->getSessionForRequest( $this->request
);
183 return [ $provider, $reset ];
186 public function testSingleton() {
187 // Temporarily clear out the global singleton, if any, to test creating
189 $rProp = new \
ReflectionProperty( AuthManager
::class, 'instance' );
190 $rProp->setAccessible( true );
191 $old = $rProp->getValue();
192 $cb = new \
ScopedCallback( [ $rProp, 'setValue' ], [ $old ] );
193 $rProp->setValue( null );
195 $singleton = AuthManager
::singleton();
196 $this->assertInstanceOf( AuthManager
::class, AuthManager
::singleton() );
197 $this->assertSame( $singleton, AuthManager
::singleton() );
198 $this->assertSame( \RequestContext
::getMain()->getRequest(), $singleton->getRequest() );
200 \RequestContext
::getMain()->getConfig(),
201 \TestingAccessWrapper
::newFromObject( $singleton )->config
204 $this->setMwGlobals( [ 'wgDisableAuthManager' => true ] );
206 AuthManager
::singleton();
207 $this->fail( 'Expected exception not thrown' );
208 } catch ( \BadMethodCallException
$ex ) {
209 $this->assertSame( '$wgDisableAuthManager is set', $ex->getMessage() );
213 public function testCanAuthenticateNow() {
214 $this->initializeManager();
216 list( $provider, $reset ) = $this->getMockSessionProvider( false );
217 $this->assertFalse( $this->manager
->canAuthenticateNow() );
218 \ScopedCallback
::consume( $reset );
220 list( $provider, $reset ) = $this->getMockSessionProvider( true );
221 $this->assertTrue( $this->manager
->canAuthenticateNow() );
222 \ScopedCallback
::consume( $reset );
225 public function testNormalizeUsername() {
227 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
228 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
229 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
230 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
232 foreach ( $mocks as $key => $mock ) {
233 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
235 $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
236 ->with( $this->identicalTo( 'XYZ' ) )
237 ->willReturn( 'Foo' );
238 $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
239 ->with( $this->identicalTo( 'XYZ' ) )
240 ->willReturn( 'Foo' );
241 $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
242 ->with( $this->identicalTo( 'XYZ' ) )
243 ->willReturn( null );
244 $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
245 ->with( $this->identicalTo( 'XYZ' ) )
246 ->willReturn( 'Bar!' );
248 $this->primaryauthMocks
= $mocks;
250 $this->initializeManager();
252 $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager
->normalizeUsername( 'XYZ' ) );
256 * @dataProvider provideSecuritySensitiveOperationStatus
257 * @param bool $mutableSession
259 public function testSecuritySensitiveOperationStatus( $mutableSession ) {
260 $this->logger
= new \Psr\Log\
NullLogger();
261 $user = \User
::newFromName( 'UTSysop' );
263 $reauth = $mutableSession ? AuthManager
::SEC_REAUTH
: AuthManager
::SEC_FAIL
;
265 list( $provider, $reset ) = $this->getMockSessionProvider(
266 $mutableSession, [ 'provideSessionInfo' ]
268 $provider->expects( $this->any() )->method( 'provideSessionInfo' )
269 ->will( $this->returnCallback( function () use ( $provider, &$provideUser ) {
270 return new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
271 'provider' => $provider,
272 'id' => \DummySessionProvider
::ID
,
274 'userInfo' => UserInfo
::newFromUser( $provideUser, true )
277 $this->initializeManager();
279 $this->config
->set( 'ReauthenticateTime', [] );
280 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [] );
281 $provideUser = new \User
;
282 $session = $provider->getManager()->getSessionForRequest( $this->request
);
283 $this->assertSame( 0, $session->getUser()->getId(), 'sanity check' );
285 // Anonymous user => reauth
286 $session->set( 'AuthManager:lastAuthId', 0 );
287 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
288 $this->assertSame( $reauth, $this->manager
->securitySensitiveOperationStatus( 'foo' ) );
290 $provideUser = $user;
291 $session = $provider->getManager()->getSessionForRequest( $this->request
);
292 $this->assertSame( $user->getId(), $session->getUser()->getId(), 'sanity check' );
294 // Error for no default (only gets thrown for non-anonymous user)
295 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
296 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
298 $this->manager
->securitySensitiveOperationStatus( 'foo' );
299 $this->fail( 'Expected exception not thrown' );
300 } catch ( \UnexpectedValueException
$ex ) {
303 ?
'$wgReauthenticateTime lacks a default'
304 : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
309 if ( $mutableSession ) {
310 $this->config
->set( 'ReauthenticateTime', [
316 // Mismatched user ID
317 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
318 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
320 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
323 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
326 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
330 $session->set( 'AuthManager:lastAuthId', $user->getId() );
331 $session->set( 'AuthManager:lastAuthTimestamp', null );
333 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
336 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
339 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
342 // Recent enough to pass
343 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
345 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
348 // Not recent enough to pass
349 $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
351 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
353 // But recent enough for the 'test' operation
355 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test' )
358 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [
364 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
368 AuthManager
::SEC_FAIL
, $this->manager
->securitySensitiveOperationStatus( 'test' )
372 // Test hook, all three possible values
374 AuthManager
::SEC_OK
=> AuthManager
::SEC_OK
,
375 AuthManager
::SEC_REAUTH
=> $reauth,
376 AuthManager
::SEC_FAIL
=> AuthManager
::SEC_FAIL
,
377 ] as $hook => $expect ) {
378 $this->hook( 'SecuritySensitiveOperationStatus', $this->exactly( 2 ) )
382 $this->callback( function ( $s ) use ( $session ) {
383 return $s->getId() === $session->getId();
385 $mutableSession ?
$this->equalTo( 500, 1 ) : $this->equalTo( -1 )
387 ->will( $this->returnCallback( function ( &$v ) use ( $hook ) {
391 $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
393 $expect, $this->manager
->securitySensitiveOperationStatus( 'test' ), "hook $hook"
396 $expect, $this->manager
->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
398 $this->unhook( 'SecuritySensitiveOperationStatus' );
401 \ScopedCallback
::consume( $reset );
404 public function onSecuritySensitiveOperationStatus( &$status, $operation, $session, $time ) {
407 public static function provideSecuritySensitiveOperationStatus() {
415 * @dataProvider provideUserCanAuthenticate
416 * @param bool $primary1Can
417 * @param bool $primary2Can
418 * @param bool $expect
420 public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
421 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
422 $mock1->expects( $this->any() )->method( 'getUniqueId' )
423 ->will( $this->returnValue( 'primary1' ) );
424 $mock1->expects( $this->any() )->method( 'testUserCanAuthenticate' )
425 ->with( $this->equalTo( 'UTSysop' ) )
426 ->will( $this->returnValue( $primary1Can ) );
427 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
428 $mock2->expects( $this->any() )->method( 'getUniqueId' )
429 ->will( $this->returnValue( 'primary2' ) );
430 $mock2->expects( $this->any() )->method( 'testUserCanAuthenticate' )
431 ->with( $this->equalTo( 'UTSysop' ) )
432 ->will( $this->returnValue( $primary2Can ) );
433 $this->primaryauthMocks
= [ $mock1, $mock2 ];
435 $this->initializeManager( true );
436 $this->assertSame( $expect, $this->manager
->userCanAuthenticate( 'UTSysop' ) );
439 public static function provideUserCanAuthenticate() {
441 [ false, false, false ],
442 [ true, false, true ],
443 [ false, true, true ],
444 [ true, true, true ],
448 public function testRevokeAccessForUser() {
449 $this->initializeManager();
451 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
452 $mock->expects( $this->any() )->method( 'getUniqueId' )
453 ->will( $this->returnValue( 'primary' ) );
454 $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
455 ->with( $this->equalTo( 'UTSysop' ) );
456 $this->primaryauthMocks
= [ $mock ];
458 $this->initializeManager( true );
459 $this->logger
->setCollect( true );
461 $this->manager
->revokeAccessForUser( 'UTSysop' );
464 [ LogLevel
::INFO
, 'Revoking access for {user}' ],
465 ], $this->logger
->getBuffer() );
468 public function testProviderCreation() {
470 'pre' => $this->getMockForAbstractClass( PreAuthenticationProvider
::class ),
471 'primary' => $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
472 'secondary' => $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class ),
474 foreach ( $mocks as $key => $mock ) {
475 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
476 $mock->expects( $this->once() )->method( 'setLogger' );
477 $mock->expects( $this->once() )->method( 'setManager' );
478 $mock->expects( $this->once() )->method( 'setConfig' );
480 $this->preauthMocks
= [ $mocks['pre'] ];
481 $this->primaryauthMocks
= [ $mocks['primary'] ];
482 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
485 $this->initializeManager();
488 $this->managerPriv
->getAuthenticationProvider( 'primary' )
492 $this->managerPriv
->getAuthenticationProvider( 'secondary' )
496 $this->managerPriv
->getAuthenticationProvider( 'pre' )
499 [ 'pre' => $mocks['pre'] ],
500 $this->managerPriv
->getPreAuthenticationProviders()
503 [ 'primary' => $mocks['primary'] ],
504 $this->managerPriv
->getPrimaryAuthenticationProviders()
507 [ 'secondary' => $mocks['secondary'] ],
508 $this->managerPriv
->getSecondaryAuthenticationProviders()
512 $mock1 = $this->getMockForAbstractClass( PreAuthenticationProvider
::class );
513 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
514 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
515 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
516 $this->preauthMocks
= [ $mock1 ];
517 $this->primaryauthMocks
= [ $mock2 ];
518 $this->secondaryauthMocks
= [];
519 $this->initializeManager( true );
521 $this->managerPriv
->getAuthenticationProvider( 'Y' );
522 $this->fail( 'Expected exception not thrown' );
523 } catch ( \RuntimeException
$ex ) {
524 $class1 = get_class( $mock1 );
525 $class2 = get_class( $mock2 );
527 "Duplicate specifications for id X (classes $class1 and $class2)", $ex->getMessage()
532 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
533 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
534 $class = get_class( $mock );
535 $this->preauthMocks
= [ $mock ];
536 $this->primaryauthMocks
= [ $mock ];
537 $this->secondaryauthMocks
= [ $mock ];
538 $this->initializeManager( true );
540 $this->managerPriv
->getPreAuthenticationProviders();
541 $this->fail( 'Expected exception not thrown' );
542 } catch ( \RuntimeException
$ex ) {
544 "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
549 $this->managerPriv
->getPrimaryAuthenticationProviders();
550 $this->fail( 'Expected exception not thrown' );
551 } catch ( \RuntimeException
$ex ) {
553 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
558 $this->managerPriv
->getSecondaryAuthenticationProviders();
559 $this->fail( 'Expected exception not thrown' );
560 } catch ( \RuntimeException
$ex ) {
562 "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
568 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
569 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
570 $mock3 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
571 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
572 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
573 $mock3->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'C' ) );
574 $this->preauthMocks
= [];
575 $this->primaryauthMocks
= [ $mock1, $mock2, $mock3 ];
576 $this->secondaryauthMocks
= [];
577 $this->initializeConfig();
578 $config = $this->config
->get( 'AuthManagerConfig' );
580 $this->initializeManager( false );
582 [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
583 $this->managerPriv
->getPrimaryAuthenticationProviders(),
587 $config['primaryauth']['A']['sort'] = 100;
588 $config['primaryauth']['C']['sort'] = -1;
589 $this->config
->set( 'AuthManagerConfig', $config );
590 $this->initializeManager( false );
592 [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
593 $this->managerPriv
->getPrimaryAuthenticationProviders()
597 public function testSetDefaultUserOptions() {
598 $this->initializeManager();
600 $context = \RequestContext
::getMain();
601 $reset = new \
ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
602 $context->setLanguage( 'de' );
603 $this->setMwGlobals( 'wgContLang', \Language
::factory( 'zh' ) );
605 $user = \User
::newFromName( self
::usernameForCreation() );
606 $user->addToDatabase();
607 $oldToken = $user->getToken();
608 $this->managerPriv
->setDefaultUserOptions( $user, false );
609 $user->saveSettings();
610 $this->assertNotEquals( $oldToken, $user->getToken() );
611 $this->assertSame( 'zh', $user->getOption( 'language' ) );
612 $this->assertSame( 'zh', $user->getOption( 'variant' ) );
614 $user = \User
::newFromName( self
::usernameForCreation() );
615 $user->addToDatabase();
616 $oldToken = $user->getToken();
617 $this->managerPriv
->setDefaultUserOptions( $user, true );
618 $user->saveSettings();
619 $this->assertNotEquals( $oldToken, $user->getToken() );
620 $this->assertSame( 'de', $user->getOption( 'language' ) );
621 $this->assertSame( 'zh', $user->getOption( 'variant' ) );
623 $this->setMwGlobals( 'wgContLang', \Language
::factory( 'en' ) );
625 $user = \User
::newFromName( self
::usernameForCreation() );
626 $user->addToDatabase();
627 $oldToken = $user->getToken();
628 $this->managerPriv
->setDefaultUserOptions( $user, true );
629 $user->saveSettings();
630 $this->assertNotEquals( $oldToken, $user->getToken() );
631 $this->assertSame( 'de', $user->getOption( 'language' ) );
632 $this->assertSame( null, $user->getOption( 'variant' ) );
635 public function testForcePrimaryAuthenticationProviders() {
636 $mockA = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
637 $mockB = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
638 $mockB2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
639 $mockA->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
640 $mockB->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
641 $mockB2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
642 $this->primaryauthMocks
= [ $mockA ];
644 $this->logger
= new \
TestLogger( true );
646 // Test without first initializing the configured providers
647 $this->initializeManager();
648 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
650 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
652 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
653 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
655 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
656 ], $this->logger
->getBuffer() );
657 $this->logger
->clearBuffer();
659 // Test with first initializing the configured providers
660 $this->initializeManager();
661 $this->assertSame( $mockA, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
662 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
663 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
664 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
665 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
667 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
669 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
670 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
671 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
673 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
676 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
679 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
681 ], $this->logger
->getBuffer() );
682 $this->logger
->clearBuffer();
684 // Test duplicate IDs
685 $this->initializeManager();
687 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
688 $this->fail( 'Expected exception not thrown' );
689 } catch ( \RuntimeException
$ex ) {
690 $class1 = get_class( $mockB );
691 $class2 = get_class( $mockB2 );
693 "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
698 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
699 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
700 $class = get_class( $mock );
702 $this->manager
->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
703 $this->fail( 'Expected exception not thrown' );
704 } catch ( \RuntimeException
$ex ) {
706 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
713 public function testBeginAuthentication() {
714 $this->initializeManager();
717 list( $provider, $reset ) = $this->getMockSessionProvider( false );
718 $this->hook( 'UserLoggedIn', $this->never() );
719 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
721 $this->manager
->beginAuthentication( [], 'http://localhost/' );
722 $this->fail( 'Expected exception not thrown' );
723 } catch ( \LogicException
$ex ) {
724 $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
726 $this->unhook( 'UserLoggedIn' );
727 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
728 \ScopedCallback
::consume( $reset );
729 $this->initializeManager( true );
731 // CreatedAccountAuthenticationRequest
732 $user = \User
::newFromName( 'UTSysop' );
734 new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
736 $this->hook( 'UserLoggedIn', $this->never() );
738 $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
739 $this->fail( 'Expected exception not thrown' );
740 } catch ( \LogicException
$ex ) {
742 'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
743 'that created the account',
747 $this->unhook( 'UserLoggedIn' );
749 $this->request
->getSession()->clear();
750 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
751 $this->managerPriv
->createdAccountAuthenticationRequests
= [ $reqs[0] ];
752 $this->hook( 'UserLoggedIn', $this->once() )
753 ->with( $this->callback( function ( $u ) use ( $user ) {
754 return $user->getId() === $u->getId() && $user->getName() === $u->getName();
756 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
757 $this->logger
->setCollect( true );
758 $ret = $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
759 $this->logger
->setCollect( false );
760 $this->unhook( 'UserLoggedIn' );
761 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
762 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
763 $this->assertSame( $user->getName(), $ret->username
);
764 $this->assertSame( $user->getId(), $this->request
->getSessionData( 'AuthManager:lastAuthId' ) );
766 time(), $this->request
->getSessionData( 'AuthManager:lastAuthTimestamp' ),
769 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
770 $this->assertSame( $user->getId(), $this->request
->getSession()->getUser()->getId() );
772 [ LogLevel
::INFO
, 'Logging in {user} after account creation' ],
773 ], $this->logger
->getBuffer() );
776 public function testCreateFromLogin() {
777 $user = \User
::newFromName( 'UTSysop' );
778 $req1 = $this->getMock( AuthenticationRequest
::class );
779 $req2 = $this->getMock( AuthenticationRequest
::class );
780 $req3 = $this->getMock( AuthenticationRequest
::class );
781 $userReq = new UsernameAuthenticationRequest
;
782 $userReq->username
= 'UTDummy';
784 $req1->returnToUrl
= 'http://localhost/';
785 $req2->returnToUrl
= 'http://localhost/';
786 $req3->returnToUrl
= 'http://localhost/';
787 $req3->username
= 'UTDummy';
788 $userReq->returnToUrl
= 'http://localhost/';
790 // Passing one into beginAuthentication(), and an immediate FAIL
791 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
792 $this->primaryauthMocks
= [ $primary ];
793 $this->initializeManager( true );
794 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
795 $res->createRequest
= $req1;
796 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
797 ->will( $this->returnValue( $res ) );
798 $createReq = new CreateFromLoginAuthenticationRequest(
799 null, [ $req2->getUniqueId() => $req2 ]
801 $this->logger
->setCollect( true );
802 $ret = $this->manager
->beginAuthentication( [ $createReq ], 'http://localhost/' );
803 $this->logger
->setCollect( false );
804 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
805 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
806 $this->assertSame( $req1, $ret->createRequest
->createRequest
);
807 $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest
->maybeLink
);
809 // UI, then FAIL in beginAuthentication()
810 $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider
::class )
811 ->setMethods( [ 'continuePrimaryAuthentication' ] )
812 ->getMockForAbstractClass();
813 $this->primaryauthMocks
= [ $primary ];
814 $this->initializeManager( true );
815 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
816 ->will( $this->returnValue(
817 AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) )
819 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
820 $res->createRequest
= $req2;
821 $primary->expects( $this->any() )->method( 'continuePrimaryAuthentication' )
822 ->will( $this->returnValue( $res ) );
823 $this->logger
->setCollect( true );
824 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
825 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
, 'sanity check' );
826 $ret = $this->manager
->continueAuthentication( [] );
827 $this->logger
->setCollect( false );
828 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
829 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
830 $this->assertSame( $req2, $ret->createRequest
->createRequest
);
831 $this->assertEquals( [], $ret->createRequest
->maybeLink
);
833 // Pass into beginAccountCreation(), see that maybeLink and createRequest get copied
834 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
835 $this->primaryauthMocks
= [ $primary ];
836 $this->initializeManager( true );
837 $createReq = new CreateFromLoginAuthenticationRequest( $req3, [ $req2 ] );
838 $createReq->returnToUrl
= 'http://localhost/';
839 $createReq->username
= 'UTDummy';
840 $res = AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) );
841 $primary->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
842 ->with( $this->anything(), $this->anything(), [ $userReq, $createReq, $req3 ] )
843 ->will( $this->returnValue( $res ) );
844 $primary->expects( $this->any() )->method( 'accountCreationType' )
845 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
846 $this->logger
->setCollect( true );
847 $ret = $this->manager
->beginAccountCreation(
848 $user, [ $userReq, $createReq ], 'http://localhost/'
850 $this->logger
->setCollect( false );
851 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
852 $state = $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' );
853 $this->assertNotNull( $state );
854 $this->assertEquals( [ $userReq, $createReq, $req3 ], $state['reqs'] );
855 $this->assertEquals( [ $req2 ], $state['maybeLink'] );
859 * @dataProvider provideAuthentication
860 * @param StatusValue $preResponse
861 * @param array $primaryResponses
862 * @param array $secondaryResponses
863 * @param array $managerResponses
864 * @param bool $link Whether the primary authentication provider is a "link" provider
866 public function testAuthentication(
867 StatusValue
$preResponse, array $primaryResponses, array $secondaryResponses,
868 array $managerResponses, $link = false
870 $this->initializeManager();
871 $user = \User
::newFromName( 'UTSysop' );
872 $id = $user->getId();
873 $name = $user->getName();
875 // Set up lots of mocks...
876 $req = new RememberMeAuthenticationRequest
;
877 $req->rememberMe
= (bool)rand( 0, 1 );
878 $req->pre
= $preResponse;
879 $req->primary
= $primaryResponses;
880 $req->secondary
= $secondaryResponses;
882 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
883 $class = ucfirst( $key ) . 'AuthenticationProvider';
884 $mocks[$key] = $this->getMockForAbstractClass(
885 "MediaWiki\\Auth\\$class", [], "Mock$class"
887 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
888 ->will( $this->returnValue( $key ) );
889 $mocks[$key . '2'] = $this->getMockForAbstractClass(
890 "MediaWiki\\Auth\\$class", [], "Mock$class"
892 $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
893 ->will( $this->returnValue( $key . '2' ) );
894 $mocks[$key . '3'] = $this->getMockForAbstractClass(
895 "MediaWiki\\Auth\\$class", [], "Mock$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', $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->assertEquals( $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 $this->assertEquals(
1063 $ret->neededRequests
,
1064 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN_CONTINUE
),
1065 "Response $i, continuation check"
1067 foreach ( $providers as $p ) {
1068 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
1072 $state = $session->getSecret( 'AuthManager::authnState' );
1073 $maybeLink = isset( $state['maybeLink'] ) ?
$state['maybeLink'] : [];
1074 if ( $link && $response->status
=== AuthenticationResponse
::RESTART
) {
1075 $this->assertEquals(
1076 $response->createRequest
->maybeLink
,
1078 "Response $i, maybeLink"
1081 $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
1086 $this->assertSame( $req->rememberMe
, $session->shouldRememberUser(),
1087 'rememberMe checkbox had effect' );
1089 $this->assertNotSame( $req->rememberMe
, $session->shouldRememberUser(),
1090 'rememberMe checkbox wasn\'t applied' );
1094 public function provideAuthentication() {
1095 $user = \User
::newFromName( 'UTSysop' );
1096 $id = $user->getId();
1097 $name = $user->getName();
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->neededRequests
= [ $rememberReq, $restartResponse2->createRequest
];
1120 'Failure in pre-auth' => [
1121 StatusValue
::newFatal( 'fail-from-pre' ),
1125 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
1126 AuthenticationResponse
::newFail(
1127 $this->message( 'authmanager-authn-not-in-progress' )
1131 'Failure in primary' => [
1132 StatusValue
::newGood(),
1134 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
1139 'All primary abstain' => [
1140 StatusValue
::newGood(),
1142 AuthenticationResponse
::newAbstain(),
1146 AuthenticationResponse
::newFail( $this->message( 'authmanager-authn-no-primary' ) )
1149 'Primary UI, then redirect, then fail' => [
1150 StatusValue
::newGood(),
1152 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1153 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
1154 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
1159 'Primary redirect, then abstain' => [
1160 StatusValue
::newGood(),
1162 $tmp = AuthenticationResponse
::newRedirect(
1163 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
1165 AuthenticationResponse
::newAbstain(),
1170 new \
DomainException(
1171 'MockPrimaryAuthenticationProvider::continuePrimaryAuthentication() returned ABSTAIN'
1175 'Primary UI, then pass with no local user' => [
1176 StatusValue
::newGood(),
1178 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1179 AuthenticationResponse
::newPass( null ),
1187 'Primary UI, then pass with no local user (link type)' => [
1188 StatusValue
::newGood(),
1190 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1191 $restartResponse2Pass,
1200 'Primary pass with invalid username' => [
1201 StatusValue
::newGood(),
1203 AuthenticationResponse
::newPass( '<>' ),
1207 new \
DomainException( 'MockPrimaryAuthenticationProvider returned an invalid username: <>' ),
1210 'Secondary fail' => [
1211 StatusValue
::newGood(),
1213 AuthenticationResponse
::newPass( $name ),
1216 AuthenticationResponse
::newFail( $this->message( 'fail-in-secondary' ) ),
1220 'Secondary UI, then abstain' => [
1221 StatusValue
::newGood(),
1223 AuthenticationResponse
::newPass( $name ),
1226 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1227 AuthenticationResponse
::newAbstain()
1231 AuthenticationResponse
::newPass( $name ),
1234 'Secondary pass' => [
1235 StatusValue
::newGood(),
1237 AuthenticationResponse
::newPass( $name ),
1240 AuthenticationResponse
::newPass()
1243 AuthenticationResponse
::newPass( $name ),
1250 * @dataProvider provideUserExists
1251 * @param bool $primary1Exists
1252 * @param bool $primary2Exists
1253 * @param bool $expect
1255 public function testUserExists( $primary1Exists, $primary2Exists, $expect ) {
1256 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1257 $mock1->expects( $this->any() )->method( 'getUniqueId' )
1258 ->will( $this->returnValue( 'primary1' ) );
1259 $mock1->expects( $this->any() )->method( 'testUserExists' )
1260 ->with( $this->equalTo( 'UTSysop' ) )
1261 ->will( $this->returnValue( $primary1Exists ) );
1262 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1263 $mock2->expects( $this->any() )->method( 'getUniqueId' )
1264 ->will( $this->returnValue( 'primary2' ) );
1265 $mock2->expects( $this->any() )->method( 'testUserExists' )
1266 ->with( $this->equalTo( 'UTSysop' ) )
1267 ->will( $this->returnValue( $primary2Exists ) );
1268 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1270 $this->initializeManager( true );
1271 $this->assertSame( $expect, $this->manager
->userExists( 'UTSysop' ) );
1274 public static function provideUserExists() {
1276 [ false, false, false ],
1277 [ true, false, true ],
1278 [ false, true, true ],
1279 [ true, true, true ],
1284 * @dataProvider provideAllowsAuthenticationDataChange
1285 * @param StatusValue $primaryReturn
1286 * @param StatusValue $secondaryReturn
1287 * @param Status $expect
1289 public function testAllowsAuthenticationDataChange( $primaryReturn, $secondaryReturn, $expect ) {
1290 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1292 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1293 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1294 $mock1->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1295 ->with( $this->equalTo( $req ) )
1296 ->will( $this->returnValue( $primaryReturn ) );
1297 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
1298 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1299 $mock2->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1300 ->with( $this->equalTo( $req ) )
1301 ->will( $this->returnValue( $secondaryReturn ) );
1303 $this->primaryauthMocks
= [ $mock1 ];
1304 $this->secondaryauthMocks
= [ $mock2 ];
1305 $this->initializeManager( true );
1306 $this->assertEquals( $expect, $this->manager
->allowsAuthenticationDataChange( $req ) );
1309 public static function provideAllowsAuthenticationDataChange() {
1310 $ignored = \Status
::newGood( 'ignored' );
1311 $ignored->warning( 'authmanager-change-not-supported' );
1313 $okFromPrimary = StatusValue
::newGood();
1314 $okFromPrimary->warning( 'warning-from-primary' );
1315 $okFromSecondary = StatusValue
::newGood();
1316 $okFromSecondary->warning( 'warning-from-secondary' );
1320 StatusValue
::newGood(),
1321 StatusValue
::newGood(),
1325 StatusValue
::newGood(),
1326 StatusValue
::newGood( 'ignore' ),
1330 StatusValue
::newGood( 'ignored' ),
1331 StatusValue
::newGood(),
1335 StatusValue
::newGood( 'ignored' ),
1336 StatusValue
::newGood( 'ignored' ),
1340 StatusValue
::newFatal( 'fail from primary' ),
1341 StatusValue
::newGood(),
1342 \Status
::newFatal( 'fail from primary' ),
1346 StatusValue
::newGood(),
1347 \Status
::wrap( $okFromPrimary ),
1350 StatusValue
::newGood(),
1351 StatusValue
::newFatal( 'fail from secondary' ),
1352 \Status
::newFatal( 'fail from secondary' ),
1355 StatusValue
::newGood(),
1357 \Status
::wrap( $okFromSecondary ),
1362 public function testChangeAuthenticationData() {
1363 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1364 $req->username
= 'UTSysop';
1366 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1367 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1368 $mock1->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1369 ->with( $this->equalTo( $req ) );
1370 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1371 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1372 $mock2->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1373 ->with( $this->equalTo( $req ) );
1375 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1376 $this->initializeManager( true );
1377 $this->logger
->setCollect( true );
1378 $this->manager
->changeAuthenticationData( $req );
1379 $this->assertSame( [
1380 [ LogLevel
::INFO
, 'Changing authentication data for {user} class {what}' ],
1381 ], $this->logger
->getBuffer() );
1384 public function testCanCreateAccounts() {
1386 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
1387 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
1388 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
1391 foreach ( $types as $type => $can ) {
1392 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1393 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
1394 $mock->expects( $this->any() )->method( 'accountCreationType' )
1395 ->will( $this->returnValue( $type ) );
1396 $this->primaryauthMocks
= [ $mock ];
1397 $this->initializeManager( true );
1398 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
1402 public function testCheckAccountCreatePermissions() {
1403 global $wgGroupPermissions;
1405 $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
1407 $this->initializeManager( true );
1409 $wgGroupPermissions['*']['createaccount'] = true;
1410 $this->assertEquals(
1412 $this->manager
->checkAccountCreatePermissions( new \User
)
1415 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1416 $this->assertEquals(
1417 \Status
::newFatal( 'readonlytext', 'Because' ),
1418 $this->manager
->checkAccountCreatePermissions( new \User
)
1420 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1422 $wgGroupPermissions['*']['createaccount'] = false;
1423 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1424 $this->assertFalse( $status->isOK() );
1425 $this->assertTrue( $status->hasMessage( 'badaccess-groups' ) );
1426 $wgGroupPermissions['*']['createaccount'] = true;
1428 $user = \User
::newFromName( 'UTBlockee' );
1429 if ( $user->getID() == 0 ) {
1430 $user->addToDatabase();
1431 \TestUser
::setPasswordForUser( $user, 'UTBlockeePassword' );
1432 $user->saveSettings();
1434 $oldBlock = \Block
::newFromTarget( 'UTBlockee' );
1436 // An old block will prevent our new one from saving.
1437 $oldBlock->delete();
1440 'address' => 'UTBlockee',
1441 'user' => $user->getID(),
1442 'reason' => __METHOD__
,
1443 'expiry' => time() +
100500,
1444 'createAccount' => true,
1446 $block = new \
Block( $blockOptions );
1448 $status = $this->manager
->checkAccountCreatePermissions( $user );
1449 $this->assertFalse( $status->isOK() );
1450 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
1453 'address' => '127.0.0.0/24',
1454 'reason' => __METHOD__
,
1455 'expiry' => time() +
100500,
1456 'createAccount' => true,
1458 $block = new \
Block( $blockOptions );
1460 $scopeVariable = new \
ScopedCallback( [ $block, 'delete' ] );
1461 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1462 $this->assertFalse( $status->isOK() );
1463 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
1464 \ScopedCallback
::consume( $scopeVariable );
1466 $this->setMwGlobals( [
1467 'wgEnableDnsBlacklist' => true,
1468 'wgDnsBlacklistUrls' => [
1469 'local.wmftest.net', // This will resolve for every subdomain, which works to test "listed?"
1471 'wgProxyWhitelist' => [],
1473 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1474 $this->assertFalse( $status->isOK() );
1475 $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
1476 $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
1477 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1478 $this->assertTrue( $status->isGood() );
1482 * @param string $uniq
1485 private static function usernameForCreation( $uniq = '' ) {
1488 $username = "UTAuthManagerTestAccountCreation" . $uniq . ++
$i;
1489 } while ( \User
::newFromName( $username )->getId() !== 0 );
1493 public function testCanCreateAccount() {
1494 $username = self
::usernameForCreation();
1495 $this->initializeManager();
1497 $this->assertEquals(
1498 \Status
::newFatal( 'authmanager-create-disabled' ),
1499 $this->manager
->canCreateAccount( $username )
1502 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1503 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1504 $mock->expects( $this->any() )->method( 'accountCreationType' )
1505 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1506 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1507 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1508 ->will( $this->returnValue( StatusValue
::newGood() ) );
1509 $this->primaryauthMocks
= [ $mock ];
1510 $this->initializeManager( true );
1512 $this->assertEquals(
1513 \Status
::newFatal( 'userexists' ),
1514 $this->manager
->canCreateAccount( $username )
1517 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1518 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1519 $mock->expects( $this->any() )->method( 'accountCreationType' )
1520 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1521 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1522 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1523 ->will( $this->returnValue( StatusValue
::newGood() ) );
1524 $this->primaryauthMocks
= [ $mock ];
1525 $this->initializeManager( true );
1527 $this->assertEquals(
1528 \Status
::newFatal( 'noname' ),
1529 $this->manager
->canCreateAccount( $username . '<>' )
1532 $this->assertEquals(
1533 \Status
::newFatal( 'userexists' ),
1534 $this->manager
->canCreateAccount( 'UTSysop' )
1537 $this->assertEquals(
1539 $this->manager
->canCreateAccount( $username )
1542 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1543 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1544 $mock->expects( $this->any() )->method( 'accountCreationType' )
1545 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1546 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1547 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1548 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1549 $this->primaryauthMocks
= [ $mock ];
1550 $this->initializeManager( true );
1552 $this->assertEquals(
1553 \Status
::newFatal( 'fail' ),
1554 $this->manager
->canCreateAccount( $username )
1558 public function testBeginAccountCreation() {
1559 $creator = \User
::newFromName( 'UTSysop' );
1560 $userReq = new UsernameAuthenticationRequest
;
1561 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1562 return $level === LogLevel
::DEBUG ?
null : $message;
1564 $this->initializeManager();
1566 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
1567 $this->hook( 'LocalUserCreated', $this->never() );
1569 $this->manager
->beginAccountCreation(
1570 $creator, [], 'http://localhost/'
1572 $this->fail( 'Expected exception not thrown' );
1573 } catch ( \LogicException
$ex ) {
1574 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1576 $this->unhook( 'LocalUserCreated' );
1578 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1581 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1582 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1583 $mock->expects( $this->any() )->method( 'accountCreationType' )
1584 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1585 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1586 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1587 ->will( $this->returnValue( StatusValue
::newGood() ) );
1588 $this->primaryauthMocks
= [ $mock ];
1589 $this->initializeManager( true );
1591 $this->hook( 'LocalUserCreated', $this->never() );
1592 $ret = $this->manager
->beginAccountCreation( $creator, [], 'http://localhost/' );
1593 $this->unhook( 'LocalUserCreated' );
1594 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1595 $this->assertSame( 'noname', $ret->message
->getKey() );
1597 $this->hook( 'LocalUserCreated', $this->never() );
1598 $userReq->username
= self
::usernameForCreation();
1599 $userReq2 = new UsernameAuthenticationRequest
;
1600 $userReq2->username
= $userReq->username
. 'X';
1601 $ret = $this->manager
->beginAccountCreation(
1602 $creator, [ $userReq, $userReq2 ], 'http://localhost/'
1604 $this->unhook( 'LocalUserCreated' );
1605 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1606 $this->assertSame( 'noname', $ret->message
->getKey() );
1608 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1609 $this->hook( 'LocalUserCreated', $this->never() );
1610 $userReq->username
= self
::usernameForCreation();
1611 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1612 $this->unhook( 'LocalUserCreated' );
1613 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1614 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
1615 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1616 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1618 $this->hook( 'LocalUserCreated', $this->never() );
1619 $userReq->username
= self
::usernameForCreation();
1620 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1621 $this->unhook( 'LocalUserCreated' );
1622 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1623 $this->assertSame( 'userexists', $ret->message
->getKey() );
1625 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1626 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1627 $mock->expects( $this->any() )->method( 'accountCreationType' )
1628 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1629 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1630 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1631 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1632 $this->primaryauthMocks
= [ $mock ];
1633 $this->initializeManager( true );
1635 $this->hook( 'LocalUserCreated', $this->never() );
1636 $userReq->username
= self
::usernameForCreation();
1637 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1638 $this->unhook( 'LocalUserCreated' );
1639 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1640 $this->assertSame( 'fail', $ret->message
->getKey() );
1642 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1643 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1644 $mock->expects( $this->any() )->method( 'accountCreationType' )
1645 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1646 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1647 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1648 ->will( $this->returnValue( StatusValue
::newGood() ) );
1649 $this->primaryauthMocks
= [ $mock ];
1650 $this->initializeManager( true );
1652 $this->hook( 'LocalUserCreated', $this->never() );
1653 $userReq->username
= self
::usernameForCreation() . '<>';
1654 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1655 $this->unhook( 'LocalUserCreated' );
1656 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1657 $this->assertSame( 'noname', $ret->message
->getKey() );
1659 $this->hook( 'LocalUserCreated', $this->never() );
1660 $userReq->username
= $creator->getName();
1661 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1662 $this->unhook( 'LocalUserCreated' );
1663 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1664 $this->assertSame( 'userexists', $ret->message
->getKey() );
1666 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1667 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1668 $mock->expects( $this->any() )->method( 'accountCreationType' )
1669 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1670 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1671 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1672 ->will( $this->returnValue( StatusValue
::newGood() ) );
1673 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
1674 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1675 $this->primaryauthMocks
= [ $mock ];
1676 $this->initializeManager( true );
1678 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1679 ->setMethods( [ 'populateUser' ] )
1681 $req->expects( $this->any() )->method( 'populateUser' )
1682 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1683 $userReq->username
= self
::usernameForCreation();
1684 $ret = $this->manager
->beginAccountCreation(
1685 $creator, [ $userReq, $req ], 'http://localhost/'
1687 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1688 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1690 $req = new UserDataAuthenticationRequest
;
1691 $userReq->username
= self
::usernameForCreation();
1693 $ret = $this->manager
->beginAccountCreation(
1694 $creator, [ $userReq, $req ], 'http://localhost/'
1696 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1697 $this->assertSame( 'fail', $ret->message
->getKey() );
1699 $this->manager
->beginAccountCreation(
1700 \User
::newFromName( $userReq->username
), [ $userReq, $req ], 'http://localhost/'
1702 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1703 $this->assertSame( 'fail', $ret->message
->getKey() );
1706 public function testContinueAccountCreation() {
1707 $creator = \User
::newFromName( 'UTSysop' );
1708 $username = self
::usernameForCreation();
1709 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1710 return $level === LogLevel
::DEBUG ?
null : $message;
1712 $this->initializeManager();
1716 'username' => $username,
1718 'creatorname' => $username,
1721 'primaryResponse' => null,
1723 'ranPreTests' => true,
1726 $this->hook( 'LocalUserCreated', $this->never() );
1728 $this->manager
->continueAccountCreation( [] );
1729 $this->fail( 'Expected exception not thrown' );
1730 } catch ( \LogicException
$ex ) {
1731 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1733 $this->unhook( 'LocalUserCreated' );
1735 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1736 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1737 $mock->expects( $this->any() )->method( 'accountCreationType' )
1738 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1739 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1740 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )->will(
1741 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
1743 $this->primaryauthMocks
= [ $mock ];
1744 $this->initializeManager( true );
1746 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', null );
1747 $this->hook( 'LocalUserCreated', $this->never() );
1748 $ret = $this->manager
->continueAccountCreation( [] );
1749 $this->unhook( 'LocalUserCreated' );
1750 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1751 $this->assertSame( 'authmanager-create-not-in-progress', $ret->message
->getKey() );
1753 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1754 [ 'username' => "$username<>" ] +
$session );
1755 $this->hook( 'LocalUserCreated', $this->never() );
1756 $ret = $this->manager
->continueAccountCreation( [] );
1757 $this->unhook( 'LocalUserCreated' );
1758 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1759 $this->assertSame( 'noname', $ret->message
->getKey() );
1761 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1764 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $session );
1765 $this->hook( 'LocalUserCreated', $this->never() );
1766 $cache = \ObjectCache
::getLocalClusterInstance();
1767 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1768 $ret = $this->manager
->continueAccountCreation( [] );
1770 $this->unhook( 'LocalUserCreated' );
1771 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1772 $this->assertSame( 'usernameinprogress', $ret->message
->getKey() );
1773 // This error shouldn't remove the existing session, because the
1774 // raced-with process "owns" it.
1776 $session, $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1779 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1780 [ 'username' => $creator->getName() ] +
$session );
1781 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1782 $this->hook( 'LocalUserCreated', $this->never() );
1783 $ret = $this->manager
->continueAccountCreation( [] );
1784 $this->unhook( 'LocalUserCreated' );
1785 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1786 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
1787 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1788 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1790 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1791 [ 'username' => $creator->getName() ] +
$session );
1792 $this->hook( 'LocalUserCreated', $this->never() );
1793 $ret = $this->manager
->continueAccountCreation( [] );
1794 $this->unhook( 'LocalUserCreated' );
1795 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1796 $this->assertSame( 'userexists', $ret->message
->getKey() );
1798 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1801 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1802 [ 'userid' => $creator->getId() ] +
$session );
1803 $this->hook( 'LocalUserCreated', $this->never() );
1805 $ret = $this->manager
->continueAccountCreation( [] );
1806 $this->fail( 'Expected exception not thrown' );
1807 } catch ( \UnexpectedValueException
$ex ) {
1808 $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
1810 $this->unhook( 'LocalUserCreated' );
1812 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1815 $id = $creator->getId();
1816 $name = $creator->getName();
1817 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1818 [ 'username' => $name, 'userid' => $id +
1 ] +
$session );
1819 $this->hook( 'LocalUserCreated', $this->never() );
1821 $ret = $this->manager
->continueAccountCreation( [] );
1822 $this->fail( 'Expected exception not thrown' );
1823 } catch ( \UnexpectedValueException
$ex ) {
1824 $this->assertEquals(
1825 "User \"{$name}\" exists, but ID $id != " . ( $id +
1 ) . '!', $ex->getMessage()
1828 $this->unhook( 'LocalUserCreated' );
1830 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1833 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1834 ->setMethods( [ 'populateUser' ] )
1836 $req->expects( $this->any() )->method( 'populateUser' )
1837 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1838 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1839 [ 'reqs' => [ $req ] ] +
$session );
1840 $ret = $this->manager
->continueAccountCreation( [] );
1841 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1842 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1844 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1849 * @dataProvider provideAccountCreation
1850 * @param StatusValue $preTest
1851 * @param StatusValue $primaryTest
1852 * @param StatusValue $secondaryTest
1853 * @param array $primaryResponses
1854 * @param array $secondaryResponses
1855 * @param array $managerResponses
1857 public function testAccountCreation(
1858 StatusValue
$preTest, $primaryTest, $secondaryTest,
1859 array $primaryResponses, array $secondaryResponses, array $managerResponses
1861 $creator = \User
::newFromName( 'UTSysop' );
1862 $username = self
::usernameForCreation();
1864 $this->initializeManager();
1866 // Set up lots of mocks...
1867 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1868 $req->preTest
= $preTest;
1869 $req->primaryTest
= $primaryTest;
1870 $req->secondaryTest
= $secondaryTest;
1871 $req->primary
= $primaryResponses;
1872 $req->secondary
= $secondaryResponses;
1874 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
1875 $class = ucfirst( $key ) . 'AuthenticationProvider';
1876 $mocks[$key] = $this->getMockForAbstractClass(
1877 "MediaWiki\\Auth\\$class", [], "Mock$class"
1879 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
1880 ->will( $this->returnValue( $key ) );
1881 $mocks[$key]->expects( $this->any() )->method( 'testUserForCreation' )
1882 ->will( $this->returnValue( StatusValue
::newGood() ) );
1883 $mocks[$key]->expects( $this->any() )->method( 'testForAccountCreation' )
1884 ->will( $this->returnCallback(
1885 function ( $user, $creatorIn, $reqs )
1886 use ( $username, $creator, $req, $key )
1888 $this->assertSame( $username, $user->getName() );
1889 $this->assertSame( $creator->getId(), $creatorIn->getId() );
1890 $this->assertSame( $creator->getName(), $creatorIn->getName() );
1892 foreach ( $reqs as $r ) {
1893 $this->assertSame( $username, $r->username
);
1894 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1896 $this->assertTrue( $foundReq, '$reqs contains $req' );
1902 for ( $i = 2; $i <= 3; $i++
) {
1903 $mocks[$key . $i] = $this->getMockForAbstractClass(
1904 "MediaWiki\\Auth\\$class", [], "Mock$class"
1906 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
1907 ->will( $this->returnValue( $key . $i ) );
1908 $mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' )
1909 ->will( $this->returnValue( StatusValue
::newGood() ) );
1910 $mocks[$key . $i]->expects( $this->atMost( 1 ) )->method( 'testForAccountCreation' )
1911 ->will( $this->returnValue( StatusValue
::newGood() ) );
1915 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
1916 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1917 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
1918 ->will( $this->returnValue( false ) );
1919 $ct = count( $req->primary
);
1920 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1921 $this->assertSame( $username, $user->getName() );
1922 $this->assertSame( 'UTSysop', $creator->getName() );
1924 foreach ( $reqs as $r ) {
1925 $this->assertSame( $username, $r->username
);
1926 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1928 $this->assertTrue( $foundReq, '$reqs contains $req' );
1929 return array_shift( $req->primary
);
1931 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
1932 ->method( 'beginPrimaryAccountCreation' )
1933 ->will( $callback );
1934 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1935 ->method( 'continuePrimaryAccountCreation' )
1936 ->will( $callback );
1938 $ct = count( $req->secondary
);
1939 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1940 $this->assertSame( $username, $user->getName() );
1941 $this->assertSame( 'UTSysop', $creator->getName() );
1943 foreach ( $reqs as $r ) {
1944 $this->assertSame( $username, $r->username
);
1945 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1947 $this->assertTrue( $foundReq, '$reqs contains $req' );
1948 return array_shift( $req->secondary
);
1950 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
1951 ->method( 'beginSecondaryAccountCreation' )
1952 ->will( $callback );
1953 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1954 ->method( 'continueSecondaryAccountCreation' )
1955 ->will( $callback );
1957 $abstain = AuthenticationResponse
::newAbstain();
1958 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
1959 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
1960 $mocks['primary2']->expects( $this->any() )->method( 'testUserExists' )
1961 ->will( $this->returnValue( false ) );
1962 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountCreation' )
1963 ->will( $this->returnValue( $abstain ) );
1964 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1965 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
1966 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_NONE
) );
1967 $mocks['primary3']->expects( $this->any() )->method( 'testUserExists' )
1968 ->will( $this->returnValue( false ) );
1969 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountCreation' );
1970 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1971 $mocks['secondary2']->expects( $this->atMost( 1 ) )
1972 ->method( 'beginSecondaryAccountCreation' )
1973 ->will( $this->returnValue( $abstain ) );
1974 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1975 $mocks['secondary3']->expects( $this->atMost( 1 ) )
1976 ->method( 'beginSecondaryAccountCreation' )
1977 ->will( $this->returnValue( $abstain ) );
1978 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1980 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
1981 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary'], $mocks['primary2'] ];
1982 $this->secondaryauthMocks
= [
1983 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2']
1986 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
1987 return $level === LogLevel
::DEBUG ?
null : $message;
1990 $this->initializeManager( true );
1992 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
1993 $this->equalTo( AuthenticationResponse
::PASS
),
1994 $this->equalTo( AuthenticationResponse
::FAIL
)
1996 $providers = array_merge(
1997 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
1999 foreach ( $providers as $p ) {
2000 $p->postCalled
= false;
2001 $p->expects( $this->atMost( 1 ) )->method( 'postAccountCreation' )
2002 ->willReturnCallback( function ( $user, $creator, $response )
2003 use ( $constraint, $p, $username )
2005 $this->assertInstanceOf( 'User', $user );
2006 $this->assertSame( $username, $user->getName() );
2007 $this->assertSame( 'UTSysop', $creator->getName() );
2008 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
2009 $this->assertThat( $response->status
, $constraint );
2010 $p->postCalled
= $response->status
;
2014 // We're testing with $wgNewUserLog = false, so assert that it worked
2015 $dbw = wfGetDB( DB_MASTER
);
2016 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2020 foreach ( $managerResponses as $i => $response ) {
2021 $success = $response instanceof AuthenticationResponse
&&
2022 $response->status
=== AuthenticationResponse
::PASS
;
2023 if ( $i === 'created' ) {
2025 $this->hook( 'LocalUserCreated', $this->once() )
2027 $this->callback( function ( $user ) use ( $username ) {
2028 return $user->getName() === $username;
2030 $this->equalTo( false )
2032 $expectLog[] = [ LogLevel
::INFO
, "Creating user {user} during account creation" ];
2034 $this->hook( 'LocalUserCreated', $this->never() );
2040 $userReq = new UsernameAuthenticationRequest
;
2041 $userReq->username
= $username;
2042 $ret = $this->manager
->beginAccountCreation(
2043 $creator, [ $userReq, $req ], 'http://localhost/'
2046 $ret = $this->manager
->continueAccountCreation( [ $req ] );
2048 if ( $response instanceof \Exception
) {
2049 $this->fail( 'Expected exception not thrown', "Response $i" );
2051 } catch ( \Exception
$ex ) {
2052 if ( !$response instanceof \Exception
) {
2055 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
2057 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2058 "Response $i, exception, session state"
2060 $this->unhook( 'LocalUserCreated' );
2064 $this->unhook( 'LocalUserCreated' );
2066 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
2069 $this->assertNotNull( $ret->loginRequest
, "Response $i, login marker" );
2070 $this->assertContains(
2071 $ret->loginRequest
, $this->managerPriv
->createdAccountAuthenticationRequests
,
2072 "Response $i, login marker"
2077 "MediaWiki\Auth\AuthManager::continueAccountCreation: Account creation succeeded for {user}"
2080 // Set some fields in the expected $response that we couldn't
2081 // know in provideAccountCreation().
2082 $response->username
= $username;
2083 $response->loginRequest
= $ret->loginRequest
;
2085 $this->assertNull( $ret->loginRequest
, "Response $i, login marker" );
2086 $this->assertSame( [], $this->managerPriv
->createdAccountAuthenticationRequests
,
2087 "Response $i, login marker" );
2089 $ret->message
= $this->message( $ret->message
);
2090 $this->assertEquals( $response, $ret, "Response $i, response" );
2091 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
2093 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2094 "Response $i, session state"
2096 foreach ( $providers as $p ) {
2097 $this->assertSame( $response->status
, $p->postCalled
,
2098 "Response $i, post-auth callback called" );
2101 $this->assertNotNull(
2102 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2103 "Response $i, session state"
2105 $this->assertEquals(
2106 $ret->neededRequests
,
2107 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_CREATE_CONTINUE
),
2108 "Response $i, continuation check"
2110 foreach ( $providers as $p ) {
2111 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
2116 $this->assertNotEquals( 0, \User
::idFromName( $username ) );
2118 $this->assertEquals( 0, \User
::idFromName( $username ) );
2124 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
2128 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2132 public function provideAccountCreation() {
2133 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
2134 $good = StatusValue
::newGood();
2137 'Pre-creation test fail in pre' => [
2138 StatusValue
::newFatal( 'fail-from-pre' ), $good, $good,
2142 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
2145 'Pre-creation test fail in primary' => [
2146 $good, StatusValue
::newFatal( 'fail-from-primary' ), $good,
2150 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2153 'Pre-creation test fail in secondary' => [
2154 $good, $good, StatusValue
::newFatal( 'fail-from-secondary' ),
2158 AuthenticationResponse
::newFail( $this->message( 'fail-from-secondary' ) ),
2161 'Failure in primary' => [
2162 $good, $good, $good,
2164 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2169 'All primary abstain' => [
2170 $good, $good, $good,
2172 AuthenticationResponse
::newAbstain(),
2176 AuthenticationResponse
::newFail( $this->message( 'authmanager-create-no-primary' ) )
2179 'Primary UI, then redirect, then fail' => [
2180 $good, $good, $good,
2182 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2183 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
2184 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
2189 'Primary redirect, then abstain' => [
2190 $good, $good, $good,
2192 $tmp = AuthenticationResponse
::newRedirect(
2193 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
2195 AuthenticationResponse
::newAbstain(),
2200 new \
DomainException(
2201 'MockPrimaryAuthenticationProvider::continuePrimaryAccountCreation() returned ABSTAIN'
2205 'Primary UI, then pass; secondary abstain' => [
2206 $good, $good, $good,
2208 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2209 AuthenticationResponse
::newPass(),
2212 AuthenticationResponse
::newAbstain(),
2216 'created' => AuthenticationResponse
::newPass( '' ),
2219 'Primary pass; secondary UI then pass' => [
2220 $good, $good, $good,
2222 AuthenticationResponse
::newPass( '' ),
2225 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2226 AuthenticationResponse
::newPass( '' ),
2230 AuthenticationResponse
::newPass( '' ),
2233 'Primary pass; secondary fail' => [
2234 $good, $good, $good,
2236 AuthenticationResponse
::newPass(),
2239 AuthenticationResponse
::newFail( $this->message( '...' ) ),
2242 'created' => new \
DomainException(
2243 'MockSecondaryAuthenticationProvider::beginSecondaryAccountCreation() returned FAIL. ' .
2244 'Secondary providers are not allowed to fail account creation, ' .
2245 'that should have been done via testForAccountCreation().'
2253 * @dataProvider provideAccountCreationLogging
2254 * @param bool $isAnon
2255 * @param string|null $logSubtype
2257 public function testAccountCreationLogging( $isAnon, $logSubtype ) {
2258 $creator = $isAnon ?
new \User
: \User
::newFromName( 'UTSysop' );
2259 $username = self
::usernameForCreation();
2261 $this->initializeManager();
2263 // Set up lots of mocks...
2264 $mock = $this->getMockForAbstractClass(
2265 "MediaWiki\\Auth\\PrimaryAuthenticationProvider", []
2267 $mock->expects( $this->any() )->method( 'getUniqueId' )
2268 ->will( $this->returnValue( 'primary' ) );
2269 $mock->expects( $this->any() )->method( 'testUserForCreation' )
2270 ->will( $this->returnValue( StatusValue
::newGood() ) );
2271 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
2272 ->will( $this->returnValue( StatusValue
::newGood() ) );
2273 $mock->expects( $this->any() )->method( 'accountCreationType' )
2274 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2275 $mock->expects( $this->any() )->method( 'testUserExists' )
2276 ->will( $this->returnValue( false ) );
2277 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
2278 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
2279 $mock->expects( $this->any() )->method( 'finishAccountCreation' )
2280 ->will( $this->returnValue( $logSubtype ) );
2282 $this->primaryauthMocks
= [ $mock ];
2283 $this->initializeManager( true );
2284 $this->logger
->setCollect( true );
2286 $this->config
->set( 'NewUserLog', true );
2288 $dbw = wfGetDB( DB_MASTER
);
2289 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2291 $userReq = new UsernameAuthenticationRequest
;
2292 $userReq->username
= $username;
2293 $reasonReq = new CreationReasonAuthenticationRequest
;
2294 $reasonReq->reason
= $this->toString();
2295 $ret = $this->manager
->beginAccountCreation(
2296 $creator, [ $userReq, $reasonReq ], 'http://localhost/'
2299 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
2301 $user = \User
::newFromName( $username );
2302 $this->assertNotEquals( 0, $user->getId(), 'sanity check' );
2303 $this->assertNotEquals( $creator->getId(), $user->getId(), 'sanity check' );
2305 $data = \DatabaseLogEntry
::getSelectQueryData();
2306 $rows = iterator_to_array( $dbw->select(
2310 'log_id > ' . (int)$maxLogId,
2311 'log_type' => 'newusers'
2317 $this->assertCount( 1, $rows );
2318 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2320 $this->assertSame( $logSubtype ?
: ( $isAnon ?
'create' : 'create2' ), $entry->getSubtype() );
2322 $isAnon ?
$user->getId() : $creator->getId(),
2323 $entry->getPerformer()->getId()
2326 $isAnon ?
$user->getName() : $creator->getName(),
2327 $entry->getPerformer()->getName()
2329 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2330 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2331 $this->assertSame( $this->toString(), $entry->getComment() );
2334 public static function provideAccountCreationLogging() {
2339 [ false, 'byemail' ],
2343 public function testAutoAccountCreation() {
2344 global $wgGroupPermissions, $wgHooks;
2346 // PHPUnit seems to have a bug where it will call the ->with()
2347 // callbacks for our hooks again after the test is run (WTF?), which
2348 // breaks here because $username no longer matches $user by the end of
2350 $workaroundPHPUnitBug = false;
2352 $username = self
::usernameForCreation();
2353 $this->initializeManager();
2355 $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
2356 $wgGroupPermissions['*']['createaccount'] = true;
2357 $wgGroupPermissions['*']['autocreateaccount'] = false;
2359 \ObjectCache
::$instances[__METHOD__
] = new \
HashBagOStuff();
2360 $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__
] );
2362 // Set up lots of mocks...
2364 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2365 $class = ucfirst( $key ) . 'AuthenticationProvider';
2366 $mocks[$key] = $this->getMockForAbstractClass(
2367 "MediaWiki\\Auth\\$class", [], "Mock$class"
2369 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2370 ->will( $this->returnValue( $key ) );
2373 $good = StatusValue
::newGood();
2374 $callback = $this->callback( function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
2375 return $workaroundPHPUnitBug ||
$user->getName() === $username;
2378 $mocks['pre']->expects( $this->exactly( 12 ) )->method( 'testUserForCreation' )
2379 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2380 ->will( $this->onConsecutiveCalls(
2381 StatusValue
::newFatal( 'ok' ), StatusValue
::newFatal( 'ok' ), // For testing permissions
2382 StatusValue
::newFatal( 'fail-in-pre' ), $good, $good,
2383 $good, // backoff test
2384 $good, // addToDatabase fails test
2385 $good, // addToDatabase throws test
2386 $good, // addToDatabase exists test
2387 $good, $good, $good // success
2390 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
2391 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2392 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
2393 ->will( $this->returnValue( true ) );
2394 $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
2395 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2396 ->will( $this->onConsecutiveCalls(
2397 StatusValue
::newFatal( 'fail-in-primary' ), $good,
2398 $good, // backoff test
2399 $good, // addToDatabase fails test
2400 $good, // addToDatabase throws test
2401 $good, // addToDatabase exists test
2404 $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2405 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) );
2407 $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
2408 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2409 ->will( $this->onConsecutiveCalls(
2410 StatusValue
::newFatal( 'fail-in-secondary' ),
2411 $good, // backoff test
2412 $good, // addToDatabase fails test
2413 $good, // addToDatabase throws test
2414 $good, // addToDatabase exists test
2417 $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2418 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) );
2420 $this->preauthMocks
= [ $mocks['pre'] ];
2421 $this->primaryauthMocks
= [ $mocks['primary'] ];
2422 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2423 $this->initializeManager( true );
2424 $session = $this->request
->getSession();
2426 $logger = new \
TestLogger( true, function ( $m ) {
2427 $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
2430 $this->manager
->setLogger( $logger );
2433 $user = \User
::newFromName( 'UTSysop' );
2434 $this->manager
->autoCreateUser( $user, 'InvalidSource', true );
2435 $this->fail( 'Expected exception not thrown' );
2436 } catch ( \InvalidArgumentException
$ex ) {
2437 $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
2440 // First, check an existing user
2442 $user = \User
::newFromName( 'UTSysop' );
2443 $this->hook( 'LocalUserCreated', $this->never() );
2444 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2445 $this->unhook( 'LocalUserCreated' );
2446 $expect = \Status
::newGood();
2447 $expect->warning( 'userexists' );
2448 $this->assertEquals( $expect, $ret );
2449 $this->assertNotEquals( 0, $user->getId() );
2450 $this->assertSame( 'UTSysop', $user->getName() );
2451 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2452 $this->assertSame( [
2453 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2454 ], $logger->getBuffer() );
2455 $logger->clearBuffer();
2458 $user = \User
::newFromName( 'UTSysop' );
2459 $this->hook( 'LocalUserCreated', $this->never() );
2460 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2461 $this->unhook( 'LocalUserCreated' );
2462 $expect = \Status
::newGood();
2463 $expect->warning( 'userexists' );
2464 $this->assertEquals( $expect, $ret );
2465 $this->assertNotEquals( 0, $user->getId() );
2466 $this->assertSame( 'UTSysop', $user->getName() );
2467 $this->assertEquals( 0, $session->getUser()->getId() );
2468 $this->assertSame( [
2469 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2470 ], $logger->getBuffer() );
2471 $logger->clearBuffer();
2473 // Wiki is read-only
2475 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
2476 $user = \User
::newFromName( $username );
2477 $this->hook( 'LocalUserCreated', $this->never() );
2478 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2479 $this->unhook( 'LocalUserCreated' );
2480 $this->assertEquals( \Status
::newFatal( 'readonlytext', 'Because' ), $ret );
2481 $this->assertEquals( 0, $user->getId() );
2482 $this->assertNotEquals( $username, $user->getName() );
2483 $this->assertEquals( 0, $session->getUser()->getId() );
2484 $this->assertSame( [
2485 [ LogLevel
::DEBUG
, 'denied by wfReadOnly(): {reason}' ],
2486 ], $logger->getBuffer() );
2487 $logger->clearBuffer();
2488 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
2490 // Session blacklisted
2492 $session->set( 'AuthManager::AutoCreateBlacklist', 'test' );
2493 $user = \User
::newFromName( $username );
2494 $this->hook( 'LocalUserCreated', $this->never() );
2495 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2496 $this->unhook( 'LocalUserCreated' );
2497 $this->assertEquals( \Status
::newFatal( 'test' ), $ret );
2498 $this->assertEquals( 0, $user->getId() );
2499 $this->assertNotEquals( $username, $user->getName() );
2500 $this->assertEquals( 0, $session->getUser()->getId() );
2501 $this->assertSame( [
2502 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2503 ], $logger->getBuffer() );
2504 $logger->clearBuffer();
2507 $session->set( 'AuthManager::AutoCreateBlacklist', StatusValue
::newFatal( 'test2' ) );
2508 $user = \User
::newFromName( $username );
2509 $this->hook( 'LocalUserCreated', $this->never() );
2510 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2511 $this->unhook( 'LocalUserCreated' );
2512 $this->assertEquals( \Status
::newFatal( 'test2' ), $ret );
2513 $this->assertEquals( 0, $user->getId() );
2514 $this->assertNotEquals( $username, $user->getName() );
2515 $this->assertEquals( 0, $session->getUser()->getId() );
2516 $this->assertSame( [
2517 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2518 ], $logger->getBuffer() );
2519 $logger->clearBuffer();
2523 $user = \User
::newFromName( $username . '@' );
2524 $this->hook( 'LocalUserCreated', $this->never() );
2525 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2526 $this->unhook( 'LocalUserCreated' );
2527 $this->assertEquals( \Status
::newFatal( 'noname' ), $ret );
2528 $this->assertEquals( 0, $user->getId() );
2529 $this->assertNotEquals( $username . '@', $user->getId() );
2530 $this->assertEquals( 0, $session->getUser()->getId() );
2531 $this->assertSame( [
2532 [ LogLevel
::DEBUG
, 'name "{username}" is not creatable' ],
2533 ], $logger->getBuffer() );
2534 $logger->clearBuffer();
2535 $this->assertSame( 'noname', $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2537 // IP unable to create accounts
2538 $wgGroupPermissions['*']['createaccount'] = false;
2539 $wgGroupPermissions['*']['autocreateaccount'] = false;
2541 $user = \User
::newFromName( $username );
2542 $this->hook( 'LocalUserCreated', $this->never() );
2543 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2544 $this->unhook( 'LocalUserCreated' );
2545 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-noperm' ), $ret );
2546 $this->assertEquals( 0, $user->getId() );
2547 $this->assertNotEquals( $username, $user->getName() );
2548 $this->assertEquals( 0, $session->getUser()->getId() );
2549 $this->assertSame( [
2550 [ LogLevel
::DEBUG
, 'IP lacks the ability to create or autocreate accounts' ],
2551 ], $logger->getBuffer() );
2552 $logger->clearBuffer();
2554 'authmanager-autocreate-noperm', $session->get( 'AuthManager::AutoCreateBlacklist' )
2557 // Test that both permutations of permissions are allowed
2558 // (this hits the two "ok" entries in $mocks['pre'])
2559 $wgGroupPermissions['*']['createaccount'] = false;
2560 $wgGroupPermissions['*']['autocreateaccount'] = true;
2562 $user = \User
::newFromName( $username );
2563 $this->hook( 'LocalUserCreated', $this->never() );
2564 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2565 $this->unhook( 'LocalUserCreated' );
2566 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2568 $wgGroupPermissions['*']['createaccount'] = true;
2569 $wgGroupPermissions['*']['autocreateaccount'] = false;
2571 $user = \User
::newFromName( $username );
2572 $this->hook( 'LocalUserCreated', $this->never() );
2573 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2574 $this->unhook( 'LocalUserCreated' );
2575 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2576 $logger->clearBuffer();
2580 $user = \User
::newFromName( $username );
2581 $this->hook( 'LocalUserCreated', $this->never() );
2582 $cache = \ObjectCache
::getLocalClusterInstance();
2583 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2584 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2586 $this->unhook( 'LocalUserCreated' );
2587 $this->assertEquals( \Status
::newFatal( 'usernameinprogress' ), $ret );
2588 $this->assertEquals( 0, $user->getId() );
2589 $this->assertNotEquals( $username, $user->getName() );
2590 $this->assertEquals( 0, $session->getUser()->getId() );
2591 $this->assertSame( [
2592 [ LogLevel
::DEBUG
, 'Could not acquire account creation lock' ],
2593 ], $logger->getBuffer() );
2594 $logger->clearBuffer();
2596 // Test pre-authentication provider fail
2598 $user = \User
::newFromName( $username );
2599 $this->hook( 'LocalUserCreated', $this->never() );
2600 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2601 $this->unhook( 'LocalUserCreated' );
2602 $this->assertEquals( \Status
::newFatal( 'fail-in-pre' ), $ret );
2603 $this->assertEquals( 0, $user->getId() );
2604 $this->assertNotEquals( $username, $user->getName() );
2605 $this->assertEquals( 0, $session->getUser()->getId() );
2606 $this->assertSame( [
2607 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2608 ], $logger->getBuffer() );
2609 $logger->clearBuffer();
2610 $this->assertEquals(
2611 StatusValue
::newFatal( 'fail-in-pre' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2615 $user = \User
::newFromName( $username );
2616 $this->hook( 'LocalUserCreated', $this->never() );
2617 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2618 $this->unhook( 'LocalUserCreated' );
2619 $this->assertEquals( \Status
::newFatal( 'fail-in-primary' ), $ret );
2620 $this->assertEquals( 0, $user->getId() );
2621 $this->assertNotEquals( $username, $user->getName() );
2622 $this->assertEquals( 0, $session->getUser()->getId() );
2623 $this->assertSame( [
2624 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2625 ], $logger->getBuffer() );
2626 $logger->clearBuffer();
2627 $this->assertEquals(
2628 StatusValue
::newFatal( 'fail-in-primary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2632 $user = \User
::newFromName( $username );
2633 $this->hook( 'LocalUserCreated', $this->never() );
2634 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2635 $this->unhook( 'LocalUserCreated' );
2636 $this->assertEquals( \Status
::newFatal( 'fail-in-secondary' ), $ret );
2637 $this->assertEquals( 0, $user->getId() );
2638 $this->assertNotEquals( $username, $user->getName() );
2639 $this->assertEquals( 0, $session->getUser()->getId() );
2640 $this->assertSame( [
2641 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2642 ], $logger->getBuffer() );
2643 $logger->clearBuffer();
2644 $this->assertEquals(
2645 StatusValue
::newFatal( 'fail-in-secondary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2649 $cache = \ObjectCache
::getLocalClusterInstance();
2650 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2651 $cache->set( $backoffKey, true );
2653 $user = \User
::newFromName( $username );
2654 $this->hook( 'LocalUserCreated', $this->never() );
2655 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2656 $this->unhook( 'LocalUserCreated' );
2657 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-exception' ), $ret );
2658 $this->assertEquals( 0, $user->getId() );
2659 $this->assertNotEquals( $username, $user->getName() );
2660 $this->assertEquals( 0, $session->getUser()->getId() );
2661 $this->assertSame( [
2662 [ LogLevel
::DEBUG
, '{username} denied by prior creation attempt failures' ],
2663 ], $logger->getBuffer() );
2664 $logger->clearBuffer();
2665 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2666 $cache->delete( $backoffKey );
2668 // Test addToDatabase fails
2670 $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2671 $user->expects( $this->once() )->method( 'addToDatabase' )
2672 ->will( $this->returnValue( \Status
::newFatal( 'because' ) ) );
2673 $user->setName( $username );
2674 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2675 $this->assertEquals( \Status
::newFatal( 'because' ), $ret );
2676 $this->assertEquals( 0, $user->getId() );
2677 $this->assertNotEquals( $username, $user->getName() );
2678 $this->assertEquals( 0, $session->getUser()->getId() );
2679 $this->assertSame( [
2680 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2681 [ LogLevel
::ERROR
, '{username} failed with message {message}' ],
2682 ], $logger->getBuffer() );
2683 $logger->clearBuffer();
2684 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2686 // Test addToDatabase throws an exception
2687 $cache = \ObjectCache
::getLocalClusterInstance();
2688 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2689 $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
2691 $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2692 $user->expects( $this->once() )->method( 'addToDatabase' )
2693 ->will( $this->throwException( new \
Exception( 'Excepted' ) ) );
2694 $user->setName( $username );
2696 $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2697 $this->fail( 'Expected exception not thrown' );
2698 } catch ( \Exception
$ex ) {
2699 $this->assertSame( 'Excepted', $ex->getMessage() );
2701 $this->assertEquals( 0, $user->getId() );
2702 $this->assertEquals( 0, $session->getUser()->getId() );
2703 $this->assertSame( [
2704 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2705 [ LogLevel
::ERROR
, '{username} failed with exception {exception}' ],
2706 ], $logger->getBuffer() );
2707 $logger->clearBuffer();
2708 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2709 $this->assertNotEquals( false, $cache->get( $backoffKey ) );
2710 $cache->delete( $backoffKey );
2712 // Test addToDatabase fails because the user already exists.
2714 $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2715 $user->expects( $this->once() )->method( 'addToDatabase' )
2716 ->will( $this->returnCallback( function () use ( $username ) {
2717 $status = \User
::newFromName( $username )->addToDatabase();
2718 $this->assertTrue( $status->isOK(), 'sanity check' );
2719 return \Status
::newFatal( 'userexists' );
2721 $user->setName( $username );
2722 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2723 $expect = \Status
::newGood();
2724 $expect->warning( 'userexists' );
2725 $this->assertEquals( $expect, $ret );
2726 $this->assertNotEquals( 0, $user->getId() );
2727 $this->assertEquals( $username, $user->getName() );
2728 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2729 $this->assertSame( [
2730 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2731 [ LogLevel
::INFO
, '{username} already exists locally (race)' ],
2732 ], $logger->getBuffer() );
2733 $logger->clearBuffer();
2734 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2738 $username = self
::usernameForCreation();
2739 $user = \User
::newFromName( $username );
2740 $this->hook( 'AuthPluginAutoCreate', $this->once() )
2741 ->with( $callback );
2742 $this->hideDeprecated( 'AuthPluginAutoCreate hook (used in ' .
2743 get_class( $wgHooks['AuthPluginAutoCreate'][0] ) . '::onAuthPluginAutoCreate)' );
2744 $this->hook( 'LocalUserCreated', $this->once() )
2745 ->with( $callback, $this->equalTo( true ) );
2746 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2747 $this->unhook( 'LocalUserCreated' );
2748 $this->unhook( 'AuthPluginAutoCreate' );
2749 $this->assertEquals( \Status
::newGood(), $ret );
2750 $this->assertNotEquals( 0, $user->getId() );
2751 $this->assertEquals( $username, $user->getName() );
2752 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2753 $this->assertSame( [
2754 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2755 ], $logger->getBuffer() );
2756 $logger->clearBuffer();
2758 $dbw = wfGetDB( DB_MASTER
);
2759 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2761 $username = self
::usernameForCreation();
2762 $user = \User
::newFromName( $username );
2763 $this->hook( 'LocalUserCreated', $this->once() )
2764 ->with( $callback, $this->equalTo( true ) );
2765 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2766 $this->unhook( 'LocalUserCreated' );
2767 $this->assertEquals( \Status
::newGood(), $ret );
2768 $this->assertNotEquals( 0, $user->getId() );
2769 $this->assertEquals( $username, $user->getName() );
2770 $this->assertEquals( 0, $session->getUser()->getId() );
2771 $this->assertSame( [
2772 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2773 ], $logger->getBuffer() );
2774 $logger->clearBuffer();
2777 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2780 $this->config
->set( 'NewUserLog', true );
2782 $username = self
::usernameForCreation();
2783 $user = \User
::newFromName( $username );
2784 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2785 $this->assertEquals( \Status
::newGood(), $ret );
2786 $logger->clearBuffer();
2788 $data = \DatabaseLogEntry
::getSelectQueryData();
2789 $rows = iterator_to_array( $dbw->select(
2793 'log_id > ' . (int)$maxLogId,
2794 'log_type' => 'newusers'
2800 $this->assertCount( 1, $rows );
2801 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2803 $this->assertSame( 'autocreate', $entry->getSubtype() );
2804 $this->assertSame( $user->getId(), $entry->getPerformer()->getId() );
2805 $this->assertSame( $user->getName(), $entry->getPerformer()->getName() );
2806 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2807 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2809 $workaroundPHPUnitBug = true;
2813 * @dataProvider provideGetAuthenticationRequests
2814 * @param string $action
2815 * @param array $expect
2816 * @param array $state
2818 public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
2819 $makeReq = function ( $key ) use ( $action ) {
2820 $req = $this->getMock( AuthenticationRequest
::class );
2821 $req->expects( $this->any() )->method( 'getUniqueId' )
2822 ->will( $this->returnValue( $key ) );
2823 $req->action
= $action === AuthManager
::ACTION_UNLINK ? AuthManager
::ACTION_REMOVE
: $action;
2827 $cmpReqs = function ( $a, $b ) {
2828 $ret = strcmp( get_class( $a ), get_class( $b ) );
2830 $ret = strcmp( $a->key
, $b->key
);
2835 $good = StatusValue
::newGood();
2838 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2839 $class = ucfirst( $key ) . 'AuthenticationProvider';
2840 $mocks[$key] = $this->getMockForAbstractClass(
2841 "MediaWiki\\Auth\\$class", [], "Mock$class"
2843 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2844 ->will( $this->returnValue( $key ) );
2845 $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2846 ->will( $this->returnCallback( function ( $action ) use ( $key, $makeReq ) {
2847 return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
2849 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
2850 ->will( $this->returnValue( $good ) );
2855 PrimaryAuthenticationProvider
::TYPE_NONE
,
2856 PrimaryAuthenticationProvider
::TYPE_CREATE
,
2857 PrimaryAuthenticationProvider
::TYPE_LINK
2859 $class = 'PrimaryAuthenticationProvider';
2860 $mocks["primary-$type"] = $this->getMockForAbstractClass(
2861 "MediaWiki\\Auth\\$class", [], "Mock$class"
2863 $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
2864 ->will( $this->returnValue( "primary-$type" ) );
2865 $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
2866 ->will( $this->returnValue( $type ) );
2867 $mocks["primary-$type"]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2868 ->will( $this->returnCallback( function ( $action ) use ( $type, $makeReq ) {
2869 return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
2871 $mocks["primary-$type"]->expects( $this->any() )
2872 ->method( 'providerAllowsAuthenticationDataChange' )
2873 ->will( $this->returnValue( $good ) );
2874 $this->primaryauthMocks
[] = $mocks["primary-$type"];
2877 $mocks['primary2'] = $this->getMockForAbstractClass(
2878 PrimaryAuthenticationProvider
::class, [], "MockPrimaryAuthenticationProvider"
2880 $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
2881 ->will( $this->returnValue( 'primary2' ) );
2882 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
2883 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
2884 $mocks['primary2']->expects( $this->any() )->method( 'getAuthenticationRequests' )
2885 ->will( $this->returnValue( [] ) );
2886 $mocks['primary2']->expects( $this->any() )
2887 ->method( 'providerAllowsAuthenticationDataChange' )
2888 ->will( $this->returnCallback( function ( $req ) use ( $good ) {
2889 return $req->key
=== 'generic' ? StatusValue
::newFatal( 'no' ) : $good;
2891 $this->primaryauthMocks
[] = $mocks['primary2'];
2893 $this->preauthMocks
= [ $mocks['pre'] ];
2894 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2895 $this->initializeManager( true );
2898 if ( isset( $state['continueRequests'] ) ) {
2899 $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
2901 if ( $action === AuthManager
::ACTION_LOGIN_CONTINUE
) {
2902 $this->request
->getSession()->setSecret( 'AuthManager::authnState', $state );
2903 } elseif ( $action === AuthManager
::ACTION_CREATE_CONTINUE
) {
2904 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $state );
2905 } elseif ( $action === AuthManager
::ACTION_LINK_CONTINUE
) {
2906 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', $state );
2910 $expectReqs = array_map( $makeReq, $expect );
2911 if ( $action === AuthManager
::ACTION_LOGIN
) {
2912 $req = new RememberMeAuthenticationRequest
;
2913 $req->action
= $action;
2914 $req->required
= AuthenticationRequest
::REQUIRED
;
2915 $expectReqs[] = $req;
2916 } elseif ( $action === AuthManager
::ACTION_CREATE
) {
2917 $req = new UsernameAuthenticationRequest
;
2918 $req->action
= $action;
2919 $expectReqs[] = $req;
2920 $req = new UserDataAuthenticationRequest
;
2921 $req->action
= $action;
2922 $req->required
= AuthenticationRequest
::REQUIRED
;
2923 $expectReqs[] = $req;
2925 usort( $expectReqs, $cmpReqs );
2927 $actual = $this->manager
->getAuthenticationRequests( $action );
2928 foreach ( $actual as $req ) {
2929 // Don't test this here.
2930 $req->required
= AuthenticationRequest
::REQUIRED
;
2932 usort( $actual, $cmpReqs );
2934 $this->assertEquals( $expectReqs, $actual );
2936 // Test CreationReasonAuthenticationRequest gets returned
2937 if ( $action === AuthManager
::ACTION_CREATE
) {
2938 $req = new CreationReasonAuthenticationRequest
;
2939 $req->action
= $action;
2940 $req->required
= AuthenticationRequest
::REQUIRED
;
2941 $expectReqs[] = $req;
2942 usort( $expectReqs, $cmpReqs );
2944 $actual = $this->manager
->getAuthenticationRequests( $action, \User
::newFromName( 'UTSysop' ) );
2945 foreach ( $actual as $req ) {
2946 // Don't test this here.
2947 $req->required
= AuthenticationRequest
::REQUIRED
;
2949 usort( $actual, $cmpReqs );
2951 $this->assertEquals( $expectReqs, $actual );
2955 public static function provideGetAuthenticationRequests() {
2958 AuthManager
::ACTION_LOGIN
,
2959 [ 'pre-login', 'primary-none-login', 'primary-create-login',
2960 'primary-link-login', 'secondary-login', 'generic' ],
2963 AuthManager
::ACTION_CREATE
,
2964 [ 'pre-create', 'primary-none-create', 'primary-create-create',
2965 'primary-link-create', 'secondary-create', 'generic' ],
2968 AuthManager
::ACTION_LINK
,
2969 [ 'primary-link-link', 'generic' ],
2972 AuthManager
::ACTION_CHANGE
,
2973 [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
2974 'secondary-change' ],
2977 AuthManager
::ACTION_REMOVE
,
2978 [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
2979 'secondary-remove' ],
2982 AuthManager
::ACTION_UNLINK
,
2983 [ 'primary-link-remove' ],
2986 AuthManager
::ACTION_LOGIN_CONTINUE
,
2990 AuthManager
::ACTION_LOGIN_CONTINUE
,
2991 $reqs = [ 'continue-login', 'foo', 'bar' ],
2993 'continueRequests' => $reqs,
2997 AuthManager
::ACTION_CREATE_CONTINUE
,
3001 AuthManager
::ACTION_CREATE_CONTINUE
,
3002 $reqs = [ 'continue-create', 'foo', 'bar' ],
3004 'continueRequests' => $reqs,
3008 AuthManager
::ACTION_LINK_CONTINUE
,
3012 AuthManager
::ACTION_LINK_CONTINUE
,
3013 $reqs = [ 'continue-link', 'foo', 'bar' ],
3015 'continueRequests' => $reqs,
3021 public function testGetAuthenticationRequestsRequired() {
3022 $makeReq = function ( $key, $required ) {
3023 $req = $this->getMock( AuthenticationRequest
::class );
3024 $req->expects( $this->any() )->method( 'getUniqueId' )
3025 ->will( $this->returnValue( $key ) );
3026 $req->action
= AuthManager
::ACTION_LOGIN
;
3028 $req->required
= $required;
3031 $cmpReqs = function ( $a, $b ) {
3032 $ret = strcmp( get_class( $a ), get_class( $b ) );
3034 $ret = strcmp( $a->key
, $b->key
);
3039 $good = StatusValue
::newGood();
3041 $primary1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3042 $primary1->expects( $this->any() )->method( 'getUniqueId' )
3043 ->will( $this->returnValue( 'primary1' ) );
3044 $primary1->expects( $this->any() )->method( 'accountCreationType' )
3045 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3046 $primary1->expects( $this->any() )->method( 'getAuthenticationRequests' )
3047 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3049 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3050 $makeReq( "required", AuthenticationRequest
::REQUIRED
),
3051 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3052 $makeReq( "foo", AuthenticationRequest
::REQUIRED
),
3053 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3054 $makeReq( "baz", AuthenticationRequest
::OPTIONAL
),
3058 $primary2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3059 $primary2->expects( $this->any() )->method( 'getUniqueId' )
3060 ->will( $this->returnValue( 'primary2' ) );
3061 $primary2->expects( $this->any() )->method( 'accountCreationType' )
3062 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3063 $primary2->expects( $this->any() )->method( 'getAuthenticationRequests' )
3064 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3066 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3067 $makeReq( "required2", AuthenticationRequest
::REQUIRED
),
3068 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3072 $secondary = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3073 $secondary->expects( $this->any() )->method( 'getUniqueId' )
3074 ->will( $this->returnValue( 'secondary' ) );
3075 $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
3076 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3078 $makeReq( "foo", AuthenticationRequest
::OPTIONAL
),
3079 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3080 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3084 $rememberReq = new RememberMeAuthenticationRequest
;
3085 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
3087 $this->primaryauthMocks
= [ $primary1, $primary2 ];
3088 $this->secondaryauthMocks
= [ $secondary ];
3089 $this->initializeManager( true );
3091 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3094 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3095 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3096 $makeReq( "required2", AuthenticationRequest
::PRIMARY_REQUIRED
),
3097 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3098 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3099 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3100 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3101 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3103 usort( $actual, $cmpReqs );
3104 usort( $expected, $cmpReqs );
3105 $this->assertEquals( $expected, $actual );
3107 $this->primaryauthMocks
= [ $primary1 ];
3108 $this->secondaryauthMocks
= [ $secondary ];
3109 $this->initializeManager( true );
3111 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3114 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3115 $makeReq( "required", AuthenticationRequest
::REQUIRED
),
3116 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3117 $makeReq( "foo", AuthenticationRequest
::REQUIRED
),
3118 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3119 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3121 usort( $actual, $cmpReqs );
3122 usort( $expected, $cmpReqs );
3123 $this->assertEquals( $expected, $actual );
3126 public function testAllowsPropertyChange() {
3128 foreach ( [ 'primary', 'secondary' ] as $key ) {
3129 $class = ucfirst( $key ) . 'AuthenticationProvider';
3130 $mocks[$key] = $this->getMockForAbstractClass(
3131 "MediaWiki\\Auth\\$class", [], "Mock$class"
3133 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3134 ->will( $this->returnValue( $key ) );
3135 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
3136 ->will( $this->returnCallback( function ( $prop ) use ( $key ) {
3137 return $prop !== $key;
3141 $this->primaryauthMocks
= [ $mocks['primary'] ];
3142 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
3143 $this->initializeManager( true );
3145 $this->assertTrue( $this->manager
->allowsPropertyChange( 'foo' ) );
3146 $this->assertFalse( $this->manager
->allowsPropertyChange( 'primary' ) );
3147 $this->assertFalse( $this->manager
->allowsPropertyChange( 'secondary' ) );
3150 public function testAutoCreateOnLogin() {
3151 $username = self
::usernameForCreation();
3153 $req = $this->getMock( AuthenticationRequest
::class );
3155 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3156 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3157 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3158 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3159 $mock->expects( $this->any() )->method( 'accountCreationType' )
3160 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3161 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3162 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3163 ->will( $this->returnValue( StatusValue
::newGood() ) );
3165 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3166 $mock2->expects( $this->any() )->method( 'getUniqueId' )
3167 ->will( $this->returnValue( 'secondary' ) );
3168 $mock2->expects( $this->any() )->method( 'beginSecondaryAuthentication' )->will(
3170 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) )
3173 $mock2->expects( $this->any() )->method( 'continueSecondaryAuthentication' )
3174 ->will( $this->returnValue( AuthenticationResponse
::newAbstain() ) );
3175 $mock2->expects( $this->any() )->method( 'testUserForCreation' )
3176 ->will( $this->returnValue( StatusValue
::newGood() ) );
3178 $this->primaryauthMocks
= [ $mock ];
3179 $this->secondaryauthMocks
= [ $mock2 ];
3180 $this->initializeManager( true );
3181 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3182 $session = $this->request
->getSession();
3185 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3188 $callback = $this->callback( function ( $user ) use ( $username ) {
3189 return $user->getName() === $username;
3192 $this->hook( 'UserLoggedIn', $this->never() );
3193 $this->hook( 'LocalUserCreated', $this->once() )->with( $callback, $this->equalTo( true ) );
3194 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3195 $this->unhook( 'LocalUserCreated' );
3196 $this->unhook( 'UserLoggedIn' );
3197 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
3199 $id = (int)\User
::newFromName( $username )->getId();
3200 $this->assertNotSame( 0, \User
::newFromName( $username )->getId() );
3201 $this->assertSame( 0, $session->getUser()->getId() );
3203 $this->hook( 'UserLoggedIn', $this->once() )->with( $callback );
3204 $this->hook( 'LocalUserCreated', $this->never() );
3205 $ret = $this->manager
->continueAuthentication( [] );
3206 $this->unhook( 'LocalUserCreated' );
3207 $this->unhook( 'UserLoggedIn' );
3208 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
3209 $this->assertSame( $username, $ret->username
);
3210 $this->assertSame( $id, $session->getUser()->getId() );
3213 public function testAutoCreateFailOnLogin() {
3214 $username = self
::usernameForCreation();
3216 $mock = $this->getMockForAbstractClass(
3217 PrimaryAuthenticationProvider
::class, [], "MockPrimaryAuthenticationProvider" );
3218 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3219 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3220 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3221 $mock->expects( $this->any() )->method( 'accountCreationType' )
3222 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3223 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3224 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3225 ->will( $this->returnValue( StatusValue
::newFatal( 'fail-from-primary' ) ) );
3227 $this->primaryauthMocks
= [ $mock ];
3228 $this->initializeManager( true );
3229 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3230 $session = $this->request
->getSession();
3233 $this->assertSame( 0, $session->getUser()->getId(),
3235 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3238 $this->hook( 'UserLoggedIn', $this->never() );
3239 $this->hook( 'LocalUserCreated', $this->never() );
3240 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3241 $this->unhook( 'LocalUserCreated' );
3242 $this->unhook( 'UserLoggedIn' );
3243 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3244 $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message
->getKey() );
3246 $this->assertSame( 0, \User
::newFromName( $username )->getId() );
3247 $this->assertSame( 0, $session->getUser()->getId() );
3250 public function testAuthenticationSessionData() {
3251 $this->initializeManager( true );
3253 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3254 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3255 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3256 $this->assertSame( 'foo!', $this->manager
->getAuthenticationSessionData( 'foo' ) );
3257 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3258 $this->manager
->removeAuthenticationSessionData( 'foo' );
3259 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3260 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3261 $this->manager
->removeAuthenticationSessionData( 'bar' );
3262 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3264 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3265 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3266 $this->manager
->removeAuthenticationSessionData( null );
3267 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3268 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3272 public function testCanLinkAccounts() {
3274 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
3275 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
3276 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
3279 foreach ( $types as $type => $can ) {
3280 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3281 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
3282 $mock->expects( $this->any() )->method( 'accountCreationType' )
3283 ->will( $this->returnValue( $type ) );
3284 $this->primaryauthMocks
= [ $mock ];
3285 $this->initializeManager( true );
3286 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
3290 public function testBeginAccountLink() {
3291 $user = \User
::newFromName( 'UTSysop' );
3292 $this->initializeManager();
3294 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', 'test' );
3296 $this->manager
->beginAccountLink( $user, [], 'http://localhost/' );
3297 $this->fail( 'Expected exception not thrown' );
3298 } catch ( \LogicException
$ex ) {
3299 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3301 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3303 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3304 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3305 $mock->expects( $this->any() )->method( 'accountCreationType' )
3306 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3307 $this->primaryauthMocks
= [ $mock ];
3308 $this->initializeManager( true );
3310 $ret = $this->manager
->beginAccountLink( new \User
, [], 'http://localhost/' );
3311 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3312 $this->assertSame( 'noname', $ret->message
->getKey() );
3314 $ret = $this->manager
->beginAccountLink(
3315 \User
::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
3317 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3318 $this->assertSame( 'authmanager-userdoesnotexist', $ret->message
->getKey() );
3321 public function testContinueAccountLink() {
3322 $user = \User
::newFromName( 'UTSysop' );
3323 $this->initializeManager();
3326 'userid' => $user->getId(),
3327 'username' => $user->getName(),
3332 $this->manager
->continueAccountLink( [] );
3333 $this->fail( 'Expected exception not thrown' );
3334 } catch ( \LogicException
$ex ) {
3335 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3338 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3339 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3340 $mock->expects( $this->any() )->method( 'accountCreationType' )
3341 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3342 $mock->expects( $this->any() )->method( 'beginPrimaryAccountLink' )->will(
3343 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
3345 $this->primaryauthMocks
= [ $mock ];
3346 $this->initializeManager( true );
3348 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', null );
3349 $ret = $this->manager
->continueAccountLink( [] );
3350 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3351 $this->assertSame( 'authmanager-link-not-in-progress', $ret->message
->getKey() );
3353 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3354 [ 'username' => $user->getName() . '<>' ] +
$session );
3355 $ret = $this->manager
->continueAccountLink( [] );
3356 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3357 $this->assertSame( 'noname', $ret->message
->getKey() );
3358 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3360 $id = $user->getId();
3361 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3362 [ 'userid' => $id +
1 ] +
$session );
3364 $ret = $this->manager
->continueAccountLink( [] );
3365 $this->fail( 'Expected exception not thrown' );
3366 } catch ( \UnexpectedValueException
$ex ) {
3367 $this->assertEquals(
3368 "User \"{$user->getName()}\" is valid, but ID $id != " . ( $id +
1 ) . '!',
3372 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3376 * @dataProvider provideAccountLink
3377 * @param StatusValue $preTest
3378 * @param array $primaryResponses
3379 * @param array $managerResponses
3381 public function testAccountLink(
3382 StatusValue
$preTest, array $primaryResponses, array $managerResponses
3384 $user = \User
::newFromName( 'UTSysop' );
3386 $this->initializeManager();
3388 // Set up lots of mocks...
3389 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3390 $req->primary
= $primaryResponses;
3393 foreach ( [ 'pre', 'primary' ] as $key ) {
3394 $class = ucfirst( $key ) . 'AuthenticationProvider';
3395 $mocks[$key] = $this->getMockForAbstractClass(
3396 "MediaWiki\\Auth\\$class", [], "Mock$class"
3398 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3399 ->will( $this->returnValue( $key ) );
3401 for ( $i = 2; $i <= 3; $i++
) {
3402 $mocks[$key . $i] = $this->getMockForAbstractClass(
3403 "MediaWiki\\Auth\\$class", [], "Mock$class"
3405 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
3406 ->will( $this->returnValue( $key . $i ) );
3410 $mocks['pre']->expects( $this->any() )->method( 'testForAccountLink' )
3411 ->will( $this->returnCallback(
3413 use ( $user, $preTest )
3415 $this->assertSame( $user->getId(), $u->getId() );
3416 $this->assertSame( $user->getName(), $u->getName() );
3421 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
3422 ->will( $this->returnValue( StatusValue
::newGood() ) );
3424 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
3425 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3426 $ct = count( $req->primary
);
3427 $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req ) {
3428 $this->assertSame( $user->getId(), $u->getId() );
3429 $this->assertSame( $user->getName(), $u->getName() );
3431 foreach ( $reqs as $r ) {
3432 $this->assertSame( $user->getName(), $r->username
);
3433 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
3435 $this->assertTrue( $foundReq, '$reqs contains $req' );
3436 return array_shift( $req->primary
);
3438 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
3439 ->method( 'beginPrimaryAccountLink' )
3440 ->will( $callback );
3441 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
3442 ->method( 'continuePrimaryAccountLink' )
3443 ->will( $callback );
3445 $abstain = AuthenticationResponse
::newAbstain();
3446 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
3447 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3448 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
3449 ->will( $this->returnValue( $abstain ) );
3450 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3451 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
3452 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3453 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
3454 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3456 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
3457 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
3458 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
3459 return $level === LogLevel
::DEBUG ?
null : $message;
3461 $this->initializeManager( true );
3463 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
3464 $this->equalTo( AuthenticationResponse
::PASS
),
3465 $this->equalTo( AuthenticationResponse
::FAIL
)
3467 $providers = array_merge( $this->preauthMocks
, $this->primaryauthMocks
);
3468 foreach ( $providers as $p ) {
3469 $p->postCalled
= false;
3470 $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
3471 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
3472 $this->assertInstanceOf( 'User', $user );
3473 $this->assertSame( 'UTSysop', $user->getName() );
3474 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
3475 $this->assertThat( $response->status
, $constraint );
3476 $p->postCalled
= $response->status
;
3483 foreach ( $managerResponses as $i => $response ) {
3484 if ( $response instanceof AuthenticationResponse
&&
3485 $response->status
=== AuthenticationResponse
::PASS
3487 $expectLog[] = [ LogLevel
::INFO
, 'Account linked to {user} by primary' ];
3493 $ret = $this->manager
->beginAccountLink( $user, [ $req ], 'http://localhost/' );
3495 $ret = $this->manager
->continueAccountLink( [ $req ] );
3497 if ( $response instanceof \Exception
) {
3498 $this->fail( 'Expected exception not thrown', "Response $i" );
3500 } catch ( \Exception
$ex ) {
3501 if ( !$response instanceof \Exception
) {
3504 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
3505 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3506 "Response $i, exception, session state" );
3510 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
3512 $ret->message
= $this->message( $ret->message
);
3513 $this->assertEquals( $response, $ret, "Response $i, response" );
3514 if ( $response->status
=== AuthenticationResponse
::PASS ||
3515 $response->status
=== AuthenticationResponse
::FAIL
3517 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3518 "Response $i, session state" );
3519 foreach ( $providers as $p ) {
3520 $this->assertSame( $response->status
, $p->postCalled
,
3521 "Response $i, post-auth callback called" );
3524 $this->assertNotNull(
3525 $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3526 "Response $i, session state"
3528 $this->assertEquals(
3529 $ret->neededRequests
,
3530 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LINK_CONTINUE
),
3531 "Response $i, continuation check"
3533 foreach ( $providers as $p ) {
3534 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
3541 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
3544 public function provideAccountLink() {
3545 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3546 $good = StatusValue
::newGood();
3549 'Pre-link test fail in pre' => [
3550 StatusValue
::newFatal( 'fail-from-pre' ),
3553 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
3556 'Failure in primary' => [
3559 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
3563 'All primary abstain' => [
3566 AuthenticationResponse
::newAbstain(),
3569 AuthenticationResponse
::newFail( $this->message( 'authmanager-link-no-primary' ) )
3572 'Primary UI, then redirect, then fail' => [
3575 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3576 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
3577 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
3581 'Primary redirect, then abstain' => [
3584 $tmp = AuthenticationResponse
::newRedirect(
3585 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
3587 AuthenticationResponse
::newAbstain(),
3591 new \
DomainException(
3592 'MockPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
3596 'Primary UI, then pass' => [
3599 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3600 AuthenticationResponse
::newPass(),
3604 AuthenticationResponse
::newPass( '' ),
3610 AuthenticationResponse
::newPass( '' ),
3613 AuthenticationResponse
::newPass( '' ),