3 namespace MediaWiki\Auth
;
5 use MediaWiki\Session\SessionInfo
;
6 use MediaWiki\Session\UserInfo
;
13 * @covers MediaWiki\Auth\AuthManager
15 class AuthManagerTest
extends \MediaWikiTestCase
{
16 /** @var WebRequest */
20 /** @var \\Psr\\Log\\LoggerInterface */
23 protected $preauthMocks = [];
24 protected $primaryauthMocks = [];
25 protected $secondaryauthMocks = [];
27 /** @var AuthManager */
29 /** @var TestingAccessWrapper */
30 protected $managerPriv;
32 protected function setUp() {
35 $this->setMwGlobals( [ 'wgAuth' => null ] );
36 $this->stashMwGlobals( [ 'wgHooks' ] );
40 * Sets a mock on a hook
42 * @param object $expect From $this->once(), $this->never(), etc.
43 * @return object $mock->expects( $expect )->method( ... ).
45 protected function hook( $hook, $expect ) {
47 $mock = $this->getMock( __CLASS__
, [ "on$hook" ] );
48 $wgHooks[$hook] = [ $mock ];
49 return $mock->expects( $expect )->method( "on$hook" );
56 protected function unhook( $hook ) {
62 * Ensure a value is a clean Message object
63 * @param string|Message $key
64 * @param array $params
67 protected function message( $key, $params = [] ) {
68 if ( $key === null ) {
71 if ( $key instanceof \MessageSpecifier
) {
72 $params = $key->getParams();
73 $key = $key->getKey();
75 return new \
Message( $key, $params, \Language
::factory( 'en' ) );
79 * Initialize the AuthManagerConfig variable in $this->config
81 * Uses data from the various 'mocks' fields.
83 protected function initializeConfig() {
93 foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
94 $key = $type . 'Mocks';
95 foreach ( $this->$key as $mock ) {
96 $config[$type][$mock->getUniqueId()] = [ 'factory' => function () use ( $mock ) {
102 $this->config
->set( 'AuthManagerConfig', $config );
103 $this->config
->set( 'LanguageCode', 'en' );
104 $this->config
->set( 'NewUserLog', false );
108 * Initialize $this->manager
109 * @param bool $regen Force a call to $this->initializeConfig()
111 protected function initializeManager( $regen = false ) {
112 if ( $regen ||
!$this->config
) {
113 $this->config
= new \
HashConfig();
115 if ( $regen ||
!$this->request
) {
116 $this->request
= new \
FauxRequest();
118 if ( !$this->logger
) {
119 $this->logger
= new \
TestLogger();
122 if ( $regen ||
!$this->config
->has( 'AuthManagerConfig' ) ) {
123 $this->initializeConfig();
125 $this->manager
= new AuthManager( $this->request
, $this->config
);
126 $this->manager
->setLogger( $this->logger
);
127 $this->managerPriv
= \TestingAccessWrapper
::newFromObject( $this->manager
);
131 * Setup SessionManager with a mock session provider
132 * @param bool|null $canChangeUser If non-null, canChangeUser will be mocked to return this
133 * @param array $methods Additional methods to mock
134 * @return array (MediaWiki\Session\SessionProvider, ScopedCallback)
136 protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
137 if ( !$this->config
) {
138 $this->config
= new \
HashConfig();
139 $this->initializeConfig();
141 $this->config
->set( 'ObjectCacheSessionExpiry', 100 );
143 $methods[] = '__toString';
144 $methods[] = 'describe';
145 if ( $canChangeUser !== null ) {
146 $methods[] = 'canChangeUser';
148 $provider = $this->getMockBuilder( 'DummySessionProvider' )
149 ->setMethods( $methods )
151 $provider->expects( $this->any() )->method( '__toString' )
152 ->will( $this->returnValue( 'MockSessionProvider' ) );
153 $provider->expects( $this->any() )->method( 'describe' )
154 ->will( $this->returnValue( 'MockSessionProvider sessions' ) );
155 if ( $canChangeUser !== null ) {
156 $provider->expects( $this->any() )->method( 'canChangeUser' )
157 ->will( $this->returnValue( $canChangeUser ) );
159 $this->config
->set( 'SessionProviders', [
160 [ 'factory' => function () use ( $provider ) {
165 $manager = new \MediaWiki\Session\
SessionManager( [
166 'config' => $this->config
,
167 'logger' => new \Psr\Log\
NullLogger(),
168 'store' => new \
HashBagOStuff(),
170 \TestingAccessWrapper
::newFromObject( $manager )->getProvider( (string)$provider );
172 $reset = \MediaWiki\Session\TestUtils
::setSessionManagerSingleton( $manager );
174 if ( $this->request
) {
175 $manager->getSessionForRequest( $this->request
);
178 return [ $provider, $reset ];
181 public function testSingleton() {
182 // Temporarily clear out the global singleton, if any, to test creating
184 $rProp = new \
ReflectionProperty( AuthManager
::class, 'instance' );
185 $rProp->setAccessible( true );
186 $old = $rProp->getValue();
187 $cb = new \
ScopedCallback( [ $rProp, 'setValue' ], [ $old ] );
188 $rProp->setValue( null );
190 $singleton = AuthManager
::singleton();
191 $this->assertInstanceOf( AuthManager
::class, AuthManager
::singleton() );
192 $this->assertSame( $singleton, AuthManager
::singleton() );
193 $this->assertSame( \RequestContext
::getMain()->getRequest(), $singleton->getRequest() );
195 \RequestContext
::getMain()->getConfig(),
196 \TestingAccessWrapper
::newFromObject( $singleton )->config
200 public function testCanAuthenticateNow() {
201 $this->initializeManager();
203 list( $provider, $reset ) = $this->getMockSessionProvider( false );
204 $this->assertFalse( $this->manager
->canAuthenticateNow() );
205 \ScopedCallback
::consume( $reset );
207 list( $provider, $reset ) = $this->getMockSessionProvider( true );
208 $this->assertTrue( $this->manager
->canAuthenticateNow() );
209 \ScopedCallback
::consume( $reset );
212 public function testNormalizeUsername() {
214 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
215 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
216 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
217 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
219 foreach ( $mocks as $key => $mock ) {
220 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
222 $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
223 ->with( $this->identicalTo( 'XYZ' ) )
224 ->willReturn( 'Foo' );
225 $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
226 ->with( $this->identicalTo( 'XYZ' ) )
227 ->willReturn( 'Foo' );
228 $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
229 ->with( $this->identicalTo( 'XYZ' ) )
230 ->willReturn( null );
231 $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
232 ->with( $this->identicalTo( 'XYZ' ) )
233 ->willReturn( 'Bar!' );
235 $this->primaryauthMocks
= $mocks;
237 $this->initializeManager();
239 $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager
->normalizeUsername( 'XYZ' ) );
243 * @dataProvider provideSecuritySensitiveOperationStatus
244 * @param bool $mutableSession
246 public function testSecuritySensitiveOperationStatus( $mutableSession ) {
247 $this->logger
= new \Psr\Log\
NullLogger();
248 $user = \User
::newFromName( 'UTSysop' );
250 $reauth = $mutableSession ? AuthManager
::SEC_REAUTH
: AuthManager
::SEC_FAIL
;
252 list( $provider, $reset ) = $this->getMockSessionProvider(
253 $mutableSession, [ 'provideSessionInfo' ]
255 $provider->expects( $this->any() )->method( 'provideSessionInfo' )
256 ->will( $this->returnCallback( function () use ( $provider, &$provideUser ) {
257 return new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
258 'provider' => $provider,
259 'id' => \DummySessionProvider
::ID
,
261 'userInfo' => UserInfo
::newFromUser( $provideUser, true )
264 $this->initializeManager();
266 $this->config
->set( 'ReauthenticateTime', [] );
267 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [] );
268 $provideUser = new \User
;
269 $session = $provider->getManager()->getSessionForRequest( $this->request
);
270 $this->assertSame( 0, $session->getUser()->getId(), 'sanity check' );
272 // Anonymous user => reauth
273 $session->set( 'AuthManager:lastAuthId', 0 );
274 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
275 $this->assertSame( $reauth, $this->manager
->securitySensitiveOperationStatus( 'foo' ) );
277 $provideUser = $user;
278 $session = $provider->getManager()->getSessionForRequest( $this->request
);
279 $this->assertSame( $user->getId(), $session->getUser()->getId(), 'sanity check' );
281 // Error for no default (only gets thrown for non-anonymous user)
282 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
283 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
285 $this->manager
->securitySensitiveOperationStatus( 'foo' );
286 $this->fail( 'Expected exception not thrown' );
287 } catch ( \UnexpectedValueException
$ex ) {
290 ?
'$wgReauthenticateTime lacks a default'
291 : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
296 if ( $mutableSession ) {
297 $this->config
->set( 'ReauthenticateTime', [
303 // Mismatched user ID
304 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
305 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
307 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
310 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
313 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
317 $session->set( 'AuthManager:lastAuthId', $user->getId() );
318 $session->set( 'AuthManager:lastAuthTimestamp', null );
320 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
323 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
326 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
329 // Recent enough to pass
330 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
332 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
335 // Not recent enough to pass
336 $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
338 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
340 // But recent enough for the 'test' operation
342 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test' )
345 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [
351 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
355 AuthManager
::SEC_FAIL
, $this->manager
->securitySensitiveOperationStatus( 'test' )
359 // Test hook, all three possible values
361 AuthManager
::SEC_OK
=> AuthManager
::SEC_OK
,
362 AuthManager
::SEC_REAUTH
=> $reauth,
363 AuthManager
::SEC_FAIL
=> AuthManager
::SEC_FAIL
,
364 ] as $hook => $expect ) {
365 $this->hook( 'SecuritySensitiveOperationStatus', $this->exactly( 2 ) )
369 $this->callback( function ( $s ) use ( $session ) {
370 return $s->getId() === $session->getId();
372 $mutableSession ?
$this->equalTo( 500, 1 ) : $this->equalTo( -1 )
374 ->will( $this->returnCallback( function ( &$v ) use ( $hook ) {
378 $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
380 $expect, $this->manager
->securitySensitiveOperationStatus( 'test' ), "hook $hook"
383 $expect, $this->manager
->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
385 $this->unhook( 'SecuritySensitiveOperationStatus' );
388 \ScopedCallback
::consume( $reset );
391 public function onSecuritySensitiveOperationStatus( &$status, $operation, $session, $time ) {
394 public static function provideSecuritySensitiveOperationStatus() {
402 * @dataProvider provideUserCanAuthenticate
403 * @param bool $primary1Can
404 * @param bool $primary2Can
405 * @param bool $expect
407 public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
408 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
409 $mock1->expects( $this->any() )->method( 'getUniqueId' )
410 ->will( $this->returnValue( 'primary1' ) );
411 $mock1->expects( $this->any() )->method( 'testUserCanAuthenticate' )
412 ->with( $this->equalTo( 'UTSysop' ) )
413 ->will( $this->returnValue( $primary1Can ) );
414 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
415 $mock2->expects( $this->any() )->method( 'getUniqueId' )
416 ->will( $this->returnValue( 'primary2' ) );
417 $mock2->expects( $this->any() )->method( 'testUserCanAuthenticate' )
418 ->with( $this->equalTo( 'UTSysop' ) )
419 ->will( $this->returnValue( $primary2Can ) );
420 $this->primaryauthMocks
= [ $mock1, $mock2 ];
422 $this->initializeManager( true );
423 $this->assertSame( $expect, $this->manager
->userCanAuthenticate( 'UTSysop' ) );
426 public static function provideUserCanAuthenticate() {
428 [ false, false, false ],
429 [ true, false, true ],
430 [ false, true, true ],
431 [ true, true, true ],
435 public function testRevokeAccessForUser() {
436 $this->initializeManager();
438 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
439 $mock->expects( $this->any() )->method( 'getUniqueId' )
440 ->will( $this->returnValue( 'primary' ) );
441 $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
442 ->with( $this->equalTo( 'UTSysop' ) );
443 $this->primaryauthMocks
= [ $mock ];
445 $this->initializeManager( true );
446 $this->logger
->setCollect( true );
448 $this->manager
->revokeAccessForUser( 'UTSysop' );
451 [ LogLevel
::INFO
, 'Revoking access for {user}' ],
452 ], $this->logger
->getBuffer() );
455 public function testProviderCreation() {
457 'pre' => $this->getMockForAbstractClass( PreAuthenticationProvider
::class ),
458 'primary' => $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
459 'secondary' => $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class ),
461 foreach ( $mocks as $key => $mock ) {
462 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
463 $mock->expects( $this->once() )->method( 'setLogger' );
464 $mock->expects( $this->once() )->method( 'setManager' );
465 $mock->expects( $this->once() )->method( 'setConfig' );
467 $this->preauthMocks
= [ $mocks['pre'] ];
468 $this->primaryauthMocks
= [ $mocks['primary'] ];
469 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
472 $this->initializeManager();
475 $this->managerPriv
->getAuthenticationProvider( 'primary' )
479 $this->managerPriv
->getAuthenticationProvider( 'secondary' )
483 $this->managerPriv
->getAuthenticationProvider( 'pre' )
486 [ 'pre' => $mocks['pre'] ],
487 $this->managerPriv
->getPreAuthenticationProviders()
490 [ 'primary' => $mocks['primary'] ],
491 $this->managerPriv
->getPrimaryAuthenticationProviders()
494 [ 'secondary' => $mocks['secondary'] ],
495 $this->managerPriv
->getSecondaryAuthenticationProviders()
499 $mock1 = $this->getMockForAbstractClass( PreAuthenticationProvider
::class );
500 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
501 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
502 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
503 $this->preauthMocks
= [ $mock1 ];
504 $this->primaryauthMocks
= [ $mock2 ];
505 $this->secondaryauthMocks
= [];
506 $this->initializeManager( true );
508 $this->managerPriv
->getAuthenticationProvider( 'Y' );
509 $this->fail( 'Expected exception not thrown' );
510 } catch ( \RuntimeException
$ex ) {
511 $class1 = get_class( $mock1 );
512 $class2 = get_class( $mock2 );
514 "Duplicate specifications for id X (classes $class1 and $class2)", $ex->getMessage()
519 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
520 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
521 $class = get_class( $mock );
522 $this->preauthMocks
= [ $mock ];
523 $this->primaryauthMocks
= [ $mock ];
524 $this->secondaryauthMocks
= [ $mock ];
525 $this->initializeManager( true );
527 $this->managerPriv
->getPreAuthenticationProviders();
528 $this->fail( 'Expected exception not thrown' );
529 } catch ( \RuntimeException
$ex ) {
531 "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
536 $this->managerPriv
->getPrimaryAuthenticationProviders();
537 $this->fail( 'Expected exception not thrown' );
538 } catch ( \RuntimeException
$ex ) {
540 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
545 $this->managerPriv
->getSecondaryAuthenticationProviders();
546 $this->fail( 'Expected exception not thrown' );
547 } catch ( \RuntimeException
$ex ) {
549 "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
555 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
556 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
557 $mock3 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
558 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
559 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
560 $mock3->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'C' ) );
561 $this->preauthMocks
= [];
562 $this->primaryauthMocks
= [ $mock1, $mock2, $mock3 ];
563 $this->secondaryauthMocks
= [];
564 $this->initializeConfig();
565 $config = $this->config
->get( 'AuthManagerConfig' );
567 $this->initializeManager( false );
569 [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
570 $this->managerPriv
->getPrimaryAuthenticationProviders(),
574 $config['primaryauth']['A']['sort'] = 100;
575 $config['primaryauth']['C']['sort'] = -1;
576 $this->config
->set( 'AuthManagerConfig', $config );
577 $this->initializeManager( false );
579 [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
580 $this->managerPriv
->getPrimaryAuthenticationProviders()
584 public function testSetDefaultUserOptions() {
585 $this->initializeManager();
587 $context = \RequestContext
::getMain();
588 $reset = new \
ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
589 $context->setLanguage( 'de' );
590 $this->setMwGlobals( 'wgContLang', \Language
::factory( 'zh' ) );
592 $user = \User
::newFromName( self
::usernameForCreation() );
593 $user->addToDatabase();
594 $oldToken = $user->getToken();
595 $this->managerPriv
->setDefaultUserOptions( $user, false );
596 $user->saveSettings();
597 $this->assertNotEquals( $oldToken, $user->getToken() );
598 $this->assertSame( 'zh', $user->getOption( 'language' ) );
599 $this->assertSame( 'zh', $user->getOption( 'variant' ) );
601 $user = \User
::newFromName( self
::usernameForCreation() );
602 $user->addToDatabase();
603 $oldToken = $user->getToken();
604 $this->managerPriv
->setDefaultUserOptions( $user, true );
605 $user->saveSettings();
606 $this->assertNotEquals( $oldToken, $user->getToken() );
607 $this->assertSame( 'de', $user->getOption( 'language' ) );
608 $this->assertSame( 'zh', $user->getOption( 'variant' ) );
610 $this->setMwGlobals( 'wgContLang', \Language
::factory( 'en' ) );
612 $user = \User
::newFromName( self
::usernameForCreation() );
613 $user->addToDatabase();
614 $oldToken = $user->getToken();
615 $this->managerPriv
->setDefaultUserOptions( $user, true );
616 $user->saveSettings();
617 $this->assertNotEquals( $oldToken, $user->getToken() );
618 $this->assertSame( 'de', $user->getOption( 'language' ) );
619 $this->assertSame( null, $user->getOption( 'variant' ) );
622 public function testForcePrimaryAuthenticationProviders() {
623 $mockA = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
624 $mockB = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
625 $mockB2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
626 $mockA->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
627 $mockB->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
628 $mockB2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
629 $this->primaryauthMocks
= [ $mockA ];
631 $this->logger
= new \
TestLogger( true );
633 // Test without first initializing the configured providers
634 $this->initializeManager();
635 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
637 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
639 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
640 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
642 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
643 ], $this->logger
->getBuffer() );
644 $this->logger
->clearBuffer();
646 // Test with first initializing the configured providers
647 $this->initializeManager();
648 $this->assertSame( $mockA, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
649 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
650 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
651 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
652 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
654 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
656 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
657 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
658 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
660 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
663 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
666 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
668 ], $this->logger
->getBuffer() );
669 $this->logger
->clearBuffer();
671 // Test duplicate IDs
672 $this->initializeManager();
674 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
675 $this->fail( 'Expected exception not thrown' );
676 } catch ( \RuntimeException
$ex ) {
677 $class1 = get_class( $mockB );
678 $class2 = get_class( $mockB2 );
680 "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
685 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
686 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
687 $class = get_class( $mock );
689 $this->manager
->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
690 $this->fail( 'Expected exception not thrown' );
691 } catch ( \RuntimeException
$ex ) {
693 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
700 public function testBeginAuthentication() {
701 $this->initializeManager();
704 list( $provider, $reset ) = $this->getMockSessionProvider( false );
705 $this->hook( 'UserLoggedIn', $this->never() );
706 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
708 $this->manager
->beginAuthentication( [], 'http://localhost/' );
709 $this->fail( 'Expected exception not thrown' );
710 } catch ( \LogicException
$ex ) {
711 $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
713 $this->unhook( 'UserLoggedIn' );
714 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
715 \ScopedCallback
::consume( $reset );
716 $this->initializeManager( true );
718 // CreatedAccountAuthenticationRequest
719 $user = \User
::newFromName( 'UTSysop' );
721 new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
723 $this->hook( 'UserLoggedIn', $this->never() );
725 $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
726 $this->fail( 'Expected exception not thrown' );
727 } catch ( \LogicException
$ex ) {
729 'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
730 'that created the account',
734 $this->unhook( 'UserLoggedIn' );
736 $this->request
->getSession()->clear();
737 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
738 $this->managerPriv
->createdAccountAuthenticationRequests
= [ $reqs[0] ];
739 $this->hook( 'UserLoggedIn', $this->once() )
740 ->with( $this->callback( function ( $u ) use ( $user ) {
741 return $user->getId() === $u->getId() && $user->getName() === $u->getName();
743 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
744 $this->logger
->setCollect( true );
745 $ret = $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
746 $this->logger
->setCollect( false );
747 $this->unhook( 'UserLoggedIn' );
748 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
749 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
750 $this->assertSame( $user->getName(), $ret->username
);
751 $this->assertSame( $user->getId(), $this->request
->getSessionData( 'AuthManager:lastAuthId' ) );
753 time(), $this->request
->getSessionData( 'AuthManager:lastAuthTimestamp' ),
756 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
757 $this->assertSame( $user->getId(), $this->request
->getSession()->getUser()->getId() );
759 [ LogLevel
::INFO
, 'Logging in {user} after account creation' ],
760 ], $this->logger
->getBuffer() );
763 public function testCreateFromLogin() {
764 $user = \User
::newFromName( 'UTSysop' );
765 $req1 = $this->getMock( AuthenticationRequest
::class );
766 $req2 = $this->getMock( AuthenticationRequest
::class );
767 $req3 = $this->getMock( AuthenticationRequest
::class );
768 $userReq = new UsernameAuthenticationRequest
;
769 $userReq->username
= 'UTDummy';
771 $req1->returnToUrl
= 'http://localhost/';
772 $req2->returnToUrl
= 'http://localhost/';
773 $req3->returnToUrl
= 'http://localhost/';
774 $req3->username
= 'UTDummy';
775 $userReq->returnToUrl
= 'http://localhost/';
777 // Passing one into beginAuthentication(), and an immediate FAIL
778 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
779 $this->primaryauthMocks
= [ $primary ];
780 $this->initializeManager( true );
781 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
782 $res->createRequest
= $req1;
783 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
784 ->will( $this->returnValue( $res ) );
785 $createReq = new CreateFromLoginAuthenticationRequest(
786 null, [ $req2->getUniqueId() => $req2 ]
788 $this->logger
->setCollect( true );
789 $ret = $this->manager
->beginAuthentication( [ $createReq ], 'http://localhost/' );
790 $this->logger
->setCollect( false );
791 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
792 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
793 $this->assertSame( $req1, $ret->createRequest
->createRequest
);
794 $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest
->maybeLink
);
796 // UI, then FAIL in beginAuthentication()
797 $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider
::class )
798 ->setMethods( [ 'continuePrimaryAuthentication' ] )
799 ->getMockForAbstractClass();
800 $this->primaryauthMocks
= [ $primary ];
801 $this->initializeManager( true );
802 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
803 ->will( $this->returnValue(
804 AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) )
806 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
807 $res->createRequest
= $req2;
808 $primary->expects( $this->any() )->method( 'continuePrimaryAuthentication' )
809 ->will( $this->returnValue( $res ) );
810 $this->logger
->setCollect( true );
811 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
812 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
, 'sanity check' );
813 $ret = $this->manager
->continueAuthentication( [] );
814 $this->logger
->setCollect( false );
815 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
816 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
817 $this->assertSame( $req2, $ret->createRequest
->createRequest
);
818 $this->assertEquals( [], $ret->createRequest
->maybeLink
);
820 // Pass into beginAccountCreation(), see that maybeLink and createRequest get copied
821 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
822 $this->primaryauthMocks
= [ $primary ];
823 $this->initializeManager( true );
824 $createReq = new CreateFromLoginAuthenticationRequest( $req3, [ $req2 ] );
825 $createReq->returnToUrl
= 'http://localhost/';
826 $createReq->username
= 'UTDummy';
827 $res = AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) );
828 $primary->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
829 ->with( $this->anything(), $this->anything(), [ $userReq, $createReq, $req3 ] )
830 ->will( $this->returnValue( $res ) );
831 $primary->expects( $this->any() )->method( 'accountCreationType' )
832 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
833 $this->logger
->setCollect( true );
834 $ret = $this->manager
->beginAccountCreation(
835 $user, [ $userReq, $createReq ], 'http://localhost/'
837 $this->logger
->setCollect( false );
838 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
839 $state = $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' );
840 $this->assertNotNull( $state );
841 $this->assertEquals( [ $userReq, $createReq, $req3 ], $state['reqs'] );
842 $this->assertEquals( [ $req2 ], $state['maybeLink'] );
846 * @dataProvider provideAuthentication
847 * @param StatusValue $preResponse
848 * @param array $primaryResponses
849 * @param array $secondaryResponses
850 * @param array $managerResponses
851 * @param bool $link Whether the primary authentication provider is a "link" provider
853 public function testAuthentication(
854 StatusValue
$preResponse, array $primaryResponses, array $secondaryResponses,
855 array $managerResponses, $link = false
857 $this->initializeManager();
858 $user = \User
::newFromName( 'UTSysop' );
859 $id = $user->getId();
860 $name = $user->getName();
862 // Set up lots of mocks...
863 $req = new RememberMeAuthenticationRequest
;
864 $req->rememberMe
= (bool)rand( 0, 1 );
865 $req->pre
= $preResponse;
866 $req->primary
= $primaryResponses;
867 $req->secondary
= $secondaryResponses;
869 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
870 $class = ucfirst( $key ) . 'AuthenticationProvider';
871 $mocks[$key] = $this->getMockForAbstractClass(
872 "MediaWiki\\Auth\\$class", [], "Mock$class"
874 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
875 ->will( $this->returnValue( $key ) );
876 $mocks[$key . '2'] = $this->getMockForAbstractClass(
877 "MediaWiki\\Auth\\$class", [], "Mock$class"
879 $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
880 ->will( $this->returnValue( $key . '2' ) );
881 $mocks[$key . '3'] = $this->getMockForAbstractClass(
882 "MediaWiki\\Auth\\$class", [], "Mock$class"
884 $mocks[$key . '3']->expects( $this->any() )->method( 'getUniqueId' )
885 ->will( $this->returnValue( $key . '3' ) );
887 foreach ( $mocks as $mock ) {
888 $mock->expects( $this->any() )->method( 'getAuthenticationRequests' )
889 ->will( $this->returnValue( [] ) );
892 $mocks['pre']->expects( $this->once() )->method( 'testForAuthentication' )
893 ->will( $this->returnCallback( function ( $reqs ) use ( $req ) {
894 $this->assertContains( $req, $reqs );
898 $ct = count( $req->primary
);
899 $callback = $this->returnCallback( function ( $reqs ) use ( $req ) {
900 $this->assertContains( $req, $reqs );
901 return array_shift( $req->primary
);
903 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
904 ->method( 'beginPrimaryAuthentication' )
906 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
907 ->method( 'continuePrimaryAuthentication' )
910 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
911 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
914 $ct = count( $req->secondary
);
915 $callback = $this->returnCallback( function ( $user, $reqs ) use ( $id, $name, $req ) {
916 $this->assertSame( $id, $user->getId() );
917 $this->assertSame( $name, $user->getName() );
918 $this->assertContains( $req, $reqs );
919 return array_shift( $req->secondary
);
921 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
922 ->method( 'beginSecondaryAuthentication' )
924 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
925 ->method( 'continueSecondaryAuthentication' )
928 $abstain = AuthenticationResponse
::newAbstain();
929 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAuthentication' )
930 ->will( $this->returnValue( StatusValue
::newGood() ) );
931 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAuthentication' )
932 ->will( $this->returnValue( $abstain ) );
933 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAuthentication' );
934 $mocks['secondary2']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
935 ->will( $this->returnValue( $abstain ) );
936 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
937 $mocks['secondary3']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
938 ->will( $this->returnValue( $abstain ) );
939 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
941 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
942 $this->primaryauthMocks
= [ $mocks['primary'], $mocks['primary2'] ];
943 $this->secondaryauthMocks
= [
944 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2'],
945 // So linking happens
946 new ConfirmLinkSecondaryAuthenticationProvider
,
948 $this->initializeManager( true );
949 $this->logger
->setCollect( true );
951 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
952 $this->equalTo( AuthenticationResponse
::PASS
),
953 $this->equalTo( AuthenticationResponse
::FAIL
)
955 $providers = array_filter(
957 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
960 return is_callable( [ $p, 'expects' ] );
963 foreach ( $providers as $p ) {
964 $p->postCalled
= false;
965 $p->expects( $this->atMost( 1 ) )->method( 'postAuthentication' )
966 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
967 if ( $user !== null ) {
968 $this->assertInstanceOf( 'User', $user );
969 $this->assertSame( 'UTSysop', $user->getName() );
971 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
972 $this->assertThat( $response->status
, $constraint );
973 $p->postCalled
= $response->status
;
977 $session = $this->request
->getSession();
978 $session->setRememberUser( !$req->rememberMe
);
980 foreach ( $managerResponses as $i => $response ) {
981 $success = $response instanceof AuthenticationResponse
&&
982 $response->status
=== AuthenticationResponse
::PASS
;
984 $this->hook( 'UserLoggedIn', $this->once() )
985 ->with( $this->callback( function ( $user ) use ( $id, $name ) {
986 return $user->getId() === $id && $user->getName() === $name;
989 $this->hook( 'UserLoggedIn', $this->never() );
992 $response instanceof AuthenticationResponse
&&
993 $response->status
=== AuthenticationResponse
::FAIL
&&
994 $response->message
->getKey() !== 'authmanager-authn-not-in-progress' &&
995 $response->message
->getKey() !== 'authmanager-authn-no-primary'
998 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
1000 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->never() );
1006 $ret = $this->manager
->beginAuthentication( [ $req ], 'http://localhost/' );
1008 $ret = $this->manager
->continueAuthentication( [ $req ] );
1010 if ( $response instanceof \Exception
) {
1011 $this->fail( 'Expected exception not thrown', "Response $i" );
1013 } catch ( \Exception
$ex ) {
1014 if ( !$response instanceof \Exception
) {
1017 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
1018 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1019 "Response $i, exception, session state" );
1020 $this->unhook( 'UserLoggedIn' );
1021 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1025 $this->unhook( 'UserLoggedIn' );
1026 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1028 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
1030 $ret->message
= $this->message( $ret->message
);
1031 $this->assertEquals( $response, $ret, "Response $i, response" );
1033 $this->assertSame( $id, $session->getUser()->getId(),
1034 "Response $i, authn" );
1036 $this->assertSame( 0, $session->getUser()->getId(),
1037 "Response $i, authn" );
1039 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
1040 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1041 "Response $i, session state" );
1042 foreach ( $providers as $p ) {
1043 $this->assertSame( $response->status
, $p->postCalled
,
1044 "Response $i, post-auth callback called" );
1047 $this->assertNotNull( $session->getSecret( 'AuthManager::authnState' ),
1048 "Response $i, session state" );
1049 foreach ( $ret->neededRequests
as $neededReq ) {
1050 $this->assertEquals( AuthManager
::ACTION_LOGIN
, $neededReq->action
,
1051 "Response $i, neededRequest action" );
1053 $this->assertEquals(
1054 $ret->neededRequests
,
1055 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN_CONTINUE
),
1056 "Response $i, continuation check"
1058 foreach ( $providers as $p ) {
1059 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
1063 $state = $session->getSecret( 'AuthManager::authnState' );
1064 $maybeLink = isset( $state['maybeLink'] ) ?
$state['maybeLink'] : [];
1065 if ( $link && $response->status
=== AuthenticationResponse
::RESTART
) {
1066 $this->assertEquals(
1067 $response->createRequest
->maybeLink
,
1069 "Response $i, maybeLink"
1072 $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
1077 $this->assertSame( $req->rememberMe
, $session->shouldRememberUser(),
1078 'rememberMe checkbox had effect' );
1080 $this->assertNotSame( $req->rememberMe
, $session->shouldRememberUser(),
1081 'rememberMe checkbox wasn\'t applied' );
1085 public function provideAuthentication() {
1086 $user = \User
::newFromName( 'UTSysop' );
1087 $id = $user->getId();
1088 $name = $user->getName();
1090 $rememberReq = new RememberMeAuthenticationRequest
;
1091 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
1093 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1094 $req->foobar
= 'baz';
1095 $restartResponse = AuthenticationResponse
::newRestart(
1096 $this->message( 'authmanager-authn-no-local-user' )
1098 $restartResponse->neededRequests
= [ $rememberReq ];
1100 $restartResponse2Pass = AuthenticationResponse
::newPass( null );
1101 $restartResponse2Pass->linkRequest
= $req;
1102 $restartResponse2 = AuthenticationResponse
::newRestart(
1103 $this->message( 'authmanager-authn-no-local-user-link' )
1105 $restartResponse2->createRequest
= new CreateFromLoginAuthenticationRequest(
1106 null, [ $req->getUniqueId() => $req ]
1108 $restartResponse2->createRequest
->action
= AuthManager
::ACTION_LOGIN
;
1109 $restartResponse2->neededRequests
= [ $rememberReq, $restartResponse2->createRequest
];
1112 'Failure in pre-auth' => [
1113 StatusValue
::newFatal( 'fail-from-pre' ),
1117 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
1118 AuthenticationResponse
::newFail(
1119 $this->message( 'authmanager-authn-not-in-progress' )
1123 'Failure in primary' => [
1124 StatusValue
::newGood(),
1126 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
1131 'All primary abstain' => [
1132 StatusValue
::newGood(),
1134 AuthenticationResponse
::newAbstain(),
1138 AuthenticationResponse
::newFail( $this->message( 'authmanager-authn-no-primary' ) )
1141 'Primary UI, then redirect, then fail' => [
1142 StatusValue
::newGood(),
1144 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1145 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
1146 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
1151 'Primary redirect, then abstain' => [
1152 StatusValue
::newGood(),
1154 $tmp = AuthenticationResponse
::newRedirect(
1155 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
1157 AuthenticationResponse
::newAbstain(),
1162 new \
DomainException(
1163 'MockPrimaryAuthenticationProvider::continuePrimaryAuthentication() returned ABSTAIN'
1167 'Primary UI, then pass with no local user' => [
1168 StatusValue
::newGood(),
1170 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1171 AuthenticationResponse
::newPass( null ),
1179 'Primary UI, then pass with no local user (link type)' => [
1180 StatusValue
::newGood(),
1182 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1183 $restartResponse2Pass,
1192 'Primary pass with invalid username' => [
1193 StatusValue
::newGood(),
1195 AuthenticationResponse
::newPass( '<>' ),
1199 new \
DomainException( 'MockPrimaryAuthenticationProvider returned an invalid username: <>' ),
1202 'Secondary fail' => [
1203 StatusValue
::newGood(),
1205 AuthenticationResponse
::newPass( $name ),
1208 AuthenticationResponse
::newFail( $this->message( 'fail-in-secondary' ) ),
1212 'Secondary UI, then abstain' => [
1213 StatusValue
::newGood(),
1215 AuthenticationResponse
::newPass( $name ),
1218 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1219 AuthenticationResponse
::newAbstain()
1223 AuthenticationResponse
::newPass( $name ),
1226 'Secondary pass' => [
1227 StatusValue
::newGood(),
1229 AuthenticationResponse
::newPass( $name ),
1232 AuthenticationResponse
::newPass()
1235 AuthenticationResponse
::newPass( $name ),
1242 * @dataProvider provideUserExists
1243 * @param bool $primary1Exists
1244 * @param bool $primary2Exists
1245 * @param bool $expect
1247 public function testUserExists( $primary1Exists, $primary2Exists, $expect ) {
1248 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1249 $mock1->expects( $this->any() )->method( 'getUniqueId' )
1250 ->will( $this->returnValue( 'primary1' ) );
1251 $mock1->expects( $this->any() )->method( 'testUserExists' )
1252 ->with( $this->equalTo( 'UTSysop' ) )
1253 ->will( $this->returnValue( $primary1Exists ) );
1254 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1255 $mock2->expects( $this->any() )->method( 'getUniqueId' )
1256 ->will( $this->returnValue( 'primary2' ) );
1257 $mock2->expects( $this->any() )->method( 'testUserExists' )
1258 ->with( $this->equalTo( 'UTSysop' ) )
1259 ->will( $this->returnValue( $primary2Exists ) );
1260 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1262 $this->initializeManager( true );
1263 $this->assertSame( $expect, $this->manager
->userExists( 'UTSysop' ) );
1266 public static function provideUserExists() {
1268 [ false, false, false ],
1269 [ true, false, true ],
1270 [ false, true, true ],
1271 [ true, true, true ],
1276 * @dataProvider provideAllowsAuthenticationDataChange
1277 * @param StatusValue $primaryReturn
1278 * @param StatusValue $secondaryReturn
1279 * @param Status $expect
1281 public function testAllowsAuthenticationDataChange( $primaryReturn, $secondaryReturn, $expect ) {
1282 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1284 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1285 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1286 $mock1->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1287 ->with( $this->equalTo( $req ) )
1288 ->will( $this->returnValue( $primaryReturn ) );
1289 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
1290 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1291 $mock2->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1292 ->with( $this->equalTo( $req ) )
1293 ->will( $this->returnValue( $secondaryReturn ) );
1295 $this->primaryauthMocks
= [ $mock1 ];
1296 $this->secondaryauthMocks
= [ $mock2 ];
1297 $this->initializeManager( true );
1298 $this->assertEquals( $expect, $this->manager
->allowsAuthenticationDataChange( $req ) );
1301 public static function provideAllowsAuthenticationDataChange() {
1302 $ignored = \Status
::newGood( 'ignored' );
1303 $ignored->warning( 'authmanager-change-not-supported' );
1305 $okFromPrimary = StatusValue
::newGood();
1306 $okFromPrimary->warning( 'warning-from-primary' );
1307 $okFromSecondary = StatusValue
::newGood();
1308 $okFromSecondary->warning( 'warning-from-secondary' );
1312 StatusValue
::newGood(),
1313 StatusValue
::newGood(),
1317 StatusValue
::newGood(),
1318 StatusValue
::newGood( 'ignore' ),
1322 StatusValue
::newGood( 'ignored' ),
1323 StatusValue
::newGood(),
1327 StatusValue
::newGood( 'ignored' ),
1328 StatusValue
::newGood( 'ignored' ),
1332 StatusValue
::newFatal( 'fail from primary' ),
1333 StatusValue
::newGood(),
1334 \Status
::newFatal( 'fail from primary' ),
1338 StatusValue
::newGood(),
1339 \Status
::wrap( $okFromPrimary ),
1342 StatusValue
::newGood(),
1343 StatusValue
::newFatal( 'fail from secondary' ),
1344 \Status
::newFatal( 'fail from secondary' ),
1347 StatusValue
::newGood(),
1349 \Status
::wrap( $okFromSecondary ),
1354 public function testChangeAuthenticationData() {
1355 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1356 $req->username
= 'UTSysop';
1358 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1359 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1360 $mock1->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1361 ->with( $this->equalTo( $req ) );
1362 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1363 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1364 $mock2->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1365 ->with( $this->equalTo( $req ) );
1367 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1368 $this->initializeManager( true );
1369 $this->logger
->setCollect( true );
1370 $this->manager
->changeAuthenticationData( $req );
1371 $this->assertSame( [
1372 [ LogLevel
::INFO
, 'Changing authentication data for {user} class {what}' ],
1373 ], $this->logger
->getBuffer() );
1376 public function testCanCreateAccounts() {
1378 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
1379 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
1380 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
1383 foreach ( $types as $type => $can ) {
1384 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1385 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
1386 $mock->expects( $this->any() )->method( 'accountCreationType' )
1387 ->will( $this->returnValue( $type ) );
1388 $this->primaryauthMocks
= [ $mock ];
1389 $this->initializeManager( true );
1390 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
1394 public function testCheckAccountCreatePermissions() {
1395 global $wgGroupPermissions;
1397 $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
1399 $this->initializeManager( true );
1401 $wgGroupPermissions['*']['createaccount'] = true;
1402 $this->assertEquals(
1404 $this->manager
->checkAccountCreatePermissions( new \User
)
1407 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1408 $this->assertEquals(
1409 \Status
::newFatal( 'readonlytext', 'Because' ),
1410 $this->manager
->checkAccountCreatePermissions( new \User
)
1412 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1414 $wgGroupPermissions['*']['createaccount'] = false;
1415 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1416 $this->assertFalse( $status->isOK() );
1417 $this->assertTrue( $status->hasMessage( 'badaccess-groups' ) );
1418 $wgGroupPermissions['*']['createaccount'] = true;
1420 $user = \User
::newFromName( 'UTBlockee' );
1421 if ( $user->getID() == 0 ) {
1422 $user->addToDatabase();
1423 \TestUser
::setPasswordForUser( $user, 'UTBlockeePassword' );
1424 $user->saveSettings();
1426 $oldBlock = \Block
::newFromTarget( 'UTBlockee' );
1428 // An old block will prevent our new one from saving.
1429 $oldBlock->delete();
1432 'address' => 'UTBlockee',
1433 'user' => $user->getID(),
1434 'reason' => __METHOD__
,
1435 'expiry' => time() +
100500,
1436 'createAccount' => true,
1438 $block = new \
Block( $blockOptions );
1440 $status = $this->manager
->checkAccountCreatePermissions( $user );
1441 $this->assertFalse( $status->isOK() );
1442 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
1445 'address' => '127.0.0.0/24',
1446 'reason' => __METHOD__
,
1447 'expiry' => time() +
100500,
1448 'createAccount' => true,
1450 $block = new \
Block( $blockOptions );
1452 $scopeVariable = new \
ScopedCallback( [ $block, 'delete' ] );
1453 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1454 $this->assertFalse( $status->isOK() );
1455 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
1456 \ScopedCallback
::consume( $scopeVariable );
1458 $this->setMwGlobals( [
1459 'wgEnableDnsBlacklist' => true,
1460 'wgDnsBlacklistUrls' => [
1461 'local.wmftest.net', // This will resolve for every subdomain, which works to test "listed?"
1463 'wgProxyWhitelist' => [],
1465 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1466 $this->assertFalse( $status->isOK() );
1467 $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
1468 $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
1469 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1470 $this->assertTrue( $status->isGood() );
1474 * @param string $uniq
1477 private static function usernameForCreation( $uniq = '' ) {
1480 $username = "UTAuthManagerTestAccountCreation" . $uniq . ++
$i;
1481 } while ( \User
::newFromName( $username )->getId() !== 0 );
1485 public function testCanCreateAccount() {
1486 $username = self
::usernameForCreation();
1487 $this->initializeManager();
1489 $this->assertEquals(
1490 \Status
::newFatal( 'authmanager-create-disabled' ),
1491 $this->manager
->canCreateAccount( $username )
1494 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1495 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1496 $mock->expects( $this->any() )->method( 'accountCreationType' )
1497 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1498 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1499 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1500 ->will( $this->returnValue( StatusValue
::newGood() ) );
1501 $this->primaryauthMocks
= [ $mock ];
1502 $this->initializeManager( true );
1504 $this->assertEquals(
1505 \Status
::newFatal( 'userexists' ),
1506 $this->manager
->canCreateAccount( $username )
1509 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1510 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1511 $mock->expects( $this->any() )->method( 'accountCreationType' )
1512 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1513 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1514 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1515 ->will( $this->returnValue( StatusValue
::newGood() ) );
1516 $this->primaryauthMocks
= [ $mock ];
1517 $this->initializeManager( true );
1519 $this->assertEquals(
1520 \Status
::newFatal( 'noname' ),
1521 $this->manager
->canCreateAccount( $username . '<>' )
1524 $this->assertEquals(
1525 \Status
::newFatal( 'userexists' ),
1526 $this->manager
->canCreateAccount( 'UTSysop' )
1529 $this->assertEquals(
1531 $this->manager
->canCreateAccount( $username )
1534 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1535 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1536 $mock->expects( $this->any() )->method( 'accountCreationType' )
1537 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1538 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1539 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1540 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1541 $this->primaryauthMocks
= [ $mock ];
1542 $this->initializeManager( true );
1544 $this->assertEquals(
1545 \Status
::newFatal( 'fail' ),
1546 $this->manager
->canCreateAccount( $username )
1550 public function testBeginAccountCreation() {
1551 $creator = \User
::newFromName( 'UTSysop' );
1552 $userReq = new UsernameAuthenticationRequest
;
1553 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1554 return $level === LogLevel
::DEBUG ?
null : $message;
1556 $this->initializeManager();
1558 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
1559 $this->hook( 'LocalUserCreated', $this->never() );
1561 $this->manager
->beginAccountCreation(
1562 $creator, [], 'http://localhost/'
1564 $this->fail( 'Expected exception not thrown' );
1565 } catch ( \LogicException
$ex ) {
1566 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1568 $this->unhook( 'LocalUserCreated' );
1570 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1573 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1574 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1575 $mock->expects( $this->any() )->method( 'accountCreationType' )
1576 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1577 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1578 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1579 ->will( $this->returnValue( StatusValue
::newGood() ) );
1580 $this->primaryauthMocks
= [ $mock ];
1581 $this->initializeManager( true );
1583 $this->hook( 'LocalUserCreated', $this->never() );
1584 $ret = $this->manager
->beginAccountCreation( $creator, [], 'http://localhost/' );
1585 $this->unhook( 'LocalUserCreated' );
1586 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1587 $this->assertSame( 'noname', $ret->message
->getKey() );
1589 $this->hook( 'LocalUserCreated', $this->never() );
1590 $userReq->username
= self
::usernameForCreation();
1591 $userReq2 = new UsernameAuthenticationRequest
;
1592 $userReq2->username
= $userReq->username
. 'X';
1593 $ret = $this->manager
->beginAccountCreation(
1594 $creator, [ $userReq, $userReq2 ], 'http://localhost/'
1596 $this->unhook( 'LocalUserCreated' );
1597 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1598 $this->assertSame( 'noname', $ret->message
->getKey() );
1600 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1601 $this->hook( 'LocalUserCreated', $this->never() );
1602 $userReq->username
= self
::usernameForCreation();
1603 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1604 $this->unhook( 'LocalUserCreated' );
1605 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1606 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
1607 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1608 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1610 $this->hook( 'LocalUserCreated', $this->never() );
1611 $userReq->username
= self
::usernameForCreation();
1612 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1613 $this->unhook( 'LocalUserCreated' );
1614 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1615 $this->assertSame( 'userexists', $ret->message
->getKey() );
1617 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1618 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1619 $mock->expects( $this->any() )->method( 'accountCreationType' )
1620 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1621 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1622 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1623 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1624 $this->primaryauthMocks
= [ $mock ];
1625 $this->initializeManager( true );
1627 $this->hook( 'LocalUserCreated', $this->never() );
1628 $userReq->username
= self
::usernameForCreation();
1629 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1630 $this->unhook( 'LocalUserCreated' );
1631 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1632 $this->assertSame( 'fail', $ret->message
->getKey() );
1634 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1635 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1636 $mock->expects( $this->any() )->method( 'accountCreationType' )
1637 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1638 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1639 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1640 ->will( $this->returnValue( StatusValue
::newGood() ) );
1641 $this->primaryauthMocks
= [ $mock ];
1642 $this->initializeManager( true );
1644 $this->hook( 'LocalUserCreated', $this->never() );
1645 $userReq->username
= self
::usernameForCreation() . '<>';
1646 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1647 $this->unhook( 'LocalUserCreated' );
1648 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1649 $this->assertSame( 'noname', $ret->message
->getKey() );
1651 $this->hook( 'LocalUserCreated', $this->never() );
1652 $userReq->username
= $creator->getName();
1653 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1654 $this->unhook( 'LocalUserCreated' );
1655 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1656 $this->assertSame( 'userexists', $ret->message
->getKey() );
1658 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1659 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1660 $mock->expects( $this->any() )->method( 'accountCreationType' )
1661 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1662 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1663 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1664 ->will( $this->returnValue( StatusValue
::newGood() ) );
1665 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
1666 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1667 $this->primaryauthMocks
= [ $mock ];
1668 $this->initializeManager( true );
1670 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1671 ->setMethods( [ 'populateUser' ] )
1673 $req->expects( $this->any() )->method( 'populateUser' )
1674 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1675 $userReq->username
= self
::usernameForCreation();
1676 $ret = $this->manager
->beginAccountCreation(
1677 $creator, [ $userReq, $req ], 'http://localhost/'
1679 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1680 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1682 $req = new UserDataAuthenticationRequest
;
1683 $userReq->username
= self
::usernameForCreation();
1685 $ret = $this->manager
->beginAccountCreation(
1686 $creator, [ $userReq, $req ], 'http://localhost/'
1688 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1689 $this->assertSame( 'fail', $ret->message
->getKey() );
1691 $this->manager
->beginAccountCreation(
1692 \User
::newFromName( $userReq->username
), [ $userReq, $req ], 'http://localhost/'
1694 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1695 $this->assertSame( 'fail', $ret->message
->getKey() );
1698 public function testContinueAccountCreation() {
1699 $creator = \User
::newFromName( 'UTSysop' );
1700 $username = self
::usernameForCreation();
1701 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1702 return $level === LogLevel
::DEBUG ?
null : $message;
1704 $this->initializeManager();
1708 'username' => $username,
1710 'creatorname' => $username,
1713 'primaryResponse' => null,
1715 'ranPreTests' => true,
1718 $this->hook( 'LocalUserCreated', $this->never() );
1720 $this->manager
->continueAccountCreation( [] );
1721 $this->fail( 'Expected exception not thrown' );
1722 } catch ( \LogicException
$ex ) {
1723 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1725 $this->unhook( 'LocalUserCreated' );
1727 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1728 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1729 $mock->expects( $this->any() )->method( 'accountCreationType' )
1730 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1731 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1732 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )->will(
1733 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
1735 $this->primaryauthMocks
= [ $mock ];
1736 $this->initializeManager( true );
1738 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', null );
1739 $this->hook( 'LocalUserCreated', $this->never() );
1740 $ret = $this->manager
->continueAccountCreation( [] );
1741 $this->unhook( 'LocalUserCreated' );
1742 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1743 $this->assertSame( 'authmanager-create-not-in-progress', $ret->message
->getKey() );
1745 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1746 [ 'username' => "$username<>" ] +
$session );
1747 $this->hook( 'LocalUserCreated', $this->never() );
1748 $ret = $this->manager
->continueAccountCreation( [] );
1749 $this->unhook( 'LocalUserCreated' );
1750 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1751 $this->assertSame( 'noname', $ret->message
->getKey() );
1753 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1756 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $session );
1757 $this->hook( 'LocalUserCreated', $this->never() );
1758 $cache = \ObjectCache
::getLocalClusterInstance();
1759 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1760 $ret = $this->manager
->continueAccountCreation( [] );
1762 $this->unhook( 'LocalUserCreated' );
1763 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1764 $this->assertSame( 'usernameinprogress', $ret->message
->getKey() );
1765 // This error shouldn't remove the existing session, because the
1766 // raced-with process "owns" it.
1768 $session, $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1771 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1772 [ 'username' => $creator->getName() ] +
$session );
1773 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1774 $this->hook( 'LocalUserCreated', $this->never() );
1775 $ret = $this->manager
->continueAccountCreation( [] );
1776 $this->unhook( 'LocalUserCreated' );
1777 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1778 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
1779 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1780 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1782 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1783 [ 'username' => $creator->getName() ] +
$session );
1784 $this->hook( 'LocalUserCreated', $this->never() );
1785 $ret = $this->manager
->continueAccountCreation( [] );
1786 $this->unhook( 'LocalUserCreated' );
1787 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1788 $this->assertSame( 'userexists', $ret->message
->getKey() );
1790 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1793 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1794 [ 'userid' => $creator->getId() ] +
$session );
1795 $this->hook( 'LocalUserCreated', $this->never() );
1797 $ret = $this->manager
->continueAccountCreation( [] );
1798 $this->fail( 'Expected exception not thrown' );
1799 } catch ( \UnexpectedValueException
$ex ) {
1800 $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
1802 $this->unhook( 'LocalUserCreated' );
1804 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1807 $id = $creator->getId();
1808 $name = $creator->getName();
1809 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1810 [ 'username' => $name, 'userid' => $id +
1 ] +
$session );
1811 $this->hook( 'LocalUserCreated', $this->never() );
1813 $ret = $this->manager
->continueAccountCreation( [] );
1814 $this->fail( 'Expected exception not thrown' );
1815 } catch ( \UnexpectedValueException
$ex ) {
1816 $this->assertEquals(
1817 "User \"{$name}\" exists, but ID $id != " . ( $id +
1 ) . '!', $ex->getMessage()
1820 $this->unhook( 'LocalUserCreated' );
1822 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1825 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1826 ->setMethods( [ 'populateUser' ] )
1828 $req->expects( $this->any() )->method( 'populateUser' )
1829 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1830 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1831 [ 'reqs' => [ $req ] ] +
$session );
1832 $ret = $this->manager
->continueAccountCreation( [] );
1833 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1834 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1836 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1841 * @dataProvider provideAccountCreation
1842 * @param StatusValue $preTest
1843 * @param StatusValue $primaryTest
1844 * @param StatusValue $secondaryTest
1845 * @param array $primaryResponses
1846 * @param array $secondaryResponses
1847 * @param array $managerResponses
1849 public function testAccountCreation(
1850 StatusValue
$preTest, $primaryTest, $secondaryTest,
1851 array $primaryResponses, array $secondaryResponses, array $managerResponses
1853 $creator = \User
::newFromName( 'UTSysop' );
1854 $username = self
::usernameForCreation();
1856 $this->initializeManager();
1858 // Set up lots of mocks...
1859 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1860 $req->preTest
= $preTest;
1861 $req->primaryTest
= $primaryTest;
1862 $req->secondaryTest
= $secondaryTest;
1863 $req->primary
= $primaryResponses;
1864 $req->secondary
= $secondaryResponses;
1866 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
1867 $class = ucfirst( $key ) . 'AuthenticationProvider';
1868 $mocks[$key] = $this->getMockForAbstractClass(
1869 "MediaWiki\\Auth\\$class", [], "Mock$class"
1871 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
1872 ->will( $this->returnValue( $key ) );
1873 $mocks[$key]->expects( $this->any() )->method( 'testUserForCreation' )
1874 ->will( $this->returnValue( StatusValue
::newGood() ) );
1875 $mocks[$key]->expects( $this->any() )->method( 'testForAccountCreation' )
1876 ->will( $this->returnCallback(
1877 function ( $user, $creatorIn, $reqs )
1878 use ( $username, $creator, $req, $key )
1880 $this->assertSame( $username, $user->getName() );
1881 $this->assertSame( $creator->getId(), $creatorIn->getId() );
1882 $this->assertSame( $creator->getName(), $creatorIn->getName() );
1884 foreach ( $reqs as $r ) {
1885 $this->assertSame( $username, $r->username
);
1886 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1888 $this->assertTrue( $foundReq, '$reqs contains $req' );
1894 for ( $i = 2; $i <= 3; $i++
) {
1895 $mocks[$key . $i] = $this->getMockForAbstractClass(
1896 "MediaWiki\\Auth\\$class", [], "Mock$class"
1898 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
1899 ->will( $this->returnValue( $key . $i ) );
1900 $mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' )
1901 ->will( $this->returnValue( StatusValue
::newGood() ) );
1902 $mocks[$key . $i]->expects( $this->atMost( 1 ) )->method( 'testForAccountCreation' )
1903 ->will( $this->returnValue( StatusValue
::newGood() ) );
1907 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
1908 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1909 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
1910 ->will( $this->returnValue( false ) );
1911 $ct = count( $req->primary
);
1912 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1913 $this->assertSame( $username, $user->getName() );
1914 $this->assertSame( 'UTSysop', $creator->getName() );
1916 foreach ( $reqs as $r ) {
1917 $this->assertSame( $username, $r->username
);
1918 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1920 $this->assertTrue( $foundReq, '$reqs contains $req' );
1921 return array_shift( $req->primary
);
1923 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
1924 ->method( 'beginPrimaryAccountCreation' )
1925 ->will( $callback );
1926 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1927 ->method( 'continuePrimaryAccountCreation' )
1928 ->will( $callback );
1930 $ct = count( $req->secondary
);
1931 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1932 $this->assertSame( $username, $user->getName() );
1933 $this->assertSame( 'UTSysop', $creator->getName() );
1935 foreach ( $reqs as $r ) {
1936 $this->assertSame( $username, $r->username
);
1937 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1939 $this->assertTrue( $foundReq, '$reqs contains $req' );
1940 return array_shift( $req->secondary
);
1942 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
1943 ->method( 'beginSecondaryAccountCreation' )
1944 ->will( $callback );
1945 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1946 ->method( 'continueSecondaryAccountCreation' )
1947 ->will( $callback );
1949 $abstain = AuthenticationResponse
::newAbstain();
1950 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
1951 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
1952 $mocks['primary2']->expects( $this->any() )->method( 'testUserExists' )
1953 ->will( $this->returnValue( false ) );
1954 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountCreation' )
1955 ->will( $this->returnValue( $abstain ) );
1956 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1957 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
1958 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_NONE
) );
1959 $mocks['primary3']->expects( $this->any() )->method( 'testUserExists' )
1960 ->will( $this->returnValue( false ) );
1961 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountCreation' );
1962 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1963 $mocks['secondary2']->expects( $this->atMost( 1 ) )
1964 ->method( 'beginSecondaryAccountCreation' )
1965 ->will( $this->returnValue( $abstain ) );
1966 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1967 $mocks['secondary3']->expects( $this->atMost( 1 ) )
1968 ->method( 'beginSecondaryAccountCreation' )
1969 ->will( $this->returnValue( $abstain ) );
1970 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1972 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
1973 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary'], $mocks['primary2'] ];
1974 $this->secondaryauthMocks
= [
1975 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2']
1978 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
1979 return $level === LogLevel
::DEBUG ?
null : $message;
1982 $this->initializeManager( true );
1984 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
1985 $this->equalTo( AuthenticationResponse
::PASS
),
1986 $this->equalTo( AuthenticationResponse
::FAIL
)
1988 $providers = array_merge(
1989 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
1991 foreach ( $providers as $p ) {
1992 $p->postCalled
= false;
1993 $p->expects( $this->atMost( 1 ) )->method( 'postAccountCreation' )
1994 ->willReturnCallback( function ( $user, $creator, $response )
1995 use ( $constraint, $p, $username )
1997 $this->assertInstanceOf( 'User', $user );
1998 $this->assertSame( $username, $user->getName() );
1999 $this->assertSame( 'UTSysop', $creator->getName() );
2000 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
2001 $this->assertThat( $response->status
, $constraint );
2002 $p->postCalled
= $response->status
;
2006 // We're testing with $wgNewUserLog = false, so assert that it worked
2007 $dbw = wfGetDB( DB_MASTER
);
2008 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2012 foreach ( $managerResponses as $i => $response ) {
2013 $success = $response instanceof AuthenticationResponse
&&
2014 $response->status
=== AuthenticationResponse
::PASS
;
2015 if ( $i === 'created' ) {
2017 $this->hook( 'LocalUserCreated', $this->once() )
2019 $this->callback( function ( $user ) use ( $username ) {
2020 return $user->getName() === $username;
2022 $this->equalTo( false )
2024 $expectLog[] = [ LogLevel
::INFO
, "Creating user {user} during account creation" ];
2026 $this->hook( 'LocalUserCreated', $this->never() );
2032 $userReq = new UsernameAuthenticationRequest
;
2033 $userReq->username
= $username;
2034 $ret = $this->manager
->beginAccountCreation(
2035 $creator, [ $userReq, $req ], 'http://localhost/'
2038 $ret = $this->manager
->continueAccountCreation( [ $req ] );
2040 if ( $response instanceof \Exception
) {
2041 $this->fail( 'Expected exception not thrown', "Response $i" );
2043 } catch ( \Exception
$ex ) {
2044 if ( !$response instanceof \Exception
) {
2047 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
2049 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2050 "Response $i, exception, session state"
2052 $this->unhook( 'LocalUserCreated' );
2056 $this->unhook( 'LocalUserCreated' );
2058 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
2061 $this->assertNotNull( $ret->loginRequest
, "Response $i, login marker" );
2062 $this->assertContains(
2063 $ret->loginRequest
, $this->managerPriv
->createdAccountAuthenticationRequests
,
2064 "Response $i, login marker"
2069 "MediaWiki\Auth\AuthManager::continueAccountCreation: Account creation succeeded for {user}"
2072 // Set some fields in the expected $response that we couldn't
2073 // know in provideAccountCreation().
2074 $response->username
= $username;
2075 $response->loginRequest
= $ret->loginRequest
;
2077 $this->assertNull( $ret->loginRequest
, "Response $i, login marker" );
2078 $this->assertSame( [], $this->managerPriv
->createdAccountAuthenticationRequests
,
2079 "Response $i, login marker" );
2081 $ret->message
= $this->message( $ret->message
);
2082 $this->assertEquals( $response, $ret, "Response $i, response" );
2083 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
2085 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2086 "Response $i, session state"
2088 foreach ( $providers as $p ) {
2089 $this->assertSame( $response->status
, $p->postCalled
,
2090 "Response $i, post-auth callback called" );
2093 $this->assertNotNull(
2094 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2095 "Response $i, session state"
2097 foreach ( $ret->neededRequests
as $neededReq ) {
2098 $this->assertEquals( AuthManager
::ACTION_CREATE
, $neededReq->action
,
2099 "Response $i, neededRequest action" );
2101 $this->assertEquals(
2102 $ret->neededRequests
,
2103 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_CREATE_CONTINUE
),
2104 "Response $i, continuation check"
2106 foreach ( $providers as $p ) {
2107 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
2112 $this->assertNotEquals( 0, \User
::idFromName( $username ) );
2114 $this->assertEquals( 0, \User
::idFromName( $username ) );
2120 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
2124 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2128 public function provideAccountCreation() {
2129 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
2130 $good = StatusValue
::newGood();
2133 'Pre-creation test fail in pre' => [
2134 StatusValue
::newFatal( 'fail-from-pre' ), $good, $good,
2138 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
2141 'Pre-creation test fail in primary' => [
2142 $good, StatusValue
::newFatal( 'fail-from-primary' ), $good,
2146 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2149 'Pre-creation test fail in secondary' => [
2150 $good, $good, StatusValue
::newFatal( 'fail-from-secondary' ),
2154 AuthenticationResponse
::newFail( $this->message( 'fail-from-secondary' ) ),
2157 'Failure in primary' => [
2158 $good, $good, $good,
2160 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2165 'All primary abstain' => [
2166 $good, $good, $good,
2168 AuthenticationResponse
::newAbstain(),
2172 AuthenticationResponse
::newFail( $this->message( 'authmanager-create-no-primary' ) )
2175 'Primary UI, then redirect, then fail' => [
2176 $good, $good, $good,
2178 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2179 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
2180 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
2185 'Primary redirect, then abstain' => [
2186 $good, $good, $good,
2188 $tmp = AuthenticationResponse
::newRedirect(
2189 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
2191 AuthenticationResponse
::newAbstain(),
2196 new \
DomainException(
2197 'MockPrimaryAuthenticationProvider::continuePrimaryAccountCreation() returned ABSTAIN'
2201 'Primary UI, then pass; secondary abstain' => [
2202 $good, $good, $good,
2204 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2205 AuthenticationResponse
::newPass(),
2208 AuthenticationResponse
::newAbstain(),
2212 'created' => AuthenticationResponse
::newPass( '' ),
2215 'Primary pass; secondary UI then pass' => [
2216 $good, $good, $good,
2218 AuthenticationResponse
::newPass( '' ),
2221 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2222 AuthenticationResponse
::newPass( '' ),
2226 AuthenticationResponse
::newPass( '' ),
2229 'Primary pass; secondary fail' => [
2230 $good, $good, $good,
2232 AuthenticationResponse
::newPass(),
2235 AuthenticationResponse
::newFail( $this->message( '...' ) ),
2238 'created' => new \
DomainException(
2239 'MockSecondaryAuthenticationProvider::beginSecondaryAccountCreation() returned FAIL. ' .
2240 'Secondary providers are not allowed to fail account creation, ' .
2241 'that should have been done via testForAccountCreation().'
2249 * @dataProvider provideAccountCreationLogging
2250 * @param bool $isAnon
2251 * @param string|null $logSubtype
2253 public function testAccountCreationLogging( $isAnon, $logSubtype ) {
2254 $creator = $isAnon ?
new \User
: \User
::newFromName( 'UTSysop' );
2255 $username = self
::usernameForCreation();
2257 $this->initializeManager();
2259 // Set up lots of mocks...
2260 $mock = $this->getMockForAbstractClass(
2261 "MediaWiki\\Auth\\PrimaryAuthenticationProvider", []
2263 $mock->expects( $this->any() )->method( 'getUniqueId' )
2264 ->will( $this->returnValue( 'primary' ) );
2265 $mock->expects( $this->any() )->method( 'testUserForCreation' )
2266 ->will( $this->returnValue( StatusValue
::newGood() ) );
2267 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
2268 ->will( $this->returnValue( StatusValue
::newGood() ) );
2269 $mock->expects( $this->any() )->method( 'accountCreationType' )
2270 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2271 $mock->expects( $this->any() )->method( 'testUserExists' )
2272 ->will( $this->returnValue( false ) );
2273 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
2274 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
2275 $mock->expects( $this->any() )->method( 'finishAccountCreation' )
2276 ->will( $this->returnValue( $logSubtype ) );
2278 $this->primaryauthMocks
= [ $mock ];
2279 $this->initializeManager( true );
2280 $this->logger
->setCollect( true );
2282 $this->config
->set( 'NewUserLog', true );
2284 $dbw = wfGetDB( DB_MASTER
);
2285 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2287 $userReq = new UsernameAuthenticationRequest
;
2288 $userReq->username
= $username;
2289 $reasonReq = new CreationReasonAuthenticationRequest
;
2290 $reasonReq->reason
= $this->toString();
2291 $ret = $this->manager
->beginAccountCreation(
2292 $creator, [ $userReq, $reasonReq ], 'http://localhost/'
2295 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
2297 $user = \User
::newFromName( $username );
2298 $this->assertNotEquals( 0, $user->getId(), 'sanity check' );
2299 $this->assertNotEquals( $creator->getId(), $user->getId(), 'sanity check' );
2301 $data = \DatabaseLogEntry
::getSelectQueryData();
2302 $rows = iterator_to_array( $dbw->select(
2306 'log_id > ' . (int)$maxLogId,
2307 'log_type' => 'newusers'
2313 $this->assertCount( 1, $rows );
2314 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2316 $this->assertSame( $logSubtype ?
: ( $isAnon ?
'create' : 'create2' ), $entry->getSubtype() );
2318 $isAnon ?
$user->getId() : $creator->getId(),
2319 $entry->getPerformer()->getId()
2322 $isAnon ?
$user->getName() : $creator->getName(),
2323 $entry->getPerformer()->getName()
2325 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2326 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2327 $this->assertSame( $this->toString(), $entry->getComment() );
2330 public static function provideAccountCreationLogging() {
2335 [ false, 'byemail' ],
2339 public function testAutoAccountCreation() {
2340 global $wgGroupPermissions, $wgHooks;
2342 // PHPUnit seems to have a bug where it will call the ->with()
2343 // callbacks for our hooks again after the test is run (WTF?), which
2344 // breaks here because $username no longer matches $user by the end of
2346 $workaroundPHPUnitBug = false;
2348 $username = self
::usernameForCreation();
2349 $this->initializeManager();
2351 $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
2352 $wgGroupPermissions['*']['createaccount'] = true;
2353 $wgGroupPermissions['*']['autocreateaccount'] = false;
2355 \ObjectCache
::$instances[__METHOD__
] = new \
HashBagOStuff();
2356 $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__
] );
2358 // Set up lots of mocks...
2360 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2361 $class = ucfirst( $key ) . 'AuthenticationProvider';
2362 $mocks[$key] = $this->getMockForAbstractClass(
2363 "MediaWiki\\Auth\\$class", [], "Mock$class"
2365 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2366 ->will( $this->returnValue( $key ) );
2369 $good = StatusValue
::newGood();
2370 $callback = $this->callback( function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
2371 return $workaroundPHPUnitBug ||
$user->getName() === $username;
2374 $mocks['pre']->expects( $this->exactly( 12 ) )->method( 'testUserForCreation' )
2375 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2376 ->will( $this->onConsecutiveCalls(
2377 StatusValue
::newFatal( 'ok' ), StatusValue
::newFatal( 'ok' ), // For testing permissions
2378 StatusValue
::newFatal( 'fail-in-pre' ), $good, $good,
2379 $good, // backoff test
2380 $good, // addToDatabase fails test
2381 $good, // addToDatabase throws test
2382 $good, // addToDatabase exists test
2383 $good, $good, $good // success
2386 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
2387 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2388 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
2389 ->will( $this->returnValue( true ) );
2390 $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
2391 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2392 ->will( $this->onConsecutiveCalls(
2393 StatusValue
::newFatal( 'fail-in-primary' ), $good,
2394 $good, // backoff test
2395 $good, // addToDatabase fails test
2396 $good, // addToDatabase throws test
2397 $good, // addToDatabase exists test
2400 $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2401 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) );
2403 $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
2404 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2405 ->will( $this->onConsecutiveCalls(
2406 StatusValue
::newFatal( 'fail-in-secondary' ),
2407 $good, // backoff test
2408 $good, // addToDatabase fails test
2409 $good, // addToDatabase throws test
2410 $good, // addToDatabase exists test
2413 $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2414 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) );
2416 $this->preauthMocks
= [ $mocks['pre'] ];
2417 $this->primaryauthMocks
= [ $mocks['primary'] ];
2418 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2419 $this->initializeManager( true );
2420 $session = $this->request
->getSession();
2422 $logger = new \
TestLogger( true, function ( $m ) {
2423 $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
2426 $this->manager
->setLogger( $logger );
2429 $user = \User
::newFromName( 'UTSysop' );
2430 $this->manager
->autoCreateUser( $user, 'InvalidSource', true );
2431 $this->fail( 'Expected exception not thrown' );
2432 } catch ( \InvalidArgumentException
$ex ) {
2433 $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
2436 // First, check an existing user
2438 $user = \User
::newFromName( 'UTSysop' );
2439 $this->hook( 'LocalUserCreated', $this->never() );
2440 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2441 $this->unhook( 'LocalUserCreated' );
2442 $expect = \Status
::newGood();
2443 $expect->warning( 'userexists' );
2444 $this->assertEquals( $expect, $ret );
2445 $this->assertNotEquals( 0, $user->getId() );
2446 $this->assertSame( 'UTSysop', $user->getName() );
2447 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2448 $this->assertSame( [
2449 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2450 ], $logger->getBuffer() );
2451 $logger->clearBuffer();
2454 $user = \User
::newFromName( 'UTSysop' );
2455 $this->hook( 'LocalUserCreated', $this->never() );
2456 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2457 $this->unhook( 'LocalUserCreated' );
2458 $expect = \Status
::newGood();
2459 $expect->warning( 'userexists' );
2460 $this->assertEquals( $expect, $ret );
2461 $this->assertNotEquals( 0, $user->getId() );
2462 $this->assertSame( 'UTSysop', $user->getName() );
2463 $this->assertEquals( 0, $session->getUser()->getId() );
2464 $this->assertSame( [
2465 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2466 ], $logger->getBuffer() );
2467 $logger->clearBuffer();
2469 // Wiki is read-only
2471 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
2472 $user = \User
::newFromName( $username );
2473 $this->hook( 'LocalUserCreated', $this->never() );
2474 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2475 $this->unhook( 'LocalUserCreated' );
2476 $this->assertEquals( \Status
::newFatal( 'readonlytext', 'Because' ), $ret );
2477 $this->assertEquals( 0, $user->getId() );
2478 $this->assertNotEquals( $username, $user->getName() );
2479 $this->assertEquals( 0, $session->getUser()->getId() );
2480 $this->assertSame( [
2481 [ LogLevel
::DEBUG
, 'denied by wfReadOnly(): {reason}' ],
2482 ], $logger->getBuffer() );
2483 $logger->clearBuffer();
2484 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
2486 // Session blacklisted
2488 $session->set( 'AuthManager::AutoCreateBlacklist', 'test' );
2489 $user = \User
::newFromName( $username );
2490 $this->hook( 'LocalUserCreated', $this->never() );
2491 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2492 $this->unhook( 'LocalUserCreated' );
2493 $this->assertEquals( \Status
::newFatal( 'test' ), $ret );
2494 $this->assertEquals( 0, $user->getId() );
2495 $this->assertNotEquals( $username, $user->getName() );
2496 $this->assertEquals( 0, $session->getUser()->getId() );
2497 $this->assertSame( [
2498 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2499 ], $logger->getBuffer() );
2500 $logger->clearBuffer();
2503 $session->set( 'AuthManager::AutoCreateBlacklist', StatusValue
::newFatal( 'test2' ) );
2504 $user = \User
::newFromName( $username );
2505 $this->hook( 'LocalUserCreated', $this->never() );
2506 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2507 $this->unhook( 'LocalUserCreated' );
2508 $this->assertEquals( \Status
::newFatal( 'test2' ), $ret );
2509 $this->assertEquals( 0, $user->getId() );
2510 $this->assertNotEquals( $username, $user->getName() );
2511 $this->assertEquals( 0, $session->getUser()->getId() );
2512 $this->assertSame( [
2513 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2514 ], $logger->getBuffer() );
2515 $logger->clearBuffer();
2519 $user = \User
::newFromName( $username . '@' );
2520 $this->hook( 'LocalUserCreated', $this->never() );
2521 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2522 $this->unhook( 'LocalUserCreated' );
2523 $this->assertEquals( \Status
::newFatal( 'noname' ), $ret );
2524 $this->assertEquals( 0, $user->getId() );
2525 $this->assertNotEquals( $username . '@', $user->getId() );
2526 $this->assertEquals( 0, $session->getUser()->getId() );
2527 $this->assertSame( [
2528 [ LogLevel
::DEBUG
, 'name "{username}" is not creatable' ],
2529 ], $logger->getBuffer() );
2530 $logger->clearBuffer();
2531 $this->assertSame( 'noname', $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2533 // IP unable to create accounts
2534 $wgGroupPermissions['*']['createaccount'] = false;
2535 $wgGroupPermissions['*']['autocreateaccount'] = false;
2537 $user = \User
::newFromName( $username );
2538 $this->hook( 'LocalUserCreated', $this->never() );
2539 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2540 $this->unhook( 'LocalUserCreated' );
2541 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-noperm' ), $ret );
2542 $this->assertEquals( 0, $user->getId() );
2543 $this->assertNotEquals( $username, $user->getName() );
2544 $this->assertEquals( 0, $session->getUser()->getId() );
2545 $this->assertSame( [
2546 [ LogLevel
::DEBUG
, 'IP lacks the ability to create or autocreate accounts' ],
2547 ], $logger->getBuffer() );
2548 $logger->clearBuffer();
2550 'authmanager-autocreate-noperm', $session->get( 'AuthManager::AutoCreateBlacklist' )
2553 // Test that both permutations of permissions are allowed
2554 // (this hits the two "ok" entries in $mocks['pre'])
2555 $wgGroupPermissions['*']['createaccount'] = false;
2556 $wgGroupPermissions['*']['autocreateaccount'] = true;
2558 $user = \User
::newFromName( $username );
2559 $this->hook( 'LocalUserCreated', $this->never() );
2560 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2561 $this->unhook( 'LocalUserCreated' );
2562 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2564 $wgGroupPermissions['*']['createaccount'] = true;
2565 $wgGroupPermissions['*']['autocreateaccount'] = false;
2567 $user = \User
::newFromName( $username );
2568 $this->hook( 'LocalUserCreated', $this->never() );
2569 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2570 $this->unhook( 'LocalUserCreated' );
2571 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2572 $logger->clearBuffer();
2576 $user = \User
::newFromName( $username );
2577 $this->hook( 'LocalUserCreated', $this->never() );
2578 $cache = \ObjectCache
::getLocalClusterInstance();
2579 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2580 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2582 $this->unhook( 'LocalUserCreated' );
2583 $this->assertEquals( \Status
::newFatal( 'usernameinprogress' ), $ret );
2584 $this->assertEquals( 0, $user->getId() );
2585 $this->assertNotEquals( $username, $user->getName() );
2586 $this->assertEquals( 0, $session->getUser()->getId() );
2587 $this->assertSame( [
2588 [ LogLevel
::DEBUG
, 'Could not acquire account creation lock' ],
2589 ], $logger->getBuffer() );
2590 $logger->clearBuffer();
2592 // Test pre-authentication provider fail
2594 $user = \User
::newFromName( $username );
2595 $this->hook( 'LocalUserCreated', $this->never() );
2596 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2597 $this->unhook( 'LocalUserCreated' );
2598 $this->assertEquals( \Status
::newFatal( 'fail-in-pre' ), $ret );
2599 $this->assertEquals( 0, $user->getId() );
2600 $this->assertNotEquals( $username, $user->getName() );
2601 $this->assertEquals( 0, $session->getUser()->getId() );
2602 $this->assertSame( [
2603 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2604 ], $logger->getBuffer() );
2605 $logger->clearBuffer();
2606 $this->assertEquals(
2607 StatusValue
::newFatal( 'fail-in-pre' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2611 $user = \User
::newFromName( $username );
2612 $this->hook( 'LocalUserCreated', $this->never() );
2613 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2614 $this->unhook( 'LocalUserCreated' );
2615 $this->assertEquals( \Status
::newFatal( 'fail-in-primary' ), $ret );
2616 $this->assertEquals( 0, $user->getId() );
2617 $this->assertNotEquals( $username, $user->getName() );
2618 $this->assertEquals( 0, $session->getUser()->getId() );
2619 $this->assertSame( [
2620 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2621 ], $logger->getBuffer() );
2622 $logger->clearBuffer();
2623 $this->assertEquals(
2624 StatusValue
::newFatal( 'fail-in-primary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2628 $user = \User
::newFromName( $username );
2629 $this->hook( 'LocalUserCreated', $this->never() );
2630 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2631 $this->unhook( 'LocalUserCreated' );
2632 $this->assertEquals( \Status
::newFatal( 'fail-in-secondary' ), $ret );
2633 $this->assertEquals( 0, $user->getId() );
2634 $this->assertNotEquals( $username, $user->getName() );
2635 $this->assertEquals( 0, $session->getUser()->getId() );
2636 $this->assertSame( [
2637 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2638 ], $logger->getBuffer() );
2639 $logger->clearBuffer();
2640 $this->assertEquals(
2641 StatusValue
::newFatal( 'fail-in-secondary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2645 $cache = \ObjectCache
::getLocalClusterInstance();
2646 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2647 $cache->set( $backoffKey, true );
2649 $user = \User
::newFromName( $username );
2650 $this->hook( 'LocalUserCreated', $this->never() );
2651 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2652 $this->unhook( 'LocalUserCreated' );
2653 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-exception' ), $ret );
2654 $this->assertEquals( 0, $user->getId() );
2655 $this->assertNotEquals( $username, $user->getName() );
2656 $this->assertEquals( 0, $session->getUser()->getId() );
2657 $this->assertSame( [
2658 [ LogLevel
::DEBUG
, '{username} denied by prior creation attempt failures' ],
2659 ], $logger->getBuffer() );
2660 $logger->clearBuffer();
2661 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2662 $cache->delete( $backoffKey );
2664 // Test addToDatabase fails
2666 $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2667 $user->expects( $this->once() )->method( 'addToDatabase' )
2668 ->will( $this->returnValue( \Status
::newFatal( 'because' ) ) );
2669 $user->setName( $username );
2670 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2671 $this->assertEquals( \Status
::newFatal( 'because' ), $ret );
2672 $this->assertEquals( 0, $user->getId() );
2673 $this->assertNotEquals( $username, $user->getName() );
2674 $this->assertEquals( 0, $session->getUser()->getId() );
2675 $this->assertSame( [
2676 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2677 [ LogLevel
::ERROR
, '{username} failed with message {message}' ],
2678 ], $logger->getBuffer() );
2679 $logger->clearBuffer();
2680 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2682 // Test addToDatabase throws an exception
2683 $cache = \ObjectCache
::getLocalClusterInstance();
2684 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2685 $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
2687 $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2688 $user->expects( $this->once() )->method( 'addToDatabase' )
2689 ->will( $this->throwException( new \
Exception( 'Excepted' ) ) );
2690 $user->setName( $username );
2692 $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2693 $this->fail( 'Expected exception not thrown' );
2694 } catch ( \Exception
$ex ) {
2695 $this->assertSame( 'Excepted', $ex->getMessage() );
2697 $this->assertEquals( 0, $user->getId() );
2698 $this->assertEquals( 0, $session->getUser()->getId() );
2699 $this->assertSame( [
2700 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2701 [ LogLevel
::ERROR
, '{username} failed with exception {exception}' ],
2702 ], $logger->getBuffer() );
2703 $logger->clearBuffer();
2704 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2705 $this->assertNotEquals( false, $cache->get( $backoffKey ) );
2706 $cache->delete( $backoffKey );
2708 // Test addToDatabase fails because the user already exists.
2710 $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2711 $user->expects( $this->once() )->method( 'addToDatabase' )
2712 ->will( $this->returnCallback( function () use ( $username ) {
2713 $status = \User
::newFromName( $username )->addToDatabase();
2714 $this->assertTrue( $status->isOK(), 'sanity check' );
2715 return \Status
::newFatal( 'userexists' );
2717 $user->setName( $username );
2718 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2719 $expect = \Status
::newGood();
2720 $expect->warning( 'userexists' );
2721 $this->assertEquals( $expect, $ret );
2722 $this->assertNotEquals( 0, $user->getId() );
2723 $this->assertEquals( $username, $user->getName() );
2724 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2725 $this->assertSame( [
2726 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2727 [ LogLevel
::INFO
, '{username} already exists locally (race)' ],
2728 ], $logger->getBuffer() );
2729 $logger->clearBuffer();
2730 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2734 $username = self
::usernameForCreation();
2735 $user = \User
::newFromName( $username );
2736 $this->hook( 'AuthPluginAutoCreate', $this->once() )
2737 ->with( $callback );
2738 $this->hideDeprecated( 'AuthPluginAutoCreate hook (used in ' .
2739 get_class( $wgHooks['AuthPluginAutoCreate'][0] ) . '::onAuthPluginAutoCreate)' );
2740 $this->hook( 'LocalUserCreated', $this->once() )
2741 ->with( $callback, $this->equalTo( true ) );
2742 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2743 $this->unhook( 'LocalUserCreated' );
2744 $this->unhook( 'AuthPluginAutoCreate' );
2745 $this->assertEquals( \Status
::newGood(), $ret );
2746 $this->assertNotEquals( 0, $user->getId() );
2747 $this->assertEquals( $username, $user->getName() );
2748 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2749 $this->assertSame( [
2750 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2751 ], $logger->getBuffer() );
2752 $logger->clearBuffer();
2754 $dbw = wfGetDB( DB_MASTER
);
2755 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2757 $username = self
::usernameForCreation();
2758 $user = \User
::newFromName( $username );
2759 $this->hook( 'LocalUserCreated', $this->once() )
2760 ->with( $callback, $this->equalTo( true ) );
2761 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2762 $this->unhook( 'LocalUserCreated' );
2763 $this->assertEquals( \Status
::newGood(), $ret );
2764 $this->assertNotEquals( 0, $user->getId() );
2765 $this->assertEquals( $username, $user->getName() );
2766 $this->assertEquals( 0, $session->getUser()->getId() );
2767 $this->assertSame( [
2768 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2769 ], $logger->getBuffer() );
2770 $logger->clearBuffer();
2773 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2776 $this->config
->set( 'NewUserLog', true );
2778 $username = self
::usernameForCreation();
2779 $user = \User
::newFromName( $username );
2780 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2781 $this->assertEquals( \Status
::newGood(), $ret );
2782 $logger->clearBuffer();
2784 $data = \DatabaseLogEntry
::getSelectQueryData();
2785 $rows = iterator_to_array( $dbw->select(
2789 'log_id > ' . (int)$maxLogId,
2790 'log_type' => 'newusers'
2796 $this->assertCount( 1, $rows );
2797 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2799 $this->assertSame( 'autocreate', $entry->getSubtype() );
2800 $this->assertSame( $user->getId(), $entry->getPerformer()->getId() );
2801 $this->assertSame( $user->getName(), $entry->getPerformer()->getName() );
2802 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2803 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2805 $workaroundPHPUnitBug = true;
2809 * @dataProvider provideGetAuthenticationRequests
2810 * @param string $action
2811 * @param array $expect
2812 * @param array $state
2814 public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
2815 $makeReq = function ( $key ) use ( $action ) {
2816 $req = $this->getMock( AuthenticationRequest
::class );
2817 $req->expects( $this->any() )->method( 'getUniqueId' )
2818 ->will( $this->returnValue( $key ) );
2819 $req->action
= $action === AuthManager
::ACTION_UNLINK ? AuthManager
::ACTION_REMOVE
: $action;
2823 $cmpReqs = function ( $a, $b ) {
2824 $ret = strcmp( get_class( $a ), get_class( $b ) );
2826 $ret = strcmp( $a->key
, $b->key
);
2831 $good = StatusValue
::newGood();
2834 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2835 $class = ucfirst( $key ) . 'AuthenticationProvider';
2836 $mocks[$key] = $this->getMockForAbstractClass(
2837 "MediaWiki\\Auth\\$class", [], "Mock$class"
2839 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2840 ->will( $this->returnValue( $key ) );
2841 $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2842 ->will( $this->returnCallback( function ( $action ) use ( $key, $makeReq ) {
2843 return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
2845 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
2846 ->will( $this->returnValue( $good ) );
2851 PrimaryAuthenticationProvider
::TYPE_NONE
,
2852 PrimaryAuthenticationProvider
::TYPE_CREATE
,
2853 PrimaryAuthenticationProvider
::TYPE_LINK
2855 $class = 'PrimaryAuthenticationProvider';
2856 $mocks["primary-$type"] = $this->getMockForAbstractClass(
2857 "MediaWiki\\Auth\\$class", [], "Mock$class"
2859 $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
2860 ->will( $this->returnValue( "primary-$type" ) );
2861 $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
2862 ->will( $this->returnValue( $type ) );
2863 $mocks["primary-$type"]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2864 ->will( $this->returnCallback( function ( $action ) use ( $type, $makeReq ) {
2865 return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
2867 $mocks["primary-$type"]->expects( $this->any() )
2868 ->method( 'providerAllowsAuthenticationDataChange' )
2869 ->will( $this->returnValue( $good ) );
2870 $this->primaryauthMocks
[] = $mocks["primary-$type"];
2873 $mocks['primary2'] = $this->getMockForAbstractClass(
2874 PrimaryAuthenticationProvider
::class, [], "MockPrimaryAuthenticationProvider"
2876 $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
2877 ->will( $this->returnValue( 'primary2' ) );
2878 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
2879 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
2880 $mocks['primary2']->expects( $this->any() )->method( 'getAuthenticationRequests' )
2881 ->will( $this->returnValue( [] ) );
2882 $mocks['primary2']->expects( $this->any() )
2883 ->method( 'providerAllowsAuthenticationDataChange' )
2884 ->will( $this->returnCallback( function ( $req ) use ( $good ) {
2885 return $req->key
=== 'generic' ? StatusValue
::newFatal( 'no' ) : $good;
2887 $this->primaryauthMocks
[] = $mocks['primary2'];
2889 $this->preauthMocks
= [ $mocks['pre'] ];
2890 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2891 $this->initializeManager( true );
2894 if ( isset( $state['continueRequests'] ) ) {
2895 $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
2897 if ( $action === AuthManager
::ACTION_LOGIN_CONTINUE
) {
2898 $this->request
->getSession()->setSecret( 'AuthManager::authnState', $state );
2899 } elseif ( $action === AuthManager
::ACTION_CREATE_CONTINUE
) {
2900 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $state );
2901 } elseif ( $action === AuthManager
::ACTION_LINK_CONTINUE
) {
2902 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', $state );
2906 $expectReqs = array_map( $makeReq, $expect );
2907 if ( $action === AuthManager
::ACTION_LOGIN
) {
2908 $req = new RememberMeAuthenticationRequest
;
2909 $req->action
= $action;
2910 $req->required
= AuthenticationRequest
::REQUIRED
;
2911 $expectReqs[] = $req;
2912 } elseif ( $action === AuthManager
::ACTION_CREATE
) {
2913 $req = new UsernameAuthenticationRequest
;
2914 $req->action
= $action;
2915 $expectReqs[] = $req;
2916 $req = new UserDataAuthenticationRequest
;
2917 $req->action
= $action;
2918 $req->required
= AuthenticationRequest
::REQUIRED
;
2919 $expectReqs[] = $req;
2921 usort( $expectReqs, $cmpReqs );
2923 $actual = $this->manager
->getAuthenticationRequests( $action );
2924 foreach ( $actual as $req ) {
2925 // Don't test this here.
2926 $req->required
= AuthenticationRequest
::REQUIRED
;
2928 usort( $actual, $cmpReqs );
2930 $this->assertEquals( $expectReqs, $actual );
2932 // Test CreationReasonAuthenticationRequest gets returned
2933 if ( $action === AuthManager
::ACTION_CREATE
) {
2934 $req = new CreationReasonAuthenticationRequest
;
2935 $req->action
= $action;
2936 $req->required
= AuthenticationRequest
::REQUIRED
;
2937 $expectReqs[] = $req;
2938 usort( $expectReqs, $cmpReqs );
2940 $actual = $this->manager
->getAuthenticationRequests( $action, \User
::newFromName( 'UTSysop' ) );
2941 foreach ( $actual as $req ) {
2942 // Don't test this here.
2943 $req->required
= AuthenticationRequest
::REQUIRED
;
2945 usort( $actual, $cmpReqs );
2947 $this->assertEquals( $expectReqs, $actual );
2951 public static function provideGetAuthenticationRequests() {
2954 AuthManager
::ACTION_LOGIN
,
2955 [ 'pre-login', 'primary-none-login', 'primary-create-login',
2956 'primary-link-login', 'secondary-login', 'generic' ],
2959 AuthManager
::ACTION_CREATE
,
2960 [ 'pre-create', 'primary-none-create', 'primary-create-create',
2961 'primary-link-create', 'secondary-create', 'generic' ],
2964 AuthManager
::ACTION_LINK
,
2965 [ 'primary-link-link', 'generic' ],
2968 AuthManager
::ACTION_CHANGE
,
2969 [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
2970 'secondary-change' ],
2973 AuthManager
::ACTION_REMOVE
,
2974 [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
2975 'secondary-remove' ],
2978 AuthManager
::ACTION_UNLINK
,
2979 [ 'primary-link-remove' ],
2982 AuthManager
::ACTION_LOGIN_CONTINUE
,
2986 AuthManager
::ACTION_LOGIN_CONTINUE
,
2987 $reqs = [ 'continue-login', 'foo', 'bar' ],
2989 'continueRequests' => $reqs,
2993 AuthManager
::ACTION_CREATE_CONTINUE
,
2997 AuthManager
::ACTION_CREATE_CONTINUE
,
2998 $reqs = [ 'continue-create', 'foo', 'bar' ],
3000 'continueRequests' => $reqs,
3004 AuthManager
::ACTION_LINK_CONTINUE
,
3008 AuthManager
::ACTION_LINK_CONTINUE
,
3009 $reqs = [ 'continue-link', 'foo', 'bar' ],
3011 'continueRequests' => $reqs,
3017 public function testGetAuthenticationRequestsRequired() {
3018 $makeReq = function ( $key, $required ) {
3019 $req = $this->getMock( AuthenticationRequest
::class );
3020 $req->expects( $this->any() )->method( 'getUniqueId' )
3021 ->will( $this->returnValue( $key ) );
3022 $req->action
= AuthManager
::ACTION_LOGIN
;
3024 $req->required
= $required;
3027 $cmpReqs = function ( $a, $b ) {
3028 $ret = strcmp( get_class( $a ), get_class( $b ) );
3030 $ret = strcmp( $a->key
, $b->key
);
3035 $good = StatusValue
::newGood();
3037 $primary1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3038 $primary1->expects( $this->any() )->method( 'getUniqueId' )
3039 ->will( $this->returnValue( 'primary1' ) );
3040 $primary1->expects( $this->any() )->method( 'accountCreationType' )
3041 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3042 $primary1->expects( $this->any() )->method( 'getAuthenticationRequests' )
3043 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3045 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3046 $makeReq( "required", AuthenticationRequest
::REQUIRED
),
3047 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3048 $makeReq( "foo", AuthenticationRequest
::REQUIRED
),
3049 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3050 $makeReq( "baz", AuthenticationRequest
::OPTIONAL
),
3054 $primary2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3055 $primary2->expects( $this->any() )->method( 'getUniqueId' )
3056 ->will( $this->returnValue( 'primary2' ) );
3057 $primary2->expects( $this->any() )->method( 'accountCreationType' )
3058 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3059 $primary2->expects( $this->any() )->method( 'getAuthenticationRequests' )
3060 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3062 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3063 $makeReq( "required2", AuthenticationRequest
::REQUIRED
),
3064 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3068 $secondary = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3069 $secondary->expects( $this->any() )->method( 'getUniqueId' )
3070 ->will( $this->returnValue( 'secondary' ) );
3071 $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
3072 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3074 $makeReq( "foo", AuthenticationRequest
::OPTIONAL
),
3075 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3076 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3080 $rememberReq = new RememberMeAuthenticationRequest
;
3081 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
3083 $this->primaryauthMocks
= [ $primary1, $primary2 ];
3084 $this->secondaryauthMocks
= [ $secondary ];
3085 $this->initializeManager( true );
3087 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3090 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3091 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3092 $makeReq( "required2", AuthenticationRequest
::PRIMARY_REQUIRED
),
3093 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3094 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3095 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3096 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3097 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3099 usort( $actual, $cmpReqs );
3100 usort( $expected, $cmpReqs );
3101 $this->assertEquals( $expected, $actual );
3103 $this->primaryauthMocks
= [ $primary1 ];
3104 $this->secondaryauthMocks
= [ $secondary ];
3105 $this->initializeManager( true );
3107 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3110 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3111 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3112 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3113 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3114 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3115 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3117 usort( $actual, $cmpReqs );
3118 usort( $expected, $cmpReqs );
3119 $this->assertEquals( $expected, $actual );
3122 public function testAllowsPropertyChange() {
3124 foreach ( [ 'primary', 'secondary' ] as $key ) {
3125 $class = ucfirst( $key ) . 'AuthenticationProvider';
3126 $mocks[$key] = $this->getMockForAbstractClass(
3127 "MediaWiki\\Auth\\$class", [], "Mock$class"
3129 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3130 ->will( $this->returnValue( $key ) );
3131 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
3132 ->will( $this->returnCallback( function ( $prop ) use ( $key ) {
3133 return $prop !== $key;
3137 $this->primaryauthMocks
= [ $mocks['primary'] ];
3138 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
3139 $this->initializeManager( true );
3141 $this->assertTrue( $this->manager
->allowsPropertyChange( 'foo' ) );
3142 $this->assertFalse( $this->manager
->allowsPropertyChange( 'primary' ) );
3143 $this->assertFalse( $this->manager
->allowsPropertyChange( 'secondary' ) );
3146 public function testAutoCreateOnLogin() {
3147 $username = self
::usernameForCreation();
3149 $req = $this->getMock( AuthenticationRequest
::class );
3151 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3152 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3153 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3154 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3155 $mock->expects( $this->any() )->method( 'accountCreationType' )
3156 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3157 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3158 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3159 ->will( $this->returnValue( StatusValue
::newGood() ) );
3161 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3162 $mock2->expects( $this->any() )->method( 'getUniqueId' )
3163 ->will( $this->returnValue( 'secondary' ) );
3164 $mock2->expects( $this->any() )->method( 'beginSecondaryAuthentication' )->will(
3166 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) )
3169 $mock2->expects( $this->any() )->method( 'continueSecondaryAuthentication' )
3170 ->will( $this->returnValue( AuthenticationResponse
::newAbstain() ) );
3171 $mock2->expects( $this->any() )->method( 'testUserForCreation' )
3172 ->will( $this->returnValue( StatusValue
::newGood() ) );
3174 $this->primaryauthMocks
= [ $mock ];
3175 $this->secondaryauthMocks
= [ $mock2 ];
3176 $this->initializeManager( true );
3177 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3178 $session = $this->request
->getSession();
3181 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3184 $callback = $this->callback( function ( $user ) use ( $username ) {
3185 return $user->getName() === $username;
3188 $this->hook( 'UserLoggedIn', $this->never() );
3189 $this->hook( 'LocalUserCreated', $this->once() )->with( $callback, $this->equalTo( true ) );
3190 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3191 $this->unhook( 'LocalUserCreated' );
3192 $this->unhook( 'UserLoggedIn' );
3193 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
3195 $id = (int)\User
::newFromName( $username )->getId();
3196 $this->assertNotSame( 0, \User
::newFromName( $username )->getId() );
3197 $this->assertSame( 0, $session->getUser()->getId() );
3199 $this->hook( 'UserLoggedIn', $this->once() )->with( $callback );
3200 $this->hook( 'LocalUserCreated', $this->never() );
3201 $ret = $this->manager
->continueAuthentication( [] );
3202 $this->unhook( 'LocalUserCreated' );
3203 $this->unhook( 'UserLoggedIn' );
3204 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
3205 $this->assertSame( $username, $ret->username
);
3206 $this->assertSame( $id, $session->getUser()->getId() );
3209 public function testAutoCreateFailOnLogin() {
3210 $username = self
::usernameForCreation();
3212 $mock = $this->getMockForAbstractClass(
3213 PrimaryAuthenticationProvider
::class, [], "MockPrimaryAuthenticationProvider" );
3214 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3215 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3216 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3217 $mock->expects( $this->any() )->method( 'accountCreationType' )
3218 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3219 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3220 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3221 ->will( $this->returnValue( StatusValue
::newFatal( 'fail-from-primary' ) ) );
3223 $this->primaryauthMocks
= [ $mock ];
3224 $this->initializeManager( true );
3225 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3226 $session = $this->request
->getSession();
3229 $this->assertSame( 0, $session->getUser()->getId(),
3231 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3234 $this->hook( 'UserLoggedIn', $this->never() );
3235 $this->hook( 'LocalUserCreated', $this->never() );
3236 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3237 $this->unhook( 'LocalUserCreated' );
3238 $this->unhook( 'UserLoggedIn' );
3239 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3240 $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message
->getKey() );
3242 $this->assertSame( 0, \User
::newFromName( $username )->getId() );
3243 $this->assertSame( 0, $session->getUser()->getId() );
3246 public function testAuthenticationSessionData() {
3247 $this->initializeManager( true );
3249 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3250 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3251 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3252 $this->assertSame( 'foo!', $this->manager
->getAuthenticationSessionData( 'foo' ) );
3253 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3254 $this->manager
->removeAuthenticationSessionData( 'foo' );
3255 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3256 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3257 $this->manager
->removeAuthenticationSessionData( 'bar' );
3258 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3260 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3261 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3262 $this->manager
->removeAuthenticationSessionData( null );
3263 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3264 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3268 public function testCanLinkAccounts() {
3270 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
3271 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
3272 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
3275 foreach ( $types as $type => $can ) {
3276 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3277 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
3278 $mock->expects( $this->any() )->method( 'accountCreationType' )
3279 ->will( $this->returnValue( $type ) );
3280 $this->primaryauthMocks
= [ $mock ];
3281 $this->initializeManager( true );
3282 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
3286 public function testBeginAccountLink() {
3287 $user = \User
::newFromName( 'UTSysop' );
3288 $this->initializeManager();
3290 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', 'test' );
3292 $this->manager
->beginAccountLink( $user, [], 'http://localhost/' );
3293 $this->fail( 'Expected exception not thrown' );
3294 } catch ( \LogicException
$ex ) {
3295 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3297 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3299 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3300 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3301 $mock->expects( $this->any() )->method( 'accountCreationType' )
3302 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3303 $this->primaryauthMocks
= [ $mock ];
3304 $this->initializeManager( true );
3306 $ret = $this->manager
->beginAccountLink( new \User
, [], 'http://localhost/' );
3307 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3308 $this->assertSame( 'noname', $ret->message
->getKey() );
3310 $ret = $this->manager
->beginAccountLink(
3311 \User
::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
3313 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3314 $this->assertSame( 'authmanager-userdoesnotexist', $ret->message
->getKey() );
3317 public function testContinueAccountLink() {
3318 $user = \User
::newFromName( 'UTSysop' );
3319 $this->initializeManager();
3322 'userid' => $user->getId(),
3323 'username' => $user->getName(),
3328 $this->manager
->continueAccountLink( [] );
3329 $this->fail( 'Expected exception not thrown' );
3330 } catch ( \LogicException
$ex ) {
3331 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3334 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3335 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3336 $mock->expects( $this->any() )->method( 'accountCreationType' )
3337 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3338 $mock->expects( $this->any() )->method( 'beginPrimaryAccountLink' )->will(
3339 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
3341 $this->primaryauthMocks
= [ $mock ];
3342 $this->initializeManager( true );
3344 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', null );
3345 $ret = $this->manager
->continueAccountLink( [] );
3346 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3347 $this->assertSame( 'authmanager-link-not-in-progress', $ret->message
->getKey() );
3349 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3350 [ 'username' => $user->getName() . '<>' ] +
$session );
3351 $ret = $this->manager
->continueAccountLink( [] );
3352 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3353 $this->assertSame( 'noname', $ret->message
->getKey() );
3354 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3356 $id = $user->getId();
3357 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3358 [ 'userid' => $id +
1 ] +
$session );
3360 $ret = $this->manager
->continueAccountLink( [] );
3361 $this->fail( 'Expected exception not thrown' );
3362 } catch ( \UnexpectedValueException
$ex ) {
3363 $this->assertEquals(
3364 "User \"{$user->getName()}\" is valid, but ID $id != " . ( $id +
1 ) . '!',
3368 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3372 * @dataProvider provideAccountLink
3373 * @param StatusValue $preTest
3374 * @param array $primaryResponses
3375 * @param array $managerResponses
3377 public function testAccountLink(
3378 StatusValue
$preTest, array $primaryResponses, array $managerResponses
3380 $user = \User
::newFromName( 'UTSysop' );
3382 $this->initializeManager();
3384 // Set up lots of mocks...
3385 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3386 $req->primary
= $primaryResponses;
3389 foreach ( [ 'pre', 'primary' ] as $key ) {
3390 $class = ucfirst( $key ) . 'AuthenticationProvider';
3391 $mocks[$key] = $this->getMockForAbstractClass(
3392 "MediaWiki\\Auth\\$class", [], "Mock$class"
3394 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3395 ->will( $this->returnValue( $key ) );
3397 for ( $i = 2; $i <= 3; $i++
) {
3398 $mocks[$key . $i] = $this->getMockForAbstractClass(
3399 "MediaWiki\\Auth\\$class", [], "Mock$class"
3401 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
3402 ->will( $this->returnValue( $key . $i ) );
3406 $mocks['pre']->expects( $this->any() )->method( 'testForAccountLink' )
3407 ->will( $this->returnCallback(
3409 use ( $user, $preTest )
3411 $this->assertSame( $user->getId(), $u->getId() );
3412 $this->assertSame( $user->getName(), $u->getName() );
3417 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
3418 ->will( $this->returnValue( StatusValue
::newGood() ) );
3420 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
3421 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3422 $ct = count( $req->primary
);
3423 $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req ) {
3424 $this->assertSame( $user->getId(), $u->getId() );
3425 $this->assertSame( $user->getName(), $u->getName() );
3427 foreach ( $reqs as $r ) {
3428 $this->assertSame( $user->getName(), $r->username
);
3429 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
3431 $this->assertTrue( $foundReq, '$reqs contains $req' );
3432 return array_shift( $req->primary
);
3434 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
3435 ->method( 'beginPrimaryAccountLink' )
3436 ->will( $callback );
3437 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
3438 ->method( 'continuePrimaryAccountLink' )
3439 ->will( $callback );
3441 $abstain = AuthenticationResponse
::newAbstain();
3442 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
3443 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3444 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
3445 ->will( $this->returnValue( $abstain ) );
3446 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3447 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
3448 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3449 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
3450 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3452 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
3453 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
3454 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
3455 return $level === LogLevel
::DEBUG ?
null : $message;
3457 $this->initializeManager( true );
3459 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
3460 $this->equalTo( AuthenticationResponse
::PASS
),
3461 $this->equalTo( AuthenticationResponse
::FAIL
)
3463 $providers = array_merge( $this->preauthMocks
, $this->primaryauthMocks
);
3464 foreach ( $providers as $p ) {
3465 $p->postCalled
= false;
3466 $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
3467 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
3468 $this->assertInstanceOf( 'User', $user );
3469 $this->assertSame( 'UTSysop', $user->getName() );
3470 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
3471 $this->assertThat( $response->status
, $constraint );
3472 $p->postCalled
= $response->status
;
3479 foreach ( $managerResponses as $i => $response ) {
3480 if ( $response instanceof AuthenticationResponse
&&
3481 $response->status
=== AuthenticationResponse
::PASS
3483 $expectLog[] = [ LogLevel
::INFO
, 'Account linked to {user} by primary' ];
3489 $ret = $this->manager
->beginAccountLink( $user, [ $req ], 'http://localhost/' );
3491 $ret = $this->manager
->continueAccountLink( [ $req ] );
3493 if ( $response instanceof \Exception
) {
3494 $this->fail( 'Expected exception not thrown', "Response $i" );
3496 } catch ( \Exception
$ex ) {
3497 if ( !$response instanceof \Exception
) {
3500 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
3501 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3502 "Response $i, exception, session state" );
3506 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
3508 $ret->message
= $this->message( $ret->message
);
3509 $this->assertEquals( $response, $ret, "Response $i, response" );
3510 if ( $response->status
=== AuthenticationResponse
::PASS ||
3511 $response->status
=== AuthenticationResponse
::FAIL
3513 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3514 "Response $i, session state" );
3515 foreach ( $providers as $p ) {
3516 $this->assertSame( $response->status
, $p->postCalled
,
3517 "Response $i, post-auth callback called" );
3520 $this->assertNotNull(
3521 $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3522 "Response $i, session state"
3524 foreach ( $ret->neededRequests
as $neededReq ) {
3525 $this->assertEquals( AuthManager
::ACTION_LINK
, $neededReq->action
,
3526 "Response $i, neededRequest action" );
3528 $this->assertEquals(
3529 $ret->neededRequests
,
3530 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LINK_CONTINUE
),
3531 "Response $i, continuation check"
3533 foreach ( $providers as $p ) {
3534 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
3541 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
3544 public function provideAccountLink() {
3545 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3546 $good = StatusValue
::newGood();
3549 'Pre-link test fail in pre' => [
3550 StatusValue
::newFatal( 'fail-from-pre' ),
3553 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
3556 'Failure in primary' => [
3559 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
3563 'All primary abstain' => [
3566 AuthenticationResponse
::newAbstain(),
3569 AuthenticationResponse
::newFail( $this->message( 'authmanager-link-no-primary' ) )
3572 'Primary UI, then redirect, then fail' => [
3575 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3576 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
3577 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
3581 'Primary redirect, then abstain' => [
3584 $tmp = AuthenticationResponse
::newRedirect(
3585 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
3587 AuthenticationResponse
::newAbstain(),
3591 new \
DomainException(
3592 'MockPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
3596 'Primary UI, then pass' => [
3599 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3600 AuthenticationResponse
::newPass(),
3604 AuthenticationResponse
::newPass( '' ),
3610 AuthenticationResponse
::newPass( '' ),
3613 AuthenticationResponse
::newPass( '' ),