3 namespace MediaWiki\Auth
;
5 use MediaWiki\Session\SessionInfo
;
6 use MediaWiki\Session\UserInfo
;
9 use Wikimedia\ScopedCallback
;
14 * @covers MediaWiki\Auth\AuthManager
16 class AuthManagerTest
extends \MediaWikiTestCase
{
17 /** @var WebRequest */
21 /** @var \\Psr\\Log\\LoggerInterface */
24 protected $preauthMocks = [];
25 protected $primaryauthMocks = [];
26 protected $secondaryauthMocks = [];
28 /** @var AuthManager */
30 /** @var TestingAccessWrapper */
31 protected $managerPriv;
33 protected function setUp() {
36 $this->setMwGlobals( [ 'wgAuth' => null ] );
37 $this->stashMwGlobals( [ 'wgHooks' ] );
41 * Sets a mock on a hook
43 * @param object $expect From $this->once(), $this->never(), etc.
44 * @return object $mock->expects( $expect )->method( ... ).
46 protected function hook( $hook, $expect ) {
48 $mock = $this->getMock( __CLASS__
, [ "on$hook" ] );
49 $wgHooks[$hook] = [ $mock ];
50 return $mock->expects( $expect )->method( "on$hook" );
57 protected function unhook( $hook ) {
63 * Ensure a value is a clean Message object
64 * @param string|Message $key
65 * @param array $params
68 protected function message( $key, $params = [] ) {
69 if ( $key === null ) {
72 if ( $key instanceof \MessageSpecifier
) {
73 $params = $key->getParams();
74 $key = $key->getKey();
76 return new \
Message( $key, $params, \Language
::factory( 'en' ) );
80 * Initialize the AuthManagerConfig variable in $this->config
82 * Uses data from the various 'mocks' fields.
84 protected function initializeConfig() {
94 foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
95 $key = $type . 'Mocks';
96 foreach ( $this->$key as $mock ) {
97 $config[$type][$mock->getUniqueId()] = [ 'factory' => function () use ( $mock ) {
103 $this->config
->set( 'AuthManagerConfig', $config );
104 $this->config
->set( 'LanguageCode', 'en' );
105 $this->config
->set( 'NewUserLog', false );
109 * Initialize $this->manager
110 * @param bool $regen Force a call to $this->initializeConfig()
112 protected function initializeManager( $regen = false ) {
113 if ( $regen ||
!$this->config
) {
114 $this->config
= new \
HashConfig();
116 if ( $regen ||
!$this->request
) {
117 $this->request
= new \
FauxRequest();
119 if ( !$this->logger
) {
120 $this->logger
= new \
TestLogger();
123 if ( $regen ||
!$this->config
->has( 'AuthManagerConfig' ) ) {
124 $this->initializeConfig();
126 $this->manager
= new AuthManager( $this->request
, $this->config
);
127 $this->manager
->setLogger( $this->logger
);
128 $this->managerPriv
= \TestingAccessWrapper
::newFromObject( $this->manager
);
132 * Setup SessionManager with a mock session provider
133 * @param bool|null $canChangeUser If non-null, canChangeUser will be mocked to return this
134 * @param array $methods Additional methods to mock
135 * @return array (MediaWiki\Session\SessionProvider, ScopedCallback)
137 protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
138 if ( !$this->config
) {
139 $this->config
= new \
HashConfig();
140 $this->initializeConfig();
142 $this->config
->set( 'ObjectCacheSessionExpiry', 100 );
144 $methods[] = '__toString';
145 $methods[] = 'describe';
146 if ( $canChangeUser !== null ) {
147 $methods[] = 'canChangeUser';
149 $provider = $this->getMockBuilder( 'DummySessionProvider' )
150 ->setMethods( $methods )
152 $provider->expects( $this->any() )->method( '__toString' )
153 ->will( $this->returnValue( 'MockSessionProvider' ) );
154 $provider->expects( $this->any() )->method( 'describe' )
155 ->will( $this->returnValue( 'MockSessionProvider sessions' ) );
156 if ( $canChangeUser !== null ) {
157 $provider->expects( $this->any() )->method( 'canChangeUser' )
158 ->will( $this->returnValue( $canChangeUser ) );
160 $this->config
->set( 'SessionProviders', [
161 [ 'factory' => function () use ( $provider ) {
166 $manager = new \MediaWiki\Session\
SessionManager( [
167 'config' => $this->config
,
168 'logger' => new \Psr\Log\
NullLogger(),
169 'store' => new \
HashBagOStuff(),
171 \TestingAccessWrapper
::newFromObject( $manager )->getProvider( (string)$provider );
173 $reset = \MediaWiki\Session\TestUtils
::setSessionManagerSingleton( $manager );
175 if ( $this->request
) {
176 $manager->getSessionForRequest( $this->request
);
179 return [ $provider, $reset ];
182 public function testSingleton() {
183 // Temporarily clear out the global singleton, if any, to test creating
185 $rProp = new \
ReflectionProperty( AuthManager
::class, 'instance' );
186 $rProp->setAccessible( true );
187 $old = $rProp->getValue();
188 $cb = new ScopedCallback( [ $rProp, 'setValue' ], [ $old ] );
189 $rProp->setValue( null );
191 $singleton = AuthManager
::singleton();
192 $this->assertInstanceOf( AuthManager
::class, AuthManager
::singleton() );
193 $this->assertSame( $singleton, AuthManager
::singleton() );
194 $this->assertSame( \RequestContext
::getMain()->getRequest(), $singleton->getRequest() );
196 \RequestContext
::getMain()->getConfig(),
197 \TestingAccessWrapper
::newFromObject( $singleton )->config
201 public function testCanAuthenticateNow() {
202 $this->initializeManager();
204 list( $provider, $reset ) = $this->getMockSessionProvider( false );
205 $this->assertFalse( $this->manager
->canAuthenticateNow() );
206 ScopedCallback
::consume( $reset );
208 list( $provider, $reset ) = $this->getMockSessionProvider( true );
209 $this->assertTrue( $this->manager
->canAuthenticateNow() );
210 ScopedCallback
::consume( $reset );
213 public function testNormalizeUsername() {
215 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
216 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
217 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
218 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
220 foreach ( $mocks as $key => $mock ) {
221 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
223 $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
224 ->with( $this->identicalTo( 'XYZ' ) )
225 ->willReturn( 'Foo' );
226 $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
227 ->with( $this->identicalTo( 'XYZ' ) )
228 ->willReturn( 'Foo' );
229 $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
230 ->with( $this->identicalTo( 'XYZ' ) )
231 ->willReturn( null );
232 $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
233 ->with( $this->identicalTo( 'XYZ' ) )
234 ->willReturn( 'Bar!' );
236 $this->primaryauthMocks
= $mocks;
238 $this->initializeManager();
240 $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager
->normalizeUsername( 'XYZ' ) );
244 * @dataProvider provideSecuritySensitiveOperationStatus
245 * @param bool $mutableSession
247 public function testSecuritySensitiveOperationStatus( $mutableSession ) {
248 $this->logger
= new \Psr\Log\
NullLogger();
249 $user = \User
::newFromName( 'UTSysop' );
251 $reauth = $mutableSession ? AuthManager
::SEC_REAUTH
: AuthManager
::SEC_FAIL
;
253 list( $provider, $reset ) = $this->getMockSessionProvider(
254 $mutableSession, [ 'provideSessionInfo' ]
256 $provider->expects( $this->any() )->method( 'provideSessionInfo' )
257 ->will( $this->returnCallback( function () use ( $provider, &$provideUser ) {
258 return new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
259 'provider' => $provider,
260 'id' => \DummySessionProvider
::ID
,
262 'userInfo' => UserInfo
::newFromUser( $provideUser, true )
265 $this->initializeManager();
267 $this->config
->set( 'ReauthenticateTime', [] );
268 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [] );
269 $provideUser = new \User
;
270 $session = $provider->getManager()->getSessionForRequest( $this->request
);
271 $this->assertSame( 0, $session->getUser()->getId(), 'sanity check' );
273 // Anonymous user => reauth
274 $session->set( 'AuthManager:lastAuthId', 0 );
275 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
276 $this->assertSame( $reauth, $this->manager
->securitySensitiveOperationStatus( 'foo' ) );
278 $provideUser = $user;
279 $session = $provider->getManager()->getSessionForRequest( $this->request
);
280 $this->assertSame( $user->getId(), $session->getUser()->getId(), 'sanity check' );
282 // Error for no default (only gets thrown for non-anonymous user)
283 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
284 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
286 $this->manager
->securitySensitiveOperationStatus( 'foo' );
287 $this->fail( 'Expected exception not thrown' );
288 } catch ( \UnexpectedValueException
$ex ) {
291 ?
'$wgReauthenticateTime lacks a default'
292 : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
297 if ( $mutableSession ) {
298 $this->config
->set( 'ReauthenticateTime', [
304 // Mismatched user ID
305 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
306 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
308 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
311 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
314 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
318 $session->set( 'AuthManager:lastAuthId', $user->getId() );
319 $session->set( 'AuthManager:lastAuthTimestamp', null );
321 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
324 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
327 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
330 // Recent enough to pass
331 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
333 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
336 // Not recent enough to pass
337 $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
339 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
341 // But recent enough for the 'test' operation
343 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test' )
346 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [
352 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
356 AuthManager
::SEC_FAIL
, $this->manager
->securitySensitiveOperationStatus( 'test' )
360 // Test hook, all three possible values
362 AuthManager
::SEC_OK
=> AuthManager
::SEC_OK
,
363 AuthManager
::SEC_REAUTH
=> $reauth,
364 AuthManager
::SEC_FAIL
=> AuthManager
::SEC_FAIL
,
365 ] as $hook => $expect ) {
366 $this->hook( 'SecuritySensitiveOperationStatus', $this->exactly( 2 ) )
370 $this->callback( function ( $s ) use ( $session ) {
371 return $s->getId() === $session->getId();
373 $mutableSession ?
$this->equalTo( 500, 1 ) : $this->equalTo( -1 )
375 ->will( $this->returnCallback( function ( &$v ) use ( $hook ) {
379 $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
381 $expect, $this->manager
->securitySensitiveOperationStatus( 'test' ), "hook $hook"
384 $expect, $this->manager
->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
386 $this->unhook( 'SecuritySensitiveOperationStatus' );
389 ScopedCallback
::consume( $reset );
392 public function onSecuritySensitiveOperationStatus( &$status, $operation, $session, $time ) {
395 public static function provideSecuritySensitiveOperationStatus() {
403 * @dataProvider provideUserCanAuthenticate
404 * @param bool $primary1Can
405 * @param bool $primary2Can
406 * @param bool $expect
408 public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
409 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
410 $mock1->expects( $this->any() )->method( 'getUniqueId' )
411 ->will( $this->returnValue( 'primary1' ) );
412 $mock1->expects( $this->any() )->method( 'testUserCanAuthenticate' )
413 ->with( $this->equalTo( 'UTSysop' ) )
414 ->will( $this->returnValue( $primary1Can ) );
415 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
416 $mock2->expects( $this->any() )->method( 'getUniqueId' )
417 ->will( $this->returnValue( 'primary2' ) );
418 $mock2->expects( $this->any() )->method( 'testUserCanAuthenticate' )
419 ->with( $this->equalTo( 'UTSysop' ) )
420 ->will( $this->returnValue( $primary2Can ) );
421 $this->primaryauthMocks
= [ $mock1, $mock2 ];
423 $this->initializeManager( true );
424 $this->assertSame( $expect, $this->manager
->userCanAuthenticate( 'UTSysop' ) );
427 public static function provideUserCanAuthenticate() {
429 [ false, false, false ],
430 [ true, false, true ],
431 [ false, true, true ],
432 [ true, true, true ],
436 public function testRevokeAccessForUser() {
437 $this->initializeManager();
439 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
440 $mock->expects( $this->any() )->method( 'getUniqueId' )
441 ->will( $this->returnValue( 'primary' ) );
442 $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
443 ->with( $this->equalTo( 'UTSysop' ) );
444 $this->primaryauthMocks
= [ $mock ];
446 $this->initializeManager( true );
447 $this->logger
->setCollect( true );
449 $this->manager
->revokeAccessForUser( 'UTSysop' );
452 [ LogLevel
::INFO
, 'Revoking access for {user}' ],
453 ], $this->logger
->getBuffer() );
456 public function testProviderCreation() {
458 'pre' => $this->getMockForAbstractClass( PreAuthenticationProvider
::class ),
459 'primary' => $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
460 'secondary' => $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class ),
462 foreach ( $mocks as $key => $mock ) {
463 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
464 $mock->expects( $this->once() )->method( 'setLogger' );
465 $mock->expects( $this->once() )->method( 'setManager' );
466 $mock->expects( $this->once() )->method( 'setConfig' );
468 $this->preauthMocks
= [ $mocks['pre'] ];
469 $this->primaryauthMocks
= [ $mocks['primary'] ];
470 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
473 $this->initializeManager();
476 $this->managerPriv
->getAuthenticationProvider( 'primary' )
480 $this->managerPriv
->getAuthenticationProvider( 'secondary' )
484 $this->managerPriv
->getAuthenticationProvider( 'pre' )
487 [ 'pre' => $mocks['pre'] ],
488 $this->managerPriv
->getPreAuthenticationProviders()
491 [ 'primary' => $mocks['primary'] ],
492 $this->managerPriv
->getPrimaryAuthenticationProviders()
495 [ 'secondary' => $mocks['secondary'] ],
496 $this->managerPriv
->getSecondaryAuthenticationProviders()
500 $mock1 = $this->getMockForAbstractClass( PreAuthenticationProvider
::class );
501 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
502 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
503 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
504 $this->preauthMocks
= [ $mock1 ];
505 $this->primaryauthMocks
= [ $mock2 ];
506 $this->secondaryauthMocks
= [];
507 $this->initializeManager( true );
509 $this->managerPriv
->getAuthenticationProvider( 'Y' );
510 $this->fail( 'Expected exception not thrown' );
511 } catch ( \RuntimeException
$ex ) {
512 $class1 = get_class( $mock1 );
513 $class2 = get_class( $mock2 );
515 "Duplicate specifications for id X (classes $class1 and $class2)", $ex->getMessage()
520 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
521 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
522 $class = get_class( $mock );
523 $this->preauthMocks
= [ $mock ];
524 $this->primaryauthMocks
= [ $mock ];
525 $this->secondaryauthMocks
= [ $mock ];
526 $this->initializeManager( true );
528 $this->managerPriv
->getPreAuthenticationProviders();
529 $this->fail( 'Expected exception not thrown' );
530 } catch ( \RuntimeException
$ex ) {
532 "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
537 $this->managerPriv
->getPrimaryAuthenticationProviders();
538 $this->fail( 'Expected exception not thrown' );
539 } catch ( \RuntimeException
$ex ) {
541 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
546 $this->managerPriv
->getSecondaryAuthenticationProviders();
547 $this->fail( 'Expected exception not thrown' );
548 } catch ( \RuntimeException
$ex ) {
550 "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
556 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
557 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
558 $mock3 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
559 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
560 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
561 $mock3->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'C' ) );
562 $this->preauthMocks
= [];
563 $this->primaryauthMocks
= [ $mock1, $mock2, $mock3 ];
564 $this->secondaryauthMocks
= [];
565 $this->initializeConfig();
566 $config = $this->config
->get( 'AuthManagerConfig' );
568 $this->initializeManager( false );
570 [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
571 $this->managerPriv
->getPrimaryAuthenticationProviders(),
575 $config['primaryauth']['A']['sort'] = 100;
576 $config['primaryauth']['C']['sort'] = -1;
577 $this->config
->set( 'AuthManagerConfig', $config );
578 $this->initializeManager( false );
580 [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
581 $this->managerPriv
->getPrimaryAuthenticationProviders()
585 public function testSetDefaultUserOptions() {
586 $this->initializeManager();
588 $context = \RequestContext
::getMain();
589 $reset = new ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
590 $context->setLanguage( 'de' );
591 $this->setMwGlobals( 'wgContLang', \Language
::factory( 'zh' ) );
593 $user = \User
::newFromName( self
::usernameForCreation() );
594 $user->addToDatabase();
595 $oldToken = $user->getToken();
596 $this->managerPriv
->setDefaultUserOptions( $user, false );
597 $user->saveSettings();
598 $this->assertNotEquals( $oldToken, $user->getToken() );
599 $this->assertSame( 'zh', $user->getOption( 'language' ) );
600 $this->assertSame( 'zh', $user->getOption( 'variant' ) );
602 $user = \User
::newFromName( self
::usernameForCreation() );
603 $user->addToDatabase();
604 $oldToken = $user->getToken();
605 $this->managerPriv
->setDefaultUserOptions( $user, true );
606 $user->saveSettings();
607 $this->assertNotEquals( $oldToken, $user->getToken() );
608 $this->assertSame( 'de', $user->getOption( 'language' ) );
609 $this->assertSame( 'zh', $user->getOption( 'variant' ) );
611 $this->setMwGlobals( 'wgContLang', \Language
::factory( 'en' ) );
613 $user = \User
::newFromName( self
::usernameForCreation() );
614 $user->addToDatabase();
615 $oldToken = $user->getToken();
616 $this->managerPriv
->setDefaultUserOptions( $user, true );
617 $user->saveSettings();
618 $this->assertNotEquals( $oldToken, $user->getToken() );
619 $this->assertSame( 'de', $user->getOption( 'language' ) );
620 $this->assertSame( null, $user->getOption( 'variant' ) );
623 public function testForcePrimaryAuthenticationProviders() {
624 $mockA = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
625 $mockB = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
626 $mockB2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
627 $mockA->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
628 $mockB->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
629 $mockB2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
630 $this->primaryauthMocks
= [ $mockA ];
632 $this->logger
= new \
TestLogger( true );
634 // Test without first initializing the configured providers
635 $this->initializeManager();
636 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
638 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
640 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
641 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
643 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
644 ], $this->logger
->getBuffer() );
645 $this->logger
->clearBuffer();
647 // Test with first initializing the configured providers
648 $this->initializeManager();
649 $this->assertSame( $mockA, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
650 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
651 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
652 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
653 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
655 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
657 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
658 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
659 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
661 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
664 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
667 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
669 ], $this->logger
->getBuffer() );
670 $this->logger
->clearBuffer();
672 // Test duplicate IDs
673 $this->initializeManager();
675 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
676 $this->fail( 'Expected exception not thrown' );
677 } catch ( \RuntimeException
$ex ) {
678 $class1 = get_class( $mockB );
679 $class2 = get_class( $mockB2 );
681 "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
686 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
687 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
688 $class = get_class( $mock );
690 $this->manager
->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
691 $this->fail( 'Expected exception not thrown' );
692 } catch ( \RuntimeException
$ex ) {
694 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
701 public function testBeginAuthentication() {
702 $this->initializeManager();
705 list( $provider, $reset ) = $this->getMockSessionProvider( false );
706 $this->hook( 'UserLoggedIn', $this->never() );
707 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
709 $this->manager
->beginAuthentication( [], 'http://localhost/' );
710 $this->fail( 'Expected exception not thrown' );
711 } catch ( \LogicException
$ex ) {
712 $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
714 $this->unhook( 'UserLoggedIn' );
715 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
716 ScopedCallback
::consume( $reset );
717 $this->initializeManager( true );
719 // CreatedAccountAuthenticationRequest
720 $user = \User
::newFromName( 'UTSysop' );
722 new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
724 $this->hook( 'UserLoggedIn', $this->never() );
726 $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
727 $this->fail( 'Expected exception not thrown' );
728 } catch ( \LogicException
$ex ) {
730 'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
731 'that created the account',
735 $this->unhook( 'UserLoggedIn' );
737 $this->request
->getSession()->clear();
738 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
739 $this->managerPriv
->createdAccountAuthenticationRequests
= [ $reqs[0] ];
740 $this->hook( 'UserLoggedIn', $this->once() )
741 ->with( $this->callback( function ( $u ) use ( $user ) {
742 return $user->getId() === $u->getId() && $user->getName() === $u->getName();
744 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
745 $this->logger
->setCollect( true );
746 $ret = $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
747 $this->logger
->setCollect( false );
748 $this->unhook( 'UserLoggedIn' );
749 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
750 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
751 $this->assertSame( $user->getName(), $ret->username
);
752 $this->assertSame( $user->getId(), $this->request
->getSessionData( 'AuthManager:lastAuthId' ) );
754 time(), $this->request
->getSessionData( 'AuthManager:lastAuthTimestamp' ),
757 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
758 $this->assertSame( $user->getId(), $this->request
->getSession()->getUser()->getId() );
760 [ LogLevel
::INFO
, 'Logging in {user} after account creation' ],
761 ], $this->logger
->getBuffer() );
764 public function testCreateFromLogin() {
765 $user = \User
::newFromName( 'UTSysop' );
766 $req1 = $this->getMock( AuthenticationRequest
::class );
767 $req2 = $this->getMock( AuthenticationRequest
::class );
768 $req3 = $this->getMock( AuthenticationRequest
::class );
769 $userReq = new UsernameAuthenticationRequest
;
770 $userReq->username
= 'UTDummy';
772 $req1->returnToUrl
= 'http://localhost/';
773 $req2->returnToUrl
= 'http://localhost/';
774 $req3->returnToUrl
= 'http://localhost/';
775 $req3->username
= 'UTDummy';
776 $userReq->returnToUrl
= 'http://localhost/';
778 // Passing one into beginAuthentication(), and an immediate FAIL
779 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
780 $this->primaryauthMocks
= [ $primary ];
781 $this->initializeManager( true );
782 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
783 $res->createRequest
= $req1;
784 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
785 ->will( $this->returnValue( $res ) );
786 $createReq = new CreateFromLoginAuthenticationRequest(
787 null, [ $req2->getUniqueId() => $req2 ]
789 $this->logger
->setCollect( true );
790 $ret = $this->manager
->beginAuthentication( [ $createReq ], 'http://localhost/' );
791 $this->logger
->setCollect( false );
792 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
793 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
794 $this->assertSame( $req1, $ret->createRequest
->createRequest
);
795 $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest
->maybeLink
);
797 // UI, then FAIL in beginAuthentication()
798 $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider
::class )
799 ->setMethods( [ 'continuePrimaryAuthentication' ] )
800 ->getMockForAbstractClass();
801 $this->primaryauthMocks
= [ $primary ];
802 $this->initializeManager( true );
803 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
804 ->will( $this->returnValue(
805 AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) )
807 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
808 $res->createRequest
= $req2;
809 $primary->expects( $this->any() )->method( 'continuePrimaryAuthentication' )
810 ->will( $this->returnValue( $res ) );
811 $this->logger
->setCollect( true );
812 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
813 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
, 'sanity check' );
814 $ret = $this->manager
->continueAuthentication( [] );
815 $this->logger
->setCollect( false );
816 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
817 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
818 $this->assertSame( $req2, $ret->createRequest
->createRequest
);
819 $this->assertEquals( [], $ret->createRequest
->maybeLink
);
821 // Pass into beginAccountCreation(), see that maybeLink and createRequest get copied
822 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
823 $this->primaryauthMocks
= [ $primary ];
824 $this->initializeManager( true );
825 $createReq = new CreateFromLoginAuthenticationRequest( $req3, [ $req2 ] );
826 $createReq->returnToUrl
= 'http://localhost/';
827 $createReq->username
= 'UTDummy';
828 $res = AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) );
829 $primary->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
830 ->with( $this->anything(), $this->anything(), [ $userReq, $createReq, $req3 ] )
831 ->will( $this->returnValue( $res ) );
832 $primary->expects( $this->any() )->method( 'accountCreationType' )
833 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
834 $this->logger
->setCollect( true );
835 $ret = $this->manager
->beginAccountCreation(
836 $user, [ $userReq, $createReq ], 'http://localhost/'
838 $this->logger
->setCollect( false );
839 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
840 $state = $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' );
841 $this->assertNotNull( $state );
842 $this->assertEquals( [ $userReq, $createReq, $req3 ], $state['reqs'] );
843 $this->assertEquals( [ $req2 ], $state['maybeLink'] );
847 * @dataProvider provideAuthentication
848 * @param StatusValue $preResponse
849 * @param array $primaryResponses
850 * @param array $secondaryResponses
851 * @param array $managerResponses
852 * @param bool $link Whether the primary authentication provider is a "link" provider
854 public function testAuthentication(
855 StatusValue
$preResponse, array $primaryResponses, array $secondaryResponses,
856 array $managerResponses, $link = false
858 $this->initializeManager();
859 $user = \User
::newFromName( 'UTSysop' );
860 $id = $user->getId();
861 $name = $user->getName();
863 // Set up lots of mocks...
864 $req = new RememberMeAuthenticationRequest
;
865 $req->rememberMe
= (bool)rand( 0, 1 );
866 $req->pre
= $preResponse;
867 $req->primary
= $primaryResponses;
868 $req->secondary
= $secondaryResponses;
870 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
871 $class = ucfirst( $key ) . 'AuthenticationProvider';
872 $mocks[$key] = $this->getMockForAbstractClass(
873 "MediaWiki\\Auth\\$class", [], "Mock$class"
875 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
876 ->will( $this->returnValue( $key ) );
877 $mocks[$key . '2'] = $this->getMockForAbstractClass(
878 "MediaWiki\\Auth\\$class", [], "Mock$class"
880 $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
881 ->will( $this->returnValue( $key . '2' ) );
882 $mocks[$key . '3'] = $this->getMockForAbstractClass(
883 "MediaWiki\\Auth\\$class", [], "Mock$class"
885 $mocks[$key . '3']->expects( $this->any() )->method( 'getUniqueId' )
886 ->will( $this->returnValue( $key . '3' ) );
888 foreach ( $mocks as $mock ) {
889 $mock->expects( $this->any() )->method( 'getAuthenticationRequests' )
890 ->will( $this->returnValue( [] ) );
893 $mocks['pre']->expects( $this->once() )->method( 'testForAuthentication' )
894 ->will( $this->returnCallback( function ( $reqs ) use ( $req ) {
895 $this->assertContains( $req, $reqs );
899 $ct = count( $req->primary
);
900 $callback = $this->returnCallback( function ( $reqs ) use ( $req ) {
901 $this->assertContains( $req, $reqs );
902 return array_shift( $req->primary
);
904 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
905 ->method( 'beginPrimaryAuthentication' )
907 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
908 ->method( 'continuePrimaryAuthentication' )
911 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
912 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
915 $ct = count( $req->secondary
);
916 $callback = $this->returnCallback( function ( $user, $reqs ) use ( $id, $name, $req ) {
917 $this->assertSame( $id, $user->getId() );
918 $this->assertSame( $name, $user->getName() );
919 $this->assertContains( $req, $reqs );
920 return array_shift( $req->secondary
);
922 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
923 ->method( 'beginSecondaryAuthentication' )
925 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
926 ->method( 'continueSecondaryAuthentication' )
929 $abstain = AuthenticationResponse
::newAbstain();
930 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAuthentication' )
931 ->will( $this->returnValue( StatusValue
::newGood() ) );
932 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAuthentication' )
933 ->will( $this->returnValue( $abstain ) );
934 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAuthentication' );
935 $mocks['secondary2']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
936 ->will( $this->returnValue( $abstain ) );
937 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
938 $mocks['secondary3']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
939 ->will( $this->returnValue( $abstain ) );
940 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
942 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
943 $this->primaryauthMocks
= [ $mocks['primary'], $mocks['primary2'] ];
944 $this->secondaryauthMocks
= [
945 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2'],
946 // So linking happens
947 new ConfirmLinkSecondaryAuthenticationProvider
,
949 $this->initializeManager( true );
950 $this->logger
->setCollect( true );
952 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
953 $this->equalTo( AuthenticationResponse
::PASS
),
954 $this->equalTo( AuthenticationResponse
::FAIL
)
956 $providers = array_filter(
958 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
961 return is_callable( [ $p, 'expects' ] );
964 foreach ( $providers as $p ) {
965 $p->postCalled
= false;
966 $p->expects( $this->atMost( 1 ) )->method( 'postAuthentication' )
967 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
968 if ( $user !== null ) {
969 $this->assertInstanceOf( 'User', $user );
970 $this->assertSame( 'UTSysop', $user->getName() );
972 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
973 $this->assertThat( $response->status
, $constraint );
974 $p->postCalled
= $response->status
;
978 $session = $this->request
->getSession();
979 $session->setRememberUser( !$req->rememberMe
);
981 foreach ( $managerResponses as $i => $response ) {
982 $success = $response instanceof AuthenticationResponse
&&
983 $response->status
=== AuthenticationResponse
::PASS
;
985 $this->hook( 'UserLoggedIn', $this->once() )
986 ->with( $this->callback( function ( $user ) use ( $id, $name ) {
987 return $user->getId() === $id && $user->getName() === $name;
990 $this->hook( 'UserLoggedIn', $this->never() );
993 $response instanceof AuthenticationResponse
&&
994 $response->status
=== AuthenticationResponse
::FAIL
&&
995 $response->message
->getKey() !== 'authmanager-authn-not-in-progress' &&
996 $response->message
->getKey() !== 'authmanager-authn-no-primary'
999 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
1001 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->never() );
1007 $ret = $this->manager
->beginAuthentication( [ $req ], 'http://localhost/' );
1009 $ret = $this->manager
->continueAuthentication( [ $req ] );
1011 if ( $response instanceof \Exception
) {
1012 $this->fail( 'Expected exception not thrown', "Response $i" );
1014 } catch ( \Exception
$ex ) {
1015 if ( !$response instanceof \Exception
) {
1018 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
1019 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1020 "Response $i, exception, session state" );
1021 $this->unhook( 'UserLoggedIn' );
1022 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1026 $this->unhook( 'UserLoggedIn' );
1027 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1029 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
1031 $ret->message
= $this->message( $ret->message
);
1032 $this->assertEquals( $response, $ret, "Response $i, response" );
1034 $this->assertSame( $id, $session->getUser()->getId(),
1035 "Response $i, authn" );
1037 $this->assertSame( 0, $session->getUser()->getId(),
1038 "Response $i, authn" );
1040 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
1041 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1042 "Response $i, session state" );
1043 foreach ( $providers as $p ) {
1044 $this->assertSame( $response->status
, $p->postCalled
,
1045 "Response $i, post-auth callback called" );
1048 $this->assertNotNull( $session->getSecret( 'AuthManager::authnState' ),
1049 "Response $i, session state" );
1050 foreach ( $ret->neededRequests
as $neededReq ) {
1051 $this->assertEquals( AuthManager
::ACTION_LOGIN
, $neededReq->action
,
1052 "Response $i, neededRequest action" );
1054 $this->assertEquals(
1055 $ret->neededRequests
,
1056 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN_CONTINUE
),
1057 "Response $i, continuation check"
1059 foreach ( $providers as $p ) {
1060 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
1064 $state = $session->getSecret( 'AuthManager::authnState' );
1065 $maybeLink = isset( $state['maybeLink'] ) ?
$state['maybeLink'] : [];
1066 if ( $link && $response->status
=== AuthenticationResponse
::RESTART
) {
1067 $this->assertEquals(
1068 $response->createRequest
->maybeLink
,
1070 "Response $i, maybeLink"
1073 $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
1078 $this->assertSame( $req->rememberMe
, $session->shouldRememberUser(),
1079 'rememberMe checkbox had effect' );
1081 $this->assertNotSame( $req->rememberMe
, $session->shouldRememberUser(),
1082 'rememberMe checkbox wasn\'t applied' );
1086 public function provideAuthentication() {
1087 $user = \User
::newFromName( 'UTSysop' );
1088 $id = $user->getId();
1089 $name = $user->getName();
1091 $rememberReq = new RememberMeAuthenticationRequest
;
1092 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
1094 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1095 $req->foobar
= 'baz';
1096 $restartResponse = AuthenticationResponse
::newRestart(
1097 $this->message( 'authmanager-authn-no-local-user' )
1099 $restartResponse->neededRequests
= [ $rememberReq ];
1101 $restartResponse2Pass = AuthenticationResponse
::newPass( null );
1102 $restartResponse2Pass->linkRequest
= $req;
1103 $restartResponse2 = AuthenticationResponse
::newRestart(
1104 $this->message( 'authmanager-authn-no-local-user-link' )
1106 $restartResponse2->createRequest
= new CreateFromLoginAuthenticationRequest(
1107 null, [ $req->getUniqueId() => $req ]
1109 $restartResponse2->createRequest
->action
= AuthManager
::ACTION_LOGIN
;
1110 $restartResponse2->neededRequests
= [ $rememberReq, $restartResponse2->createRequest
];
1113 'Failure in pre-auth' => [
1114 StatusValue
::newFatal( 'fail-from-pre' ),
1118 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
1119 AuthenticationResponse
::newFail(
1120 $this->message( 'authmanager-authn-not-in-progress' )
1124 'Failure in primary' => [
1125 StatusValue
::newGood(),
1127 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
1132 'All primary abstain' => [
1133 StatusValue
::newGood(),
1135 AuthenticationResponse
::newAbstain(),
1139 AuthenticationResponse
::newFail( $this->message( 'authmanager-authn-no-primary' ) )
1142 'Primary UI, then redirect, then fail' => [
1143 StatusValue
::newGood(),
1145 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1146 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
1147 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
1152 'Primary redirect, then abstain' => [
1153 StatusValue
::newGood(),
1155 $tmp = AuthenticationResponse
::newRedirect(
1156 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
1158 AuthenticationResponse
::newAbstain(),
1163 new \
DomainException(
1164 'MockPrimaryAuthenticationProvider::continuePrimaryAuthentication() returned ABSTAIN'
1168 'Primary UI, then pass with no local user' => [
1169 StatusValue
::newGood(),
1171 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1172 AuthenticationResponse
::newPass( null ),
1180 'Primary UI, then pass with no local user (link type)' => [
1181 StatusValue
::newGood(),
1183 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1184 $restartResponse2Pass,
1193 'Primary pass with invalid username' => [
1194 StatusValue
::newGood(),
1196 AuthenticationResponse
::newPass( '<>' ),
1200 new \
DomainException( 'MockPrimaryAuthenticationProvider returned an invalid username: <>' ),
1203 'Secondary fail' => [
1204 StatusValue
::newGood(),
1206 AuthenticationResponse
::newPass( $name ),
1209 AuthenticationResponse
::newFail( $this->message( 'fail-in-secondary' ) ),
1213 'Secondary UI, then abstain' => [
1214 StatusValue
::newGood(),
1216 AuthenticationResponse
::newPass( $name ),
1219 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1220 AuthenticationResponse
::newAbstain()
1224 AuthenticationResponse
::newPass( $name ),
1227 'Secondary pass' => [
1228 StatusValue
::newGood(),
1230 AuthenticationResponse
::newPass( $name ),
1233 AuthenticationResponse
::newPass()
1236 AuthenticationResponse
::newPass( $name ),
1243 * @dataProvider provideUserExists
1244 * @param bool $primary1Exists
1245 * @param bool $primary2Exists
1246 * @param bool $expect
1248 public function testUserExists( $primary1Exists, $primary2Exists, $expect ) {
1249 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1250 $mock1->expects( $this->any() )->method( 'getUniqueId' )
1251 ->will( $this->returnValue( 'primary1' ) );
1252 $mock1->expects( $this->any() )->method( 'testUserExists' )
1253 ->with( $this->equalTo( 'UTSysop' ) )
1254 ->will( $this->returnValue( $primary1Exists ) );
1255 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1256 $mock2->expects( $this->any() )->method( 'getUniqueId' )
1257 ->will( $this->returnValue( 'primary2' ) );
1258 $mock2->expects( $this->any() )->method( 'testUserExists' )
1259 ->with( $this->equalTo( 'UTSysop' ) )
1260 ->will( $this->returnValue( $primary2Exists ) );
1261 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1263 $this->initializeManager( true );
1264 $this->assertSame( $expect, $this->manager
->userExists( 'UTSysop' ) );
1267 public static function provideUserExists() {
1269 [ false, false, false ],
1270 [ true, false, true ],
1271 [ false, true, true ],
1272 [ true, true, true ],
1277 * @dataProvider provideAllowsAuthenticationDataChange
1278 * @param StatusValue $primaryReturn
1279 * @param StatusValue $secondaryReturn
1280 * @param Status $expect
1282 public function testAllowsAuthenticationDataChange( $primaryReturn, $secondaryReturn, $expect ) {
1283 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1285 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1286 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1287 $mock1->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1288 ->with( $this->equalTo( $req ) )
1289 ->will( $this->returnValue( $primaryReturn ) );
1290 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
1291 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1292 $mock2->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1293 ->with( $this->equalTo( $req ) )
1294 ->will( $this->returnValue( $secondaryReturn ) );
1296 $this->primaryauthMocks
= [ $mock1 ];
1297 $this->secondaryauthMocks
= [ $mock2 ];
1298 $this->initializeManager( true );
1299 $this->assertEquals( $expect, $this->manager
->allowsAuthenticationDataChange( $req ) );
1302 public static function provideAllowsAuthenticationDataChange() {
1303 $ignored = \Status
::newGood( 'ignored' );
1304 $ignored->warning( 'authmanager-change-not-supported' );
1306 $okFromPrimary = StatusValue
::newGood();
1307 $okFromPrimary->warning( 'warning-from-primary' );
1308 $okFromSecondary = StatusValue
::newGood();
1309 $okFromSecondary->warning( 'warning-from-secondary' );
1313 StatusValue
::newGood(),
1314 StatusValue
::newGood(),
1318 StatusValue
::newGood(),
1319 StatusValue
::newGood( 'ignore' ),
1323 StatusValue
::newGood( 'ignored' ),
1324 StatusValue
::newGood(),
1328 StatusValue
::newGood( 'ignored' ),
1329 StatusValue
::newGood( 'ignored' ),
1333 StatusValue
::newFatal( 'fail from primary' ),
1334 StatusValue
::newGood(),
1335 \Status
::newFatal( 'fail from primary' ),
1339 StatusValue
::newGood(),
1340 \Status
::wrap( $okFromPrimary ),
1343 StatusValue
::newGood(),
1344 StatusValue
::newFatal( 'fail from secondary' ),
1345 \Status
::newFatal( 'fail from secondary' ),
1348 StatusValue
::newGood(),
1350 \Status
::wrap( $okFromSecondary ),
1355 public function testChangeAuthenticationData() {
1356 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1357 $req->username
= 'UTSysop';
1359 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1360 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1361 $mock1->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1362 ->with( $this->equalTo( $req ) );
1363 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1364 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1365 $mock2->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1366 ->with( $this->equalTo( $req ) );
1368 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1369 $this->initializeManager( true );
1370 $this->logger
->setCollect( true );
1371 $this->manager
->changeAuthenticationData( $req );
1372 $this->assertSame( [
1373 [ LogLevel
::INFO
, 'Changing authentication data for {user} class {what}' ],
1374 ], $this->logger
->getBuffer() );
1377 public function testCanCreateAccounts() {
1379 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
1380 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
1381 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
1384 foreach ( $types as $type => $can ) {
1385 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1386 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
1387 $mock->expects( $this->any() )->method( 'accountCreationType' )
1388 ->will( $this->returnValue( $type ) );
1389 $this->primaryauthMocks
= [ $mock ];
1390 $this->initializeManager( true );
1391 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
1395 public function testCheckAccountCreatePermissions() {
1396 global $wgGroupPermissions;
1398 $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
1400 $this->initializeManager( true );
1402 $wgGroupPermissions['*']['createaccount'] = true;
1403 $this->assertEquals(
1405 $this->manager
->checkAccountCreatePermissions( new \User
)
1408 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1409 $this->assertEquals(
1410 \Status
::newFatal( 'readonlytext', 'Because' ),
1411 $this->manager
->checkAccountCreatePermissions( new \User
)
1413 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1415 $wgGroupPermissions['*']['createaccount'] = false;
1416 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1417 $this->assertFalse( $status->isOK() );
1418 $this->assertTrue( $status->hasMessage( 'badaccess-groups' ) );
1419 $wgGroupPermissions['*']['createaccount'] = true;
1421 $user = \User
::newFromName( 'UTBlockee' );
1422 if ( $user->getID() == 0 ) {
1423 $user->addToDatabase();
1424 \TestUser
::setPasswordForUser( $user, 'UTBlockeePassword' );
1425 $user->saveSettings();
1427 $oldBlock = \Block
::newFromTarget( 'UTBlockee' );
1429 // An old block will prevent our new one from saving.
1430 $oldBlock->delete();
1433 'address' => 'UTBlockee',
1434 'user' => $user->getID(),
1435 'reason' => __METHOD__
,
1436 'expiry' => time() +
100500,
1437 'createAccount' => true,
1439 $block = new \
Block( $blockOptions );
1441 $status = $this->manager
->checkAccountCreatePermissions( $user );
1442 $this->assertFalse( $status->isOK() );
1443 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
1446 'address' => '127.0.0.0/24',
1447 'reason' => __METHOD__
,
1448 'expiry' => time() +
100500,
1449 'createAccount' => true,
1451 $block = new \
Block( $blockOptions );
1453 $scopeVariable = new ScopedCallback( [ $block, 'delete' ] );
1454 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1455 $this->assertFalse( $status->isOK() );
1456 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
1457 ScopedCallback
::consume( $scopeVariable );
1459 $this->setMwGlobals( [
1460 'wgEnableDnsBlacklist' => true,
1461 'wgDnsBlacklistUrls' => [
1462 'local.wmftest.net', // This will resolve for every subdomain, which works to test "listed?"
1464 'wgProxyWhitelist' => [],
1466 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1467 $this->assertFalse( $status->isOK() );
1468 $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
1469 $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
1470 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1471 $this->assertTrue( $status->isGood() );
1475 * @param string $uniq
1478 private static function usernameForCreation( $uniq = '' ) {
1481 $username = "UTAuthManagerTestAccountCreation" . $uniq . ++
$i;
1482 } while ( \User
::newFromName( $username )->getId() !== 0 );
1486 public function testCanCreateAccount() {
1487 $username = self
::usernameForCreation();
1488 $this->initializeManager();
1490 $this->assertEquals(
1491 \Status
::newFatal( 'authmanager-create-disabled' ),
1492 $this->manager
->canCreateAccount( $username )
1495 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1496 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1497 $mock->expects( $this->any() )->method( 'accountCreationType' )
1498 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1499 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1500 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1501 ->will( $this->returnValue( StatusValue
::newGood() ) );
1502 $this->primaryauthMocks
= [ $mock ];
1503 $this->initializeManager( true );
1505 $this->assertEquals(
1506 \Status
::newFatal( 'userexists' ),
1507 $this->manager
->canCreateAccount( $username )
1510 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1511 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1512 $mock->expects( $this->any() )->method( 'accountCreationType' )
1513 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1514 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1515 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1516 ->will( $this->returnValue( StatusValue
::newGood() ) );
1517 $this->primaryauthMocks
= [ $mock ];
1518 $this->initializeManager( true );
1520 $this->assertEquals(
1521 \Status
::newFatal( 'noname' ),
1522 $this->manager
->canCreateAccount( $username . '<>' )
1525 $this->assertEquals(
1526 \Status
::newFatal( 'userexists' ),
1527 $this->manager
->canCreateAccount( 'UTSysop' )
1530 $this->assertEquals(
1532 $this->manager
->canCreateAccount( $username )
1535 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1536 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1537 $mock->expects( $this->any() )->method( 'accountCreationType' )
1538 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1539 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1540 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1541 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1542 $this->primaryauthMocks
= [ $mock ];
1543 $this->initializeManager( true );
1545 $this->assertEquals(
1546 \Status
::newFatal( 'fail' ),
1547 $this->manager
->canCreateAccount( $username )
1551 public function testBeginAccountCreation() {
1552 $creator = \User
::newFromName( 'UTSysop' );
1553 $userReq = new UsernameAuthenticationRequest
;
1554 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1555 return $level === LogLevel
::DEBUG ?
null : $message;
1557 $this->initializeManager();
1559 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
1560 $this->hook( 'LocalUserCreated', $this->never() );
1562 $this->manager
->beginAccountCreation(
1563 $creator, [], 'http://localhost/'
1565 $this->fail( 'Expected exception not thrown' );
1566 } catch ( \LogicException
$ex ) {
1567 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1569 $this->unhook( 'LocalUserCreated' );
1571 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1574 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1575 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1576 $mock->expects( $this->any() )->method( 'accountCreationType' )
1577 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1578 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1579 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1580 ->will( $this->returnValue( StatusValue
::newGood() ) );
1581 $this->primaryauthMocks
= [ $mock ];
1582 $this->initializeManager( true );
1584 $this->hook( 'LocalUserCreated', $this->never() );
1585 $ret = $this->manager
->beginAccountCreation( $creator, [], 'http://localhost/' );
1586 $this->unhook( 'LocalUserCreated' );
1587 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1588 $this->assertSame( 'noname', $ret->message
->getKey() );
1590 $this->hook( 'LocalUserCreated', $this->never() );
1591 $userReq->username
= self
::usernameForCreation();
1592 $userReq2 = new UsernameAuthenticationRequest
;
1593 $userReq2->username
= $userReq->username
. 'X';
1594 $ret = $this->manager
->beginAccountCreation(
1595 $creator, [ $userReq, $userReq2 ], 'http://localhost/'
1597 $this->unhook( 'LocalUserCreated' );
1598 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1599 $this->assertSame( 'noname', $ret->message
->getKey() );
1601 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1602 $this->hook( 'LocalUserCreated', $this->never() );
1603 $userReq->username
= self
::usernameForCreation();
1604 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1605 $this->unhook( 'LocalUserCreated' );
1606 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1607 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
1608 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1609 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1611 $this->hook( 'LocalUserCreated', $this->never() );
1612 $userReq->username
= self
::usernameForCreation();
1613 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1614 $this->unhook( 'LocalUserCreated' );
1615 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1616 $this->assertSame( 'userexists', $ret->message
->getKey() );
1618 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1619 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1620 $mock->expects( $this->any() )->method( 'accountCreationType' )
1621 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1622 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1623 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1624 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1625 $this->primaryauthMocks
= [ $mock ];
1626 $this->initializeManager( true );
1628 $this->hook( 'LocalUserCreated', $this->never() );
1629 $userReq->username
= self
::usernameForCreation();
1630 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1631 $this->unhook( 'LocalUserCreated' );
1632 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1633 $this->assertSame( 'fail', $ret->message
->getKey() );
1635 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1636 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1637 $mock->expects( $this->any() )->method( 'accountCreationType' )
1638 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1639 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1640 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1641 ->will( $this->returnValue( StatusValue
::newGood() ) );
1642 $this->primaryauthMocks
= [ $mock ];
1643 $this->initializeManager( true );
1645 $this->hook( 'LocalUserCreated', $this->never() );
1646 $userReq->username
= self
::usernameForCreation() . '<>';
1647 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1648 $this->unhook( 'LocalUserCreated' );
1649 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1650 $this->assertSame( 'noname', $ret->message
->getKey() );
1652 $this->hook( 'LocalUserCreated', $this->never() );
1653 $userReq->username
= $creator->getName();
1654 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1655 $this->unhook( 'LocalUserCreated' );
1656 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1657 $this->assertSame( 'userexists', $ret->message
->getKey() );
1659 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1660 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1661 $mock->expects( $this->any() )->method( 'accountCreationType' )
1662 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1663 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1664 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1665 ->will( $this->returnValue( StatusValue
::newGood() ) );
1666 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
1667 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1668 $this->primaryauthMocks
= [ $mock ];
1669 $this->initializeManager( true );
1671 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1672 ->setMethods( [ 'populateUser' ] )
1674 $req->expects( $this->any() )->method( 'populateUser' )
1675 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1676 $userReq->username
= self
::usernameForCreation();
1677 $ret = $this->manager
->beginAccountCreation(
1678 $creator, [ $userReq, $req ], 'http://localhost/'
1680 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1681 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1683 $req = new UserDataAuthenticationRequest
;
1684 $userReq->username
= self
::usernameForCreation();
1686 $ret = $this->manager
->beginAccountCreation(
1687 $creator, [ $userReq, $req ], 'http://localhost/'
1689 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1690 $this->assertSame( 'fail', $ret->message
->getKey() );
1692 $this->manager
->beginAccountCreation(
1693 \User
::newFromName( $userReq->username
), [ $userReq, $req ], 'http://localhost/'
1695 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1696 $this->assertSame( 'fail', $ret->message
->getKey() );
1699 public function testContinueAccountCreation() {
1700 $creator = \User
::newFromName( 'UTSysop' );
1701 $username = self
::usernameForCreation();
1702 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1703 return $level === LogLevel
::DEBUG ?
null : $message;
1705 $this->initializeManager();
1709 'username' => $username,
1711 'creatorname' => $username,
1714 'primaryResponse' => null,
1716 'ranPreTests' => true,
1719 $this->hook( 'LocalUserCreated', $this->never() );
1721 $this->manager
->continueAccountCreation( [] );
1722 $this->fail( 'Expected exception not thrown' );
1723 } catch ( \LogicException
$ex ) {
1724 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1726 $this->unhook( 'LocalUserCreated' );
1728 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1729 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1730 $mock->expects( $this->any() )->method( 'accountCreationType' )
1731 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1732 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1733 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )->will(
1734 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
1736 $this->primaryauthMocks
= [ $mock ];
1737 $this->initializeManager( true );
1739 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', null );
1740 $this->hook( 'LocalUserCreated', $this->never() );
1741 $ret = $this->manager
->continueAccountCreation( [] );
1742 $this->unhook( 'LocalUserCreated' );
1743 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1744 $this->assertSame( 'authmanager-create-not-in-progress', $ret->message
->getKey() );
1746 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1747 [ 'username' => "$username<>" ] +
$session );
1748 $this->hook( 'LocalUserCreated', $this->never() );
1749 $ret = $this->manager
->continueAccountCreation( [] );
1750 $this->unhook( 'LocalUserCreated' );
1751 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1752 $this->assertSame( 'noname', $ret->message
->getKey() );
1754 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1757 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $session );
1758 $this->hook( 'LocalUserCreated', $this->never() );
1759 $cache = \ObjectCache
::getLocalClusterInstance();
1760 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1761 $ret = $this->manager
->continueAccountCreation( [] );
1763 $this->unhook( 'LocalUserCreated' );
1764 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1765 $this->assertSame( 'usernameinprogress', $ret->message
->getKey() );
1766 // This error shouldn't remove the existing session, because the
1767 // raced-with process "owns" it.
1769 $session, $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1772 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1773 [ 'username' => $creator->getName() ] +
$session );
1774 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1775 $this->hook( 'LocalUserCreated', $this->never() );
1776 $ret = $this->manager
->continueAccountCreation( [] );
1777 $this->unhook( 'LocalUserCreated' );
1778 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1779 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
1780 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1781 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1783 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1784 [ 'username' => $creator->getName() ] +
$session );
1785 $this->hook( 'LocalUserCreated', $this->never() );
1786 $ret = $this->manager
->continueAccountCreation( [] );
1787 $this->unhook( 'LocalUserCreated' );
1788 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1789 $this->assertSame( 'userexists', $ret->message
->getKey() );
1791 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1794 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1795 [ 'userid' => $creator->getId() ] +
$session );
1796 $this->hook( 'LocalUserCreated', $this->never() );
1798 $ret = $this->manager
->continueAccountCreation( [] );
1799 $this->fail( 'Expected exception not thrown' );
1800 } catch ( \UnexpectedValueException
$ex ) {
1801 $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
1803 $this->unhook( 'LocalUserCreated' );
1805 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1808 $id = $creator->getId();
1809 $name = $creator->getName();
1810 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1811 [ 'username' => $name, 'userid' => $id +
1 ] +
$session );
1812 $this->hook( 'LocalUserCreated', $this->never() );
1814 $ret = $this->manager
->continueAccountCreation( [] );
1815 $this->fail( 'Expected exception not thrown' );
1816 } catch ( \UnexpectedValueException
$ex ) {
1817 $this->assertEquals(
1818 "User \"{$name}\" exists, but ID $id != " . ( $id +
1 ) . '!', $ex->getMessage()
1821 $this->unhook( 'LocalUserCreated' );
1823 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1826 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1827 ->setMethods( [ 'populateUser' ] )
1829 $req->expects( $this->any() )->method( 'populateUser' )
1830 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1831 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1832 [ 'reqs' => [ $req ] ] +
$session );
1833 $ret = $this->manager
->continueAccountCreation( [] );
1834 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1835 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1837 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1842 * @dataProvider provideAccountCreation
1843 * @param StatusValue $preTest
1844 * @param StatusValue $primaryTest
1845 * @param StatusValue $secondaryTest
1846 * @param array $primaryResponses
1847 * @param array $secondaryResponses
1848 * @param array $managerResponses
1850 public function testAccountCreation(
1851 StatusValue
$preTest, $primaryTest, $secondaryTest,
1852 array $primaryResponses, array $secondaryResponses, array $managerResponses
1854 $creator = \User
::newFromName( 'UTSysop' );
1855 $username = self
::usernameForCreation();
1857 $this->initializeManager();
1859 // Set up lots of mocks...
1860 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1861 $req->preTest
= $preTest;
1862 $req->primaryTest
= $primaryTest;
1863 $req->secondaryTest
= $secondaryTest;
1864 $req->primary
= $primaryResponses;
1865 $req->secondary
= $secondaryResponses;
1867 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
1868 $class = ucfirst( $key ) . 'AuthenticationProvider';
1869 $mocks[$key] = $this->getMockForAbstractClass(
1870 "MediaWiki\\Auth\\$class", [], "Mock$class"
1872 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
1873 ->will( $this->returnValue( $key ) );
1874 $mocks[$key]->expects( $this->any() )->method( 'testUserForCreation' )
1875 ->will( $this->returnValue( StatusValue
::newGood() ) );
1876 $mocks[$key]->expects( $this->any() )->method( 'testForAccountCreation' )
1877 ->will( $this->returnCallback(
1878 function ( $user, $creatorIn, $reqs )
1879 use ( $username, $creator, $req, $key )
1881 $this->assertSame( $username, $user->getName() );
1882 $this->assertSame( $creator->getId(), $creatorIn->getId() );
1883 $this->assertSame( $creator->getName(), $creatorIn->getName() );
1885 foreach ( $reqs as $r ) {
1886 $this->assertSame( $username, $r->username
);
1887 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1889 $this->assertTrue( $foundReq, '$reqs contains $req' );
1895 for ( $i = 2; $i <= 3; $i++
) {
1896 $mocks[$key . $i] = $this->getMockForAbstractClass(
1897 "MediaWiki\\Auth\\$class", [], "Mock$class"
1899 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
1900 ->will( $this->returnValue( $key . $i ) );
1901 $mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' )
1902 ->will( $this->returnValue( StatusValue
::newGood() ) );
1903 $mocks[$key . $i]->expects( $this->atMost( 1 ) )->method( 'testForAccountCreation' )
1904 ->will( $this->returnValue( StatusValue
::newGood() ) );
1908 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
1909 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1910 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
1911 ->will( $this->returnValue( false ) );
1912 $ct = count( $req->primary
);
1913 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1914 $this->assertSame( $username, $user->getName() );
1915 $this->assertSame( 'UTSysop', $creator->getName() );
1917 foreach ( $reqs as $r ) {
1918 $this->assertSame( $username, $r->username
);
1919 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1921 $this->assertTrue( $foundReq, '$reqs contains $req' );
1922 return array_shift( $req->primary
);
1924 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
1925 ->method( 'beginPrimaryAccountCreation' )
1926 ->will( $callback );
1927 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1928 ->method( 'continuePrimaryAccountCreation' )
1929 ->will( $callback );
1931 $ct = count( $req->secondary
);
1932 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1933 $this->assertSame( $username, $user->getName() );
1934 $this->assertSame( 'UTSysop', $creator->getName() );
1936 foreach ( $reqs as $r ) {
1937 $this->assertSame( $username, $r->username
);
1938 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1940 $this->assertTrue( $foundReq, '$reqs contains $req' );
1941 return array_shift( $req->secondary
);
1943 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
1944 ->method( 'beginSecondaryAccountCreation' )
1945 ->will( $callback );
1946 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1947 ->method( 'continueSecondaryAccountCreation' )
1948 ->will( $callback );
1950 $abstain = AuthenticationResponse
::newAbstain();
1951 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
1952 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
1953 $mocks['primary2']->expects( $this->any() )->method( 'testUserExists' )
1954 ->will( $this->returnValue( false ) );
1955 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountCreation' )
1956 ->will( $this->returnValue( $abstain ) );
1957 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1958 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
1959 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_NONE
) );
1960 $mocks['primary3']->expects( $this->any() )->method( 'testUserExists' )
1961 ->will( $this->returnValue( false ) );
1962 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountCreation' );
1963 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1964 $mocks['secondary2']->expects( $this->atMost( 1 ) )
1965 ->method( 'beginSecondaryAccountCreation' )
1966 ->will( $this->returnValue( $abstain ) );
1967 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1968 $mocks['secondary3']->expects( $this->atMost( 1 ) )
1969 ->method( 'beginSecondaryAccountCreation' )
1970 ->will( $this->returnValue( $abstain ) );
1971 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1973 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
1974 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary'], $mocks['primary2'] ];
1975 $this->secondaryauthMocks
= [
1976 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2']
1979 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
1980 return $level === LogLevel
::DEBUG ?
null : $message;
1983 $this->initializeManager( true );
1985 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
1986 $this->equalTo( AuthenticationResponse
::PASS
),
1987 $this->equalTo( AuthenticationResponse
::FAIL
)
1989 $providers = array_merge(
1990 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
1992 foreach ( $providers as $p ) {
1993 $p->postCalled
= false;
1994 $p->expects( $this->atMost( 1 ) )->method( 'postAccountCreation' )
1995 ->willReturnCallback( function ( $user, $creator, $response )
1996 use ( $constraint, $p, $username )
1998 $this->assertInstanceOf( 'User', $user );
1999 $this->assertSame( $username, $user->getName() );
2000 $this->assertSame( 'UTSysop', $creator->getName() );
2001 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
2002 $this->assertThat( $response->status
, $constraint );
2003 $p->postCalled
= $response->status
;
2007 // We're testing with $wgNewUserLog = false, so assert that it worked
2008 $dbw = wfGetDB( DB_MASTER
);
2009 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2013 foreach ( $managerResponses as $i => $response ) {
2014 $success = $response instanceof AuthenticationResponse
&&
2015 $response->status
=== AuthenticationResponse
::PASS
;
2016 if ( $i === 'created' ) {
2018 $this->hook( 'LocalUserCreated', $this->once() )
2020 $this->callback( function ( $user ) use ( $username ) {
2021 return $user->getName() === $username;
2023 $this->equalTo( false )
2025 $expectLog[] = [ LogLevel
::INFO
, "Creating user {user} during account creation" ];
2027 $this->hook( 'LocalUserCreated', $this->never() );
2033 $userReq = new UsernameAuthenticationRequest
;
2034 $userReq->username
= $username;
2035 $ret = $this->manager
->beginAccountCreation(
2036 $creator, [ $userReq, $req ], 'http://localhost/'
2039 $ret = $this->manager
->continueAccountCreation( [ $req ] );
2041 if ( $response instanceof \Exception
) {
2042 $this->fail( 'Expected exception not thrown', "Response $i" );
2044 } catch ( \Exception
$ex ) {
2045 if ( !$response instanceof \Exception
) {
2048 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
2050 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2051 "Response $i, exception, session state"
2053 $this->unhook( 'LocalUserCreated' );
2057 $this->unhook( 'LocalUserCreated' );
2059 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
2062 $this->assertNotNull( $ret->loginRequest
, "Response $i, login marker" );
2063 $this->assertContains(
2064 $ret->loginRequest
, $this->managerPriv
->createdAccountAuthenticationRequests
,
2065 "Response $i, login marker"
2070 "MediaWiki\Auth\AuthManager::continueAccountCreation: Account creation succeeded for {user}"
2073 // Set some fields in the expected $response that we couldn't
2074 // know in provideAccountCreation().
2075 $response->username
= $username;
2076 $response->loginRequest
= $ret->loginRequest
;
2078 $this->assertNull( $ret->loginRequest
, "Response $i, login marker" );
2079 $this->assertSame( [], $this->managerPriv
->createdAccountAuthenticationRequests
,
2080 "Response $i, login marker" );
2082 $ret->message
= $this->message( $ret->message
);
2083 $this->assertEquals( $response, $ret, "Response $i, response" );
2084 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
2086 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2087 "Response $i, session state"
2089 foreach ( $providers as $p ) {
2090 $this->assertSame( $response->status
, $p->postCalled
,
2091 "Response $i, post-auth callback called" );
2094 $this->assertNotNull(
2095 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2096 "Response $i, session state"
2098 foreach ( $ret->neededRequests
as $neededReq ) {
2099 $this->assertEquals( AuthManager
::ACTION_CREATE
, $neededReq->action
,
2100 "Response $i, neededRequest action" );
2102 $this->assertEquals(
2103 $ret->neededRequests
,
2104 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_CREATE_CONTINUE
),
2105 "Response $i, continuation check"
2107 foreach ( $providers as $p ) {
2108 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
2113 $this->assertNotEquals( 0, \User
::idFromName( $username ) );
2115 $this->assertEquals( 0, \User
::idFromName( $username ) );
2121 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
2125 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2129 public function provideAccountCreation() {
2130 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
2131 $good = StatusValue
::newGood();
2134 'Pre-creation test fail in pre' => [
2135 StatusValue
::newFatal( 'fail-from-pre' ), $good, $good,
2139 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
2142 'Pre-creation test fail in primary' => [
2143 $good, StatusValue
::newFatal( 'fail-from-primary' ), $good,
2147 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2150 'Pre-creation test fail in secondary' => [
2151 $good, $good, StatusValue
::newFatal( 'fail-from-secondary' ),
2155 AuthenticationResponse
::newFail( $this->message( 'fail-from-secondary' ) ),
2158 'Failure in primary' => [
2159 $good, $good, $good,
2161 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2166 'All primary abstain' => [
2167 $good, $good, $good,
2169 AuthenticationResponse
::newAbstain(),
2173 AuthenticationResponse
::newFail( $this->message( 'authmanager-create-no-primary' ) )
2176 'Primary UI, then redirect, then fail' => [
2177 $good, $good, $good,
2179 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2180 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
2181 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
2186 'Primary redirect, then abstain' => [
2187 $good, $good, $good,
2189 $tmp = AuthenticationResponse
::newRedirect(
2190 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
2192 AuthenticationResponse
::newAbstain(),
2197 new \
DomainException(
2198 'MockPrimaryAuthenticationProvider::continuePrimaryAccountCreation() returned ABSTAIN'
2202 'Primary UI, then pass; secondary abstain' => [
2203 $good, $good, $good,
2205 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2206 AuthenticationResponse
::newPass(),
2209 AuthenticationResponse
::newAbstain(),
2213 'created' => AuthenticationResponse
::newPass( '' ),
2216 'Primary pass; secondary UI then pass' => [
2217 $good, $good, $good,
2219 AuthenticationResponse
::newPass( '' ),
2222 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2223 AuthenticationResponse
::newPass( '' ),
2227 AuthenticationResponse
::newPass( '' ),
2230 'Primary pass; secondary fail' => [
2231 $good, $good, $good,
2233 AuthenticationResponse
::newPass(),
2236 AuthenticationResponse
::newFail( $this->message( '...' ) ),
2239 'created' => new \
DomainException(
2240 'MockSecondaryAuthenticationProvider::beginSecondaryAccountCreation() returned FAIL. ' .
2241 'Secondary providers are not allowed to fail account creation, ' .
2242 'that should have been done via testForAccountCreation().'
2250 * @dataProvider provideAccountCreationLogging
2251 * @param bool $isAnon
2252 * @param string|null $logSubtype
2254 public function testAccountCreationLogging( $isAnon, $logSubtype ) {
2255 $creator = $isAnon ?
new \User
: \User
::newFromName( 'UTSysop' );
2256 $username = self
::usernameForCreation();
2258 $this->initializeManager();
2260 // Set up lots of mocks...
2261 $mock = $this->getMockForAbstractClass(
2262 "MediaWiki\\Auth\\PrimaryAuthenticationProvider", []
2264 $mock->expects( $this->any() )->method( 'getUniqueId' )
2265 ->will( $this->returnValue( 'primary' ) );
2266 $mock->expects( $this->any() )->method( 'testUserForCreation' )
2267 ->will( $this->returnValue( StatusValue
::newGood() ) );
2268 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
2269 ->will( $this->returnValue( StatusValue
::newGood() ) );
2270 $mock->expects( $this->any() )->method( 'accountCreationType' )
2271 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2272 $mock->expects( $this->any() )->method( 'testUserExists' )
2273 ->will( $this->returnValue( false ) );
2274 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
2275 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
2276 $mock->expects( $this->any() )->method( 'finishAccountCreation' )
2277 ->will( $this->returnValue( $logSubtype ) );
2279 $this->primaryauthMocks
= [ $mock ];
2280 $this->initializeManager( true );
2281 $this->logger
->setCollect( true );
2283 $this->config
->set( 'NewUserLog', true );
2285 $dbw = wfGetDB( DB_MASTER
);
2286 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2288 $userReq = new UsernameAuthenticationRequest
;
2289 $userReq->username
= $username;
2290 $reasonReq = new CreationReasonAuthenticationRequest
;
2291 $reasonReq->reason
= $this->toString();
2292 $ret = $this->manager
->beginAccountCreation(
2293 $creator, [ $userReq, $reasonReq ], 'http://localhost/'
2296 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
2298 $user = \User
::newFromName( $username );
2299 $this->assertNotEquals( 0, $user->getId(), 'sanity check' );
2300 $this->assertNotEquals( $creator->getId(), $user->getId(), 'sanity check' );
2302 $data = \DatabaseLogEntry
::getSelectQueryData();
2303 $rows = iterator_to_array( $dbw->select(
2307 'log_id > ' . (int)$maxLogId,
2308 'log_type' => 'newusers'
2314 $this->assertCount( 1, $rows );
2315 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2317 $this->assertSame( $logSubtype ?
: ( $isAnon ?
'create' : 'create2' ), $entry->getSubtype() );
2319 $isAnon ?
$user->getId() : $creator->getId(),
2320 $entry->getPerformer()->getId()
2323 $isAnon ?
$user->getName() : $creator->getName(),
2324 $entry->getPerformer()->getName()
2326 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2327 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2328 $this->assertSame( $this->toString(), $entry->getComment() );
2331 public static function provideAccountCreationLogging() {
2336 [ false, 'byemail' ],
2340 public function testAutoAccountCreation() {
2341 global $wgGroupPermissions, $wgHooks;
2343 // PHPUnit seems to have a bug where it will call the ->with()
2344 // callbacks for our hooks again after the test is run (WTF?), which
2345 // breaks here because $username no longer matches $user by the end of
2347 $workaroundPHPUnitBug = false;
2349 $username = self
::usernameForCreation();
2350 $this->initializeManager();
2352 $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
2353 $wgGroupPermissions['*']['createaccount'] = true;
2354 $wgGroupPermissions['*']['autocreateaccount'] = false;
2356 \ObjectCache
::$instances[__METHOD__
] = new \
HashBagOStuff();
2357 $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__
] );
2359 // Set up lots of mocks...
2361 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2362 $class = ucfirst( $key ) . 'AuthenticationProvider';
2363 $mocks[$key] = $this->getMockForAbstractClass(
2364 "MediaWiki\\Auth\\$class", [], "Mock$class"
2366 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2367 ->will( $this->returnValue( $key ) );
2370 $good = StatusValue
::newGood();
2371 $callback = $this->callback( function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
2372 return $workaroundPHPUnitBug ||
$user->getName() === $username;
2375 $mocks['pre']->expects( $this->exactly( 12 ) )->method( 'testUserForCreation' )
2376 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2377 ->will( $this->onConsecutiveCalls(
2378 StatusValue
::newFatal( 'ok' ), StatusValue
::newFatal( 'ok' ), // For testing permissions
2379 StatusValue
::newFatal( 'fail-in-pre' ), $good, $good,
2380 $good, // backoff test
2381 $good, // addToDatabase fails test
2382 $good, // addToDatabase throws test
2383 $good, // addToDatabase exists test
2384 $good, $good, $good // success
2387 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
2388 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2389 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
2390 ->will( $this->returnValue( true ) );
2391 $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
2392 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2393 ->will( $this->onConsecutiveCalls(
2394 StatusValue
::newFatal( 'fail-in-primary' ), $good,
2395 $good, // backoff test
2396 $good, // addToDatabase fails test
2397 $good, // addToDatabase throws test
2398 $good, // addToDatabase exists test
2401 $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2402 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) );
2404 $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
2405 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2406 ->will( $this->onConsecutiveCalls(
2407 StatusValue
::newFatal( 'fail-in-secondary' ),
2408 $good, // backoff test
2409 $good, // addToDatabase fails test
2410 $good, // addToDatabase throws test
2411 $good, // addToDatabase exists test
2414 $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2415 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) );
2417 $this->preauthMocks
= [ $mocks['pre'] ];
2418 $this->primaryauthMocks
= [ $mocks['primary'] ];
2419 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2420 $this->initializeManager( true );
2421 $session = $this->request
->getSession();
2423 $logger = new \
TestLogger( true, function ( $m ) {
2424 $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
2427 $this->manager
->setLogger( $logger );
2430 $user = \User
::newFromName( 'UTSysop' );
2431 $this->manager
->autoCreateUser( $user, 'InvalidSource', true );
2432 $this->fail( 'Expected exception not thrown' );
2433 } catch ( \InvalidArgumentException
$ex ) {
2434 $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
2437 // First, check an existing user
2439 $user = \User
::newFromName( 'UTSysop' );
2440 $this->hook( 'LocalUserCreated', $this->never() );
2441 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2442 $this->unhook( 'LocalUserCreated' );
2443 $expect = \Status
::newGood();
2444 $expect->warning( 'userexists' );
2445 $this->assertEquals( $expect, $ret );
2446 $this->assertNotEquals( 0, $user->getId() );
2447 $this->assertSame( 'UTSysop', $user->getName() );
2448 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2449 $this->assertSame( [
2450 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2451 ], $logger->getBuffer() );
2452 $logger->clearBuffer();
2455 $user = \User
::newFromName( 'UTSysop' );
2456 $this->hook( 'LocalUserCreated', $this->never() );
2457 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2458 $this->unhook( 'LocalUserCreated' );
2459 $expect = \Status
::newGood();
2460 $expect->warning( 'userexists' );
2461 $this->assertEquals( $expect, $ret );
2462 $this->assertNotEquals( 0, $user->getId() );
2463 $this->assertSame( 'UTSysop', $user->getName() );
2464 $this->assertEquals( 0, $session->getUser()->getId() );
2465 $this->assertSame( [
2466 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2467 ], $logger->getBuffer() );
2468 $logger->clearBuffer();
2470 // Wiki is read-only
2472 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
2473 $user = \User
::newFromName( $username );
2474 $this->hook( 'LocalUserCreated', $this->never() );
2475 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2476 $this->unhook( 'LocalUserCreated' );
2477 $this->assertEquals( \Status
::newFatal( 'readonlytext', 'Because' ), $ret );
2478 $this->assertEquals( 0, $user->getId() );
2479 $this->assertNotEquals( $username, $user->getName() );
2480 $this->assertEquals( 0, $session->getUser()->getId() );
2481 $this->assertSame( [
2482 [ LogLevel
::DEBUG
, 'denied by wfReadOnly(): {reason}' ],
2483 ], $logger->getBuffer() );
2484 $logger->clearBuffer();
2485 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
2487 // Session blacklisted
2489 $session->set( 'AuthManager::AutoCreateBlacklist', 'test' );
2490 $user = \User
::newFromName( $username );
2491 $this->hook( 'LocalUserCreated', $this->never() );
2492 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2493 $this->unhook( 'LocalUserCreated' );
2494 $this->assertEquals( \Status
::newFatal( 'test' ), $ret );
2495 $this->assertEquals( 0, $user->getId() );
2496 $this->assertNotEquals( $username, $user->getName() );
2497 $this->assertEquals( 0, $session->getUser()->getId() );
2498 $this->assertSame( [
2499 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2500 ], $logger->getBuffer() );
2501 $logger->clearBuffer();
2504 $session->set( 'AuthManager::AutoCreateBlacklist', StatusValue
::newFatal( 'test2' ) );
2505 $user = \User
::newFromName( $username );
2506 $this->hook( 'LocalUserCreated', $this->never() );
2507 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2508 $this->unhook( 'LocalUserCreated' );
2509 $this->assertEquals( \Status
::newFatal( 'test2' ), $ret );
2510 $this->assertEquals( 0, $user->getId() );
2511 $this->assertNotEquals( $username, $user->getName() );
2512 $this->assertEquals( 0, $session->getUser()->getId() );
2513 $this->assertSame( [
2514 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2515 ], $logger->getBuffer() );
2516 $logger->clearBuffer();
2520 $user = \User
::newFromName( $username . '@' );
2521 $this->hook( 'LocalUserCreated', $this->never() );
2522 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2523 $this->unhook( 'LocalUserCreated' );
2524 $this->assertEquals( \Status
::newFatal( 'noname' ), $ret );
2525 $this->assertEquals( 0, $user->getId() );
2526 $this->assertNotEquals( $username . '@', $user->getId() );
2527 $this->assertEquals( 0, $session->getUser()->getId() );
2528 $this->assertSame( [
2529 [ LogLevel
::DEBUG
, 'name "{username}" is not creatable' ],
2530 ], $logger->getBuffer() );
2531 $logger->clearBuffer();
2532 $this->assertSame( 'noname', $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2534 // IP unable to create accounts
2535 $wgGroupPermissions['*']['createaccount'] = false;
2536 $wgGroupPermissions['*']['autocreateaccount'] = false;
2538 $user = \User
::newFromName( $username );
2539 $this->hook( 'LocalUserCreated', $this->never() );
2540 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2541 $this->unhook( 'LocalUserCreated' );
2542 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-noperm' ), $ret );
2543 $this->assertEquals( 0, $user->getId() );
2544 $this->assertNotEquals( $username, $user->getName() );
2545 $this->assertEquals( 0, $session->getUser()->getId() );
2546 $this->assertSame( [
2547 [ LogLevel
::DEBUG
, 'IP lacks the ability to create or autocreate accounts' ],
2548 ], $logger->getBuffer() );
2549 $logger->clearBuffer();
2551 'authmanager-autocreate-noperm', $session->get( 'AuthManager::AutoCreateBlacklist' )
2554 // Test that both permutations of permissions are allowed
2555 // (this hits the two "ok" entries in $mocks['pre'])
2556 $wgGroupPermissions['*']['createaccount'] = false;
2557 $wgGroupPermissions['*']['autocreateaccount'] = true;
2559 $user = \User
::newFromName( $username );
2560 $this->hook( 'LocalUserCreated', $this->never() );
2561 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2562 $this->unhook( 'LocalUserCreated' );
2563 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2565 $wgGroupPermissions['*']['createaccount'] = true;
2566 $wgGroupPermissions['*']['autocreateaccount'] = false;
2568 $user = \User
::newFromName( $username );
2569 $this->hook( 'LocalUserCreated', $this->never() );
2570 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2571 $this->unhook( 'LocalUserCreated' );
2572 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2573 $logger->clearBuffer();
2577 $user = \User
::newFromName( $username );
2578 $this->hook( 'LocalUserCreated', $this->never() );
2579 $cache = \ObjectCache
::getLocalClusterInstance();
2580 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2581 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2583 $this->unhook( 'LocalUserCreated' );
2584 $this->assertEquals( \Status
::newFatal( 'usernameinprogress' ), $ret );
2585 $this->assertEquals( 0, $user->getId() );
2586 $this->assertNotEquals( $username, $user->getName() );
2587 $this->assertEquals( 0, $session->getUser()->getId() );
2588 $this->assertSame( [
2589 [ LogLevel
::DEBUG
, 'Could not acquire account creation lock' ],
2590 ], $logger->getBuffer() );
2591 $logger->clearBuffer();
2593 // Test pre-authentication provider fail
2595 $user = \User
::newFromName( $username );
2596 $this->hook( 'LocalUserCreated', $this->never() );
2597 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2598 $this->unhook( 'LocalUserCreated' );
2599 $this->assertEquals( \Status
::newFatal( 'fail-in-pre' ), $ret );
2600 $this->assertEquals( 0, $user->getId() );
2601 $this->assertNotEquals( $username, $user->getName() );
2602 $this->assertEquals( 0, $session->getUser()->getId() );
2603 $this->assertSame( [
2604 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2605 ], $logger->getBuffer() );
2606 $logger->clearBuffer();
2607 $this->assertEquals(
2608 StatusValue
::newFatal( 'fail-in-pre' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2612 $user = \User
::newFromName( $username );
2613 $this->hook( 'LocalUserCreated', $this->never() );
2614 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2615 $this->unhook( 'LocalUserCreated' );
2616 $this->assertEquals( \Status
::newFatal( 'fail-in-primary' ), $ret );
2617 $this->assertEquals( 0, $user->getId() );
2618 $this->assertNotEquals( $username, $user->getName() );
2619 $this->assertEquals( 0, $session->getUser()->getId() );
2620 $this->assertSame( [
2621 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2622 ], $logger->getBuffer() );
2623 $logger->clearBuffer();
2624 $this->assertEquals(
2625 StatusValue
::newFatal( 'fail-in-primary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2629 $user = \User
::newFromName( $username );
2630 $this->hook( 'LocalUserCreated', $this->never() );
2631 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2632 $this->unhook( 'LocalUserCreated' );
2633 $this->assertEquals( \Status
::newFatal( 'fail-in-secondary' ), $ret );
2634 $this->assertEquals( 0, $user->getId() );
2635 $this->assertNotEquals( $username, $user->getName() );
2636 $this->assertEquals( 0, $session->getUser()->getId() );
2637 $this->assertSame( [
2638 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2639 ], $logger->getBuffer() );
2640 $logger->clearBuffer();
2641 $this->assertEquals(
2642 StatusValue
::newFatal( 'fail-in-secondary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2646 $cache = \ObjectCache
::getLocalClusterInstance();
2647 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2648 $cache->set( $backoffKey, true );
2650 $user = \User
::newFromName( $username );
2651 $this->hook( 'LocalUserCreated', $this->never() );
2652 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2653 $this->unhook( 'LocalUserCreated' );
2654 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-exception' ), $ret );
2655 $this->assertEquals( 0, $user->getId() );
2656 $this->assertNotEquals( $username, $user->getName() );
2657 $this->assertEquals( 0, $session->getUser()->getId() );
2658 $this->assertSame( [
2659 [ LogLevel
::DEBUG
, '{username} denied by prior creation attempt failures' ],
2660 ], $logger->getBuffer() );
2661 $logger->clearBuffer();
2662 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2663 $cache->delete( $backoffKey );
2665 // Test addToDatabase fails
2667 $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2668 $user->expects( $this->once() )->method( 'addToDatabase' )
2669 ->will( $this->returnValue( \Status
::newFatal( 'because' ) ) );
2670 $user->setName( $username );
2671 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2672 $this->assertEquals( \Status
::newFatal( 'because' ), $ret );
2673 $this->assertEquals( 0, $user->getId() );
2674 $this->assertNotEquals( $username, $user->getName() );
2675 $this->assertEquals( 0, $session->getUser()->getId() );
2676 $this->assertSame( [
2677 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2678 [ LogLevel
::ERROR
, '{username} failed with message {msg}' ],
2679 ], $logger->getBuffer() );
2680 $logger->clearBuffer();
2681 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2683 // Test addToDatabase throws an exception
2684 $cache = \ObjectCache
::getLocalClusterInstance();
2685 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2686 $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
2688 $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2689 $user->expects( $this->once() )->method( 'addToDatabase' )
2690 ->will( $this->throwException( new \
Exception( 'Excepted' ) ) );
2691 $user->setName( $username );
2693 $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2694 $this->fail( 'Expected exception not thrown' );
2695 } catch ( \Exception
$ex ) {
2696 $this->assertSame( 'Excepted', $ex->getMessage() );
2698 $this->assertEquals( 0, $user->getId() );
2699 $this->assertEquals( 0, $session->getUser()->getId() );
2700 $this->assertSame( [
2701 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2702 [ LogLevel
::ERROR
, '{username} failed with exception {exception}' ],
2703 ], $logger->getBuffer() );
2704 $logger->clearBuffer();
2705 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2706 $this->assertNotEquals( false, $cache->get( $backoffKey ) );
2707 $cache->delete( $backoffKey );
2709 // Test addToDatabase fails because the user already exists.
2711 $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2712 $user->expects( $this->once() )->method( 'addToDatabase' )
2713 ->will( $this->returnCallback( function () use ( $username, &$user ) {
2714 $oldUser = \User
::newFromName( $username );
2715 $status = $oldUser->addToDatabase();
2716 $this->assertTrue( $status->isOK(), 'sanity check' );
2717 $user->setId( $oldUser->getId() );
2718 return \Status
::newFatal( 'userexists' );
2720 $user->setName( $username );
2721 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2722 $expect = \Status
::newGood();
2723 $expect->warning( 'userexists' );
2724 $this->assertEquals( $expect, $ret );
2725 $this->assertNotEquals( 0, $user->getId() );
2726 $this->assertEquals( $username, $user->getName() );
2727 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2728 $this->assertSame( [
2729 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2730 [ LogLevel
::INFO
, '{username} already exists locally (race)' ],
2731 ], $logger->getBuffer() );
2732 $logger->clearBuffer();
2733 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2737 $username = self
::usernameForCreation();
2738 $user = \User
::newFromName( $username );
2739 $this->hook( 'AuthPluginAutoCreate', $this->once() )
2740 ->with( $callback );
2741 $this->hideDeprecated( 'AuthPluginAutoCreate hook (used in ' .
2742 get_class( $wgHooks['AuthPluginAutoCreate'][0] ) . '::onAuthPluginAutoCreate)' );
2743 $this->hook( 'LocalUserCreated', $this->once() )
2744 ->with( $callback, $this->equalTo( true ) );
2745 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2746 $this->unhook( 'LocalUserCreated' );
2747 $this->unhook( 'AuthPluginAutoCreate' );
2748 $this->assertEquals( \Status
::newGood(), $ret );
2749 $this->assertNotEquals( 0, $user->getId() );
2750 $this->assertEquals( $username, $user->getName() );
2751 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2752 $this->assertSame( [
2753 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2754 ], $logger->getBuffer() );
2755 $logger->clearBuffer();
2757 $dbw = wfGetDB( DB_MASTER
);
2758 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2760 $username = self
::usernameForCreation();
2761 $user = \User
::newFromName( $username );
2762 $this->hook( 'LocalUserCreated', $this->once() )
2763 ->with( $callback, $this->equalTo( true ) );
2764 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2765 $this->unhook( 'LocalUserCreated' );
2766 $this->assertEquals( \Status
::newGood(), $ret );
2767 $this->assertNotEquals( 0, $user->getId() );
2768 $this->assertEquals( $username, $user->getName() );
2769 $this->assertEquals( 0, $session->getUser()->getId() );
2770 $this->assertSame( [
2771 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2772 ], $logger->getBuffer() );
2773 $logger->clearBuffer();
2776 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2779 $this->config
->set( 'NewUserLog', true );
2781 $username = self
::usernameForCreation();
2782 $user = \User
::newFromName( $username );
2783 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2784 $this->assertEquals( \Status
::newGood(), $ret );
2785 $logger->clearBuffer();
2787 $data = \DatabaseLogEntry
::getSelectQueryData();
2788 $rows = iterator_to_array( $dbw->select(
2792 'log_id > ' . (int)$maxLogId,
2793 'log_type' => 'newusers'
2799 $this->assertCount( 1, $rows );
2800 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2802 $this->assertSame( 'autocreate', $entry->getSubtype() );
2803 $this->assertSame( $user->getId(), $entry->getPerformer()->getId() );
2804 $this->assertSame( $user->getName(), $entry->getPerformer()->getName() );
2805 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2806 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2808 $workaroundPHPUnitBug = true;
2812 * @dataProvider provideGetAuthenticationRequests
2813 * @param string $action
2814 * @param array $expect
2815 * @param array $state
2817 public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
2818 $makeReq = function ( $key ) use ( $action ) {
2819 $req = $this->getMock( AuthenticationRequest
::class );
2820 $req->expects( $this->any() )->method( 'getUniqueId' )
2821 ->will( $this->returnValue( $key ) );
2822 $req->action
= $action === AuthManager
::ACTION_UNLINK ? AuthManager
::ACTION_REMOVE
: $action;
2826 $cmpReqs = function ( $a, $b ) {
2827 $ret = strcmp( get_class( $a ), get_class( $b ) );
2829 $ret = strcmp( $a->key
, $b->key
);
2834 $good = StatusValue
::newGood();
2837 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2838 $class = ucfirst( $key ) . 'AuthenticationProvider';
2839 $mocks[$key] = $this->getMockForAbstractClass(
2840 "MediaWiki\\Auth\\$class", [], "Mock$class"
2842 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2843 ->will( $this->returnValue( $key ) );
2844 $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2845 ->will( $this->returnCallback( function ( $action ) use ( $key, $makeReq ) {
2846 return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
2848 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
2849 ->will( $this->returnValue( $good ) );
2854 PrimaryAuthenticationProvider
::TYPE_NONE
,
2855 PrimaryAuthenticationProvider
::TYPE_CREATE
,
2856 PrimaryAuthenticationProvider
::TYPE_LINK
2858 $class = 'PrimaryAuthenticationProvider';
2859 $mocks["primary-$type"] = $this->getMockForAbstractClass(
2860 "MediaWiki\\Auth\\$class", [], "Mock$class"
2862 $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
2863 ->will( $this->returnValue( "primary-$type" ) );
2864 $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
2865 ->will( $this->returnValue( $type ) );
2866 $mocks["primary-$type"]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2867 ->will( $this->returnCallback( function ( $action ) use ( $type, $makeReq ) {
2868 return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
2870 $mocks["primary-$type"]->expects( $this->any() )
2871 ->method( 'providerAllowsAuthenticationDataChange' )
2872 ->will( $this->returnValue( $good ) );
2873 $this->primaryauthMocks
[] = $mocks["primary-$type"];
2876 $mocks['primary2'] = $this->getMockForAbstractClass(
2877 PrimaryAuthenticationProvider
::class, [], "MockPrimaryAuthenticationProvider"
2879 $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
2880 ->will( $this->returnValue( 'primary2' ) );
2881 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
2882 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
2883 $mocks['primary2']->expects( $this->any() )->method( 'getAuthenticationRequests' )
2884 ->will( $this->returnValue( [] ) );
2885 $mocks['primary2']->expects( $this->any() )
2886 ->method( 'providerAllowsAuthenticationDataChange' )
2887 ->will( $this->returnCallback( function ( $req ) use ( $good ) {
2888 return $req->key
=== 'generic' ? StatusValue
::newFatal( 'no' ) : $good;
2890 $this->primaryauthMocks
[] = $mocks['primary2'];
2892 $this->preauthMocks
= [ $mocks['pre'] ];
2893 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2894 $this->initializeManager( true );
2897 if ( isset( $state['continueRequests'] ) ) {
2898 $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
2900 if ( $action === AuthManager
::ACTION_LOGIN_CONTINUE
) {
2901 $this->request
->getSession()->setSecret( 'AuthManager::authnState', $state );
2902 } elseif ( $action === AuthManager
::ACTION_CREATE_CONTINUE
) {
2903 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $state );
2904 } elseif ( $action === AuthManager
::ACTION_LINK_CONTINUE
) {
2905 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', $state );
2909 $expectReqs = array_map( $makeReq, $expect );
2910 if ( $action === AuthManager
::ACTION_LOGIN
) {
2911 $req = new RememberMeAuthenticationRequest
;
2912 $req->action
= $action;
2913 $req->required
= AuthenticationRequest
::REQUIRED
;
2914 $expectReqs[] = $req;
2915 } elseif ( $action === AuthManager
::ACTION_CREATE
) {
2916 $req = new UsernameAuthenticationRequest
;
2917 $req->action
= $action;
2918 $expectReqs[] = $req;
2919 $req = new UserDataAuthenticationRequest
;
2920 $req->action
= $action;
2921 $req->required
= AuthenticationRequest
::REQUIRED
;
2922 $expectReqs[] = $req;
2924 usort( $expectReqs, $cmpReqs );
2926 $actual = $this->manager
->getAuthenticationRequests( $action );
2927 foreach ( $actual as $req ) {
2928 // Don't test this here.
2929 $req->required
= AuthenticationRequest
::REQUIRED
;
2931 usort( $actual, $cmpReqs );
2933 $this->assertEquals( $expectReqs, $actual );
2935 // Test CreationReasonAuthenticationRequest gets returned
2936 if ( $action === AuthManager
::ACTION_CREATE
) {
2937 $req = new CreationReasonAuthenticationRequest
;
2938 $req->action
= $action;
2939 $req->required
= AuthenticationRequest
::REQUIRED
;
2940 $expectReqs[] = $req;
2941 usort( $expectReqs, $cmpReqs );
2943 $actual = $this->manager
->getAuthenticationRequests( $action, \User
::newFromName( 'UTSysop' ) );
2944 foreach ( $actual as $req ) {
2945 // Don't test this here.
2946 $req->required
= AuthenticationRequest
::REQUIRED
;
2948 usort( $actual, $cmpReqs );
2950 $this->assertEquals( $expectReqs, $actual );
2954 public static function provideGetAuthenticationRequests() {
2957 AuthManager
::ACTION_LOGIN
,
2958 [ 'pre-login', 'primary-none-login', 'primary-create-login',
2959 'primary-link-login', 'secondary-login', 'generic' ],
2962 AuthManager
::ACTION_CREATE
,
2963 [ 'pre-create', 'primary-none-create', 'primary-create-create',
2964 'primary-link-create', 'secondary-create', 'generic' ],
2967 AuthManager
::ACTION_LINK
,
2968 [ 'primary-link-link', 'generic' ],
2971 AuthManager
::ACTION_CHANGE
,
2972 [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
2973 'secondary-change' ],
2976 AuthManager
::ACTION_REMOVE
,
2977 [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
2978 'secondary-remove' ],
2981 AuthManager
::ACTION_UNLINK
,
2982 [ 'primary-link-remove' ],
2985 AuthManager
::ACTION_LOGIN_CONTINUE
,
2989 AuthManager
::ACTION_LOGIN_CONTINUE
,
2990 $reqs = [ 'continue-login', 'foo', 'bar' ],
2992 'continueRequests' => $reqs,
2996 AuthManager
::ACTION_CREATE_CONTINUE
,
3000 AuthManager
::ACTION_CREATE_CONTINUE
,
3001 $reqs = [ 'continue-create', 'foo', 'bar' ],
3003 'continueRequests' => $reqs,
3007 AuthManager
::ACTION_LINK_CONTINUE
,
3011 AuthManager
::ACTION_LINK_CONTINUE
,
3012 $reqs = [ 'continue-link', 'foo', 'bar' ],
3014 'continueRequests' => $reqs,
3020 public function testGetAuthenticationRequestsRequired() {
3021 $makeReq = function ( $key, $required ) {
3022 $req = $this->getMock( AuthenticationRequest
::class );
3023 $req->expects( $this->any() )->method( 'getUniqueId' )
3024 ->will( $this->returnValue( $key ) );
3025 $req->action
= AuthManager
::ACTION_LOGIN
;
3027 $req->required
= $required;
3030 $cmpReqs = function ( $a, $b ) {
3031 $ret = strcmp( get_class( $a ), get_class( $b ) );
3033 $ret = strcmp( $a->key
, $b->key
);
3038 $good = StatusValue
::newGood();
3040 $primary1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3041 $primary1->expects( $this->any() )->method( 'getUniqueId' )
3042 ->will( $this->returnValue( 'primary1' ) );
3043 $primary1->expects( $this->any() )->method( 'accountCreationType' )
3044 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3045 $primary1->expects( $this->any() )->method( 'getAuthenticationRequests' )
3046 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3048 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3049 $makeReq( "required", AuthenticationRequest
::REQUIRED
),
3050 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3051 $makeReq( "foo", AuthenticationRequest
::REQUIRED
),
3052 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3053 $makeReq( "baz", AuthenticationRequest
::OPTIONAL
),
3057 $primary2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3058 $primary2->expects( $this->any() )->method( 'getUniqueId' )
3059 ->will( $this->returnValue( 'primary2' ) );
3060 $primary2->expects( $this->any() )->method( 'accountCreationType' )
3061 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3062 $primary2->expects( $this->any() )->method( 'getAuthenticationRequests' )
3063 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3065 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3066 $makeReq( "required2", AuthenticationRequest
::REQUIRED
),
3067 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3071 $secondary = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3072 $secondary->expects( $this->any() )->method( 'getUniqueId' )
3073 ->will( $this->returnValue( 'secondary' ) );
3074 $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
3075 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3077 $makeReq( "foo", AuthenticationRequest
::OPTIONAL
),
3078 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3079 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3083 $rememberReq = new RememberMeAuthenticationRequest
;
3084 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
3086 $this->primaryauthMocks
= [ $primary1, $primary2 ];
3087 $this->secondaryauthMocks
= [ $secondary ];
3088 $this->initializeManager( true );
3090 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3093 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3094 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3095 $makeReq( "required2", AuthenticationRequest
::PRIMARY_REQUIRED
),
3096 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3097 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3098 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3099 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3100 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3102 usort( $actual, $cmpReqs );
3103 usort( $expected, $cmpReqs );
3104 $this->assertEquals( $expected, $actual );
3106 $this->primaryauthMocks
= [ $primary1 ];
3107 $this->secondaryauthMocks
= [ $secondary ];
3108 $this->initializeManager( true );
3110 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3113 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3114 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3115 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3116 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3117 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3118 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3120 usort( $actual, $cmpReqs );
3121 usort( $expected, $cmpReqs );
3122 $this->assertEquals( $expected, $actual );
3125 public function testAllowsPropertyChange() {
3127 foreach ( [ 'primary', 'secondary' ] as $key ) {
3128 $class = ucfirst( $key ) . 'AuthenticationProvider';
3129 $mocks[$key] = $this->getMockForAbstractClass(
3130 "MediaWiki\\Auth\\$class", [], "Mock$class"
3132 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3133 ->will( $this->returnValue( $key ) );
3134 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
3135 ->will( $this->returnCallback( function ( $prop ) use ( $key ) {
3136 return $prop !== $key;
3140 $this->primaryauthMocks
= [ $mocks['primary'] ];
3141 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
3142 $this->initializeManager( true );
3144 $this->assertTrue( $this->manager
->allowsPropertyChange( 'foo' ) );
3145 $this->assertFalse( $this->manager
->allowsPropertyChange( 'primary' ) );
3146 $this->assertFalse( $this->manager
->allowsPropertyChange( 'secondary' ) );
3149 public function testAutoCreateOnLogin() {
3150 $username = self
::usernameForCreation();
3152 $req = $this->getMock( AuthenticationRequest
::class );
3154 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3155 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3156 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3157 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3158 $mock->expects( $this->any() )->method( 'accountCreationType' )
3159 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3160 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3161 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3162 ->will( $this->returnValue( StatusValue
::newGood() ) );
3164 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3165 $mock2->expects( $this->any() )->method( 'getUniqueId' )
3166 ->will( $this->returnValue( 'secondary' ) );
3167 $mock2->expects( $this->any() )->method( 'beginSecondaryAuthentication' )->will(
3169 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) )
3172 $mock2->expects( $this->any() )->method( 'continueSecondaryAuthentication' )
3173 ->will( $this->returnValue( AuthenticationResponse
::newAbstain() ) );
3174 $mock2->expects( $this->any() )->method( 'testUserForCreation' )
3175 ->will( $this->returnValue( StatusValue
::newGood() ) );
3177 $this->primaryauthMocks
= [ $mock ];
3178 $this->secondaryauthMocks
= [ $mock2 ];
3179 $this->initializeManager( true );
3180 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3181 $session = $this->request
->getSession();
3184 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3187 $callback = $this->callback( function ( $user ) use ( $username ) {
3188 return $user->getName() === $username;
3191 $this->hook( 'UserLoggedIn', $this->never() );
3192 $this->hook( 'LocalUserCreated', $this->once() )->with( $callback, $this->equalTo( true ) );
3193 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3194 $this->unhook( 'LocalUserCreated' );
3195 $this->unhook( 'UserLoggedIn' );
3196 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
3198 $id = (int)\User
::newFromName( $username )->getId();
3199 $this->assertNotSame( 0, \User
::newFromName( $username )->getId() );
3200 $this->assertSame( 0, $session->getUser()->getId() );
3202 $this->hook( 'UserLoggedIn', $this->once() )->with( $callback );
3203 $this->hook( 'LocalUserCreated', $this->never() );
3204 $ret = $this->manager
->continueAuthentication( [] );
3205 $this->unhook( 'LocalUserCreated' );
3206 $this->unhook( 'UserLoggedIn' );
3207 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
3208 $this->assertSame( $username, $ret->username
);
3209 $this->assertSame( $id, $session->getUser()->getId() );
3212 public function testAutoCreateFailOnLogin() {
3213 $username = self
::usernameForCreation();
3215 $mock = $this->getMockForAbstractClass(
3216 PrimaryAuthenticationProvider
::class, [], "MockPrimaryAuthenticationProvider" );
3217 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3218 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3219 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3220 $mock->expects( $this->any() )->method( 'accountCreationType' )
3221 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3222 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3223 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3224 ->will( $this->returnValue( StatusValue
::newFatal( 'fail-from-primary' ) ) );
3226 $this->primaryauthMocks
= [ $mock ];
3227 $this->initializeManager( true );
3228 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3229 $session = $this->request
->getSession();
3232 $this->assertSame( 0, $session->getUser()->getId(),
3234 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3237 $this->hook( 'UserLoggedIn', $this->never() );
3238 $this->hook( 'LocalUserCreated', $this->never() );
3239 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3240 $this->unhook( 'LocalUserCreated' );
3241 $this->unhook( 'UserLoggedIn' );
3242 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3243 $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message
->getKey() );
3245 $this->assertSame( 0, \User
::newFromName( $username )->getId() );
3246 $this->assertSame( 0, $session->getUser()->getId() );
3249 public function testAuthenticationSessionData() {
3250 $this->initializeManager( true );
3252 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3253 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3254 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3255 $this->assertSame( 'foo!', $this->manager
->getAuthenticationSessionData( 'foo' ) );
3256 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3257 $this->manager
->removeAuthenticationSessionData( 'foo' );
3258 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3259 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3260 $this->manager
->removeAuthenticationSessionData( 'bar' );
3261 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3263 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3264 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3265 $this->manager
->removeAuthenticationSessionData( null );
3266 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3267 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3271 public function testCanLinkAccounts() {
3273 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
3274 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
3275 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
3278 foreach ( $types as $type => $can ) {
3279 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3280 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
3281 $mock->expects( $this->any() )->method( 'accountCreationType' )
3282 ->will( $this->returnValue( $type ) );
3283 $this->primaryauthMocks
= [ $mock ];
3284 $this->initializeManager( true );
3285 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
3289 public function testBeginAccountLink() {
3290 $user = \User
::newFromName( 'UTSysop' );
3291 $this->initializeManager();
3293 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', 'test' );
3295 $this->manager
->beginAccountLink( $user, [], 'http://localhost/' );
3296 $this->fail( 'Expected exception not thrown' );
3297 } catch ( \LogicException
$ex ) {
3298 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3300 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3302 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3303 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3304 $mock->expects( $this->any() )->method( 'accountCreationType' )
3305 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3306 $this->primaryauthMocks
= [ $mock ];
3307 $this->initializeManager( true );
3309 $ret = $this->manager
->beginAccountLink( new \User
, [], 'http://localhost/' );
3310 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3311 $this->assertSame( 'noname', $ret->message
->getKey() );
3313 $ret = $this->manager
->beginAccountLink(
3314 \User
::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
3316 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3317 $this->assertSame( 'authmanager-userdoesnotexist', $ret->message
->getKey() );
3320 public function testContinueAccountLink() {
3321 $user = \User
::newFromName( 'UTSysop' );
3322 $this->initializeManager();
3325 'userid' => $user->getId(),
3326 'username' => $user->getName(),
3331 $this->manager
->continueAccountLink( [] );
3332 $this->fail( 'Expected exception not thrown' );
3333 } catch ( \LogicException
$ex ) {
3334 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3337 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3338 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3339 $mock->expects( $this->any() )->method( 'accountCreationType' )
3340 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3341 $mock->expects( $this->any() )->method( 'beginPrimaryAccountLink' )->will(
3342 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
3344 $this->primaryauthMocks
= [ $mock ];
3345 $this->initializeManager( true );
3347 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', null );
3348 $ret = $this->manager
->continueAccountLink( [] );
3349 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3350 $this->assertSame( 'authmanager-link-not-in-progress', $ret->message
->getKey() );
3352 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3353 [ 'username' => $user->getName() . '<>' ] +
$session );
3354 $ret = $this->manager
->continueAccountLink( [] );
3355 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3356 $this->assertSame( 'noname', $ret->message
->getKey() );
3357 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3359 $id = $user->getId();
3360 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3361 [ 'userid' => $id +
1 ] +
$session );
3363 $ret = $this->manager
->continueAccountLink( [] );
3364 $this->fail( 'Expected exception not thrown' );
3365 } catch ( \UnexpectedValueException
$ex ) {
3366 $this->assertEquals(
3367 "User \"{$user->getName()}\" is valid, but ID $id != " . ( $id +
1 ) . '!',
3371 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3375 * @dataProvider provideAccountLink
3376 * @param StatusValue $preTest
3377 * @param array $primaryResponses
3378 * @param array $managerResponses
3380 public function testAccountLink(
3381 StatusValue
$preTest, array $primaryResponses, array $managerResponses
3383 $user = \User
::newFromName( 'UTSysop' );
3385 $this->initializeManager();
3387 // Set up lots of mocks...
3388 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3389 $req->primary
= $primaryResponses;
3392 foreach ( [ 'pre', 'primary' ] as $key ) {
3393 $class = ucfirst( $key ) . 'AuthenticationProvider';
3394 $mocks[$key] = $this->getMockForAbstractClass(
3395 "MediaWiki\\Auth\\$class", [], "Mock$class"
3397 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3398 ->will( $this->returnValue( $key ) );
3400 for ( $i = 2; $i <= 3; $i++
) {
3401 $mocks[$key . $i] = $this->getMockForAbstractClass(
3402 "MediaWiki\\Auth\\$class", [], "Mock$class"
3404 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
3405 ->will( $this->returnValue( $key . $i ) );
3409 $mocks['pre']->expects( $this->any() )->method( 'testForAccountLink' )
3410 ->will( $this->returnCallback(
3412 use ( $user, $preTest )
3414 $this->assertSame( $user->getId(), $u->getId() );
3415 $this->assertSame( $user->getName(), $u->getName() );
3420 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
3421 ->will( $this->returnValue( StatusValue
::newGood() ) );
3423 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
3424 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3425 $ct = count( $req->primary
);
3426 $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req ) {
3427 $this->assertSame( $user->getId(), $u->getId() );
3428 $this->assertSame( $user->getName(), $u->getName() );
3430 foreach ( $reqs as $r ) {
3431 $this->assertSame( $user->getName(), $r->username
);
3432 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
3434 $this->assertTrue( $foundReq, '$reqs contains $req' );
3435 return array_shift( $req->primary
);
3437 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
3438 ->method( 'beginPrimaryAccountLink' )
3439 ->will( $callback );
3440 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
3441 ->method( 'continuePrimaryAccountLink' )
3442 ->will( $callback );
3444 $abstain = AuthenticationResponse
::newAbstain();
3445 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
3446 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3447 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
3448 ->will( $this->returnValue( $abstain ) );
3449 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3450 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
3451 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3452 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
3453 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3455 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
3456 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
3457 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
3458 return $level === LogLevel
::DEBUG ?
null : $message;
3460 $this->initializeManager( true );
3462 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
3463 $this->equalTo( AuthenticationResponse
::PASS
),
3464 $this->equalTo( AuthenticationResponse
::FAIL
)
3466 $providers = array_merge( $this->preauthMocks
, $this->primaryauthMocks
);
3467 foreach ( $providers as $p ) {
3468 $p->postCalled
= false;
3469 $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
3470 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
3471 $this->assertInstanceOf( 'User', $user );
3472 $this->assertSame( 'UTSysop', $user->getName() );
3473 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
3474 $this->assertThat( $response->status
, $constraint );
3475 $p->postCalled
= $response->status
;
3482 foreach ( $managerResponses as $i => $response ) {
3483 if ( $response instanceof AuthenticationResponse
&&
3484 $response->status
=== AuthenticationResponse
::PASS
3486 $expectLog[] = [ LogLevel
::INFO
, 'Account linked to {user} by primary' ];
3492 $ret = $this->manager
->beginAccountLink( $user, [ $req ], 'http://localhost/' );
3494 $ret = $this->manager
->continueAccountLink( [ $req ] );
3496 if ( $response instanceof \Exception
) {
3497 $this->fail( 'Expected exception not thrown', "Response $i" );
3499 } catch ( \Exception
$ex ) {
3500 if ( !$response instanceof \Exception
) {
3503 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
3504 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3505 "Response $i, exception, session state" );
3509 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
3511 $ret->message
= $this->message( $ret->message
);
3512 $this->assertEquals( $response, $ret, "Response $i, response" );
3513 if ( $response->status
=== AuthenticationResponse
::PASS ||
3514 $response->status
=== AuthenticationResponse
::FAIL
3516 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3517 "Response $i, session state" );
3518 foreach ( $providers as $p ) {
3519 $this->assertSame( $response->status
, $p->postCalled
,
3520 "Response $i, post-auth callback called" );
3523 $this->assertNotNull(
3524 $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3525 "Response $i, session state"
3527 foreach ( $ret->neededRequests
as $neededReq ) {
3528 $this->assertEquals( AuthManager
::ACTION_LINK
, $neededReq->action
,
3529 "Response $i, neededRequest action" );
3531 $this->assertEquals(
3532 $ret->neededRequests
,
3533 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LINK_CONTINUE
),
3534 "Response $i, continuation check"
3536 foreach ( $providers as $p ) {
3537 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
3544 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
3547 public function provideAccountLink() {
3548 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3549 $good = StatusValue
::newGood();
3552 'Pre-link test fail in pre' => [
3553 StatusValue
::newFatal( 'fail-from-pre' ),
3556 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
3559 'Failure in primary' => [
3562 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
3566 'All primary abstain' => [
3569 AuthenticationResponse
::newAbstain(),
3572 AuthenticationResponse
::newFail( $this->message( 'authmanager-link-no-primary' ) )
3575 'Primary UI, then redirect, then fail' => [
3578 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3579 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
3580 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
3584 'Primary redirect, then abstain' => [
3587 $tmp = AuthenticationResponse
::newRedirect(
3588 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
3590 AuthenticationResponse
::newAbstain(),
3594 new \
DomainException(
3595 'MockPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
3599 'Primary UI, then pass' => [
3602 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3603 AuthenticationResponse
::newPass(),
3607 AuthenticationResponse
::newPass( '' ),
3613 AuthenticationResponse
::newPass( '' ),
3616 AuthenticationResponse
::newPass( '' ),