3 namespace MediaWiki\Auth
;
6 use MediaWiki\Session\SessionInfo
;
7 use MediaWiki\Session\UserInfo
;
8 use Psr\Log\LoggerInterface
;
12 use Wikimedia\ScopedCallback
;
13 use Wikimedia\TestingAccessWrapper
;
18 * @covers MediaWiki\Auth\AuthManager
20 class AuthManagerTest
extends \MediaWikiTestCase
{
21 /** @var WebRequest */
25 /** @var LoggerInterface */
28 protected $preauthMocks = [];
29 protected $primaryauthMocks = [];
30 protected $secondaryauthMocks = [];
32 /** @var AuthManager */
34 /** @var TestingAccessWrapper */
35 protected $managerPriv;
37 protected function setUp() {
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->getMockBuilder( __CLASS__
)
53 ->setMethods( [ "on$hook" ] )
55 $wgHooks[$hook] = [ $mock ];
56 return $mock->expects( $expect )->method( "on$hook" );
63 protected function unhook( $hook ) {
69 * Ensure a value is a clean Message object
70 * @param string|Message $key
71 * @param array $params
74 protected function message( $key, $params = [] ) {
75 if ( $key === null ) {
78 if ( $key instanceof \MessageSpecifier
) {
79 $params = $key->getParams();
80 $key = $key->getKey();
82 return new \
Message( $key, $params, \Language
::factory( 'en' ) );
86 * Initialize the AuthManagerConfig variable in $this->config
88 * Uses data from the various 'mocks' fields.
90 protected function initializeConfig() {
100 foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
101 $key = $type . 'Mocks';
102 foreach ( $this->$key as $mock ) {
103 $config[$type][$mock->getUniqueId()] = [ 'factory' => function () use ( $mock ) {
109 $this->config
->set( 'AuthManagerConfig', $config );
110 $this->config
->set( 'LanguageCode', 'en' );
111 $this->config
->set( 'NewUserLog', false );
115 * Initialize $this->manager
116 * @param bool $regen Force a call to $this->initializeConfig()
118 protected function initializeManager( $regen = false ) {
119 if ( $regen ||
!$this->config
) {
120 $this->config
= new \
HashConfig();
122 if ( $regen ||
!$this->request
) {
123 $this->request
= new \
FauxRequest();
125 if ( !$this->logger
) {
126 $this->logger
= new \
TestLogger();
129 if ( $regen ||
!$this->config
->has( 'AuthManagerConfig' ) ) {
130 $this->initializeConfig();
132 $this->manager
= new AuthManager( $this->request
, $this->config
);
133 $this->manager
->setLogger( $this->logger
);
134 $this->managerPriv
= TestingAccessWrapper
::newFromObject( $this->manager
);
138 * Setup SessionManager with a mock session provider
139 * @param bool|null $canChangeUser If non-null, canChangeUser will be mocked to return this
140 * @param array $methods Additional methods to mock
141 * @return array (MediaWiki\Session\SessionProvider, ScopedCallback)
143 protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
144 if ( !$this->config
) {
145 $this->config
= new \
HashConfig();
146 $this->initializeConfig();
148 $this->config
->set( 'ObjectCacheSessionExpiry', 100 );
150 $methods[] = '__toString';
151 $methods[] = 'describe';
152 if ( $canChangeUser !== null ) {
153 $methods[] = 'canChangeUser';
155 $provider = $this->getMockBuilder( \DummySessionProvider
::class )
156 ->setMethods( $methods )
158 $provider->expects( $this->any() )->method( '__toString' )
159 ->will( $this->returnValue( 'MockSessionProvider' ) );
160 $provider->expects( $this->any() )->method( 'describe' )
161 ->will( $this->returnValue( 'MockSessionProvider sessions' ) );
162 if ( $canChangeUser !== null ) {
163 $provider->expects( $this->any() )->method( 'canChangeUser' )
164 ->will( $this->returnValue( $canChangeUser ) );
166 $this->config
->set( 'SessionProviders', [
167 [ 'factory' => function () use ( $provider ) {
172 $manager = new \MediaWiki\Session\
SessionManager( [
173 'config' => $this->config
,
174 'logger' => new \Psr\Log\
NullLogger(),
175 'store' => new \
HashBagOStuff(),
177 TestingAccessWrapper
::newFromObject( $manager )->getProvider( (string)$provider );
179 $reset = \MediaWiki\Session\TestUtils
::setSessionManagerSingleton( $manager );
181 if ( $this->request
) {
182 $manager->getSessionForRequest( $this->request
);
185 return [ $provider, $reset ];
188 public function testSingleton() {
189 // Temporarily clear out the global singleton, if any, to test creating
191 $rProp = new \
ReflectionProperty( AuthManager
::class, 'instance' );
192 $rProp->setAccessible( true );
193 $old = $rProp->getValue();
194 $cb = new ScopedCallback( [ $rProp, 'setValue' ], [ $old ] );
195 $rProp->setValue( null );
197 $singleton = AuthManager
::singleton();
198 $this->assertInstanceOf( AuthManager
::class, AuthManager
::singleton() );
199 $this->assertSame( $singleton, AuthManager
::singleton() );
200 $this->assertSame( \RequestContext
::getMain()->getRequest(), $singleton->getRequest() );
202 \RequestContext
::getMain()->getConfig(),
203 TestingAccessWrapper
::newFromObject( $singleton )->config
207 public function testCanAuthenticateNow() {
208 $this->initializeManager();
210 list( $provider, $reset ) = $this->getMockSessionProvider( false );
211 $this->assertFalse( $this->manager
->canAuthenticateNow() );
212 ScopedCallback
::consume( $reset );
214 list( $provider, $reset ) = $this->getMockSessionProvider( true );
215 $this->assertTrue( $this->manager
->canAuthenticateNow() );
216 ScopedCallback
::consume( $reset );
219 public function testNormalizeUsername() {
221 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
222 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
223 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
224 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
226 foreach ( $mocks as $key => $mock ) {
227 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
229 $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
230 ->with( $this->identicalTo( 'XYZ' ) )
231 ->willReturn( 'Foo' );
232 $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
233 ->with( $this->identicalTo( 'XYZ' ) )
234 ->willReturn( 'Foo' );
235 $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
236 ->with( $this->identicalTo( 'XYZ' ) )
237 ->willReturn( null );
238 $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
239 ->with( $this->identicalTo( 'XYZ' ) )
240 ->willReturn( 'Bar!' );
242 $this->primaryauthMocks
= $mocks;
244 $this->initializeManager();
246 $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager
->normalizeUsername( 'XYZ' ) );
250 * @dataProvider provideSecuritySensitiveOperationStatus
251 * @param bool $mutableSession
253 public function testSecuritySensitiveOperationStatus( $mutableSession ) {
254 $this->logger
= new \Psr\Log\
NullLogger();
255 $user = \User
::newFromName( 'UTSysop' );
257 $reauth = $mutableSession ? AuthManager
::SEC_REAUTH
: AuthManager
::SEC_FAIL
;
259 list( $provider, $reset ) = $this->getMockSessionProvider(
260 $mutableSession, [ 'provideSessionInfo' ]
262 $provider->expects( $this->any() )->method( 'provideSessionInfo' )
263 ->will( $this->returnCallback( function () use ( $provider, &$provideUser ) {
264 return new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
265 'provider' => $provider,
266 'id' => \DummySessionProvider
::ID
,
268 'userInfo' => UserInfo
::newFromUser( $provideUser, true )
271 $this->initializeManager();
273 $this->config
->set( 'ReauthenticateTime', [] );
274 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [] );
275 $provideUser = new \User
;
276 $session = $provider->getManager()->getSessionForRequest( $this->request
);
277 $this->assertSame( 0, $session->getUser()->getId(), 'sanity check' );
279 // Anonymous user => reauth
280 $session->set( 'AuthManager:lastAuthId', 0 );
281 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
282 $this->assertSame( $reauth, $this->manager
->securitySensitiveOperationStatus( 'foo' ) );
284 $provideUser = $user;
285 $session = $provider->getManager()->getSessionForRequest( $this->request
);
286 $this->assertSame( $user->getId(), $session->getUser()->getId(), 'sanity check' );
288 // Error for no default (only gets thrown for non-anonymous user)
289 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
290 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
292 $this->manager
->securitySensitiveOperationStatus( 'foo' );
293 $this->fail( 'Expected exception not thrown' );
294 } catch ( \UnexpectedValueException
$ex ) {
297 ?
'$wgReauthenticateTime lacks a default'
298 : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
303 if ( $mutableSession ) {
304 $this->config
->set( 'ReauthenticateTime', [
310 // Mismatched user ID
311 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
312 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
314 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
317 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
320 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
324 $session->set( 'AuthManager:lastAuthId', $user->getId() );
325 $session->set( 'AuthManager:lastAuthTimestamp', null );
327 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
330 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
333 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
336 // Recent enough to pass
337 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
339 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
342 // Not recent enough to pass
343 $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
345 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
347 // But recent enough for the 'test' operation
349 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test' )
352 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [
358 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
362 AuthManager
::SEC_FAIL
, $this->manager
->securitySensitiveOperationStatus( 'test' )
366 // Test hook, all three possible values
368 AuthManager
::SEC_OK
=> AuthManager
::SEC_OK
,
369 AuthManager
::SEC_REAUTH
=> $reauth,
370 AuthManager
::SEC_FAIL
=> AuthManager
::SEC_FAIL
,
371 ] as $hook => $expect ) {
372 $this->hook( 'SecuritySensitiveOperationStatus', $this->exactly( 2 ) )
376 $this->callback( function ( $s ) use ( $session ) {
377 return $s->getId() === $session->getId();
379 $mutableSession ?
$this->equalTo( 500, 1 ) : $this->equalTo( -1 )
381 ->will( $this->returnCallback( function ( &$v ) use ( $hook ) {
385 $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
387 $expect, $this->manager
->securitySensitiveOperationStatus( 'test' ), "hook $hook"
390 $expect, $this->manager
->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
392 $this->unhook( 'SecuritySensitiveOperationStatus' );
395 ScopedCallback
::consume( $reset );
398 public function onSecuritySensitiveOperationStatus( &$status, $operation, $session, $time ) {
401 public static function provideSecuritySensitiveOperationStatus() {
409 * @dataProvider provideUserCanAuthenticate
410 * @param bool $primary1Can
411 * @param bool $primary2Can
412 * @param bool $expect
414 public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
415 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
416 $mock1->expects( $this->any() )->method( 'getUniqueId' )
417 ->will( $this->returnValue( 'primary1' ) );
418 $mock1->expects( $this->any() )->method( 'testUserCanAuthenticate' )
419 ->with( $this->equalTo( 'UTSysop' ) )
420 ->will( $this->returnValue( $primary1Can ) );
421 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
422 $mock2->expects( $this->any() )->method( 'getUniqueId' )
423 ->will( $this->returnValue( 'primary2' ) );
424 $mock2->expects( $this->any() )->method( 'testUserCanAuthenticate' )
425 ->with( $this->equalTo( 'UTSysop' ) )
426 ->will( $this->returnValue( $primary2Can ) );
427 $this->primaryauthMocks
= [ $mock1, $mock2 ];
429 $this->initializeManager( true );
430 $this->assertSame( $expect, $this->manager
->userCanAuthenticate( 'UTSysop' ) );
433 public static function provideUserCanAuthenticate() {
435 [ false, false, false ],
436 [ true, false, true ],
437 [ false, true, true ],
438 [ true, true, true ],
442 public function testRevokeAccessForUser() {
443 $this->initializeManager();
445 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
446 $mock->expects( $this->any() )->method( 'getUniqueId' )
447 ->will( $this->returnValue( 'primary' ) );
448 $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
449 ->with( $this->equalTo( 'UTSysop' ) );
450 $this->primaryauthMocks
= [ $mock ];
452 $this->initializeManager( true );
453 $this->logger
->setCollect( true );
455 $this->manager
->revokeAccessForUser( 'UTSysop' );
458 [ LogLevel
::INFO
, 'Revoking access for {user}' ],
459 ], $this->logger
->getBuffer() );
462 public function testProviderCreation() {
464 'pre' => $this->getMockForAbstractClass( PreAuthenticationProvider
::class ),
465 'primary' => $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
466 'secondary' => $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class ),
468 foreach ( $mocks as $key => $mock ) {
469 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
470 $mock->expects( $this->once() )->method( 'setLogger' );
471 $mock->expects( $this->once() )->method( 'setManager' );
472 $mock->expects( $this->once() )->method( 'setConfig' );
474 $this->preauthMocks
= [ $mocks['pre'] ];
475 $this->primaryauthMocks
= [ $mocks['primary'] ];
476 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
479 $this->initializeManager();
482 $this->managerPriv
->getAuthenticationProvider( 'primary' )
486 $this->managerPriv
->getAuthenticationProvider( 'secondary' )
490 $this->managerPriv
->getAuthenticationProvider( 'pre' )
493 [ 'pre' => $mocks['pre'] ],
494 $this->managerPriv
->getPreAuthenticationProviders()
497 [ 'primary' => $mocks['primary'] ],
498 $this->managerPriv
->getPrimaryAuthenticationProviders()
501 [ 'secondary' => $mocks['secondary'] ],
502 $this->managerPriv
->getSecondaryAuthenticationProviders()
506 $mock1 = $this->getMockForAbstractClass( PreAuthenticationProvider
::class );
507 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
508 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
509 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
510 $this->preauthMocks
= [ $mock1 ];
511 $this->primaryauthMocks
= [ $mock2 ];
512 $this->secondaryauthMocks
= [];
513 $this->initializeManager( true );
515 $this->managerPriv
->getAuthenticationProvider( 'Y' );
516 $this->fail( 'Expected exception not thrown' );
517 } catch ( \RuntimeException
$ex ) {
518 $class1 = get_class( $mock1 );
519 $class2 = get_class( $mock2 );
521 "Duplicate specifications for id X (classes $class1 and $class2)", $ex->getMessage()
526 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
527 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
528 $class = get_class( $mock );
529 $this->preauthMocks
= [ $mock ];
530 $this->primaryauthMocks
= [ $mock ];
531 $this->secondaryauthMocks
= [ $mock ];
532 $this->initializeManager( true );
534 $this->managerPriv
->getPreAuthenticationProviders();
535 $this->fail( 'Expected exception not thrown' );
536 } catch ( \RuntimeException
$ex ) {
538 "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
543 $this->managerPriv
->getPrimaryAuthenticationProviders();
544 $this->fail( 'Expected exception not thrown' );
545 } catch ( \RuntimeException
$ex ) {
547 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
552 $this->managerPriv
->getSecondaryAuthenticationProviders();
553 $this->fail( 'Expected exception not thrown' );
554 } catch ( \RuntimeException
$ex ) {
556 "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
562 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
563 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
564 $mock3 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
565 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
566 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
567 $mock3->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'C' ) );
568 $this->preauthMocks
= [];
569 $this->primaryauthMocks
= [ $mock1, $mock2, $mock3 ];
570 $this->secondaryauthMocks
= [];
571 $this->initializeConfig();
572 $config = $this->config
->get( 'AuthManagerConfig' );
574 $this->initializeManager( false );
576 [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
577 $this->managerPriv
->getPrimaryAuthenticationProviders(),
581 $config['primaryauth']['A']['sort'] = 100;
582 $config['primaryauth']['C']['sort'] = -1;
583 $this->config
->set( 'AuthManagerConfig', $config );
584 $this->initializeManager( false );
586 [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
587 $this->managerPriv
->getPrimaryAuthenticationProviders()
591 public function testSetDefaultUserOptions() {
592 $this->initializeManager();
594 $context = \RequestContext
::getMain();
595 $reset = new ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
596 $context->setLanguage( 'de' );
597 $this->setMwGlobals( 'wgContLang', \Language
::factory( 'zh' ) );
599 $user = \User
::newFromName( self
::usernameForCreation() );
600 $user->addToDatabase();
601 $oldToken = $user->getToken();
602 $this->managerPriv
->setDefaultUserOptions( $user, false );
603 $user->saveSettings();
604 $this->assertNotEquals( $oldToken, $user->getToken() );
605 $this->assertSame( 'zh', $user->getOption( 'language' ) );
606 $this->assertSame( 'zh', $user->getOption( 'variant' ) );
608 $user = \User
::newFromName( self
::usernameForCreation() );
609 $user->addToDatabase();
610 $oldToken = $user->getToken();
611 $this->managerPriv
->setDefaultUserOptions( $user, true );
612 $user->saveSettings();
613 $this->assertNotEquals( $oldToken, $user->getToken() );
614 $this->assertSame( 'de', $user->getOption( 'language' ) );
615 $this->assertSame( 'zh', $user->getOption( 'variant' ) );
617 $this->setMwGlobals( 'wgContLang', \Language
::factory( 'fr' ) );
619 $user = \User
::newFromName( self
::usernameForCreation() );
620 $user->addToDatabase();
621 $oldToken = $user->getToken();
622 $this->managerPriv
->setDefaultUserOptions( $user, true );
623 $user->saveSettings();
624 $this->assertNotEquals( $oldToken, $user->getToken() );
625 $this->assertSame( 'de', $user->getOption( 'language' ) );
626 $this->assertSame( null, $user->getOption( 'variant' ) );
629 public function testForcePrimaryAuthenticationProviders() {
630 $mockA = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
631 $mockB = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
632 $mockB2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
633 $mockA->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
634 $mockB->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
635 $mockB2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
636 $this->primaryauthMocks
= [ $mockA ];
638 $this->logger
= new \
TestLogger( true );
640 // Test without first initializing the configured providers
641 $this->initializeManager();
642 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
644 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
646 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
647 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
649 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
650 ], $this->logger
->getBuffer() );
651 $this->logger
->clearBuffer();
653 // Test with first initializing the configured providers
654 $this->initializeManager();
655 $this->assertSame( $mockA, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
656 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
657 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
658 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
659 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
661 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
663 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
664 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
665 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
667 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
670 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
673 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
675 ], $this->logger
->getBuffer() );
676 $this->logger
->clearBuffer();
678 // Test duplicate IDs
679 $this->initializeManager();
681 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
682 $this->fail( 'Expected exception not thrown' );
683 } catch ( \RuntimeException
$ex ) {
684 $class1 = get_class( $mockB );
685 $class2 = get_class( $mockB2 );
687 "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
692 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
693 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
694 $class = get_class( $mock );
696 $this->manager
->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
697 $this->fail( 'Expected exception not thrown' );
698 } catch ( \RuntimeException
$ex ) {
700 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
706 public function testBeginAuthentication() {
707 $this->initializeManager();
710 list( $provider, $reset ) = $this->getMockSessionProvider( false );
711 $this->hook( 'UserLoggedIn', $this->never() );
712 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
714 $this->manager
->beginAuthentication( [], 'http://localhost/' );
715 $this->fail( 'Expected exception not thrown' );
716 } catch ( \LogicException
$ex ) {
717 $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
719 $this->unhook( 'UserLoggedIn' );
720 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
721 ScopedCallback
::consume( $reset );
722 $this->initializeManager( true );
724 // CreatedAccountAuthenticationRequest
725 $user = \User
::newFromName( 'UTSysop' );
727 new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
729 $this->hook( 'UserLoggedIn', $this->never() );
731 $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
732 $this->fail( 'Expected exception not thrown' );
733 } catch ( \LogicException
$ex ) {
735 'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
736 'that created the account',
740 $this->unhook( 'UserLoggedIn' );
742 $this->request
->getSession()->clear();
743 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
744 $this->managerPriv
->createdAccountAuthenticationRequests
= [ $reqs[0] ];
745 $this->hook( 'UserLoggedIn', $this->once() )
746 ->with( $this->callback( function ( $u ) use ( $user ) {
747 return $user->getId() === $u->getId() && $user->getName() === $u->getName();
749 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
750 $this->logger
->setCollect( true );
751 $ret = $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
752 $this->logger
->setCollect( false );
753 $this->unhook( 'UserLoggedIn' );
754 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
755 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
756 $this->assertSame( $user->getName(), $ret->username
);
757 $this->assertSame( $user->getId(), $this->request
->getSessionData( 'AuthManager:lastAuthId' ) );
759 time(), $this->request
->getSessionData( 'AuthManager:lastAuthTimestamp' ),
762 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
763 $this->assertSame( $user->getId(), $this->request
->getSession()->getUser()->getId() );
765 [ LogLevel
::INFO
, 'Logging in {user} after account creation' ],
766 ], $this->logger
->getBuffer() );
769 public function testCreateFromLogin() {
770 $user = \User
::newFromName( 'UTSysop' );
771 $req1 = $this->createMock( AuthenticationRequest
::class );
772 $req2 = $this->createMock( AuthenticationRequest
::class );
773 $req3 = $this->createMock( AuthenticationRequest
::class );
774 $userReq = new UsernameAuthenticationRequest
;
775 $userReq->username
= 'UTDummy';
777 $req1->returnToUrl
= 'http://localhost/';
778 $req2->returnToUrl
= 'http://localhost/';
779 $req3->returnToUrl
= 'http://localhost/';
780 $req3->username
= 'UTDummy';
781 $userReq->returnToUrl
= 'http://localhost/';
783 // Passing one into beginAuthentication(), and an immediate FAIL
784 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
785 $this->primaryauthMocks
= [ $primary ];
786 $this->initializeManager( true );
787 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
788 $res->createRequest
= $req1;
789 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
790 ->will( $this->returnValue( $res ) );
791 $createReq = new CreateFromLoginAuthenticationRequest(
792 null, [ $req2->getUniqueId() => $req2 ]
794 $this->logger
->setCollect( true );
795 $ret = $this->manager
->beginAuthentication( [ $createReq ], 'http://localhost/' );
796 $this->logger
->setCollect( false );
797 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
798 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
799 $this->assertSame( $req1, $ret->createRequest
->createRequest
);
800 $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest
->maybeLink
);
802 // UI, then FAIL in beginAuthentication()
803 $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider
::class )
804 ->setMethods( [ 'continuePrimaryAuthentication' ] )
805 ->getMockForAbstractClass();
806 $this->primaryauthMocks
= [ $primary ];
807 $this->initializeManager( true );
808 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
809 ->will( $this->returnValue(
810 AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) )
812 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
813 $res->createRequest
= $req2;
814 $primary->expects( $this->any() )->method( 'continuePrimaryAuthentication' )
815 ->will( $this->returnValue( $res ) );
816 $this->logger
->setCollect( true );
817 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
818 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
, 'sanity check' );
819 $ret = $this->manager
->continueAuthentication( [] );
820 $this->logger
->setCollect( false );
821 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
822 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
823 $this->assertSame( $req2, $ret->createRequest
->createRequest
);
824 $this->assertEquals( [], $ret->createRequest
->maybeLink
);
826 // Pass into beginAccountCreation(), see that maybeLink and createRequest get copied
827 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
828 $this->primaryauthMocks
= [ $primary ];
829 $this->initializeManager( true );
830 $createReq = new CreateFromLoginAuthenticationRequest( $req3, [ $req2 ] );
831 $createReq->returnToUrl
= 'http://localhost/';
832 $createReq->username
= 'UTDummy';
833 $res = AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) );
834 $primary->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
835 ->with( $this->anything(), $this->anything(), [ $userReq, $createReq, $req3 ] )
836 ->will( $this->returnValue( $res ) );
837 $primary->expects( $this->any() )->method( 'accountCreationType' )
838 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
839 $this->logger
->setCollect( true );
840 $ret = $this->manager
->beginAccountCreation(
841 $user, [ $userReq, $createReq ], 'http://localhost/'
843 $this->logger
->setCollect( false );
844 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
845 $state = $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' );
846 $this->assertNotNull( $state );
847 $this->assertEquals( [ $userReq, $createReq, $req3 ], $state['reqs'] );
848 $this->assertEquals( [ $req2 ], $state['maybeLink'] );
852 * @dataProvider provideAuthentication
853 * @param StatusValue $preResponse
854 * @param array $primaryResponses
855 * @param array $secondaryResponses
856 * @param array $managerResponses
857 * @param bool $link Whether the primary authentication provider is a "link" provider
859 public function testAuthentication(
860 StatusValue
$preResponse, array $primaryResponses, array $secondaryResponses,
861 array $managerResponses, $link = false
863 $this->initializeManager();
864 $user = \User
::newFromName( 'UTSysop' );
865 $id = $user->getId();
866 $name = $user->getName();
868 // Set up lots of mocks...
869 $req = new RememberMeAuthenticationRequest
;
870 $req->rememberMe
= (bool)rand( 0, 1 );
871 $req->pre
= $preResponse;
872 $req->primary
= $primaryResponses;
873 $req->secondary
= $secondaryResponses;
875 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
876 $class = ucfirst( $key ) . 'AuthenticationProvider';
877 $mocks[$key] = $this->getMockForAbstractClass(
878 "MediaWiki\\Auth\\$class", [], "Mock$class"
880 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
881 ->will( $this->returnValue( $key ) );
882 $mocks[$key . '2'] = $this->getMockForAbstractClass(
883 "MediaWiki\\Auth\\$class", [], "Mock$class"
885 $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
886 ->will( $this->returnValue( $key . '2' ) );
887 $mocks[$key . '3'] = $this->getMockForAbstractClass(
888 "MediaWiki\\Auth\\$class", [], "Mock$class"
890 $mocks[$key . '3']->expects( $this->any() )->method( 'getUniqueId' )
891 ->will( $this->returnValue( $key . '3' ) );
893 foreach ( $mocks as $mock ) {
894 $mock->expects( $this->any() )->method( 'getAuthenticationRequests' )
895 ->will( $this->returnValue( [] ) );
898 $mocks['pre']->expects( $this->once() )->method( 'testForAuthentication' )
899 ->will( $this->returnCallback( function ( $reqs ) use ( $req ) {
900 $this->assertContains( $req, $reqs );
904 $ct = count( $req->primary
);
905 $callback = $this->returnCallback( function ( $reqs ) use ( $req ) {
906 $this->assertContains( $req, $reqs );
907 return array_shift( $req->primary
);
909 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
910 ->method( 'beginPrimaryAuthentication' )
912 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
913 ->method( 'continuePrimaryAuthentication' )
916 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
917 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
920 $ct = count( $req->secondary
);
921 $callback = $this->returnCallback( function ( $user, $reqs ) use ( $id, $name, $req ) {
922 $this->assertSame( $id, $user->getId() );
923 $this->assertSame( $name, $user->getName() );
924 $this->assertContains( $req, $reqs );
925 return array_shift( $req->secondary
);
927 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
928 ->method( 'beginSecondaryAuthentication' )
930 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
931 ->method( 'continueSecondaryAuthentication' )
934 $abstain = AuthenticationResponse
::newAbstain();
935 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAuthentication' )
936 ->will( $this->returnValue( StatusValue
::newGood() ) );
937 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAuthentication' )
938 ->will( $this->returnValue( $abstain ) );
939 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAuthentication' );
940 $mocks['secondary2']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
941 ->will( $this->returnValue( $abstain ) );
942 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
943 $mocks['secondary3']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
944 ->will( $this->returnValue( $abstain ) );
945 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
947 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
948 $this->primaryauthMocks
= [ $mocks['primary'], $mocks['primary2'] ];
949 $this->secondaryauthMocks
= [
950 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2'],
951 // So linking happens
952 new ConfirmLinkSecondaryAuthenticationProvider
,
954 $this->initializeManager( true );
955 $this->logger
->setCollect( true );
957 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
958 $this->equalTo( AuthenticationResponse
::PASS
),
959 $this->equalTo( AuthenticationResponse
::FAIL
)
961 $providers = array_filter(
963 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
966 return is_callable( [ $p, 'expects' ] );
969 foreach ( $providers as $p ) {
970 $p->postCalled
= false;
971 $p->expects( $this->atMost( 1 ) )->method( 'postAuthentication' )
972 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
973 if ( $user !== null ) {
974 $this->assertInstanceOf( \User
::class, $user );
975 $this->assertSame( 'UTSysop', $user->getName() );
977 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
978 $this->assertThat( $response->status
, $constraint );
979 $p->postCalled
= $response->status
;
983 $session = $this->request
->getSession();
984 $session->setRememberUser( !$req->rememberMe
);
986 foreach ( $managerResponses as $i => $response ) {
987 $success = $response instanceof AuthenticationResponse
&&
988 $response->status
=== AuthenticationResponse
::PASS
;
990 $this->hook( 'UserLoggedIn', $this->once() )
991 ->with( $this->callback( function ( $user ) use ( $id, $name ) {
992 return $user->getId() === $id && $user->getName() === $name;
995 $this->hook( 'UserLoggedIn', $this->never() );
998 $response instanceof AuthenticationResponse
&&
999 $response->status
=== AuthenticationResponse
::FAIL
&&
1000 $response->message
->getKey() !== 'authmanager-authn-not-in-progress' &&
1001 $response->message
->getKey() !== 'authmanager-authn-no-primary'
1004 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
1006 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->never() );
1012 $ret = $this->manager
->beginAuthentication( [ $req ], 'http://localhost/' );
1014 $ret = $this->manager
->continueAuthentication( [ $req ] );
1016 if ( $response instanceof \Exception
) {
1017 $this->fail( 'Expected exception not thrown', "Response $i" );
1019 } catch ( \Exception
$ex ) {
1020 if ( !$response instanceof \Exception
) {
1023 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
1024 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1025 "Response $i, exception, session state" );
1026 $this->unhook( 'UserLoggedIn' );
1027 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1031 $this->unhook( 'UserLoggedIn' );
1032 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1034 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
1036 $ret->message
= $this->message( $ret->message
);
1037 $this->assertEquals( $response, $ret, "Response $i, response" );
1039 $this->assertSame( $id, $session->getUser()->getId(),
1040 "Response $i, authn" );
1042 $this->assertSame( 0, $session->getUser()->getId(),
1043 "Response $i, authn" );
1045 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
1046 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1047 "Response $i, session state" );
1048 foreach ( $providers as $p ) {
1049 $this->assertSame( $response->status
, $p->postCalled
,
1050 "Response $i, post-auth callback called" );
1053 $this->assertNotNull( $session->getSecret( 'AuthManager::authnState' ),
1054 "Response $i, session state" );
1055 foreach ( $ret->neededRequests
as $neededReq ) {
1056 $this->assertEquals( AuthManager
::ACTION_LOGIN
, $neededReq->action
,
1057 "Response $i, neededRequest action" );
1059 $this->assertEquals(
1060 $ret->neededRequests
,
1061 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN_CONTINUE
),
1062 "Response $i, continuation check"
1064 foreach ( $providers as $p ) {
1065 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
1069 $state = $session->getSecret( 'AuthManager::authnState' );
1070 $maybeLink = isset( $state['maybeLink'] ) ?
$state['maybeLink'] : [];
1071 if ( $link && $response->status
=== AuthenticationResponse
::RESTART
) {
1072 $this->assertEquals(
1073 $response->createRequest
->maybeLink
,
1075 "Response $i, maybeLink"
1078 $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
1083 $this->assertSame( $req->rememberMe
, $session->shouldRememberUser(),
1084 'rememberMe checkbox had effect' );
1086 $this->assertNotSame( $req->rememberMe
, $session->shouldRememberUser(),
1087 'rememberMe checkbox wasn\'t applied' );
1091 public function provideAuthentication() {
1092 $rememberReq = new RememberMeAuthenticationRequest
;
1093 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
1095 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1096 $req->foobar
= 'baz';
1097 $restartResponse = AuthenticationResponse
::newRestart(
1098 $this->message( 'authmanager-authn-no-local-user' )
1100 $restartResponse->neededRequests
= [ $rememberReq ];
1102 $restartResponse2Pass = AuthenticationResponse
::newPass( null );
1103 $restartResponse2Pass->linkRequest
= $req;
1104 $restartResponse2 = AuthenticationResponse
::newRestart(
1105 $this->message( 'authmanager-authn-no-local-user-link' )
1107 $restartResponse2->createRequest
= new CreateFromLoginAuthenticationRequest(
1108 null, [ $req->getUniqueId() => $req ]
1110 $restartResponse2->createRequest
->action
= AuthManager
::ACTION_LOGIN
;
1111 $restartResponse2->neededRequests
= [ $rememberReq, $restartResponse2->createRequest
];
1113 $userName = 'UTSysop';
1116 'Failure in pre-auth' => [
1117 StatusValue
::newFatal( 'fail-from-pre' ),
1121 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
1122 AuthenticationResponse
::newFail(
1123 $this->message( 'authmanager-authn-not-in-progress' )
1127 'Failure in primary' => [
1128 StatusValue
::newGood(),
1130 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
1135 'All primary abstain' => [
1136 StatusValue
::newGood(),
1138 AuthenticationResponse
::newAbstain(),
1142 AuthenticationResponse
::newFail( $this->message( 'authmanager-authn-no-primary' ) )
1145 'Primary UI, then redirect, then fail' => [
1146 StatusValue
::newGood(),
1148 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1149 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
1150 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
1155 'Primary redirect, then abstain' => [
1156 StatusValue
::newGood(),
1158 $tmp = AuthenticationResponse
::newRedirect(
1159 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
1161 AuthenticationResponse
::newAbstain(),
1166 new \
DomainException(
1167 'MockPrimaryAuthenticationProvider::continuePrimaryAuthentication() returned ABSTAIN'
1171 'Primary UI, then pass with no local user' => [
1172 StatusValue
::newGood(),
1174 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1175 AuthenticationResponse
::newPass( null ),
1183 'Primary UI, then pass with no local user (link type)' => [
1184 StatusValue
::newGood(),
1186 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1187 $restartResponse2Pass,
1196 'Primary pass with invalid username' => [
1197 StatusValue
::newGood(),
1199 AuthenticationResponse
::newPass( '<>' ),
1203 new \
DomainException( 'MockPrimaryAuthenticationProvider returned an invalid username: <>' ),
1206 'Secondary fail' => [
1207 StatusValue
::newGood(),
1209 AuthenticationResponse
::newPass( $userName ),
1212 AuthenticationResponse
::newFail( $this->message( 'fail-in-secondary' ) ),
1216 'Secondary UI, then abstain' => [
1217 StatusValue
::newGood(),
1219 AuthenticationResponse
::newPass( $userName ),
1222 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1223 AuthenticationResponse
::newAbstain()
1227 AuthenticationResponse
::newPass( $userName ),
1230 'Secondary pass' => [
1231 StatusValue
::newGood(),
1233 AuthenticationResponse
::newPass( $userName ),
1236 AuthenticationResponse
::newPass()
1239 AuthenticationResponse
::newPass( $userName ),
1246 * @dataProvider provideUserExists
1247 * @param bool $primary1Exists
1248 * @param bool $primary2Exists
1249 * @param bool $expect
1251 public function testUserExists( $primary1Exists, $primary2Exists, $expect ) {
1252 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1253 $mock1->expects( $this->any() )->method( 'getUniqueId' )
1254 ->will( $this->returnValue( 'primary1' ) );
1255 $mock1->expects( $this->any() )->method( 'testUserExists' )
1256 ->with( $this->equalTo( 'UTSysop' ) )
1257 ->will( $this->returnValue( $primary1Exists ) );
1258 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1259 $mock2->expects( $this->any() )->method( 'getUniqueId' )
1260 ->will( $this->returnValue( 'primary2' ) );
1261 $mock2->expects( $this->any() )->method( 'testUserExists' )
1262 ->with( $this->equalTo( 'UTSysop' ) )
1263 ->will( $this->returnValue( $primary2Exists ) );
1264 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1266 $this->initializeManager( true );
1267 $this->assertSame( $expect, $this->manager
->userExists( 'UTSysop' ) );
1270 public static function provideUserExists() {
1272 [ false, false, false ],
1273 [ true, false, true ],
1274 [ false, true, true ],
1275 [ true, true, true ],
1280 * @dataProvider provideAllowsAuthenticationDataChange
1281 * @param StatusValue $primaryReturn
1282 * @param StatusValue $secondaryReturn
1283 * @param Status $expect
1285 public function testAllowsAuthenticationDataChange( $primaryReturn, $secondaryReturn, $expect ) {
1286 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1288 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1289 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1290 $mock1->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1291 ->with( $this->equalTo( $req ) )
1292 ->will( $this->returnValue( $primaryReturn ) );
1293 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
1294 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1295 $mock2->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1296 ->with( $this->equalTo( $req ) )
1297 ->will( $this->returnValue( $secondaryReturn ) );
1299 $this->primaryauthMocks
= [ $mock1 ];
1300 $this->secondaryauthMocks
= [ $mock2 ];
1301 $this->initializeManager( true );
1302 $this->assertEquals( $expect, $this->manager
->allowsAuthenticationDataChange( $req ) );
1305 public static function provideAllowsAuthenticationDataChange() {
1306 $ignored = \Status
::newGood( 'ignored' );
1307 $ignored->warning( 'authmanager-change-not-supported' );
1309 $okFromPrimary = StatusValue
::newGood();
1310 $okFromPrimary->warning( 'warning-from-primary' );
1311 $okFromSecondary = StatusValue
::newGood();
1312 $okFromSecondary->warning( 'warning-from-secondary' );
1316 StatusValue
::newGood(),
1317 StatusValue
::newGood(),
1321 StatusValue
::newGood(),
1322 StatusValue
::newGood( 'ignore' ),
1326 StatusValue
::newGood( 'ignored' ),
1327 StatusValue
::newGood(),
1331 StatusValue
::newGood( 'ignored' ),
1332 StatusValue
::newGood( 'ignored' ),
1336 StatusValue
::newFatal( 'fail from primary' ),
1337 StatusValue
::newGood(),
1338 \Status
::newFatal( 'fail from primary' ),
1342 StatusValue
::newGood(),
1343 \Status
::wrap( $okFromPrimary ),
1346 StatusValue
::newGood(),
1347 StatusValue
::newFatal( 'fail from secondary' ),
1348 \Status
::newFatal( 'fail from secondary' ),
1351 StatusValue
::newGood(),
1353 \Status
::wrap( $okFromSecondary ),
1358 public function testChangeAuthenticationData() {
1359 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1360 $req->username
= 'UTSysop';
1362 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1363 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1364 $mock1->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1365 ->with( $this->equalTo( $req ) );
1366 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1367 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1368 $mock2->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1369 ->with( $this->equalTo( $req ) );
1371 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1372 $this->initializeManager( true );
1373 $this->logger
->setCollect( true );
1374 $this->manager
->changeAuthenticationData( $req );
1375 $this->assertSame( [
1376 [ LogLevel
::INFO
, 'Changing authentication data for {user} class {what}' ],
1377 ], $this->logger
->getBuffer() );
1380 public function testCanCreateAccounts() {
1382 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
1383 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
1384 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
1387 foreach ( $types as $type => $can ) {
1388 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1389 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
1390 $mock->expects( $this->any() )->method( 'accountCreationType' )
1391 ->will( $this->returnValue( $type ) );
1392 $this->primaryauthMocks
= [ $mock ];
1393 $this->initializeManager( true );
1394 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
1398 public function testCheckAccountCreatePermissions() {
1399 global $wgGroupPermissions;
1401 $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
1403 $this->initializeManager( true );
1405 $wgGroupPermissions['*']['createaccount'] = true;
1406 $this->assertEquals(
1408 $this->manager
->checkAccountCreatePermissions( new \User
)
1411 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1412 $readOnlyMode->setReason( 'Because' );
1413 $this->assertEquals(
1414 \Status
::newFatal( wfMessage( 'readonlytext', 'Because' ) ),
1415 $this->manager
->checkAccountCreatePermissions( new \User
)
1417 $readOnlyMode->setReason( false );
1419 $wgGroupPermissions['*']['createaccount'] = false;
1420 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1421 $this->assertFalse( $status->isOK() );
1422 $this->assertTrue( $status->hasMessage( 'badaccess-groups' ) );
1423 $wgGroupPermissions['*']['createaccount'] = true;
1425 $user = \User
::newFromName( 'UTBlockee' );
1426 if ( $user->getID() == 0 ) {
1427 $user->addToDatabase();
1428 \TestUser
::setPasswordForUser( $user, 'UTBlockeePassword' );
1429 $user->saveSettings();
1431 $oldBlock = \Block
::newFromTarget( 'UTBlockee' );
1433 // An old block will prevent our new one from saving.
1434 $oldBlock->delete();
1437 'address' => 'UTBlockee',
1438 'user' => $user->getID(),
1439 'reason' => __METHOD__
,
1440 'expiry' => time() +
100500,
1441 'createAccount' => true,
1443 $block = new \
Block( $blockOptions );
1445 $status = $this->manager
->checkAccountCreatePermissions( $user );
1446 $this->assertFalse( $status->isOK() );
1447 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
1450 'address' => '127.0.0.0/24',
1451 'reason' => __METHOD__
,
1452 'expiry' => time() +
100500,
1453 'createAccount' => true,
1455 $block = new \
Block( $blockOptions );
1457 $scopeVariable = new ScopedCallback( [ $block, 'delete' ] );
1458 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1459 $this->assertFalse( $status->isOK() );
1460 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
1461 ScopedCallback
::consume( $scopeVariable );
1463 $this->setMwGlobals( [
1464 'wgEnableDnsBlacklist' => true,
1465 'wgDnsBlacklistUrls' => [
1466 'local.wmftest.net', // This will resolve for every subdomain, which works to test "listed?"
1468 'wgProxyWhitelist' => [],
1470 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1471 $this->assertFalse( $status->isOK() );
1472 $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
1473 $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
1474 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1475 $this->assertTrue( $status->isGood() );
1479 * @param string $uniq
1482 private static function usernameForCreation( $uniq = '' ) {
1485 $username = "UTAuthManagerTestAccountCreation" . $uniq . ++
$i;
1486 } while ( \User
::newFromName( $username )->getId() !== 0 );
1490 public function testCanCreateAccount() {
1491 $username = self
::usernameForCreation();
1492 $this->initializeManager();
1494 $this->assertEquals(
1495 \Status
::newFatal( 'authmanager-create-disabled' ),
1496 $this->manager
->canCreateAccount( $username )
1499 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1500 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1501 $mock->expects( $this->any() )->method( 'accountCreationType' )
1502 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1503 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1504 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1505 ->will( $this->returnValue( StatusValue
::newGood() ) );
1506 $this->primaryauthMocks
= [ $mock ];
1507 $this->initializeManager( true );
1509 $this->assertEquals(
1510 \Status
::newFatal( 'userexists' ),
1511 $this->manager
->canCreateAccount( $username )
1514 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1515 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1516 $mock->expects( $this->any() )->method( 'accountCreationType' )
1517 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1518 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1519 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1520 ->will( $this->returnValue( StatusValue
::newGood() ) );
1521 $this->primaryauthMocks
= [ $mock ];
1522 $this->initializeManager( true );
1524 $this->assertEquals(
1525 \Status
::newFatal( 'noname' ),
1526 $this->manager
->canCreateAccount( $username . '<>' )
1529 $this->assertEquals(
1530 \Status
::newFatal( 'userexists' ),
1531 $this->manager
->canCreateAccount( 'UTSysop' )
1534 $this->assertEquals(
1536 $this->manager
->canCreateAccount( $username )
1539 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1540 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1541 $mock->expects( $this->any() )->method( 'accountCreationType' )
1542 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1543 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1544 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1545 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1546 $this->primaryauthMocks
= [ $mock ];
1547 $this->initializeManager( true );
1549 $this->assertEquals(
1550 \Status
::newFatal( 'fail' ),
1551 $this->manager
->canCreateAccount( $username )
1555 public function testBeginAccountCreation() {
1556 $creator = \User
::newFromName( 'UTSysop' );
1557 $userReq = new UsernameAuthenticationRequest
;
1558 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1559 return $level === LogLevel
::DEBUG ?
null : $message;
1561 $this->initializeManager();
1563 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
1564 $this->hook( 'LocalUserCreated', $this->never() );
1566 $this->manager
->beginAccountCreation(
1567 $creator, [], 'http://localhost/'
1569 $this->fail( 'Expected exception not thrown' );
1570 } catch ( \LogicException
$ex ) {
1571 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1573 $this->unhook( 'LocalUserCreated' );
1575 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1578 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1579 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1580 $mock->expects( $this->any() )->method( 'accountCreationType' )
1581 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1582 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1583 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1584 ->will( $this->returnValue( StatusValue
::newGood() ) );
1585 $this->primaryauthMocks
= [ $mock ];
1586 $this->initializeManager( true );
1588 $this->hook( 'LocalUserCreated', $this->never() );
1589 $ret = $this->manager
->beginAccountCreation( $creator, [], 'http://localhost/' );
1590 $this->unhook( 'LocalUserCreated' );
1591 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1592 $this->assertSame( 'noname', $ret->message
->getKey() );
1594 $this->hook( 'LocalUserCreated', $this->never() );
1595 $userReq->username
= self
::usernameForCreation();
1596 $userReq2 = new UsernameAuthenticationRequest
;
1597 $userReq2->username
= $userReq->username
. 'X';
1598 $ret = $this->manager
->beginAccountCreation(
1599 $creator, [ $userReq, $userReq2 ], 'http://localhost/'
1601 $this->unhook( 'LocalUserCreated' );
1602 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1603 $this->assertSame( 'noname', $ret->message
->getKey() );
1605 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1606 $readOnlyMode->setReason( 'Because' );
1607 $this->hook( 'LocalUserCreated', $this->never() );
1608 $userReq->username
= self
::usernameForCreation();
1609 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1610 $this->unhook( 'LocalUserCreated' );
1611 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1612 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
1613 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1614 $readOnlyMode->setReason( false );
1616 $this->hook( 'LocalUserCreated', $this->never() );
1617 $userReq->username
= self
::usernameForCreation();
1618 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1619 $this->unhook( 'LocalUserCreated' );
1620 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1621 $this->assertSame( 'userexists', $ret->message
->getKey() );
1623 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1624 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1625 $mock->expects( $this->any() )->method( 'accountCreationType' )
1626 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1627 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1628 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1629 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1630 $this->primaryauthMocks
= [ $mock ];
1631 $this->initializeManager( true );
1633 $this->hook( 'LocalUserCreated', $this->never() );
1634 $userReq->username
= self
::usernameForCreation();
1635 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1636 $this->unhook( 'LocalUserCreated' );
1637 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1638 $this->assertSame( 'fail', $ret->message
->getKey() );
1640 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1641 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1642 $mock->expects( $this->any() )->method( 'accountCreationType' )
1643 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1644 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1645 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1646 ->will( $this->returnValue( StatusValue
::newGood() ) );
1647 $this->primaryauthMocks
= [ $mock ];
1648 $this->initializeManager( true );
1650 $this->hook( 'LocalUserCreated', $this->never() );
1651 $userReq->username
= self
::usernameForCreation() . '<>';
1652 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1653 $this->unhook( 'LocalUserCreated' );
1654 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1655 $this->assertSame( 'noname', $ret->message
->getKey() );
1657 $this->hook( 'LocalUserCreated', $this->never() );
1658 $userReq->username
= $creator->getName();
1659 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1660 $this->unhook( 'LocalUserCreated' );
1661 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1662 $this->assertSame( 'userexists', $ret->message
->getKey() );
1664 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1665 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1666 $mock->expects( $this->any() )->method( 'accountCreationType' )
1667 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1668 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1669 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1670 ->will( $this->returnValue( StatusValue
::newGood() ) );
1671 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
1672 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1673 $this->primaryauthMocks
= [ $mock ];
1674 $this->initializeManager( true );
1676 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1677 ->setMethods( [ 'populateUser' ] )
1679 $req->expects( $this->any() )->method( 'populateUser' )
1680 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1681 $userReq->username
= self
::usernameForCreation();
1682 $ret = $this->manager
->beginAccountCreation(
1683 $creator, [ $userReq, $req ], 'http://localhost/'
1685 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1686 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1688 $req = new UserDataAuthenticationRequest
;
1689 $userReq->username
= self
::usernameForCreation();
1691 $ret = $this->manager
->beginAccountCreation(
1692 $creator, [ $userReq, $req ], 'http://localhost/'
1694 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1695 $this->assertSame( 'fail', $ret->message
->getKey() );
1697 $this->manager
->beginAccountCreation(
1698 \User
::newFromName( $userReq->username
), [ $userReq, $req ], 'http://localhost/'
1700 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1701 $this->assertSame( 'fail', $ret->message
->getKey() );
1704 public function testContinueAccountCreation() {
1705 $creator = \User
::newFromName( 'UTSysop' );
1706 $username = self
::usernameForCreation();
1707 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1708 return $level === LogLevel
::DEBUG ?
null : $message;
1710 $this->initializeManager();
1714 'username' => $username,
1716 'creatorname' => $username,
1719 'primaryResponse' => null,
1721 'ranPreTests' => true,
1724 $this->hook( 'LocalUserCreated', $this->never() );
1726 $this->manager
->continueAccountCreation( [] );
1727 $this->fail( 'Expected exception not thrown' );
1728 } catch ( \LogicException
$ex ) {
1729 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1731 $this->unhook( 'LocalUserCreated' );
1733 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1734 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1735 $mock->expects( $this->any() )->method( 'accountCreationType' )
1736 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1737 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1738 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )->will(
1739 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
1741 $this->primaryauthMocks
= [ $mock ];
1742 $this->initializeManager( true );
1744 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', null );
1745 $this->hook( 'LocalUserCreated', $this->never() );
1746 $ret = $this->manager
->continueAccountCreation( [] );
1747 $this->unhook( 'LocalUserCreated' );
1748 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1749 $this->assertSame( 'authmanager-create-not-in-progress', $ret->message
->getKey() );
1751 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1752 [ 'username' => "$username<>" ] +
$session );
1753 $this->hook( 'LocalUserCreated', $this->never() );
1754 $ret = $this->manager
->continueAccountCreation( [] );
1755 $this->unhook( 'LocalUserCreated' );
1756 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1757 $this->assertSame( 'noname', $ret->message
->getKey() );
1759 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1762 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $session );
1763 $this->hook( 'LocalUserCreated', $this->never() );
1764 $cache = \ObjectCache
::getLocalClusterInstance();
1765 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1766 $ret = $this->manager
->continueAccountCreation( [] );
1768 $this->unhook( 'LocalUserCreated' );
1769 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1770 $this->assertSame( 'usernameinprogress', $ret->message
->getKey() );
1771 // This error shouldn't remove the existing session, because the
1772 // raced-with process "owns" it.
1774 $session, $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1777 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1778 [ 'username' => $creator->getName() ] +
$session );
1779 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1780 $readOnlyMode->setReason( 'Because' );
1781 $this->hook( 'LocalUserCreated', $this->never() );
1782 $ret = $this->manager
->continueAccountCreation( [] );
1783 $this->unhook( 'LocalUserCreated' );
1784 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1785 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
1786 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1787 $readOnlyMode->setReason( false );
1789 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1790 [ 'username' => $creator->getName() ] +
$session );
1791 $this->hook( 'LocalUserCreated', $this->never() );
1792 $ret = $this->manager
->continueAccountCreation( [] );
1793 $this->unhook( 'LocalUserCreated' );
1794 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1795 $this->assertSame( 'userexists', $ret->message
->getKey() );
1797 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1800 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1801 [ 'userid' => $creator->getId() ] +
$session );
1802 $this->hook( 'LocalUserCreated', $this->never() );
1804 $ret = $this->manager
->continueAccountCreation( [] );
1805 $this->fail( 'Expected exception not thrown' );
1806 } catch ( \UnexpectedValueException
$ex ) {
1807 $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
1809 $this->unhook( 'LocalUserCreated' );
1811 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1814 $id = $creator->getId();
1815 $name = $creator->getName();
1816 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1817 [ 'username' => $name, 'userid' => $id +
1 ] +
$session );
1818 $this->hook( 'LocalUserCreated', $this->never() );
1820 $ret = $this->manager
->continueAccountCreation( [] );
1821 $this->fail( 'Expected exception not thrown' );
1822 } catch ( \UnexpectedValueException
$ex ) {
1823 $this->assertEquals(
1824 "User \"{$name}\" exists, but ID $id != " . ( $id +
1 ) . '!', $ex->getMessage()
1827 $this->unhook( 'LocalUserCreated' );
1829 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1832 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1833 ->setMethods( [ 'populateUser' ] )
1835 $req->expects( $this->any() )->method( 'populateUser' )
1836 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1837 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1838 [ 'reqs' => [ $req ] ] +
$session );
1839 $ret = $this->manager
->continueAccountCreation( [] );
1840 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1841 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1843 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1848 * @dataProvider provideAccountCreation
1849 * @param StatusValue $preTest
1850 * @param StatusValue $primaryTest
1851 * @param StatusValue $secondaryTest
1852 * @param array $primaryResponses
1853 * @param array $secondaryResponses
1854 * @param array $managerResponses
1856 public function testAccountCreation(
1857 StatusValue
$preTest, $primaryTest, $secondaryTest,
1858 array $primaryResponses, array $secondaryResponses, array $managerResponses
1860 $creator = \User
::newFromName( 'UTSysop' );
1861 $username = self
::usernameForCreation();
1863 $this->initializeManager();
1865 // Set up lots of mocks...
1866 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1867 $req->preTest
= $preTest;
1868 $req->primaryTest
= $primaryTest;
1869 $req->secondaryTest
= $secondaryTest;
1870 $req->primary
= $primaryResponses;
1871 $req->secondary
= $secondaryResponses;
1873 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
1874 $class = ucfirst( $key ) . 'AuthenticationProvider';
1875 $mocks[$key] = $this->getMockForAbstractClass(
1876 "MediaWiki\\Auth\\$class", [], "Mock$class"
1878 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
1879 ->will( $this->returnValue( $key ) );
1880 $mocks[$key]->expects( $this->any() )->method( 'testUserForCreation' )
1881 ->will( $this->returnValue( StatusValue
::newGood() ) );
1882 $mocks[$key]->expects( $this->any() )->method( 'testForAccountCreation' )
1883 ->will( $this->returnCallback(
1884 function ( $user, $creatorIn, $reqs )
1885 use ( $username, $creator, $req, $key )
1887 $this->assertSame( $username, $user->getName() );
1888 $this->assertSame( $creator->getId(), $creatorIn->getId() );
1889 $this->assertSame( $creator->getName(), $creatorIn->getName() );
1891 foreach ( $reqs as $r ) {
1892 $this->assertSame( $username, $r->username
);
1893 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1895 $this->assertTrue( $foundReq, '$reqs contains $req' );
1901 for ( $i = 2; $i <= 3; $i++
) {
1902 $mocks[$key . $i] = $this->getMockForAbstractClass(
1903 "MediaWiki\\Auth\\$class", [], "Mock$class"
1905 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
1906 ->will( $this->returnValue( $key . $i ) );
1907 $mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' )
1908 ->will( $this->returnValue( StatusValue
::newGood() ) );
1909 $mocks[$key . $i]->expects( $this->atMost( 1 ) )->method( 'testForAccountCreation' )
1910 ->will( $this->returnValue( StatusValue
::newGood() ) );
1914 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
1915 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1916 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
1917 ->will( $this->returnValue( false ) );
1918 $ct = count( $req->primary
);
1919 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1920 $this->assertSame( $username, $user->getName() );
1921 $this->assertSame( 'UTSysop', $creator->getName() );
1923 foreach ( $reqs as $r ) {
1924 $this->assertSame( $username, $r->username
);
1925 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1927 $this->assertTrue( $foundReq, '$reqs contains $req' );
1928 return array_shift( $req->primary
);
1930 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
1931 ->method( 'beginPrimaryAccountCreation' )
1932 ->will( $callback );
1933 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1934 ->method( 'continuePrimaryAccountCreation' )
1935 ->will( $callback );
1937 $ct = count( $req->secondary
);
1938 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1939 $this->assertSame( $username, $user->getName() );
1940 $this->assertSame( 'UTSysop', $creator->getName() );
1942 foreach ( $reqs as $r ) {
1943 $this->assertSame( $username, $r->username
);
1944 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1946 $this->assertTrue( $foundReq, '$reqs contains $req' );
1947 return array_shift( $req->secondary
);
1949 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
1950 ->method( 'beginSecondaryAccountCreation' )
1951 ->will( $callback );
1952 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1953 ->method( 'continueSecondaryAccountCreation' )
1954 ->will( $callback );
1956 $abstain = AuthenticationResponse
::newAbstain();
1957 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
1958 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
1959 $mocks['primary2']->expects( $this->any() )->method( 'testUserExists' )
1960 ->will( $this->returnValue( false ) );
1961 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountCreation' )
1962 ->will( $this->returnValue( $abstain ) );
1963 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1964 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
1965 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_NONE
) );
1966 $mocks['primary3']->expects( $this->any() )->method( 'testUserExists' )
1967 ->will( $this->returnValue( false ) );
1968 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountCreation' );
1969 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1970 $mocks['secondary2']->expects( $this->atMost( 1 ) )
1971 ->method( 'beginSecondaryAccountCreation' )
1972 ->will( $this->returnValue( $abstain ) );
1973 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1974 $mocks['secondary3']->expects( $this->atMost( 1 ) )
1975 ->method( 'beginSecondaryAccountCreation' )
1976 ->will( $this->returnValue( $abstain ) );
1977 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1979 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
1980 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary'], $mocks['primary2'] ];
1981 $this->secondaryauthMocks
= [
1982 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2']
1985 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
1986 return $level === LogLevel
::DEBUG ?
null : $message;
1989 $this->initializeManager( true );
1991 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
1992 $this->equalTo( AuthenticationResponse
::PASS
),
1993 $this->equalTo( AuthenticationResponse
::FAIL
)
1995 $providers = array_merge(
1996 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
1998 foreach ( $providers as $p ) {
1999 $p->postCalled
= false;
2000 $p->expects( $this->atMost( 1 ) )->method( 'postAccountCreation' )
2001 ->willReturnCallback( function ( $user, $creator, $response )
2002 use ( $constraint, $p, $username )
2004 $this->assertInstanceOf( \User
::class, $user );
2005 $this->assertSame( $username, $user->getName() );
2006 $this->assertSame( 'UTSysop', $creator->getName() );
2007 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
2008 $this->assertThat( $response->status
, $constraint );
2009 $p->postCalled
= $response->status
;
2013 // We're testing with $wgNewUserLog = false, so assert that it worked
2014 $dbw = wfGetDB( DB_MASTER
);
2015 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2019 foreach ( $managerResponses as $i => $response ) {
2020 $success = $response instanceof AuthenticationResponse
&&
2021 $response->status
=== AuthenticationResponse
::PASS
;
2022 if ( $i === 'created' ) {
2024 $this->hook( 'LocalUserCreated', $this->once() )
2026 $this->callback( function ( $user ) use ( $username ) {
2027 return $user->getName() === $username;
2029 $this->equalTo( false )
2031 $expectLog[] = [ LogLevel
::INFO
, "Creating user {user} during account creation" ];
2033 $this->hook( 'LocalUserCreated', $this->never() );
2039 $userReq = new UsernameAuthenticationRequest
;
2040 $userReq->username
= $username;
2041 $ret = $this->manager
->beginAccountCreation(
2042 $creator, [ $userReq, $req ], 'http://localhost/'
2045 $ret = $this->manager
->continueAccountCreation( [ $req ] );
2047 if ( $response instanceof \Exception
) {
2048 $this->fail( 'Expected exception not thrown', "Response $i" );
2050 } catch ( \Exception
$ex ) {
2051 if ( !$response instanceof \Exception
) {
2054 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
2056 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2057 "Response $i, exception, session state"
2059 $this->unhook( 'LocalUserCreated' );
2063 $this->unhook( 'LocalUserCreated' );
2065 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
2068 $this->assertNotNull( $ret->loginRequest
, "Response $i, login marker" );
2069 $this->assertContains(
2070 $ret->loginRequest
, $this->managerPriv
->createdAccountAuthenticationRequests
,
2071 "Response $i, login marker"
2076 "MediaWiki\Auth\AuthManager::continueAccountCreation: Account creation succeeded for {user}"
2079 // Set some fields in the expected $response that we couldn't
2080 // know in provideAccountCreation().
2081 $response->username
= $username;
2082 $response->loginRequest
= $ret->loginRequest
;
2084 $this->assertNull( $ret->loginRequest
, "Response $i, login marker" );
2085 $this->assertSame( [], $this->managerPriv
->createdAccountAuthenticationRequests
,
2086 "Response $i, login marker" );
2088 $ret->message
= $this->message( $ret->message
);
2089 $this->assertEquals( $response, $ret, "Response $i, response" );
2090 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
2092 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2093 "Response $i, session state"
2095 foreach ( $providers as $p ) {
2096 $this->assertSame( $response->status
, $p->postCalled
,
2097 "Response $i, post-auth callback called" );
2100 $this->assertNotNull(
2101 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2102 "Response $i, session state"
2104 foreach ( $ret->neededRequests
as $neededReq ) {
2105 $this->assertEquals( AuthManager
::ACTION_CREATE
, $neededReq->action
,
2106 "Response $i, neededRequest action" );
2108 $this->assertEquals(
2109 $ret->neededRequests
,
2110 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_CREATE_CONTINUE
),
2111 "Response $i, continuation check"
2113 foreach ( $providers as $p ) {
2114 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
2119 $this->assertNotEquals( 0, \User
::idFromName( $username ) );
2121 $this->assertEquals( 0, \User
::idFromName( $username ) );
2127 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
2131 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2135 public function provideAccountCreation() {
2136 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
2137 $good = StatusValue
::newGood();
2140 'Pre-creation test fail in pre' => [
2141 StatusValue
::newFatal( 'fail-from-pre' ), $good, $good,
2145 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
2148 'Pre-creation test fail in primary' => [
2149 $good, StatusValue
::newFatal( 'fail-from-primary' ), $good,
2153 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2156 'Pre-creation test fail in secondary' => [
2157 $good, $good, StatusValue
::newFatal( 'fail-from-secondary' ),
2161 AuthenticationResponse
::newFail( $this->message( 'fail-from-secondary' ) ),
2164 'Failure in primary' => [
2165 $good, $good, $good,
2167 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2172 'All primary abstain' => [
2173 $good, $good, $good,
2175 AuthenticationResponse
::newAbstain(),
2179 AuthenticationResponse
::newFail( $this->message( 'authmanager-create-no-primary' ) )
2182 'Primary UI, then redirect, then fail' => [
2183 $good, $good, $good,
2185 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2186 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
2187 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
2192 'Primary redirect, then abstain' => [
2193 $good, $good, $good,
2195 $tmp = AuthenticationResponse
::newRedirect(
2196 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
2198 AuthenticationResponse
::newAbstain(),
2203 new \
DomainException(
2204 'MockPrimaryAuthenticationProvider::continuePrimaryAccountCreation() returned ABSTAIN'
2208 'Primary UI, then pass; secondary abstain' => [
2209 $good, $good, $good,
2211 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2212 AuthenticationResponse
::newPass(),
2215 AuthenticationResponse
::newAbstain(),
2219 'created' => AuthenticationResponse
::newPass( '' ),
2222 'Primary pass; secondary UI then pass' => [
2223 $good, $good, $good,
2225 AuthenticationResponse
::newPass( '' ),
2228 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2229 AuthenticationResponse
::newPass( '' ),
2233 AuthenticationResponse
::newPass( '' ),
2236 'Primary pass; secondary fail' => [
2237 $good, $good, $good,
2239 AuthenticationResponse
::newPass(),
2242 AuthenticationResponse
::newFail( $this->message( '...' ) ),
2245 'created' => new \
DomainException(
2246 'MockSecondaryAuthenticationProvider::beginSecondaryAccountCreation() returned FAIL. ' .
2247 'Secondary providers are not allowed to fail account creation, ' .
2248 'that should have been done via testForAccountCreation().'
2256 * @dataProvider provideAccountCreationLogging
2257 * @param bool $isAnon
2258 * @param string|null $logSubtype
2260 public function testAccountCreationLogging( $isAnon, $logSubtype ) {
2261 $creator = $isAnon ?
new \User
: \User
::newFromName( 'UTSysop' );
2262 $username = self
::usernameForCreation();
2264 $this->initializeManager();
2266 // Set up lots of mocks...
2267 $mock = $this->getMockForAbstractClass(
2268 \MediaWiki\Auth\PrimaryAuthenticationProvider
::class, []
2270 $mock->expects( $this->any() )->method( 'getUniqueId' )
2271 ->will( $this->returnValue( 'primary' ) );
2272 $mock->expects( $this->any() )->method( 'testUserForCreation' )
2273 ->will( $this->returnValue( StatusValue
::newGood() ) );
2274 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
2275 ->will( $this->returnValue( StatusValue
::newGood() ) );
2276 $mock->expects( $this->any() )->method( 'accountCreationType' )
2277 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2278 $mock->expects( $this->any() )->method( 'testUserExists' )
2279 ->will( $this->returnValue( false ) );
2280 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
2281 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
2282 $mock->expects( $this->any() )->method( 'finishAccountCreation' )
2283 ->will( $this->returnValue( $logSubtype ) );
2285 $this->primaryauthMocks
= [ $mock ];
2286 $this->initializeManager( true );
2287 $this->logger
->setCollect( true );
2289 $this->config
->set( 'NewUserLog', true );
2291 $dbw = wfGetDB( DB_MASTER
);
2292 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2294 $userReq = new UsernameAuthenticationRequest
;
2295 $userReq->username
= $username;
2296 $reasonReq = new CreationReasonAuthenticationRequest
;
2297 $reasonReq->reason
= $this->toString();
2298 $ret = $this->manager
->beginAccountCreation(
2299 $creator, [ $userReq, $reasonReq ], 'http://localhost/'
2302 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
2304 $user = \User
::newFromName( $username );
2305 $this->assertNotEquals( 0, $user->getId(), 'sanity check' );
2306 $this->assertNotEquals( $creator->getId(), $user->getId(), 'sanity check' );
2308 $data = \DatabaseLogEntry
::getSelectQueryData();
2309 $rows = iterator_to_array( $dbw->select(
2313 'log_id > ' . (int)$maxLogId,
2314 'log_type' => 'newusers'
2320 $this->assertCount( 1, $rows );
2321 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2323 $this->assertSame( $logSubtype ?
: ( $isAnon ?
'create' : 'create2' ), $entry->getSubtype() );
2325 $isAnon ?
$user->getId() : $creator->getId(),
2326 $entry->getPerformer()->getId()
2329 $isAnon ?
$user->getName() : $creator->getName(),
2330 $entry->getPerformer()->getName()
2332 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2333 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2334 $this->assertSame( $this->toString(), $entry->getComment() );
2337 public static function provideAccountCreationLogging() {
2342 [ false, 'byemail' ],
2346 public function testAutoAccountCreation() {
2347 global $wgGroupPermissions, $wgHooks;
2349 // PHPUnit seems to have a bug where it will call the ->with()
2350 // callbacks for our hooks again after the test is run (WTF?), which
2351 // breaks here because $username no longer matches $user by the end of
2353 $workaroundPHPUnitBug = false;
2355 $username = self
::usernameForCreation();
2356 $this->initializeManager();
2358 $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
2359 $wgGroupPermissions['*']['createaccount'] = true;
2360 $wgGroupPermissions['*']['autocreateaccount'] = false;
2362 \ObjectCache
::$instances[__METHOD__
] = new \
HashBagOStuff();
2363 $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__
] );
2365 // Set up lots of mocks...
2367 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2368 $class = ucfirst( $key ) . 'AuthenticationProvider';
2369 $mocks[$key] = $this->getMockForAbstractClass(
2370 "MediaWiki\\Auth\\$class", [], "Mock$class"
2372 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2373 ->will( $this->returnValue( $key ) );
2376 $good = StatusValue
::newGood();
2377 $callback = $this->callback( function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
2378 return $workaroundPHPUnitBug ||
$user->getName() === $username;
2381 $mocks['pre']->expects( $this->exactly( 12 ) )->method( 'testUserForCreation' )
2382 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2383 ->will( $this->onConsecutiveCalls(
2384 StatusValue
::newFatal( 'ok' ), StatusValue
::newFatal( 'ok' ), // For testing permissions
2385 StatusValue
::newFatal( 'fail-in-pre' ), $good, $good,
2386 $good, // backoff test
2387 $good, // addToDatabase fails test
2388 $good, // addToDatabase throws test
2389 $good, // addToDatabase exists test
2390 $good, $good, $good // success
2393 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
2394 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2395 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
2396 ->will( $this->returnValue( true ) );
2397 $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
2398 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2399 ->will( $this->onConsecutiveCalls(
2400 StatusValue
::newFatal( 'fail-in-primary' ), $good,
2401 $good, // backoff test
2402 $good, // addToDatabase fails test
2403 $good, // addToDatabase throws test
2404 $good, // addToDatabase exists test
2407 $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2408 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) );
2410 $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
2411 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2412 ->will( $this->onConsecutiveCalls(
2413 StatusValue
::newFatal( 'fail-in-secondary' ),
2414 $good, // backoff test
2415 $good, // addToDatabase fails test
2416 $good, // addToDatabase throws test
2417 $good, // addToDatabase exists test
2420 $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2421 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) );
2423 $this->preauthMocks
= [ $mocks['pre'] ];
2424 $this->primaryauthMocks
= [ $mocks['primary'] ];
2425 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2426 $this->initializeManager( true );
2427 $session = $this->request
->getSession();
2429 $logger = new \
TestLogger( true, function ( $m ) {
2430 $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
2433 $this->manager
->setLogger( $logger );
2436 $user = \User
::newFromName( 'UTSysop' );
2437 $this->manager
->autoCreateUser( $user, 'InvalidSource', true );
2438 $this->fail( 'Expected exception not thrown' );
2439 } catch ( \InvalidArgumentException
$ex ) {
2440 $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
2443 // First, check an existing user
2445 $user = \User
::newFromName( 'UTSysop' );
2446 $this->hook( 'LocalUserCreated', $this->never() );
2447 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2448 $this->unhook( 'LocalUserCreated' );
2449 $expect = \Status
::newGood();
2450 $expect->warning( 'userexists' );
2451 $this->assertEquals( $expect, $ret );
2452 $this->assertNotEquals( 0, $user->getId() );
2453 $this->assertSame( 'UTSysop', $user->getName() );
2454 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2455 $this->assertSame( [
2456 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2457 ], $logger->getBuffer() );
2458 $logger->clearBuffer();
2461 $user = \User
::newFromName( 'UTSysop' );
2462 $this->hook( 'LocalUserCreated', $this->never() );
2463 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2464 $this->unhook( 'LocalUserCreated' );
2465 $expect = \Status
::newGood();
2466 $expect->warning( 'userexists' );
2467 $this->assertEquals( $expect, $ret );
2468 $this->assertNotEquals( 0, $user->getId() );
2469 $this->assertSame( 'UTSysop', $user->getName() );
2470 $this->assertEquals( 0, $session->getUser()->getId() );
2471 $this->assertSame( [
2472 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2473 ], $logger->getBuffer() );
2474 $logger->clearBuffer();
2476 // Wiki is read-only
2478 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
2479 $readOnlyMode->setReason( 'Because' );
2480 $user = \User
::newFromName( $username );
2481 $this->hook( 'LocalUserCreated', $this->never() );
2482 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2483 $this->unhook( 'LocalUserCreated' );
2484 $this->assertEquals( \Status
::newFatal( wfMessage( 'readonlytext', 'Because' ) ), $ret );
2485 $this->assertEquals( 0, $user->getId() );
2486 $this->assertNotEquals( $username, $user->getName() );
2487 $this->assertEquals( 0, $session->getUser()->getId() );
2488 $this->assertSame( [
2489 [ LogLevel
::DEBUG
, 'denied by wfReadOnly(): {reason}' ],
2490 ], $logger->getBuffer() );
2491 $logger->clearBuffer();
2492 $readOnlyMode->setReason( false );
2494 // Session blacklisted
2496 $session->set( 'AuthManager::AutoCreateBlacklist', 'test' );
2497 $user = \User
::newFromName( $username );
2498 $this->hook( 'LocalUserCreated', $this->never() );
2499 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2500 $this->unhook( 'LocalUserCreated' );
2501 $this->assertEquals( \Status
::newFatal( 'test' ), $ret );
2502 $this->assertEquals( 0, $user->getId() );
2503 $this->assertNotEquals( $username, $user->getName() );
2504 $this->assertEquals( 0, $session->getUser()->getId() );
2505 $this->assertSame( [
2506 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2507 ], $logger->getBuffer() );
2508 $logger->clearBuffer();
2511 $session->set( 'AuthManager::AutoCreateBlacklist', StatusValue
::newFatal( 'test2' ) );
2512 $user = \User
::newFromName( $username );
2513 $this->hook( 'LocalUserCreated', $this->never() );
2514 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2515 $this->unhook( 'LocalUserCreated' );
2516 $this->assertEquals( \Status
::newFatal( 'test2' ), $ret );
2517 $this->assertEquals( 0, $user->getId() );
2518 $this->assertNotEquals( $username, $user->getName() );
2519 $this->assertEquals( 0, $session->getUser()->getId() );
2520 $this->assertSame( [
2521 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2522 ], $logger->getBuffer() );
2523 $logger->clearBuffer();
2527 $user = \User
::newFromName( $username . '@' );
2528 $this->hook( 'LocalUserCreated', $this->never() );
2529 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2530 $this->unhook( 'LocalUserCreated' );
2531 $this->assertEquals( \Status
::newFatal( 'noname' ), $ret );
2532 $this->assertEquals( 0, $user->getId() );
2533 $this->assertNotEquals( $username . '@', $user->getId() );
2534 $this->assertEquals( 0, $session->getUser()->getId() );
2535 $this->assertSame( [
2536 [ LogLevel
::DEBUG
, 'name "{username}" is not creatable' ],
2537 ], $logger->getBuffer() );
2538 $logger->clearBuffer();
2539 $this->assertSame( 'noname', $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2541 // IP unable to create accounts
2542 $wgGroupPermissions['*']['createaccount'] = false;
2543 $wgGroupPermissions['*']['autocreateaccount'] = false;
2545 $user = \User
::newFromName( $username );
2546 $this->hook( 'LocalUserCreated', $this->never() );
2547 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2548 $this->unhook( 'LocalUserCreated' );
2549 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-noperm' ), $ret );
2550 $this->assertEquals( 0, $user->getId() );
2551 $this->assertNotEquals( $username, $user->getName() );
2552 $this->assertEquals( 0, $session->getUser()->getId() );
2553 $this->assertSame( [
2554 [ LogLevel
::DEBUG
, 'IP lacks the ability to create or autocreate accounts' ],
2555 ], $logger->getBuffer() );
2556 $logger->clearBuffer();
2558 'authmanager-autocreate-noperm', $session->get( 'AuthManager::AutoCreateBlacklist' )
2561 // Test that both permutations of permissions are allowed
2562 // (this hits the two "ok" entries in $mocks['pre'])
2563 $wgGroupPermissions['*']['createaccount'] = false;
2564 $wgGroupPermissions['*']['autocreateaccount'] = true;
2566 $user = \User
::newFromName( $username );
2567 $this->hook( 'LocalUserCreated', $this->never() );
2568 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2569 $this->unhook( 'LocalUserCreated' );
2570 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2572 $wgGroupPermissions['*']['createaccount'] = true;
2573 $wgGroupPermissions['*']['autocreateaccount'] = false;
2575 $user = \User
::newFromName( $username );
2576 $this->hook( 'LocalUserCreated', $this->never() );
2577 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2578 $this->unhook( 'LocalUserCreated' );
2579 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2580 $logger->clearBuffer();
2584 $user = \User
::newFromName( $username );
2585 $this->hook( 'LocalUserCreated', $this->never() );
2586 $cache = \ObjectCache
::getLocalClusterInstance();
2587 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2588 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2590 $this->unhook( 'LocalUserCreated' );
2591 $this->assertEquals( \Status
::newFatal( 'usernameinprogress' ), $ret );
2592 $this->assertEquals( 0, $user->getId() );
2593 $this->assertNotEquals( $username, $user->getName() );
2594 $this->assertEquals( 0, $session->getUser()->getId() );
2595 $this->assertSame( [
2596 [ LogLevel
::DEBUG
, 'Could not acquire account creation lock' ],
2597 ], $logger->getBuffer() );
2598 $logger->clearBuffer();
2600 // Test pre-authentication provider fail
2602 $user = \User
::newFromName( $username );
2603 $this->hook( 'LocalUserCreated', $this->never() );
2604 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2605 $this->unhook( 'LocalUserCreated' );
2606 $this->assertEquals( \Status
::newFatal( 'fail-in-pre' ), $ret );
2607 $this->assertEquals( 0, $user->getId() );
2608 $this->assertNotEquals( $username, $user->getName() );
2609 $this->assertEquals( 0, $session->getUser()->getId() );
2610 $this->assertSame( [
2611 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2612 ], $logger->getBuffer() );
2613 $logger->clearBuffer();
2614 $this->assertEquals(
2615 StatusValue
::newFatal( 'fail-in-pre' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2619 $user = \User
::newFromName( $username );
2620 $this->hook( 'LocalUserCreated', $this->never() );
2621 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2622 $this->unhook( 'LocalUserCreated' );
2623 $this->assertEquals( \Status
::newFatal( 'fail-in-primary' ), $ret );
2624 $this->assertEquals( 0, $user->getId() );
2625 $this->assertNotEquals( $username, $user->getName() );
2626 $this->assertEquals( 0, $session->getUser()->getId() );
2627 $this->assertSame( [
2628 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2629 ], $logger->getBuffer() );
2630 $logger->clearBuffer();
2631 $this->assertEquals(
2632 StatusValue
::newFatal( 'fail-in-primary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2636 $user = \User
::newFromName( $username );
2637 $this->hook( 'LocalUserCreated', $this->never() );
2638 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2639 $this->unhook( 'LocalUserCreated' );
2640 $this->assertEquals( \Status
::newFatal( 'fail-in-secondary' ), $ret );
2641 $this->assertEquals( 0, $user->getId() );
2642 $this->assertNotEquals( $username, $user->getName() );
2643 $this->assertEquals( 0, $session->getUser()->getId() );
2644 $this->assertSame( [
2645 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2646 ], $logger->getBuffer() );
2647 $logger->clearBuffer();
2648 $this->assertEquals(
2649 StatusValue
::newFatal( 'fail-in-secondary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2653 $cache = \ObjectCache
::getLocalClusterInstance();
2654 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2655 $cache->set( $backoffKey, true );
2657 $user = \User
::newFromName( $username );
2658 $this->hook( 'LocalUserCreated', $this->never() );
2659 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2660 $this->unhook( 'LocalUserCreated' );
2661 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-exception' ), $ret );
2662 $this->assertEquals( 0, $user->getId() );
2663 $this->assertNotEquals( $username, $user->getName() );
2664 $this->assertEquals( 0, $session->getUser()->getId() );
2665 $this->assertSame( [
2666 [ LogLevel
::DEBUG
, '{username} denied by prior creation attempt failures' ],
2667 ], $logger->getBuffer() );
2668 $logger->clearBuffer();
2669 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2670 $cache->delete( $backoffKey );
2672 // Test addToDatabase fails
2674 $user = $this->getMockBuilder( \User
::class )
2675 ->setMethods( [ 'addToDatabase' ] )->getMock();
2676 $user->expects( $this->once() )->method( 'addToDatabase' )
2677 ->will( $this->returnValue( \Status
::newFatal( 'because' ) ) );
2678 $user->setName( $username );
2679 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2680 $this->assertEquals( \Status
::newFatal( 'because' ), $ret );
2681 $this->assertEquals( 0, $user->getId() );
2682 $this->assertNotEquals( $username, $user->getName() );
2683 $this->assertEquals( 0, $session->getUser()->getId() );
2684 $this->assertSame( [
2685 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2686 [ LogLevel
::ERROR
, '{username} failed with message {msg}' ],
2687 ], $logger->getBuffer() );
2688 $logger->clearBuffer();
2689 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2691 // Test addToDatabase throws an exception
2692 $cache = \ObjectCache
::getLocalClusterInstance();
2693 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2694 $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
2696 $user = $this->getMockBuilder( \User
::class )
2697 ->setMethods( [ 'addToDatabase' ] )->getMock();
2698 $user->expects( $this->once() )->method( 'addToDatabase' )
2699 ->will( $this->throwException( new \
Exception( 'Excepted' ) ) );
2700 $user->setName( $username );
2702 $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2703 $this->fail( 'Expected exception not thrown' );
2704 } catch ( \Exception
$ex ) {
2705 $this->assertSame( 'Excepted', $ex->getMessage() );
2707 $this->assertEquals( 0, $user->getId() );
2708 $this->assertEquals( 0, $session->getUser()->getId() );
2709 $this->assertSame( [
2710 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2711 [ LogLevel
::ERROR
, '{username} failed with exception {exception}' ],
2712 ], $logger->getBuffer() );
2713 $logger->clearBuffer();
2714 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2715 $this->assertNotEquals( false, $cache->get( $backoffKey ) );
2716 $cache->delete( $backoffKey );
2718 // Test addToDatabase fails because the user already exists.
2720 $user = $this->getMockBuilder( \User
::class )
2721 ->setMethods( [ 'addToDatabase' ] )->getMock();
2722 $user->expects( $this->once() )->method( 'addToDatabase' )
2723 ->will( $this->returnCallback( function () use ( $username, &$user ) {
2724 $oldUser = \User
::newFromName( $username );
2725 $status = $oldUser->addToDatabase();
2726 $this->assertTrue( $status->isOK(), 'sanity check' );
2727 $user->setId( $oldUser->getId() );
2728 return \Status
::newFatal( 'userexists' );
2730 $user->setName( $username );
2731 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2732 $expect = \Status
::newGood();
2733 $expect->warning( 'userexists' );
2734 $this->assertEquals( $expect, $ret );
2735 $this->assertNotEquals( 0, $user->getId() );
2736 $this->assertEquals( $username, $user->getName() );
2737 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2738 $this->assertSame( [
2739 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2740 [ LogLevel
::INFO
, '{username} already exists locally (race)' ],
2741 ], $logger->getBuffer() );
2742 $logger->clearBuffer();
2743 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2747 $username = self
::usernameForCreation();
2748 $user = \User
::newFromName( $username );
2749 $this->hook( 'AuthPluginAutoCreate', $this->once() )
2750 ->with( $callback );
2751 $this->hideDeprecated( 'AuthPluginAutoCreate hook (used in ' .
2752 get_class( $wgHooks['AuthPluginAutoCreate'][0] ) . '::onAuthPluginAutoCreate)' );
2753 $this->hook( 'LocalUserCreated', $this->once() )
2754 ->with( $callback, $this->equalTo( true ) );
2755 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2756 $this->unhook( 'LocalUserCreated' );
2757 $this->unhook( 'AuthPluginAutoCreate' );
2758 $this->assertEquals( \Status
::newGood(), $ret );
2759 $this->assertNotEquals( 0, $user->getId() );
2760 $this->assertEquals( $username, $user->getName() );
2761 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2762 $this->assertSame( [
2763 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2764 ], $logger->getBuffer() );
2765 $logger->clearBuffer();
2767 $dbw = wfGetDB( DB_MASTER
);
2768 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2770 $username = self
::usernameForCreation();
2771 $user = \User
::newFromName( $username );
2772 $this->hook( 'LocalUserCreated', $this->once() )
2773 ->with( $callback, $this->equalTo( true ) );
2774 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2775 $this->unhook( 'LocalUserCreated' );
2776 $this->assertEquals( \Status
::newGood(), $ret );
2777 $this->assertNotEquals( 0, $user->getId() );
2778 $this->assertEquals( $username, $user->getName() );
2779 $this->assertEquals( 0, $session->getUser()->getId() );
2780 $this->assertSame( [
2781 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2782 ], $logger->getBuffer() );
2783 $logger->clearBuffer();
2786 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2789 $this->config
->set( 'NewUserLog', true );
2791 $username = self
::usernameForCreation();
2792 $user = \User
::newFromName( $username );
2793 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2794 $this->assertEquals( \Status
::newGood(), $ret );
2795 $logger->clearBuffer();
2797 $data = \DatabaseLogEntry
::getSelectQueryData();
2798 $rows = iterator_to_array( $dbw->select(
2802 'log_id > ' . (int)$maxLogId,
2803 'log_type' => 'newusers'
2809 $this->assertCount( 1, $rows );
2810 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2812 $this->assertSame( 'autocreate', $entry->getSubtype() );
2813 $this->assertSame( $user->getId(), $entry->getPerformer()->getId() );
2814 $this->assertSame( $user->getName(), $entry->getPerformer()->getName() );
2815 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2816 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2818 $workaroundPHPUnitBug = true;
2822 * @dataProvider provideGetAuthenticationRequests
2823 * @param string $action
2824 * @param array $expect
2825 * @param array $state
2827 public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
2828 $makeReq = function ( $key ) use ( $action ) {
2829 $req = $this->createMock( AuthenticationRequest
::class );
2830 $req->expects( $this->any() )->method( 'getUniqueId' )
2831 ->will( $this->returnValue( $key ) );
2832 $req->action
= $action === AuthManager
::ACTION_UNLINK ? AuthManager
::ACTION_REMOVE
: $action;
2836 $cmpReqs = function ( $a, $b ) {
2837 $ret = strcmp( get_class( $a ), get_class( $b ) );
2839 $ret = strcmp( $a->key
, $b->key
);
2844 $good = StatusValue
::newGood();
2847 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2848 $class = ucfirst( $key ) . 'AuthenticationProvider';
2849 $mocks[$key] = $this->getMockForAbstractClass(
2850 "MediaWiki\\Auth\\$class", [], "Mock$class"
2852 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2853 ->will( $this->returnValue( $key ) );
2854 $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2855 ->will( $this->returnCallback( function ( $action ) use ( $key, $makeReq ) {
2856 return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
2858 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
2859 ->will( $this->returnValue( $good ) );
2864 PrimaryAuthenticationProvider
::TYPE_NONE
,
2865 PrimaryAuthenticationProvider
::TYPE_CREATE
,
2866 PrimaryAuthenticationProvider
::TYPE_LINK
2868 $class = 'PrimaryAuthenticationProvider';
2869 $mocks["primary-$type"] = $this->getMockForAbstractClass(
2870 "MediaWiki\\Auth\\$class", [], "Mock$class"
2872 $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
2873 ->will( $this->returnValue( "primary-$type" ) );
2874 $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
2875 ->will( $this->returnValue( $type ) );
2876 $mocks["primary-$type"]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2877 ->will( $this->returnCallback( function ( $action ) use ( $type, $makeReq ) {
2878 return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
2880 $mocks["primary-$type"]->expects( $this->any() )
2881 ->method( 'providerAllowsAuthenticationDataChange' )
2882 ->will( $this->returnValue( $good ) );
2883 $this->primaryauthMocks
[] = $mocks["primary-$type"];
2886 $mocks['primary2'] = $this->getMockForAbstractClass(
2887 PrimaryAuthenticationProvider
::class, [], "MockPrimaryAuthenticationProvider"
2889 $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
2890 ->will( $this->returnValue( 'primary2' ) );
2891 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
2892 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
2893 $mocks['primary2']->expects( $this->any() )->method( 'getAuthenticationRequests' )
2894 ->will( $this->returnValue( [] ) );
2895 $mocks['primary2']->expects( $this->any() )
2896 ->method( 'providerAllowsAuthenticationDataChange' )
2897 ->will( $this->returnCallback( function ( $req ) use ( $good ) {
2898 return $req->key
=== 'generic' ? StatusValue
::newFatal( 'no' ) : $good;
2900 $this->primaryauthMocks
[] = $mocks['primary2'];
2902 $this->preauthMocks
= [ $mocks['pre'] ];
2903 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2904 $this->initializeManager( true );
2907 if ( isset( $state['continueRequests'] ) ) {
2908 $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
2910 if ( $action === AuthManager
::ACTION_LOGIN_CONTINUE
) {
2911 $this->request
->getSession()->setSecret( 'AuthManager::authnState', $state );
2912 } elseif ( $action === AuthManager
::ACTION_CREATE_CONTINUE
) {
2913 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $state );
2914 } elseif ( $action === AuthManager
::ACTION_LINK_CONTINUE
) {
2915 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', $state );
2919 $expectReqs = array_map( $makeReq, $expect );
2920 if ( $action === AuthManager
::ACTION_LOGIN
) {
2921 $req = new RememberMeAuthenticationRequest
;
2922 $req->action
= $action;
2923 $req->required
= AuthenticationRequest
::REQUIRED
;
2924 $expectReqs[] = $req;
2925 } elseif ( $action === AuthManager
::ACTION_CREATE
) {
2926 $req = new UsernameAuthenticationRequest
;
2927 $req->action
= $action;
2928 $expectReqs[] = $req;
2929 $req = new UserDataAuthenticationRequest
;
2930 $req->action
= $action;
2931 $req->required
= AuthenticationRequest
::REQUIRED
;
2932 $expectReqs[] = $req;
2934 usort( $expectReqs, $cmpReqs );
2936 $actual = $this->manager
->getAuthenticationRequests( $action );
2937 foreach ( $actual as $req ) {
2938 // Don't test this here.
2939 $req->required
= AuthenticationRequest
::REQUIRED
;
2941 usort( $actual, $cmpReqs );
2943 $this->assertEquals( $expectReqs, $actual );
2945 // Test CreationReasonAuthenticationRequest gets returned
2946 if ( $action === AuthManager
::ACTION_CREATE
) {
2947 $req = new CreationReasonAuthenticationRequest
;
2948 $req->action
= $action;
2949 $req->required
= AuthenticationRequest
::REQUIRED
;
2950 $expectReqs[] = $req;
2951 usort( $expectReqs, $cmpReqs );
2953 $actual = $this->manager
->getAuthenticationRequests( $action, \User
::newFromName( 'UTSysop' ) );
2954 foreach ( $actual as $req ) {
2955 // Don't test this here.
2956 $req->required
= AuthenticationRequest
::REQUIRED
;
2958 usort( $actual, $cmpReqs );
2960 $this->assertEquals( $expectReqs, $actual );
2964 public static function provideGetAuthenticationRequests() {
2967 AuthManager
::ACTION_LOGIN
,
2968 [ 'pre-login', 'primary-none-login', 'primary-create-login',
2969 'primary-link-login', 'secondary-login', 'generic' ],
2972 AuthManager
::ACTION_CREATE
,
2973 [ 'pre-create', 'primary-none-create', 'primary-create-create',
2974 'primary-link-create', 'secondary-create', 'generic' ],
2977 AuthManager
::ACTION_LINK
,
2978 [ 'primary-link-link', 'generic' ],
2981 AuthManager
::ACTION_CHANGE
,
2982 [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
2983 'secondary-change' ],
2986 AuthManager
::ACTION_REMOVE
,
2987 [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
2988 'secondary-remove' ],
2991 AuthManager
::ACTION_UNLINK
,
2992 [ 'primary-link-remove' ],
2995 AuthManager
::ACTION_LOGIN_CONTINUE
,
2999 AuthManager
::ACTION_LOGIN_CONTINUE
,
3000 $reqs = [ 'continue-login', 'foo', 'bar' ],
3002 'continueRequests' => $reqs,
3006 AuthManager
::ACTION_CREATE_CONTINUE
,
3010 AuthManager
::ACTION_CREATE_CONTINUE
,
3011 $reqs = [ 'continue-create', 'foo', 'bar' ],
3013 'continueRequests' => $reqs,
3017 AuthManager
::ACTION_LINK_CONTINUE
,
3021 AuthManager
::ACTION_LINK_CONTINUE
,
3022 $reqs = [ 'continue-link', 'foo', 'bar' ],
3024 'continueRequests' => $reqs,
3030 public function testGetAuthenticationRequestsRequired() {
3031 $makeReq = function ( $key, $required ) {
3032 $req = $this->createMock( AuthenticationRequest
::class );
3033 $req->expects( $this->any() )->method( 'getUniqueId' )
3034 ->will( $this->returnValue( $key ) );
3035 $req->action
= AuthManager
::ACTION_LOGIN
;
3037 $req->required
= $required;
3040 $cmpReqs = function ( $a, $b ) {
3041 $ret = strcmp( get_class( $a ), get_class( $b ) );
3043 $ret = strcmp( $a->key
, $b->key
);
3048 $good = StatusValue
::newGood();
3050 $primary1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3051 $primary1->expects( $this->any() )->method( 'getUniqueId' )
3052 ->will( $this->returnValue( 'primary1' ) );
3053 $primary1->expects( $this->any() )->method( 'accountCreationType' )
3054 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3055 $primary1->expects( $this->any() )->method( 'getAuthenticationRequests' )
3056 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3058 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3059 $makeReq( "required", AuthenticationRequest
::REQUIRED
),
3060 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3061 $makeReq( "foo", AuthenticationRequest
::REQUIRED
),
3062 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3063 $makeReq( "baz", AuthenticationRequest
::OPTIONAL
),
3067 $primary2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3068 $primary2->expects( $this->any() )->method( 'getUniqueId' )
3069 ->will( $this->returnValue( 'primary2' ) );
3070 $primary2->expects( $this->any() )->method( 'accountCreationType' )
3071 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3072 $primary2->expects( $this->any() )->method( 'getAuthenticationRequests' )
3073 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3075 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3076 $makeReq( "required2", AuthenticationRequest
::REQUIRED
),
3077 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3081 $secondary = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3082 $secondary->expects( $this->any() )->method( 'getUniqueId' )
3083 ->will( $this->returnValue( 'secondary' ) );
3084 $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
3085 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3087 $makeReq( "foo", AuthenticationRequest
::OPTIONAL
),
3088 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3089 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3093 $rememberReq = new RememberMeAuthenticationRequest
;
3094 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
3096 $this->primaryauthMocks
= [ $primary1, $primary2 ];
3097 $this->secondaryauthMocks
= [ $secondary ];
3098 $this->initializeManager( true );
3100 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3103 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3104 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3105 $makeReq( "required2", AuthenticationRequest
::PRIMARY_REQUIRED
),
3106 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3107 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3108 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3109 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3110 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3112 usort( $actual, $cmpReqs );
3113 usort( $expected, $cmpReqs );
3114 $this->assertEquals( $expected, $actual );
3116 $this->primaryauthMocks
= [ $primary1 ];
3117 $this->secondaryauthMocks
= [ $secondary ];
3118 $this->initializeManager( true );
3120 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3123 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3124 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3125 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3126 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3127 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3128 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3130 usort( $actual, $cmpReqs );
3131 usort( $expected, $cmpReqs );
3132 $this->assertEquals( $expected, $actual );
3135 public function testAllowsPropertyChange() {
3137 foreach ( [ 'primary', 'secondary' ] as $key ) {
3138 $class = ucfirst( $key ) . 'AuthenticationProvider';
3139 $mocks[$key] = $this->getMockForAbstractClass(
3140 "MediaWiki\\Auth\\$class", [], "Mock$class"
3142 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3143 ->will( $this->returnValue( $key ) );
3144 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
3145 ->will( $this->returnCallback( function ( $prop ) use ( $key ) {
3146 return $prop !== $key;
3150 $this->primaryauthMocks
= [ $mocks['primary'] ];
3151 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
3152 $this->initializeManager( true );
3154 $this->assertTrue( $this->manager
->allowsPropertyChange( 'foo' ) );
3155 $this->assertFalse( $this->manager
->allowsPropertyChange( 'primary' ) );
3156 $this->assertFalse( $this->manager
->allowsPropertyChange( 'secondary' ) );
3159 public function testAutoCreateOnLogin() {
3160 $username = self
::usernameForCreation();
3162 $req = $this->createMock( AuthenticationRequest
::class );
3164 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3165 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3166 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3167 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3168 $mock->expects( $this->any() )->method( 'accountCreationType' )
3169 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3170 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3171 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3172 ->will( $this->returnValue( StatusValue
::newGood() ) );
3174 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3175 $mock2->expects( $this->any() )->method( 'getUniqueId' )
3176 ->will( $this->returnValue( 'secondary' ) );
3177 $mock2->expects( $this->any() )->method( 'beginSecondaryAuthentication' )->will(
3179 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) )
3182 $mock2->expects( $this->any() )->method( 'continueSecondaryAuthentication' )
3183 ->will( $this->returnValue( AuthenticationResponse
::newAbstain() ) );
3184 $mock2->expects( $this->any() )->method( 'testUserForCreation' )
3185 ->will( $this->returnValue( StatusValue
::newGood() ) );
3187 $this->primaryauthMocks
= [ $mock ];
3188 $this->secondaryauthMocks
= [ $mock2 ];
3189 $this->initializeManager( true );
3190 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3191 $session = $this->request
->getSession();
3194 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3197 $callback = $this->callback( function ( $user ) use ( $username ) {
3198 return $user->getName() === $username;
3201 $this->hook( 'UserLoggedIn', $this->never() );
3202 $this->hook( 'LocalUserCreated', $this->once() )->with( $callback, $this->equalTo( true ) );
3203 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3204 $this->unhook( 'LocalUserCreated' );
3205 $this->unhook( 'UserLoggedIn' );
3206 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
3208 $id = (int)\User
::newFromName( $username )->getId();
3209 $this->assertNotSame( 0, \User
::newFromName( $username )->getId() );
3210 $this->assertSame( 0, $session->getUser()->getId() );
3212 $this->hook( 'UserLoggedIn', $this->once() )->with( $callback );
3213 $this->hook( 'LocalUserCreated', $this->never() );
3214 $ret = $this->manager
->continueAuthentication( [] );
3215 $this->unhook( 'LocalUserCreated' );
3216 $this->unhook( 'UserLoggedIn' );
3217 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
3218 $this->assertSame( $username, $ret->username
);
3219 $this->assertSame( $id, $session->getUser()->getId() );
3222 public function testAutoCreateFailOnLogin() {
3223 $username = self
::usernameForCreation();
3225 $mock = $this->getMockForAbstractClass(
3226 PrimaryAuthenticationProvider
::class, [], "MockPrimaryAuthenticationProvider" );
3227 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3228 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3229 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3230 $mock->expects( $this->any() )->method( 'accountCreationType' )
3231 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3232 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3233 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3234 ->will( $this->returnValue( StatusValue
::newFatal( 'fail-from-primary' ) ) );
3236 $this->primaryauthMocks
= [ $mock ];
3237 $this->initializeManager( true );
3238 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3239 $session = $this->request
->getSession();
3242 $this->assertSame( 0, $session->getUser()->getId(),
3244 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3247 $this->hook( 'UserLoggedIn', $this->never() );
3248 $this->hook( 'LocalUserCreated', $this->never() );
3249 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3250 $this->unhook( 'LocalUserCreated' );
3251 $this->unhook( 'UserLoggedIn' );
3252 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3253 $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message
->getKey() );
3255 $this->assertSame( 0, \User
::newFromName( $username )->getId() );
3256 $this->assertSame( 0, $session->getUser()->getId() );
3259 public function testAuthenticationSessionData() {
3260 $this->initializeManager( true );
3262 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3263 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3264 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3265 $this->assertSame( 'foo!', $this->manager
->getAuthenticationSessionData( 'foo' ) );
3266 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3267 $this->manager
->removeAuthenticationSessionData( 'foo' );
3268 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3269 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3270 $this->manager
->removeAuthenticationSessionData( 'bar' );
3271 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3273 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3274 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3275 $this->manager
->removeAuthenticationSessionData( null );
3276 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3277 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3280 public function testCanLinkAccounts() {
3282 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
3283 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
3284 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
3287 foreach ( $types as $type => $can ) {
3288 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3289 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
3290 $mock->expects( $this->any() )->method( 'accountCreationType' )
3291 ->will( $this->returnValue( $type ) );
3292 $this->primaryauthMocks
= [ $mock ];
3293 $this->initializeManager( true );
3294 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
3298 public function testBeginAccountLink() {
3299 $user = \User
::newFromName( 'UTSysop' );
3300 $this->initializeManager();
3302 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', 'test' );
3304 $this->manager
->beginAccountLink( $user, [], 'http://localhost/' );
3305 $this->fail( 'Expected exception not thrown' );
3306 } catch ( \LogicException
$ex ) {
3307 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3309 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3311 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3312 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3313 $mock->expects( $this->any() )->method( 'accountCreationType' )
3314 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3315 $this->primaryauthMocks
= [ $mock ];
3316 $this->initializeManager( true );
3318 $ret = $this->manager
->beginAccountLink( new \User
, [], 'http://localhost/' );
3319 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3320 $this->assertSame( 'noname', $ret->message
->getKey() );
3322 $ret = $this->manager
->beginAccountLink(
3323 \User
::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
3325 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3326 $this->assertSame( 'authmanager-userdoesnotexist', $ret->message
->getKey() );
3329 public function testContinueAccountLink() {
3330 $user = \User
::newFromName( 'UTSysop' );
3331 $this->initializeManager();
3334 'userid' => $user->getId(),
3335 'username' => $user->getName(),
3340 $this->manager
->continueAccountLink( [] );
3341 $this->fail( 'Expected exception not thrown' );
3342 } catch ( \LogicException
$ex ) {
3343 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3346 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3347 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3348 $mock->expects( $this->any() )->method( 'accountCreationType' )
3349 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3350 $mock->expects( $this->any() )->method( 'beginPrimaryAccountLink' )->will(
3351 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
3353 $this->primaryauthMocks
= [ $mock ];
3354 $this->initializeManager( true );
3356 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', null );
3357 $ret = $this->manager
->continueAccountLink( [] );
3358 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3359 $this->assertSame( 'authmanager-link-not-in-progress', $ret->message
->getKey() );
3361 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3362 [ 'username' => $user->getName() . '<>' ] +
$session );
3363 $ret = $this->manager
->continueAccountLink( [] );
3364 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3365 $this->assertSame( 'noname', $ret->message
->getKey() );
3366 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3368 $id = $user->getId();
3369 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3370 [ 'userid' => $id +
1 ] +
$session );
3372 $ret = $this->manager
->continueAccountLink( [] );
3373 $this->fail( 'Expected exception not thrown' );
3374 } catch ( \UnexpectedValueException
$ex ) {
3375 $this->assertEquals(
3376 "User \"{$user->getName()}\" is valid, but ID $id != " . ( $id +
1 ) . '!',
3380 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3384 * @dataProvider provideAccountLink
3385 * @param StatusValue $preTest
3386 * @param array $primaryResponses
3387 * @param array $managerResponses
3389 public function testAccountLink(
3390 StatusValue
$preTest, array $primaryResponses, array $managerResponses
3392 $user = \User
::newFromName( 'UTSysop' );
3394 $this->initializeManager();
3396 // Set up lots of mocks...
3397 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3398 $req->primary
= $primaryResponses;
3401 foreach ( [ 'pre', 'primary' ] as $key ) {
3402 $class = ucfirst( $key ) . 'AuthenticationProvider';
3403 $mocks[$key] = $this->getMockForAbstractClass(
3404 "MediaWiki\\Auth\\$class", [], "Mock$class"
3406 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3407 ->will( $this->returnValue( $key ) );
3409 for ( $i = 2; $i <= 3; $i++
) {
3410 $mocks[$key . $i] = $this->getMockForAbstractClass(
3411 "MediaWiki\\Auth\\$class", [], "Mock$class"
3413 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
3414 ->will( $this->returnValue( $key . $i ) );
3418 $mocks['pre']->expects( $this->any() )->method( 'testForAccountLink' )
3419 ->will( $this->returnCallback(
3421 use ( $user, $preTest )
3423 $this->assertSame( $user->getId(), $u->getId() );
3424 $this->assertSame( $user->getName(), $u->getName() );
3429 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
3430 ->will( $this->returnValue( StatusValue
::newGood() ) );
3432 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
3433 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3434 $ct = count( $req->primary
);
3435 $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req ) {
3436 $this->assertSame( $user->getId(), $u->getId() );
3437 $this->assertSame( $user->getName(), $u->getName() );
3439 foreach ( $reqs as $r ) {
3440 $this->assertSame( $user->getName(), $r->username
);
3441 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
3443 $this->assertTrue( $foundReq, '$reqs contains $req' );
3444 return array_shift( $req->primary
);
3446 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
3447 ->method( 'beginPrimaryAccountLink' )
3448 ->will( $callback );
3449 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
3450 ->method( 'continuePrimaryAccountLink' )
3451 ->will( $callback );
3453 $abstain = AuthenticationResponse
::newAbstain();
3454 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
3455 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3456 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
3457 ->will( $this->returnValue( $abstain ) );
3458 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3459 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
3460 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3461 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
3462 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3464 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
3465 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
3466 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
3467 return $level === LogLevel
::DEBUG ?
null : $message;
3469 $this->initializeManager( true );
3471 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
3472 $this->equalTo( AuthenticationResponse
::PASS
),
3473 $this->equalTo( AuthenticationResponse
::FAIL
)
3475 $providers = array_merge( $this->preauthMocks
, $this->primaryauthMocks
);
3476 foreach ( $providers as $p ) {
3477 $p->postCalled
= false;
3478 $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
3479 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
3480 $this->assertInstanceOf( \User
::class, $user );
3481 $this->assertSame( 'UTSysop', $user->getName() );
3482 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
3483 $this->assertThat( $response->status
, $constraint );
3484 $p->postCalled
= $response->status
;
3491 foreach ( $managerResponses as $i => $response ) {
3492 if ( $response instanceof AuthenticationResponse
&&
3493 $response->status
=== AuthenticationResponse
::PASS
3495 $expectLog[] = [ LogLevel
::INFO
, 'Account linked to {user} by primary' ];
3501 $ret = $this->manager
->beginAccountLink( $user, [ $req ], 'http://localhost/' );
3503 $ret = $this->manager
->continueAccountLink( [ $req ] );
3505 if ( $response instanceof \Exception
) {
3506 $this->fail( 'Expected exception not thrown', "Response $i" );
3508 } catch ( \Exception
$ex ) {
3509 if ( !$response instanceof \Exception
) {
3512 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
3513 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3514 "Response $i, exception, session state" );
3518 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
3520 $ret->message
= $this->message( $ret->message
);
3521 $this->assertEquals( $response, $ret, "Response $i, response" );
3522 if ( $response->status
=== AuthenticationResponse
::PASS ||
3523 $response->status
=== AuthenticationResponse
::FAIL
3525 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3526 "Response $i, session state" );
3527 foreach ( $providers as $p ) {
3528 $this->assertSame( $response->status
, $p->postCalled
,
3529 "Response $i, post-auth callback called" );
3532 $this->assertNotNull(
3533 $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3534 "Response $i, session state"
3536 foreach ( $ret->neededRequests
as $neededReq ) {
3537 $this->assertEquals( AuthManager
::ACTION_LINK
, $neededReq->action
,
3538 "Response $i, neededRequest action" );
3540 $this->assertEquals(
3541 $ret->neededRequests
,
3542 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LINK_CONTINUE
),
3543 "Response $i, continuation check"
3545 foreach ( $providers as $p ) {
3546 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
3553 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
3556 public function provideAccountLink() {
3557 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3558 $good = StatusValue
::newGood();
3561 'Pre-link test fail in pre' => [
3562 StatusValue
::newFatal( 'fail-from-pre' ),
3565 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
3568 'Failure in primary' => [
3571 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
3575 'All primary abstain' => [
3578 AuthenticationResponse
::newAbstain(),
3581 AuthenticationResponse
::newFail( $this->message( 'authmanager-link-no-primary' ) )
3584 'Primary UI, then redirect, then fail' => [
3587 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3588 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
3589 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
3593 'Primary redirect, then abstain' => [
3596 $tmp = AuthenticationResponse
::newRedirect(
3597 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
3599 AuthenticationResponse
::newAbstain(),
3603 new \
DomainException(
3604 'MockPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
3608 'Primary UI, then pass' => [
3611 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3612 AuthenticationResponse
::newPass(),
3616 AuthenticationResponse
::newPass( '' ),
3622 AuthenticationResponse
::newPass( '' ),
3625 AuthenticationResponse
::newPass( '' ),