3 namespace MediaWiki\Auth
;
5 use MediaWiki\Session\SessionInfo
;
6 use MediaWiki\Session\UserInfo
;
9 use Wikimedia\ScopedCallback
;
10 use Wikimedia\TestingAccessWrapper
;
15 * @covers MediaWiki\Auth\AuthManager
17 class AuthManagerTest
extends \MediaWikiTestCase
{
18 /** @var WebRequest */
22 /** @var \\Psr\\Log\\LoggerInterface */
25 protected $preauthMocks = [];
26 protected $primaryauthMocks = [];
27 protected $secondaryauthMocks = [];
29 /** @var AuthManager */
31 /** @var TestingAccessWrapper */
32 protected $managerPriv;
34 protected function setUp() {
37 $this->setMwGlobals( [ 'wgAuth' => null ] );
38 $this->stashMwGlobals( [ 'wgHooks' ] );
42 * Sets a mock on a hook
44 * @param object $expect From $this->once(), $this->never(), etc.
45 * @return object $mock->expects( $expect )->method( ... ).
47 protected function hook( $hook, $expect ) {
49 $mock = $this->getMockBuilder( __CLASS__
)
50 ->setMethods( [ "on$hook" ] )
52 $wgHooks[$hook] = [ $mock ];
53 return $mock->expects( $expect )->method( "on$hook" );
60 protected function unhook( $hook ) {
66 * Ensure a value is a clean Message object
67 * @param string|Message $key
68 * @param array $params
71 protected function message( $key, $params = [] ) {
72 if ( $key === null ) {
75 if ( $key instanceof \MessageSpecifier
) {
76 $params = $key->getParams();
77 $key = $key->getKey();
79 return new \
Message( $key, $params, \Language
::factory( 'en' ) );
83 * Initialize the AuthManagerConfig variable in $this->config
85 * Uses data from the various 'mocks' fields.
87 protected function initializeConfig() {
97 foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
98 $key = $type . 'Mocks';
99 foreach ( $this->$key as $mock ) {
100 $config[$type][$mock->getUniqueId()] = [ 'factory' => function () use ( $mock ) {
106 $this->config
->set( 'AuthManagerConfig', $config );
107 $this->config
->set( 'LanguageCode', 'en' );
108 $this->config
->set( 'NewUserLog', false );
112 * Initialize $this->manager
113 * @param bool $regen Force a call to $this->initializeConfig()
115 protected function initializeManager( $regen = false ) {
116 if ( $regen ||
!$this->config
) {
117 $this->config
= new \
HashConfig();
119 if ( $regen ||
!$this->request
) {
120 $this->request
= new \
FauxRequest();
122 if ( !$this->logger
) {
123 $this->logger
= new \
TestLogger();
126 if ( $regen ||
!$this->config
->has( 'AuthManagerConfig' ) ) {
127 $this->initializeConfig();
129 $this->manager
= new AuthManager( $this->request
, $this->config
);
130 $this->manager
->setLogger( $this->logger
);
131 $this->managerPriv
= TestingAccessWrapper
::newFromObject( $this->manager
);
135 * Setup SessionManager with a mock session provider
136 * @param bool|null $canChangeUser If non-null, canChangeUser will be mocked to return this
137 * @param array $methods Additional methods to mock
138 * @return array (MediaWiki\Session\SessionProvider, ScopedCallback)
140 protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
141 if ( !$this->config
) {
142 $this->config
= new \
HashConfig();
143 $this->initializeConfig();
145 $this->config
->set( 'ObjectCacheSessionExpiry', 100 );
147 $methods[] = '__toString';
148 $methods[] = 'describe';
149 if ( $canChangeUser !== null ) {
150 $methods[] = 'canChangeUser';
152 $provider = $this->getMockBuilder( 'DummySessionProvider' )
153 ->setMethods( $methods )
155 $provider->expects( $this->any() )->method( '__toString' )
156 ->will( $this->returnValue( 'MockSessionProvider' ) );
157 $provider->expects( $this->any() )->method( 'describe' )
158 ->will( $this->returnValue( 'MockSessionProvider sessions' ) );
159 if ( $canChangeUser !== null ) {
160 $provider->expects( $this->any() )->method( 'canChangeUser' )
161 ->will( $this->returnValue( $canChangeUser ) );
163 $this->config
->set( 'SessionProviders', [
164 [ 'factory' => function () use ( $provider ) {
169 $manager = new \MediaWiki\Session\
SessionManager( [
170 'config' => $this->config
,
171 'logger' => new \Psr\Log\
NullLogger(),
172 'store' => new \
HashBagOStuff(),
174 TestingAccessWrapper
::newFromObject( $manager )->getProvider( (string)$provider );
176 $reset = \MediaWiki\Session\TestUtils
::setSessionManagerSingleton( $manager );
178 if ( $this->request
) {
179 $manager->getSessionForRequest( $this->request
);
182 return [ $provider, $reset ];
185 public function testSingleton() {
186 // Temporarily clear out the global singleton, if any, to test creating
188 $rProp = new \
ReflectionProperty( AuthManager
::class, 'instance' );
189 $rProp->setAccessible( true );
190 $old = $rProp->getValue();
191 $cb = new ScopedCallback( [ $rProp, 'setValue' ], [ $old ] );
192 $rProp->setValue( null );
194 $singleton = AuthManager
::singleton();
195 $this->assertInstanceOf( AuthManager
::class, AuthManager
::singleton() );
196 $this->assertSame( $singleton, AuthManager
::singleton() );
197 $this->assertSame( \RequestContext
::getMain()->getRequest(), $singleton->getRequest() );
199 \RequestContext
::getMain()->getConfig(),
200 TestingAccessWrapper
::newFromObject( $singleton )->config
204 public function testCanAuthenticateNow() {
205 $this->initializeManager();
207 list( $provider, $reset ) = $this->getMockSessionProvider( false );
208 $this->assertFalse( $this->manager
->canAuthenticateNow() );
209 ScopedCallback
::consume( $reset );
211 list( $provider, $reset ) = $this->getMockSessionProvider( true );
212 $this->assertTrue( $this->manager
->canAuthenticateNow() );
213 ScopedCallback
::consume( $reset );
216 public function testNormalizeUsername() {
218 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
219 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
220 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
221 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
223 foreach ( $mocks as $key => $mock ) {
224 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
226 $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
227 ->with( $this->identicalTo( 'XYZ' ) )
228 ->willReturn( 'Foo' );
229 $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
230 ->with( $this->identicalTo( 'XYZ' ) )
231 ->willReturn( 'Foo' );
232 $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
233 ->with( $this->identicalTo( 'XYZ' ) )
234 ->willReturn( null );
235 $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
236 ->with( $this->identicalTo( 'XYZ' ) )
237 ->willReturn( 'Bar!' );
239 $this->primaryauthMocks
= $mocks;
241 $this->initializeManager();
243 $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager
->normalizeUsername( 'XYZ' ) );
247 * @dataProvider provideSecuritySensitiveOperationStatus
248 * @param bool $mutableSession
250 public function testSecuritySensitiveOperationStatus( $mutableSession ) {
251 $this->logger
= new \Psr\Log\
NullLogger();
252 $user = \User
::newFromName( 'UTSysop' );
254 $reauth = $mutableSession ? AuthManager
::SEC_REAUTH
: AuthManager
::SEC_FAIL
;
256 list( $provider, $reset ) = $this->getMockSessionProvider(
257 $mutableSession, [ 'provideSessionInfo' ]
259 $provider->expects( $this->any() )->method( 'provideSessionInfo' )
260 ->will( $this->returnCallback( function () use ( $provider, &$provideUser ) {
261 return new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
262 'provider' => $provider,
263 'id' => \DummySessionProvider
::ID
,
265 'userInfo' => UserInfo
::newFromUser( $provideUser, true )
268 $this->initializeManager();
270 $this->config
->set( 'ReauthenticateTime', [] );
271 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [] );
272 $provideUser = new \User
;
273 $session = $provider->getManager()->getSessionForRequest( $this->request
);
274 $this->assertSame( 0, $session->getUser()->getId(), 'sanity check' );
276 // Anonymous user => reauth
277 $session->set( 'AuthManager:lastAuthId', 0 );
278 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
279 $this->assertSame( $reauth, $this->manager
->securitySensitiveOperationStatus( 'foo' ) );
281 $provideUser = $user;
282 $session = $provider->getManager()->getSessionForRequest( $this->request
);
283 $this->assertSame( $user->getId(), $session->getUser()->getId(), 'sanity check' );
285 // Error for no default (only gets thrown for non-anonymous user)
286 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
287 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
289 $this->manager
->securitySensitiveOperationStatus( 'foo' );
290 $this->fail( 'Expected exception not thrown' );
291 } catch ( \UnexpectedValueException
$ex ) {
294 ?
'$wgReauthenticateTime lacks a default'
295 : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
300 if ( $mutableSession ) {
301 $this->config
->set( 'ReauthenticateTime', [
307 // Mismatched user ID
308 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
309 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
311 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
314 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
317 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
321 $session->set( 'AuthManager:lastAuthId', $user->getId() );
322 $session->set( 'AuthManager:lastAuthTimestamp', null );
324 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
327 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
330 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
333 // Recent enough to pass
334 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
336 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
339 // Not recent enough to pass
340 $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
342 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
344 // But recent enough for the 'test' operation
346 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test' )
349 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [
355 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
359 AuthManager
::SEC_FAIL
, $this->manager
->securitySensitiveOperationStatus( 'test' )
363 // Test hook, all three possible values
365 AuthManager
::SEC_OK
=> AuthManager
::SEC_OK
,
366 AuthManager
::SEC_REAUTH
=> $reauth,
367 AuthManager
::SEC_FAIL
=> AuthManager
::SEC_FAIL
,
368 ] as $hook => $expect ) {
369 $this->hook( 'SecuritySensitiveOperationStatus', $this->exactly( 2 ) )
373 $this->callback( function ( $s ) use ( $session ) {
374 return $s->getId() === $session->getId();
376 $mutableSession ?
$this->equalTo( 500, 1 ) : $this->equalTo( -1 )
378 ->will( $this->returnCallback( function ( &$v ) use ( $hook ) {
382 $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
384 $expect, $this->manager
->securitySensitiveOperationStatus( 'test' ), "hook $hook"
387 $expect, $this->manager
->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
389 $this->unhook( 'SecuritySensitiveOperationStatus' );
392 ScopedCallback
::consume( $reset );
395 public function onSecuritySensitiveOperationStatus( &$status, $operation, $session, $time ) {
398 public static function provideSecuritySensitiveOperationStatus() {
406 * @dataProvider provideUserCanAuthenticate
407 * @param bool $primary1Can
408 * @param bool $primary2Can
409 * @param bool $expect
411 public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
412 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
413 $mock1->expects( $this->any() )->method( 'getUniqueId' )
414 ->will( $this->returnValue( 'primary1' ) );
415 $mock1->expects( $this->any() )->method( 'testUserCanAuthenticate' )
416 ->with( $this->equalTo( 'UTSysop' ) )
417 ->will( $this->returnValue( $primary1Can ) );
418 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
419 $mock2->expects( $this->any() )->method( 'getUniqueId' )
420 ->will( $this->returnValue( 'primary2' ) );
421 $mock2->expects( $this->any() )->method( 'testUserCanAuthenticate' )
422 ->with( $this->equalTo( 'UTSysop' ) )
423 ->will( $this->returnValue( $primary2Can ) );
424 $this->primaryauthMocks
= [ $mock1, $mock2 ];
426 $this->initializeManager( true );
427 $this->assertSame( $expect, $this->manager
->userCanAuthenticate( 'UTSysop' ) );
430 public static function provideUserCanAuthenticate() {
432 [ false, false, false ],
433 [ true, false, true ],
434 [ false, true, true ],
435 [ true, true, true ],
439 public function testRevokeAccessForUser() {
440 $this->initializeManager();
442 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
443 $mock->expects( $this->any() )->method( 'getUniqueId' )
444 ->will( $this->returnValue( 'primary' ) );
445 $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
446 ->with( $this->equalTo( 'UTSysop' ) );
447 $this->primaryauthMocks
= [ $mock ];
449 $this->initializeManager( true );
450 $this->logger
->setCollect( true );
452 $this->manager
->revokeAccessForUser( 'UTSysop' );
455 [ LogLevel
::INFO
, 'Revoking access for {user}' ],
456 ], $this->logger
->getBuffer() );
459 public function testProviderCreation() {
461 'pre' => $this->getMockForAbstractClass( PreAuthenticationProvider
::class ),
462 'primary' => $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
463 'secondary' => $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class ),
465 foreach ( $mocks as $key => $mock ) {
466 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
467 $mock->expects( $this->once() )->method( 'setLogger' );
468 $mock->expects( $this->once() )->method( 'setManager' );
469 $mock->expects( $this->once() )->method( 'setConfig' );
471 $this->preauthMocks
= [ $mocks['pre'] ];
472 $this->primaryauthMocks
= [ $mocks['primary'] ];
473 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
476 $this->initializeManager();
479 $this->managerPriv
->getAuthenticationProvider( 'primary' )
483 $this->managerPriv
->getAuthenticationProvider( 'secondary' )
487 $this->managerPriv
->getAuthenticationProvider( 'pre' )
490 [ 'pre' => $mocks['pre'] ],
491 $this->managerPriv
->getPreAuthenticationProviders()
494 [ 'primary' => $mocks['primary'] ],
495 $this->managerPriv
->getPrimaryAuthenticationProviders()
498 [ 'secondary' => $mocks['secondary'] ],
499 $this->managerPriv
->getSecondaryAuthenticationProviders()
503 $mock1 = $this->getMockForAbstractClass( PreAuthenticationProvider
::class );
504 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
505 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
506 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
507 $this->preauthMocks
= [ $mock1 ];
508 $this->primaryauthMocks
= [ $mock2 ];
509 $this->secondaryauthMocks
= [];
510 $this->initializeManager( true );
512 $this->managerPriv
->getAuthenticationProvider( 'Y' );
513 $this->fail( 'Expected exception not thrown' );
514 } catch ( \RuntimeException
$ex ) {
515 $class1 = get_class( $mock1 );
516 $class2 = get_class( $mock2 );
518 "Duplicate specifications for id X (classes $class1 and $class2)", $ex->getMessage()
523 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
524 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
525 $class = get_class( $mock );
526 $this->preauthMocks
= [ $mock ];
527 $this->primaryauthMocks
= [ $mock ];
528 $this->secondaryauthMocks
= [ $mock ];
529 $this->initializeManager( true );
531 $this->managerPriv
->getPreAuthenticationProviders();
532 $this->fail( 'Expected exception not thrown' );
533 } catch ( \RuntimeException
$ex ) {
535 "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
540 $this->managerPriv
->getPrimaryAuthenticationProviders();
541 $this->fail( 'Expected exception not thrown' );
542 } catch ( \RuntimeException
$ex ) {
544 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
549 $this->managerPriv
->getSecondaryAuthenticationProviders();
550 $this->fail( 'Expected exception not thrown' );
551 } catch ( \RuntimeException
$ex ) {
553 "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
559 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
560 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
561 $mock3 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
562 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
563 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
564 $mock3->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'C' ) );
565 $this->preauthMocks
= [];
566 $this->primaryauthMocks
= [ $mock1, $mock2, $mock3 ];
567 $this->secondaryauthMocks
= [];
568 $this->initializeConfig();
569 $config = $this->config
->get( 'AuthManagerConfig' );
571 $this->initializeManager( false );
573 [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
574 $this->managerPriv
->getPrimaryAuthenticationProviders(),
578 $config['primaryauth']['A']['sort'] = 100;
579 $config['primaryauth']['C']['sort'] = -1;
580 $this->config
->set( 'AuthManagerConfig', $config );
581 $this->initializeManager( false );
583 [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
584 $this->managerPriv
->getPrimaryAuthenticationProviders()
588 public function testSetDefaultUserOptions() {
589 $this->initializeManager();
591 $context = \RequestContext
::getMain();
592 $reset = new ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
593 $context->setLanguage( 'de' );
594 $this->setMwGlobals( 'wgContLang', \Language
::factory( 'zh' ) );
596 $user = \User
::newFromName( self
::usernameForCreation() );
597 $user->addToDatabase();
598 $oldToken = $user->getToken();
599 $this->managerPriv
->setDefaultUserOptions( $user, false );
600 $user->saveSettings();
601 $this->assertNotEquals( $oldToken, $user->getToken() );
602 $this->assertSame( 'zh', $user->getOption( 'language' ) );
603 $this->assertSame( 'zh', $user->getOption( 'variant' ) );
605 $user = \User
::newFromName( self
::usernameForCreation() );
606 $user->addToDatabase();
607 $oldToken = $user->getToken();
608 $this->managerPriv
->setDefaultUserOptions( $user, true );
609 $user->saveSettings();
610 $this->assertNotEquals( $oldToken, $user->getToken() );
611 $this->assertSame( 'de', $user->getOption( 'language' ) );
612 $this->assertSame( 'zh', $user->getOption( 'variant' ) );
614 $this->setMwGlobals( 'wgContLang', \Language
::factory( 'fr' ) );
616 $user = \User
::newFromName( self
::usernameForCreation() );
617 $user->addToDatabase();
618 $oldToken = $user->getToken();
619 $this->managerPriv
->setDefaultUserOptions( $user, true );
620 $user->saveSettings();
621 $this->assertNotEquals( $oldToken, $user->getToken() );
622 $this->assertSame( 'de', $user->getOption( 'language' ) );
623 $this->assertSame( null, $user->getOption( 'variant' ) );
626 public function testForcePrimaryAuthenticationProviders() {
627 $mockA = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
628 $mockB = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
629 $mockB2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
630 $mockA->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
631 $mockB->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
632 $mockB2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
633 $this->primaryauthMocks
= [ $mockA ];
635 $this->logger
= new \
TestLogger( true );
637 // Test without first initializing the configured providers
638 $this->initializeManager();
639 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
641 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
643 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
644 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
646 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
647 ], $this->logger
->getBuffer() );
648 $this->logger
->clearBuffer();
650 // Test with first initializing the configured providers
651 $this->initializeManager();
652 $this->assertSame( $mockA, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
653 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
654 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
655 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
656 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
658 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
660 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
661 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
662 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
664 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
667 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
670 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
672 ], $this->logger
->getBuffer() );
673 $this->logger
->clearBuffer();
675 // Test duplicate IDs
676 $this->initializeManager();
678 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
679 $this->fail( 'Expected exception not thrown' );
680 } catch ( \RuntimeException
$ex ) {
681 $class1 = get_class( $mockB );
682 $class2 = get_class( $mockB2 );
684 "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
689 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
690 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
691 $class = get_class( $mock );
693 $this->manager
->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
694 $this->fail( 'Expected exception not thrown' );
695 } catch ( \RuntimeException
$ex ) {
697 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
703 public function testBeginAuthentication() {
704 $this->initializeManager();
707 list( $provider, $reset ) = $this->getMockSessionProvider( false );
708 $this->hook( 'UserLoggedIn', $this->never() );
709 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
711 $this->manager
->beginAuthentication( [], 'http://localhost/' );
712 $this->fail( 'Expected exception not thrown' );
713 } catch ( \LogicException
$ex ) {
714 $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
716 $this->unhook( 'UserLoggedIn' );
717 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
718 ScopedCallback
::consume( $reset );
719 $this->initializeManager( true );
721 // CreatedAccountAuthenticationRequest
722 $user = \User
::newFromName( 'UTSysop' );
724 new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
726 $this->hook( 'UserLoggedIn', $this->never() );
728 $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
729 $this->fail( 'Expected exception not thrown' );
730 } catch ( \LogicException
$ex ) {
732 'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
733 'that created the account',
737 $this->unhook( 'UserLoggedIn' );
739 $this->request
->getSession()->clear();
740 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
741 $this->managerPriv
->createdAccountAuthenticationRequests
= [ $reqs[0] ];
742 $this->hook( 'UserLoggedIn', $this->once() )
743 ->with( $this->callback( function ( $u ) use ( $user ) {
744 return $user->getId() === $u->getId() && $user->getName() === $u->getName();
746 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
747 $this->logger
->setCollect( true );
748 $ret = $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
749 $this->logger
->setCollect( false );
750 $this->unhook( 'UserLoggedIn' );
751 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
752 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
753 $this->assertSame( $user->getName(), $ret->username
);
754 $this->assertSame( $user->getId(), $this->request
->getSessionData( 'AuthManager:lastAuthId' ) );
756 time(), $this->request
->getSessionData( 'AuthManager:lastAuthTimestamp' ),
759 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
760 $this->assertSame( $user->getId(), $this->request
->getSession()->getUser()->getId() );
762 [ LogLevel
::INFO
, 'Logging in {user} after account creation' ],
763 ], $this->logger
->getBuffer() );
766 public function testCreateFromLogin() {
767 $user = \User
::newFromName( 'UTSysop' );
768 $req1 = $this->createMock( AuthenticationRequest
::class );
769 $req2 = $this->createMock( AuthenticationRequest
::class );
770 $req3 = $this->createMock( AuthenticationRequest
::class );
771 $userReq = new UsernameAuthenticationRequest
;
772 $userReq->username
= 'UTDummy';
774 $req1->returnToUrl
= 'http://localhost/';
775 $req2->returnToUrl
= 'http://localhost/';
776 $req3->returnToUrl
= 'http://localhost/';
777 $req3->username
= 'UTDummy';
778 $userReq->returnToUrl
= 'http://localhost/';
780 // Passing one into beginAuthentication(), and an immediate FAIL
781 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
782 $this->primaryauthMocks
= [ $primary ];
783 $this->initializeManager( true );
784 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
785 $res->createRequest
= $req1;
786 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
787 ->will( $this->returnValue( $res ) );
788 $createReq = new CreateFromLoginAuthenticationRequest(
789 null, [ $req2->getUniqueId() => $req2 ]
791 $this->logger
->setCollect( true );
792 $ret = $this->manager
->beginAuthentication( [ $createReq ], 'http://localhost/' );
793 $this->logger
->setCollect( false );
794 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
795 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
796 $this->assertSame( $req1, $ret->createRequest
->createRequest
);
797 $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest
->maybeLink
);
799 // UI, then FAIL in beginAuthentication()
800 $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider
::class )
801 ->setMethods( [ 'continuePrimaryAuthentication' ] )
802 ->getMockForAbstractClass();
803 $this->primaryauthMocks
= [ $primary ];
804 $this->initializeManager( true );
805 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
806 ->will( $this->returnValue(
807 AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) )
809 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
810 $res->createRequest
= $req2;
811 $primary->expects( $this->any() )->method( 'continuePrimaryAuthentication' )
812 ->will( $this->returnValue( $res ) );
813 $this->logger
->setCollect( true );
814 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
815 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
, 'sanity check' );
816 $ret = $this->manager
->continueAuthentication( [] );
817 $this->logger
->setCollect( false );
818 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
819 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
820 $this->assertSame( $req2, $ret->createRequest
->createRequest
);
821 $this->assertEquals( [], $ret->createRequest
->maybeLink
);
823 // Pass into beginAccountCreation(), see that maybeLink and createRequest get copied
824 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
825 $this->primaryauthMocks
= [ $primary ];
826 $this->initializeManager( true );
827 $createReq = new CreateFromLoginAuthenticationRequest( $req3, [ $req2 ] );
828 $createReq->returnToUrl
= 'http://localhost/';
829 $createReq->username
= 'UTDummy';
830 $res = AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) );
831 $primary->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
832 ->with( $this->anything(), $this->anything(), [ $userReq, $createReq, $req3 ] )
833 ->will( $this->returnValue( $res ) );
834 $primary->expects( $this->any() )->method( 'accountCreationType' )
835 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
836 $this->logger
->setCollect( true );
837 $ret = $this->manager
->beginAccountCreation(
838 $user, [ $userReq, $createReq ], 'http://localhost/'
840 $this->logger
->setCollect( false );
841 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
842 $state = $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' );
843 $this->assertNotNull( $state );
844 $this->assertEquals( [ $userReq, $createReq, $req3 ], $state['reqs'] );
845 $this->assertEquals( [ $req2 ], $state['maybeLink'] );
849 * @dataProvider provideAuthentication
850 * @param StatusValue $preResponse
851 * @param array $primaryResponses
852 * @param array $secondaryResponses
853 * @param array $managerResponses
854 * @param bool $link Whether the primary authentication provider is a "link" provider
856 public function testAuthentication(
857 StatusValue
$preResponse, array $primaryResponses, array $secondaryResponses,
858 array $managerResponses, $link = false
860 $this->initializeManager();
861 $user = \User
::newFromName( 'UTSysop' );
862 $id = $user->getId();
863 $name = $user->getName();
865 // Set up lots of mocks...
866 $req = new RememberMeAuthenticationRequest
;
867 $req->rememberMe
= (bool)rand( 0, 1 );
868 $req->pre
= $preResponse;
869 $req->primary
= $primaryResponses;
870 $req->secondary
= $secondaryResponses;
872 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
873 $class = ucfirst( $key ) . 'AuthenticationProvider';
874 $mocks[$key] = $this->getMockForAbstractClass(
875 "MediaWiki\\Auth\\$class", [], "Mock$class"
877 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
878 ->will( $this->returnValue( $key ) );
879 $mocks[$key . '2'] = $this->getMockForAbstractClass(
880 "MediaWiki\\Auth\\$class", [], "Mock$class"
882 $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
883 ->will( $this->returnValue( $key . '2' ) );
884 $mocks[$key . '3'] = $this->getMockForAbstractClass(
885 "MediaWiki\\Auth\\$class", [], "Mock$class"
887 $mocks[$key . '3']->expects( $this->any() )->method( 'getUniqueId' )
888 ->will( $this->returnValue( $key . '3' ) );
890 foreach ( $mocks as $mock ) {
891 $mock->expects( $this->any() )->method( 'getAuthenticationRequests' )
892 ->will( $this->returnValue( [] ) );
895 $mocks['pre']->expects( $this->once() )->method( 'testForAuthentication' )
896 ->will( $this->returnCallback( function ( $reqs ) use ( $req ) {
897 $this->assertContains( $req, $reqs );
901 $ct = count( $req->primary
);
902 $callback = $this->returnCallback( function ( $reqs ) use ( $req ) {
903 $this->assertContains( $req, $reqs );
904 return array_shift( $req->primary
);
906 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
907 ->method( 'beginPrimaryAuthentication' )
909 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
910 ->method( 'continuePrimaryAuthentication' )
913 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
914 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
917 $ct = count( $req->secondary
);
918 $callback = $this->returnCallback( function ( $user, $reqs ) use ( $id, $name, $req ) {
919 $this->assertSame( $id, $user->getId() );
920 $this->assertSame( $name, $user->getName() );
921 $this->assertContains( $req, $reqs );
922 return array_shift( $req->secondary
);
924 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
925 ->method( 'beginSecondaryAuthentication' )
927 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
928 ->method( 'continueSecondaryAuthentication' )
931 $abstain = AuthenticationResponse
::newAbstain();
932 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAuthentication' )
933 ->will( $this->returnValue( StatusValue
::newGood() ) );
934 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAuthentication' )
935 ->will( $this->returnValue( $abstain ) );
936 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAuthentication' );
937 $mocks['secondary2']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
938 ->will( $this->returnValue( $abstain ) );
939 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
940 $mocks['secondary3']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
941 ->will( $this->returnValue( $abstain ) );
942 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
944 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
945 $this->primaryauthMocks
= [ $mocks['primary'], $mocks['primary2'] ];
946 $this->secondaryauthMocks
= [
947 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2'],
948 // So linking happens
949 new ConfirmLinkSecondaryAuthenticationProvider
,
951 $this->initializeManager( true );
952 $this->logger
->setCollect( true );
954 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
955 $this->equalTo( AuthenticationResponse
::PASS
),
956 $this->equalTo( AuthenticationResponse
::FAIL
)
958 $providers = array_filter(
960 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
963 return is_callable( [ $p, 'expects' ] );
966 foreach ( $providers as $p ) {
967 $p->postCalled
= false;
968 $p->expects( $this->atMost( 1 ) )->method( 'postAuthentication' )
969 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
970 if ( $user !== null ) {
971 $this->assertInstanceOf( 'User', $user );
972 $this->assertSame( 'UTSysop', $user->getName() );
974 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
975 $this->assertThat( $response->status
, $constraint );
976 $p->postCalled
= $response->status
;
980 $session = $this->request
->getSession();
981 $session->setRememberUser( !$req->rememberMe
);
983 foreach ( $managerResponses as $i => $response ) {
984 $success = $response instanceof AuthenticationResponse
&&
985 $response->status
=== AuthenticationResponse
::PASS
;
987 $this->hook( 'UserLoggedIn', $this->once() )
988 ->with( $this->callback( function ( $user ) use ( $id, $name ) {
989 return $user->getId() === $id && $user->getName() === $name;
992 $this->hook( 'UserLoggedIn', $this->never() );
995 $response instanceof AuthenticationResponse
&&
996 $response->status
=== AuthenticationResponse
::FAIL
&&
997 $response->message
->getKey() !== 'authmanager-authn-not-in-progress' &&
998 $response->message
->getKey() !== 'authmanager-authn-no-primary'
1001 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
1003 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->never() );
1009 $ret = $this->manager
->beginAuthentication( [ $req ], 'http://localhost/' );
1011 $ret = $this->manager
->continueAuthentication( [ $req ] );
1013 if ( $response instanceof \Exception
) {
1014 $this->fail( 'Expected exception not thrown', "Response $i" );
1016 } catch ( \Exception
$ex ) {
1017 if ( !$response instanceof \Exception
) {
1020 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
1021 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1022 "Response $i, exception, session state" );
1023 $this->unhook( 'UserLoggedIn' );
1024 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1028 $this->unhook( 'UserLoggedIn' );
1029 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1031 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
1033 $ret->message
= $this->message( $ret->message
);
1034 $this->assertEquals( $response, $ret, "Response $i, response" );
1036 $this->assertSame( $id, $session->getUser()->getId(),
1037 "Response $i, authn" );
1039 $this->assertSame( 0, $session->getUser()->getId(),
1040 "Response $i, authn" );
1042 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
1043 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1044 "Response $i, session state" );
1045 foreach ( $providers as $p ) {
1046 $this->assertSame( $response->status
, $p->postCalled
,
1047 "Response $i, post-auth callback called" );
1050 $this->assertNotNull( $session->getSecret( 'AuthManager::authnState' ),
1051 "Response $i, session state" );
1052 foreach ( $ret->neededRequests
as $neededReq ) {
1053 $this->assertEquals( AuthManager
::ACTION_LOGIN
, $neededReq->action
,
1054 "Response $i, neededRequest action" );
1056 $this->assertEquals(
1057 $ret->neededRequests
,
1058 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN_CONTINUE
),
1059 "Response $i, continuation check"
1061 foreach ( $providers as $p ) {
1062 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
1066 $state = $session->getSecret( 'AuthManager::authnState' );
1067 $maybeLink = isset( $state['maybeLink'] ) ?
$state['maybeLink'] : [];
1068 if ( $link && $response->status
=== AuthenticationResponse
::RESTART
) {
1069 $this->assertEquals(
1070 $response->createRequest
->maybeLink
,
1072 "Response $i, maybeLink"
1075 $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
1080 $this->assertSame( $req->rememberMe
, $session->shouldRememberUser(),
1081 'rememberMe checkbox had effect' );
1083 $this->assertNotSame( $req->rememberMe
, $session->shouldRememberUser(),
1084 'rememberMe checkbox wasn\'t applied' );
1088 public function provideAuthentication() {
1089 $rememberReq = new RememberMeAuthenticationRequest
;
1090 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
1092 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1093 $req->foobar
= 'baz';
1094 $restartResponse = AuthenticationResponse
::newRestart(
1095 $this->message( 'authmanager-authn-no-local-user' )
1097 $restartResponse->neededRequests
= [ $rememberReq ];
1099 $restartResponse2Pass = AuthenticationResponse
::newPass( null );
1100 $restartResponse2Pass->linkRequest
= $req;
1101 $restartResponse2 = AuthenticationResponse
::newRestart(
1102 $this->message( 'authmanager-authn-no-local-user-link' )
1104 $restartResponse2->createRequest
= new CreateFromLoginAuthenticationRequest(
1105 null, [ $req->getUniqueId() => $req ]
1107 $restartResponse2->createRequest
->action
= AuthManager
::ACTION_LOGIN
;
1108 $restartResponse2->neededRequests
= [ $rememberReq, $restartResponse2->createRequest
];
1110 $userName = 'UTSysop';
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( $userName ),
1209 AuthenticationResponse
::newFail( $this->message( 'fail-in-secondary' ) ),
1213 'Secondary UI, then abstain' => [
1214 StatusValue
::newGood(),
1216 AuthenticationResponse
::newPass( $userName ),
1219 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1220 AuthenticationResponse
::newAbstain()
1224 AuthenticationResponse
::newPass( $userName ),
1227 'Secondary pass' => [
1228 StatusValue
::newGood(),
1230 AuthenticationResponse
::newPass( $userName ),
1233 AuthenticationResponse
::newPass()
1236 AuthenticationResponse
::newPass( $userName ),
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 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1409 $readOnlyMode->setReason( 'Because' );
1410 $this->assertEquals(
1411 \Status
::newFatal( 'readonlytext', 'Because' ),
1412 $this->manager
->checkAccountCreatePermissions( new \User
)
1414 $readOnlyMode->setReason( false );
1416 $wgGroupPermissions['*']['createaccount'] = false;
1417 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1418 $this->assertFalse( $status->isOK() );
1419 $this->assertTrue( $status->hasMessage( 'badaccess-groups' ) );
1420 $wgGroupPermissions['*']['createaccount'] = true;
1422 $user = \User
::newFromName( 'UTBlockee' );
1423 if ( $user->getID() == 0 ) {
1424 $user->addToDatabase();
1425 \TestUser
::setPasswordForUser( $user, 'UTBlockeePassword' );
1426 $user->saveSettings();
1428 $oldBlock = \Block
::newFromTarget( 'UTBlockee' );
1430 // An old block will prevent our new one from saving.
1431 $oldBlock->delete();
1434 'address' => 'UTBlockee',
1435 'user' => $user->getID(),
1436 'reason' => __METHOD__
,
1437 'expiry' => time() +
100500,
1438 'createAccount' => true,
1440 $block = new \
Block( $blockOptions );
1442 $status = $this->manager
->checkAccountCreatePermissions( $user );
1443 $this->assertFalse( $status->isOK() );
1444 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
1447 'address' => '127.0.0.0/24',
1448 'reason' => __METHOD__
,
1449 'expiry' => time() +
100500,
1450 'createAccount' => true,
1452 $block = new \
Block( $blockOptions );
1454 $scopeVariable = new ScopedCallback( [ $block, 'delete' ] );
1455 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1456 $this->assertFalse( $status->isOK() );
1457 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
1458 ScopedCallback
::consume( $scopeVariable );
1460 $this->setMwGlobals( [
1461 'wgEnableDnsBlacklist' => true,
1462 'wgDnsBlacklistUrls' => [
1463 'local.wmftest.net', // This will resolve for every subdomain, which works to test "listed?"
1465 'wgProxyWhitelist' => [],
1467 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1468 $this->assertFalse( $status->isOK() );
1469 $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
1470 $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
1471 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1472 $this->assertTrue( $status->isGood() );
1476 * @param string $uniq
1479 private static function usernameForCreation( $uniq = '' ) {
1482 $username = "UTAuthManagerTestAccountCreation" . $uniq . ++
$i;
1483 } while ( \User
::newFromName( $username )->getId() !== 0 );
1487 public function testCanCreateAccount() {
1488 $username = self
::usernameForCreation();
1489 $this->initializeManager();
1491 $this->assertEquals(
1492 \Status
::newFatal( 'authmanager-create-disabled' ),
1493 $this->manager
->canCreateAccount( $username )
1496 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1497 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1498 $mock->expects( $this->any() )->method( 'accountCreationType' )
1499 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1500 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1501 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1502 ->will( $this->returnValue( StatusValue
::newGood() ) );
1503 $this->primaryauthMocks
= [ $mock ];
1504 $this->initializeManager( true );
1506 $this->assertEquals(
1507 \Status
::newFatal( 'userexists' ),
1508 $this->manager
->canCreateAccount( $username )
1511 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1512 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1513 $mock->expects( $this->any() )->method( 'accountCreationType' )
1514 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1515 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1516 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1517 ->will( $this->returnValue( StatusValue
::newGood() ) );
1518 $this->primaryauthMocks
= [ $mock ];
1519 $this->initializeManager( true );
1521 $this->assertEquals(
1522 \Status
::newFatal( 'noname' ),
1523 $this->manager
->canCreateAccount( $username . '<>' )
1526 $this->assertEquals(
1527 \Status
::newFatal( 'userexists' ),
1528 $this->manager
->canCreateAccount( 'UTSysop' )
1531 $this->assertEquals(
1533 $this->manager
->canCreateAccount( $username )
1536 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1537 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1538 $mock->expects( $this->any() )->method( 'accountCreationType' )
1539 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1540 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1541 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1542 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1543 $this->primaryauthMocks
= [ $mock ];
1544 $this->initializeManager( true );
1546 $this->assertEquals(
1547 \Status
::newFatal( 'fail' ),
1548 $this->manager
->canCreateAccount( $username )
1552 public function testBeginAccountCreation() {
1553 $creator = \User
::newFromName( 'UTSysop' );
1554 $userReq = new UsernameAuthenticationRequest
;
1555 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1556 return $level === LogLevel
::DEBUG ?
null : $message;
1558 $this->initializeManager();
1560 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
1561 $this->hook( 'LocalUserCreated', $this->never() );
1563 $this->manager
->beginAccountCreation(
1564 $creator, [], 'http://localhost/'
1566 $this->fail( 'Expected exception not thrown' );
1567 } catch ( \LogicException
$ex ) {
1568 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1570 $this->unhook( 'LocalUserCreated' );
1572 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1575 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1576 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1577 $mock->expects( $this->any() )->method( 'accountCreationType' )
1578 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1579 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1580 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1581 ->will( $this->returnValue( StatusValue
::newGood() ) );
1582 $this->primaryauthMocks
= [ $mock ];
1583 $this->initializeManager( true );
1585 $this->hook( 'LocalUserCreated', $this->never() );
1586 $ret = $this->manager
->beginAccountCreation( $creator, [], 'http://localhost/' );
1587 $this->unhook( 'LocalUserCreated' );
1588 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1589 $this->assertSame( 'noname', $ret->message
->getKey() );
1591 $this->hook( 'LocalUserCreated', $this->never() );
1592 $userReq->username
= self
::usernameForCreation();
1593 $userReq2 = new UsernameAuthenticationRequest
;
1594 $userReq2->username
= $userReq->username
. 'X';
1595 $ret = $this->manager
->beginAccountCreation(
1596 $creator, [ $userReq, $userReq2 ], 'http://localhost/'
1598 $this->unhook( 'LocalUserCreated' );
1599 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1600 $this->assertSame( 'noname', $ret->message
->getKey() );
1602 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1603 $readOnlyMode->setReason( 'Because' );
1604 $this->hook( 'LocalUserCreated', $this->never() );
1605 $userReq->username
= self
::usernameForCreation();
1606 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1607 $this->unhook( 'LocalUserCreated' );
1608 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1609 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
1610 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1611 $readOnlyMode->setReason( false );
1613 $this->hook( 'LocalUserCreated', $this->never() );
1614 $userReq->username
= self
::usernameForCreation();
1615 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1616 $this->unhook( 'LocalUserCreated' );
1617 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1618 $this->assertSame( 'userexists', $ret->message
->getKey() );
1620 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1621 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1622 $mock->expects( $this->any() )->method( 'accountCreationType' )
1623 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1624 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1625 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1626 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1627 $this->primaryauthMocks
= [ $mock ];
1628 $this->initializeManager( true );
1630 $this->hook( 'LocalUserCreated', $this->never() );
1631 $userReq->username
= self
::usernameForCreation();
1632 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1633 $this->unhook( 'LocalUserCreated' );
1634 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1635 $this->assertSame( 'fail', $ret->message
->getKey() );
1637 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1638 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1639 $mock->expects( $this->any() )->method( 'accountCreationType' )
1640 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1641 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1642 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1643 ->will( $this->returnValue( StatusValue
::newGood() ) );
1644 $this->primaryauthMocks
= [ $mock ];
1645 $this->initializeManager( true );
1647 $this->hook( 'LocalUserCreated', $this->never() );
1648 $userReq->username
= self
::usernameForCreation() . '<>';
1649 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1650 $this->unhook( 'LocalUserCreated' );
1651 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1652 $this->assertSame( 'noname', $ret->message
->getKey() );
1654 $this->hook( 'LocalUserCreated', $this->never() );
1655 $userReq->username
= $creator->getName();
1656 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1657 $this->unhook( 'LocalUserCreated' );
1658 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1659 $this->assertSame( 'userexists', $ret->message
->getKey() );
1661 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1662 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1663 $mock->expects( $this->any() )->method( 'accountCreationType' )
1664 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1665 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1666 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1667 ->will( $this->returnValue( StatusValue
::newGood() ) );
1668 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
1669 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1670 $this->primaryauthMocks
= [ $mock ];
1671 $this->initializeManager( true );
1673 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1674 ->setMethods( [ 'populateUser' ] )
1676 $req->expects( $this->any() )->method( 'populateUser' )
1677 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1678 $userReq->username
= self
::usernameForCreation();
1679 $ret = $this->manager
->beginAccountCreation(
1680 $creator, [ $userReq, $req ], 'http://localhost/'
1682 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1683 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1685 $req = new UserDataAuthenticationRequest
;
1686 $userReq->username
= self
::usernameForCreation();
1688 $ret = $this->manager
->beginAccountCreation(
1689 $creator, [ $userReq, $req ], 'http://localhost/'
1691 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1692 $this->assertSame( 'fail', $ret->message
->getKey() );
1694 $this->manager
->beginAccountCreation(
1695 \User
::newFromName( $userReq->username
), [ $userReq, $req ], 'http://localhost/'
1697 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1698 $this->assertSame( 'fail', $ret->message
->getKey() );
1701 public function testContinueAccountCreation() {
1702 $creator = \User
::newFromName( 'UTSysop' );
1703 $username = self
::usernameForCreation();
1704 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1705 return $level === LogLevel
::DEBUG ?
null : $message;
1707 $this->initializeManager();
1711 'username' => $username,
1713 'creatorname' => $username,
1716 'primaryResponse' => null,
1718 'ranPreTests' => true,
1721 $this->hook( 'LocalUserCreated', $this->never() );
1723 $this->manager
->continueAccountCreation( [] );
1724 $this->fail( 'Expected exception not thrown' );
1725 } catch ( \LogicException
$ex ) {
1726 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1728 $this->unhook( 'LocalUserCreated' );
1730 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1731 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1732 $mock->expects( $this->any() )->method( 'accountCreationType' )
1733 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1734 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1735 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )->will(
1736 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
1738 $this->primaryauthMocks
= [ $mock ];
1739 $this->initializeManager( true );
1741 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', null );
1742 $this->hook( 'LocalUserCreated', $this->never() );
1743 $ret = $this->manager
->continueAccountCreation( [] );
1744 $this->unhook( 'LocalUserCreated' );
1745 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1746 $this->assertSame( 'authmanager-create-not-in-progress', $ret->message
->getKey() );
1748 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1749 [ 'username' => "$username<>" ] +
$session );
1750 $this->hook( 'LocalUserCreated', $this->never() );
1751 $ret = $this->manager
->continueAccountCreation( [] );
1752 $this->unhook( 'LocalUserCreated' );
1753 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1754 $this->assertSame( 'noname', $ret->message
->getKey() );
1756 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1759 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $session );
1760 $this->hook( 'LocalUserCreated', $this->never() );
1761 $cache = \ObjectCache
::getLocalClusterInstance();
1762 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1763 $ret = $this->manager
->continueAccountCreation( [] );
1765 $this->unhook( 'LocalUserCreated' );
1766 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1767 $this->assertSame( 'usernameinprogress', $ret->message
->getKey() );
1768 // This error shouldn't remove the existing session, because the
1769 // raced-with process "owns" it.
1771 $session, $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1774 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1775 [ 'username' => $creator->getName() ] +
$session );
1776 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1777 $readOnlyMode->setReason( 'Because' );
1778 $this->hook( 'LocalUserCreated', $this->never() );
1779 $ret = $this->manager
->continueAccountCreation( [] );
1780 $this->unhook( 'LocalUserCreated' );
1781 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1782 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
1783 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1784 $readOnlyMode->setReason( false );
1786 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1787 [ 'username' => $creator->getName() ] +
$session );
1788 $this->hook( 'LocalUserCreated', $this->never() );
1789 $ret = $this->manager
->continueAccountCreation( [] );
1790 $this->unhook( 'LocalUserCreated' );
1791 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1792 $this->assertSame( 'userexists', $ret->message
->getKey() );
1794 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1797 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1798 [ 'userid' => $creator->getId() ] +
$session );
1799 $this->hook( 'LocalUserCreated', $this->never() );
1801 $ret = $this->manager
->continueAccountCreation( [] );
1802 $this->fail( 'Expected exception not thrown' );
1803 } catch ( \UnexpectedValueException
$ex ) {
1804 $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
1806 $this->unhook( 'LocalUserCreated' );
1808 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1811 $id = $creator->getId();
1812 $name = $creator->getName();
1813 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1814 [ 'username' => $name, 'userid' => $id +
1 ] +
$session );
1815 $this->hook( 'LocalUserCreated', $this->never() );
1817 $ret = $this->manager
->continueAccountCreation( [] );
1818 $this->fail( 'Expected exception not thrown' );
1819 } catch ( \UnexpectedValueException
$ex ) {
1820 $this->assertEquals(
1821 "User \"{$name}\" exists, but ID $id != " . ( $id +
1 ) . '!', $ex->getMessage()
1824 $this->unhook( 'LocalUserCreated' );
1826 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1829 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1830 ->setMethods( [ 'populateUser' ] )
1832 $req->expects( $this->any() )->method( 'populateUser' )
1833 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1834 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1835 [ 'reqs' => [ $req ] ] +
$session );
1836 $ret = $this->manager
->continueAccountCreation( [] );
1837 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1838 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1840 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1845 * @dataProvider provideAccountCreation
1846 * @param StatusValue $preTest
1847 * @param StatusValue $primaryTest
1848 * @param StatusValue $secondaryTest
1849 * @param array $primaryResponses
1850 * @param array $secondaryResponses
1851 * @param array $managerResponses
1853 public function testAccountCreation(
1854 StatusValue
$preTest, $primaryTest, $secondaryTest,
1855 array $primaryResponses, array $secondaryResponses, array $managerResponses
1857 $creator = \User
::newFromName( 'UTSysop' );
1858 $username = self
::usernameForCreation();
1860 $this->initializeManager();
1862 // Set up lots of mocks...
1863 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1864 $req->preTest
= $preTest;
1865 $req->primaryTest
= $primaryTest;
1866 $req->secondaryTest
= $secondaryTest;
1867 $req->primary
= $primaryResponses;
1868 $req->secondary
= $secondaryResponses;
1870 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
1871 $class = ucfirst( $key ) . 'AuthenticationProvider';
1872 $mocks[$key] = $this->getMockForAbstractClass(
1873 "MediaWiki\\Auth\\$class", [], "Mock$class"
1875 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
1876 ->will( $this->returnValue( $key ) );
1877 $mocks[$key]->expects( $this->any() )->method( 'testUserForCreation' )
1878 ->will( $this->returnValue( StatusValue
::newGood() ) );
1879 $mocks[$key]->expects( $this->any() )->method( 'testForAccountCreation' )
1880 ->will( $this->returnCallback(
1881 function ( $user, $creatorIn, $reqs )
1882 use ( $username, $creator, $req, $key )
1884 $this->assertSame( $username, $user->getName() );
1885 $this->assertSame( $creator->getId(), $creatorIn->getId() );
1886 $this->assertSame( $creator->getName(), $creatorIn->getName() );
1888 foreach ( $reqs as $r ) {
1889 $this->assertSame( $username, $r->username
);
1890 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1892 $this->assertTrue( $foundReq, '$reqs contains $req' );
1898 for ( $i = 2; $i <= 3; $i++
) {
1899 $mocks[$key . $i] = $this->getMockForAbstractClass(
1900 "MediaWiki\\Auth\\$class", [], "Mock$class"
1902 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
1903 ->will( $this->returnValue( $key . $i ) );
1904 $mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' )
1905 ->will( $this->returnValue( StatusValue
::newGood() ) );
1906 $mocks[$key . $i]->expects( $this->atMost( 1 ) )->method( 'testForAccountCreation' )
1907 ->will( $this->returnValue( StatusValue
::newGood() ) );
1911 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
1912 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1913 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
1914 ->will( $this->returnValue( false ) );
1915 $ct = count( $req->primary
);
1916 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1917 $this->assertSame( $username, $user->getName() );
1918 $this->assertSame( 'UTSysop', $creator->getName() );
1920 foreach ( $reqs as $r ) {
1921 $this->assertSame( $username, $r->username
);
1922 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1924 $this->assertTrue( $foundReq, '$reqs contains $req' );
1925 return array_shift( $req->primary
);
1927 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
1928 ->method( 'beginPrimaryAccountCreation' )
1929 ->will( $callback );
1930 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1931 ->method( 'continuePrimaryAccountCreation' )
1932 ->will( $callback );
1934 $ct = count( $req->secondary
);
1935 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1936 $this->assertSame( $username, $user->getName() );
1937 $this->assertSame( 'UTSysop', $creator->getName() );
1939 foreach ( $reqs as $r ) {
1940 $this->assertSame( $username, $r->username
);
1941 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1943 $this->assertTrue( $foundReq, '$reqs contains $req' );
1944 return array_shift( $req->secondary
);
1946 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
1947 ->method( 'beginSecondaryAccountCreation' )
1948 ->will( $callback );
1949 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1950 ->method( 'continueSecondaryAccountCreation' )
1951 ->will( $callback );
1953 $abstain = AuthenticationResponse
::newAbstain();
1954 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
1955 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
1956 $mocks['primary2']->expects( $this->any() )->method( 'testUserExists' )
1957 ->will( $this->returnValue( false ) );
1958 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountCreation' )
1959 ->will( $this->returnValue( $abstain ) );
1960 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1961 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
1962 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_NONE
) );
1963 $mocks['primary3']->expects( $this->any() )->method( 'testUserExists' )
1964 ->will( $this->returnValue( false ) );
1965 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountCreation' );
1966 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1967 $mocks['secondary2']->expects( $this->atMost( 1 ) )
1968 ->method( 'beginSecondaryAccountCreation' )
1969 ->will( $this->returnValue( $abstain ) );
1970 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1971 $mocks['secondary3']->expects( $this->atMost( 1 ) )
1972 ->method( 'beginSecondaryAccountCreation' )
1973 ->will( $this->returnValue( $abstain ) );
1974 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1976 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
1977 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary'], $mocks['primary2'] ];
1978 $this->secondaryauthMocks
= [
1979 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2']
1982 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
1983 return $level === LogLevel
::DEBUG ?
null : $message;
1986 $this->initializeManager( true );
1988 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
1989 $this->equalTo( AuthenticationResponse
::PASS
),
1990 $this->equalTo( AuthenticationResponse
::FAIL
)
1992 $providers = array_merge(
1993 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
1995 foreach ( $providers as $p ) {
1996 $p->postCalled
= false;
1997 $p->expects( $this->atMost( 1 ) )->method( 'postAccountCreation' )
1998 ->willReturnCallback( function ( $user, $creator, $response )
1999 use ( $constraint, $p, $username )
2001 $this->assertInstanceOf( 'User', $user );
2002 $this->assertSame( $username, $user->getName() );
2003 $this->assertSame( 'UTSysop', $creator->getName() );
2004 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
2005 $this->assertThat( $response->status
, $constraint );
2006 $p->postCalled
= $response->status
;
2010 // We're testing with $wgNewUserLog = false, so assert that it worked
2011 $dbw = wfGetDB( DB_MASTER
);
2012 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2016 foreach ( $managerResponses as $i => $response ) {
2017 $success = $response instanceof AuthenticationResponse
&&
2018 $response->status
=== AuthenticationResponse
::PASS
;
2019 if ( $i === 'created' ) {
2021 $this->hook( 'LocalUserCreated', $this->once() )
2023 $this->callback( function ( $user ) use ( $username ) {
2024 return $user->getName() === $username;
2026 $this->equalTo( false )
2028 $expectLog[] = [ LogLevel
::INFO
, "Creating user {user} during account creation" ];
2030 $this->hook( 'LocalUserCreated', $this->never() );
2036 $userReq = new UsernameAuthenticationRequest
;
2037 $userReq->username
= $username;
2038 $ret = $this->manager
->beginAccountCreation(
2039 $creator, [ $userReq, $req ], 'http://localhost/'
2042 $ret = $this->manager
->continueAccountCreation( [ $req ] );
2044 if ( $response instanceof \Exception
) {
2045 $this->fail( 'Expected exception not thrown', "Response $i" );
2047 } catch ( \Exception
$ex ) {
2048 if ( !$response instanceof \Exception
) {
2051 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
2053 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2054 "Response $i, exception, session state"
2056 $this->unhook( 'LocalUserCreated' );
2060 $this->unhook( 'LocalUserCreated' );
2062 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
2065 $this->assertNotNull( $ret->loginRequest
, "Response $i, login marker" );
2066 $this->assertContains(
2067 $ret->loginRequest
, $this->managerPriv
->createdAccountAuthenticationRequests
,
2068 "Response $i, login marker"
2073 "MediaWiki\Auth\AuthManager::continueAccountCreation: Account creation succeeded for {user}"
2076 // Set some fields in the expected $response that we couldn't
2077 // know in provideAccountCreation().
2078 $response->username
= $username;
2079 $response->loginRequest
= $ret->loginRequest
;
2081 $this->assertNull( $ret->loginRequest
, "Response $i, login marker" );
2082 $this->assertSame( [], $this->managerPriv
->createdAccountAuthenticationRequests
,
2083 "Response $i, login marker" );
2085 $ret->message
= $this->message( $ret->message
);
2086 $this->assertEquals( $response, $ret, "Response $i, response" );
2087 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
2089 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2090 "Response $i, session state"
2092 foreach ( $providers as $p ) {
2093 $this->assertSame( $response->status
, $p->postCalled
,
2094 "Response $i, post-auth callback called" );
2097 $this->assertNotNull(
2098 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2099 "Response $i, session state"
2101 foreach ( $ret->neededRequests
as $neededReq ) {
2102 $this->assertEquals( AuthManager
::ACTION_CREATE
, $neededReq->action
,
2103 "Response $i, neededRequest action" );
2105 $this->assertEquals(
2106 $ret->neededRequests
,
2107 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_CREATE_CONTINUE
),
2108 "Response $i, continuation check"
2110 foreach ( $providers as $p ) {
2111 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
2116 $this->assertNotEquals( 0, \User
::idFromName( $username ) );
2118 $this->assertEquals( 0, \User
::idFromName( $username ) );
2124 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
2128 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2132 public function provideAccountCreation() {
2133 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
2134 $good = StatusValue
::newGood();
2137 'Pre-creation test fail in pre' => [
2138 StatusValue
::newFatal( 'fail-from-pre' ), $good, $good,
2142 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
2145 'Pre-creation test fail in primary' => [
2146 $good, StatusValue
::newFatal( 'fail-from-primary' ), $good,
2150 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2153 'Pre-creation test fail in secondary' => [
2154 $good, $good, StatusValue
::newFatal( 'fail-from-secondary' ),
2158 AuthenticationResponse
::newFail( $this->message( 'fail-from-secondary' ) ),
2161 'Failure in primary' => [
2162 $good, $good, $good,
2164 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2169 'All primary abstain' => [
2170 $good, $good, $good,
2172 AuthenticationResponse
::newAbstain(),
2176 AuthenticationResponse
::newFail( $this->message( 'authmanager-create-no-primary' ) )
2179 'Primary UI, then redirect, then fail' => [
2180 $good, $good, $good,
2182 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2183 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
2184 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
2189 'Primary redirect, then abstain' => [
2190 $good, $good, $good,
2192 $tmp = AuthenticationResponse
::newRedirect(
2193 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
2195 AuthenticationResponse
::newAbstain(),
2200 new \
DomainException(
2201 'MockPrimaryAuthenticationProvider::continuePrimaryAccountCreation() returned ABSTAIN'
2205 'Primary UI, then pass; secondary abstain' => [
2206 $good, $good, $good,
2208 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2209 AuthenticationResponse
::newPass(),
2212 AuthenticationResponse
::newAbstain(),
2216 'created' => AuthenticationResponse
::newPass( '' ),
2219 'Primary pass; secondary UI then pass' => [
2220 $good, $good, $good,
2222 AuthenticationResponse
::newPass( '' ),
2225 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2226 AuthenticationResponse
::newPass( '' ),
2230 AuthenticationResponse
::newPass( '' ),
2233 'Primary pass; secondary fail' => [
2234 $good, $good, $good,
2236 AuthenticationResponse
::newPass(),
2239 AuthenticationResponse
::newFail( $this->message( '...' ) ),
2242 'created' => new \
DomainException(
2243 'MockSecondaryAuthenticationProvider::beginSecondaryAccountCreation() returned FAIL. ' .
2244 'Secondary providers are not allowed to fail account creation, ' .
2245 'that should have been done via testForAccountCreation().'
2253 * @dataProvider provideAccountCreationLogging
2254 * @param bool $isAnon
2255 * @param string|null $logSubtype
2257 public function testAccountCreationLogging( $isAnon, $logSubtype ) {
2258 $creator = $isAnon ?
new \User
: \User
::newFromName( 'UTSysop' );
2259 $username = self
::usernameForCreation();
2261 $this->initializeManager();
2263 // Set up lots of mocks...
2264 $mock = $this->getMockForAbstractClass(
2265 "MediaWiki\\Auth\\PrimaryAuthenticationProvider", []
2267 $mock->expects( $this->any() )->method( 'getUniqueId' )
2268 ->will( $this->returnValue( 'primary' ) );
2269 $mock->expects( $this->any() )->method( 'testUserForCreation' )
2270 ->will( $this->returnValue( StatusValue
::newGood() ) );
2271 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
2272 ->will( $this->returnValue( StatusValue
::newGood() ) );
2273 $mock->expects( $this->any() )->method( 'accountCreationType' )
2274 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2275 $mock->expects( $this->any() )->method( 'testUserExists' )
2276 ->will( $this->returnValue( false ) );
2277 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
2278 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
2279 $mock->expects( $this->any() )->method( 'finishAccountCreation' )
2280 ->will( $this->returnValue( $logSubtype ) );
2282 $this->primaryauthMocks
= [ $mock ];
2283 $this->initializeManager( true );
2284 $this->logger
->setCollect( true );
2286 $this->config
->set( 'NewUserLog', true );
2288 $dbw = wfGetDB( DB_MASTER
);
2289 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2291 $userReq = new UsernameAuthenticationRequest
;
2292 $userReq->username
= $username;
2293 $reasonReq = new CreationReasonAuthenticationRequest
;
2294 $reasonReq->reason
= $this->toString();
2295 $ret = $this->manager
->beginAccountCreation(
2296 $creator, [ $userReq, $reasonReq ], 'http://localhost/'
2299 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
2301 $user = \User
::newFromName( $username );
2302 $this->assertNotEquals( 0, $user->getId(), 'sanity check' );
2303 $this->assertNotEquals( $creator->getId(), $user->getId(), 'sanity check' );
2305 $data = \DatabaseLogEntry
::getSelectQueryData();
2306 $rows = iterator_to_array( $dbw->select(
2310 'log_id > ' . (int)$maxLogId,
2311 'log_type' => 'newusers'
2317 $this->assertCount( 1, $rows );
2318 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2320 $this->assertSame( $logSubtype ?
: ( $isAnon ?
'create' : 'create2' ), $entry->getSubtype() );
2322 $isAnon ?
$user->getId() : $creator->getId(),
2323 $entry->getPerformer()->getId()
2326 $isAnon ?
$user->getName() : $creator->getName(),
2327 $entry->getPerformer()->getName()
2329 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2330 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2331 $this->assertSame( $this->toString(), $entry->getComment() );
2334 public static function provideAccountCreationLogging() {
2339 [ false, 'byemail' ],
2343 public function testAutoAccountCreation() {
2344 global $wgGroupPermissions, $wgHooks;
2346 // PHPUnit seems to have a bug where it will call the ->with()
2347 // callbacks for our hooks again after the test is run (WTF?), which
2348 // breaks here because $username no longer matches $user by the end of
2350 $workaroundPHPUnitBug = false;
2352 $username = self
::usernameForCreation();
2353 $this->initializeManager();
2355 $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
2356 $wgGroupPermissions['*']['createaccount'] = true;
2357 $wgGroupPermissions['*']['autocreateaccount'] = false;
2359 \ObjectCache
::$instances[__METHOD__
] = new \
HashBagOStuff();
2360 $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__
] );
2362 // Set up lots of mocks...
2364 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2365 $class = ucfirst( $key ) . 'AuthenticationProvider';
2366 $mocks[$key] = $this->getMockForAbstractClass(
2367 "MediaWiki\\Auth\\$class", [], "Mock$class"
2369 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2370 ->will( $this->returnValue( $key ) );
2373 $good = StatusValue
::newGood();
2374 $callback = $this->callback( function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
2375 return $workaroundPHPUnitBug ||
$user->getName() === $username;
2378 $mocks['pre']->expects( $this->exactly( 12 ) )->method( 'testUserForCreation' )
2379 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2380 ->will( $this->onConsecutiveCalls(
2381 StatusValue
::newFatal( 'ok' ), StatusValue
::newFatal( 'ok' ), // For testing permissions
2382 StatusValue
::newFatal( 'fail-in-pre' ), $good, $good,
2383 $good, // backoff test
2384 $good, // addToDatabase fails test
2385 $good, // addToDatabase throws test
2386 $good, // addToDatabase exists test
2387 $good, $good, $good // success
2390 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
2391 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2392 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
2393 ->will( $this->returnValue( true ) );
2394 $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
2395 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2396 ->will( $this->onConsecutiveCalls(
2397 StatusValue
::newFatal( 'fail-in-primary' ), $good,
2398 $good, // backoff test
2399 $good, // addToDatabase fails test
2400 $good, // addToDatabase throws test
2401 $good, // addToDatabase exists test
2404 $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2405 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) );
2407 $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
2408 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2409 ->will( $this->onConsecutiveCalls(
2410 StatusValue
::newFatal( 'fail-in-secondary' ),
2411 $good, // backoff test
2412 $good, // addToDatabase fails test
2413 $good, // addToDatabase throws test
2414 $good, // addToDatabase exists test
2417 $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2418 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) );
2420 $this->preauthMocks
= [ $mocks['pre'] ];
2421 $this->primaryauthMocks
= [ $mocks['primary'] ];
2422 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2423 $this->initializeManager( true );
2424 $session = $this->request
->getSession();
2426 $logger = new \
TestLogger( true, function ( $m ) {
2427 $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
2430 $this->manager
->setLogger( $logger );
2433 $user = \User
::newFromName( 'UTSysop' );
2434 $this->manager
->autoCreateUser( $user, 'InvalidSource', true );
2435 $this->fail( 'Expected exception not thrown' );
2436 } catch ( \InvalidArgumentException
$ex ) {
2437 $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
2440 // First, check an existing user
2442 $user = \User
::newFromName( 'UTSysop' );
2443 $this->hook( 'LocalUserCreated', $this->never() );
2444 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2445 $this->unhook( 'LocalUserCreated' );
2446 $expect = \Status
::newGood();
2447 $expect->warning( 'userexists' );
2448 $this->assertEquals( $expect, $ret );
2449 $this->assertNotEquals( 0, $user->getId() );
2450 $this->assertSame( 'UTSysop', $user->getName() );
2451 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2452 $this->assertSame( [
2453 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2454 ], $logger->getBuffer() );
2455 $logger->clearBuffer();
2458 $user = \User
::newFromName( 'UTSysop' );
2459 $this->hook( 'LocalUserCreated', $this->never() );
2460 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2461 $this->unhook( 'LocalUserCreated' );
2462 $expect = \Status
::newGood();
2463 $expect->warning( 'userexists' );
2464 $this->assertEquals( $expect, $ret );
2465 $this->assertNotEquals( 0, $user->getId() );
2466 $this->assertSame( 'UTSysop', $user->getName() );
2467 $this->assertEquals( 0, $session->getUser()->getId() );
2468 $this->assertSame( [
2469 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2470 ], $logger->getBuffer() );
2471 $logger->clearBuffer();
2473 // Wiki is read-only
2475 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
2476 $readOnlyMode->setReason( 'Because' );
2477 $user = \User
::newFromName( $username );
2478 $this->hook( 'LocalUserCreated', $this->never() );
2479 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2480 $this->unhook( 'LocalUserCreated' );
2481 $this->assertEquals( \Status
::newFatal( 'readonlytext', 'Because' ), $ret );
2482 $this->assertEquals( 0, $user->getId() );
2483 $this->assertNotEquals( $username, $user->getName() );
2484 $this->assertEquals( 0, $session->getUser()->getId() );
2485 $this->assertSame( [
2486 [ LogLevel
::DEBUG
, 'denied by wfReadOnly(): {reason}' ],
2487 ], $logger->getBuffer() );
2488 $logger->clearBuffer();
2489 $readOnlyMode->setReason( false );
2491 // Session blacklisted
2493 $session->set( 'AuthManager::AutoCreateBlacklist', 'test' );
2494 $user = \User
::newFromName( $username );
2495 $this->hook( 'LocalUserCreated', $this->never() );
2496 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2497 $this->unhook( 'LocalUserCreated' );
2498 $this->assertEquals( \Status
::newFatal( 'test' ), $ret );
2499 $this->assertEquals( 0, $user->getId() );
2500 $this->assertNotEquals( $username, $user->getName() );
2501 $this->assertEquals( 0, $session->getUser()->getId() );
2502 $this->assertSame( [
2503 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2504 ], $logger->getBuffer() );
2505 $logger->clearBuffer();
2508 $session->set( 'AuthManager::AutoCreateBlacklist', StatusValue
::newFatal( 'test2' ) );
2509 $user = \User
::newFromName( $username );
2510 $this->hook( 'LocalUserCreated', $this->never() );
2511 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2512 $this->unhook( 'LocalUserCreated' );
2513 $this->assertEquals( \Status
::newFatal( 'test2' ), $ret );
2514 $this->assertEquals( 0, $user->getId() );
2515 $this->assertNotEquals( $username, $user->getName() );
2516 $this->assertEquals( 0, $session->getUser()->getId() );
2517 $this->assertSame( [
2518 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2519 ], $logger->getBuffer() );
2520 $logger->clearBuffer();
2524 $user = \User
::newFromName( $username . '@' );
2525 $this->hook( 'LocalUserCreated', $this->never() );
2526 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2527 $this->unhook( 'LocalUserCreated' );
2528 $this->assertEquals( \Status
::newFatal( 'noname' ), $ret );
2529 $this->assertEquals( 0, $user->getId() );
2530 $this->assertNotEquals( $username . '@', $user->getId() );
2531 $this->assertEquals( 0, $session->getUser()->getId() );
2532 $this->assertSame( [
2533 [ LogLevel
::DEBUG
, 'name "{username}" is not creatable' ],
2534 ], $logger->getBuffer() );
2535 $logger->clearBuffer();
2536 $this->assertSame( 'noname', $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2538 // IP unable to create accounts
2539 $wgGroupPermissions['*']['createaccount'] = false;
2540 $wgGroupPermissions['*']['autocreateaccount'] = false;
2542 $user = \User
::newFromName( $username );
2543 $this->hook( 'LocalUserCreated', $this->never() );
2544 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2545 $this->unhook( 'LocalUserCreated' );
2546 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-noperm' ), $ret );
2547 $this->assertEquals( 0, $user->getId() );
2548 $this->assertNotEquals( $username, $user->getName() );
2549 $this->assertEquals( 0, $session->getUser()->getId() );
2550 $this->assertSame( [
2551 [ LogLevel
::DEBUG
, 'IP lacks the ability to create or autocreate accounts' ],
2552 ], $logger->getBuffer() );
2553 $logger->clearBuffer();
2555 'authmanager-autocreate-noperm', $session->get( 'AuthManager::AutoCreateBlacklist' )
2558 // Test that both permutations of permissions are allowed
2559 // (this hits the two "ok" entries in $mocks['pre'])
2560 $wgGroupPermissions['*']['createaccount'] = false;
2561 $wgGroupPermissions['*']['autocreateaccount'] = true;
2563 $user = \User
::newFromName( $username );
2564 $this->hook( 'LocalUserCreated', $this->never() );
2565 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2566 $this->unhook( 'LocalUserCreated' );
2567 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2569 $wgGroupPermissions['*']['createaccount'] = true;
2570 $wgGroupPermissions['*']['autocreateaccount'] = false;
2572 $user = \User
::newFromName( $username );
2573 $this->hook( 'LocalUserCreated', $this->never() );
2574 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2575 $this->unhook( 'LocalUserCreated' );
2576 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2577 $logger->clearBuffer();
2581 $user = \User
::newFromName( $username );
2582 $this->hook( 'LocalUserCreated', $this->never() );
2583 $cache = \ObjectCache
::getLocalClusterInstance();
2584 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2585 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2587 $this->unhook( 'LocalUserCreated' );
2588 $this->assertEquals( \Status
::newFatal( 'usernameinprogress' ), $ret );
2589 $this->assertEquals( 0, $user->getId() );
2590 $this->assertNotEquals( $username, $user->getName() );
2591 $this->assertEquals( 0, $session->getUser()->getId() );
2592 $this->assertSame( [
2593 [ LogLevel
::DEBUG
, 'Could not acquire account creation lock' ],
2594 ], $logger->getBuffer() );
2595 $logger->clearBuffer();
2597 // Test pre-authentication provider fail
2599 $user = \User
::newFromName( $username );
2600 $this->hook( 'LocalUserCreated', $this->never() );
2601 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2602 $this->unhook( 'LocalUserCreated' );
2603 $this->assertEquals( \Status
::newFatal( 'fail-in-pre' ), $ret );
2604 $this->assertEquals( 0, $user->getId() );
2605 $this->assertNotEquals( $username, $user->getName() );
2606 $this->assertEquals( 0, $session->getUser()->getId() );
2607 $this->assertSame( [
2608 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2609 ], $logger->getBuffer() );
2610 $logger->clearBuffer();
2611 $this->assertEquals(
2612 StatusValue
::newFatal( 'fail-in-pre' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2616 $user = \User
::newFromName( $username );
2617 $this->hook( 'LocalUserCreated', $this->never() );
2618 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2619 $this->unhook( 'LocalUserCreated' );
2620 $this->assertEquals( \Status
::newFatal( 'fail-in-primary' ), $ret );
2621 $this->assertEquals( 0, $user->getId() );
2622 $this->assertNotEquals( $username, $user->getName() );
2623 $this->assertEquals( 0, $session->getUser()->getId() );
2624 $this->assertSame( [
2625 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2626 ], $logger->getBuffer() );
2627 $logger->clearBuffer();
2628 $this->assertEquals(
2629 StatusValue
::newFatal( 'fail-in-primary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2633 $user = \User
::newFromName( $username );
2634 $this->hook( 'LocalUserCreated', $this->never() );
2635 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2636 $this->unhook( 'LocalUserCreated' );
2637 $this->assertEquals( \Status
::newFatal( 'fail-in-secondary' ), $ret );
2638 $this->assertEquals( 0, $user->getId() );
2639 $this->assertNotEquals( $username, $user->getName() );
2640 $this->assertEquals( 0, $session->getUser()->getId() );
2641 $this->assertSame( [
2642 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2643 ], $logger->getBuffer() );
2644 $logger->clearBuffer();
2645 $this->assertEquals(
2646 StatusValue
::newFatal( 'fail-in-secondary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2650 $cache = \ObjectCache
::getLocalClusterInstance();
2651 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2652 $cache->set( $backoffKey, true );
2654 $user = \User
::newFromName( $username );
2655 $this->hook( 'LocalUserCreated', $this->never() );
2656 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2657 $this->unhook( 'LocalUserCreated' );
2658 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-exception' ), $ret );
2659 $this->assertEquals( 0, $user->getId() );
2660 $this->assertNotEquals( $username, $user->getName() );
2661 $this->assertEquals( 0, $session->getUser()->getId() );
2662 $this->assertSame( [
2663 [ LogLevel
::DEBUG
, '{username} denied by prior creation attempt failures' ],
2664 ], $logger->getBuffer() );
2665 $logger->clearBuffer();
2666 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2667 $cache->delete( $backoffKey );
2669 // Test addToDatabase fails
2671 $user = $this->getMockBuilder( 'User' )
2672 ->setMethods( [ 'addToDatabase' ] )->getMock();
2673 $user->expects( $this->once() )->method( 'addToDatabase' )
2674 ->will( $this->returnValue( \Status
::newFatal( 'because' ) ) );
2675 $user->setName( $username );
2676 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2677 $this->assertEquals( \Status
::newFatal( 'because' ), $ret );
2678 $this->assertEquals( 0, $user->getId() );
2679 $this->assertNotEquals( $username, $user->getName() );
2680 $this->assertEquals( 0, $session->getUser()->getId() );
2681 $this->assertSame( [
2682 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2683 [ LogLevel
::ERROR
, '{username} failed with message {msg}' ],
2684 ], $logger->getBuffer() );
2685 $logger->clearBuffer();
2686 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2688 // Test addToDatabase throws an exception
2689 $cache = \ObjectCache
::getLocalClusterInstance();
2690 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2691 $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
2693 $user = $this->getMockBuilder( 'User' )
2694 ->setMethods( [ 'addToDatabase' ] )->getMock();
2695 $user->expects( $this->once() )->method( 'addToDatabase' )
2696 ->will( $this->throwException( new \
Exception( 'Excepted' ) ) );
2697 $user->setName( $username );
2699 $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2700 $this->fail( 'Expected exception not thrown' );
2701 } catch ( \Exception
$ex ) {
2702 $this->assertSame( 'Excepted', $ex->getMessage() );
2704 $this->assertEquals( 0, $user->getId() );
2705 $this->assertEquals( 0, $session->getUser()->getId() );
2706 $this->assertSame( [
2707 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2708 [ LogLevel
::ERROR
, '{username} failed with exception {exception}' ],
2709 ], $logger->getBuffer() );
2710 $logger->clearBuffer();
2711 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2712 $this->assertNotEquals( false, $cache->get( $backoffKey ) );
2713 $cache->delete( $backoffKey );
2715 // Test addToDatabase fails because the user already exists.
2717 $user = $this->getMockBuilder( 'User' )
2718 ->setMethods( [ 'addToDatabase' ] )->getMock();
2719 $user->expects( $this->once() )->method( 'addToDatabase' )
2720 ->will( $this->returnCallback( function () use ( $username, &$user ) {
2721 $oldUser = \User
::newFromName( $username );
2722 $status = $oldUser->addToDatabase();
2723 $this->assertTrue( $status->isOK(), 'sanity check' );
2724 $user->setId( $oldUser->getId() );
2725 return \Status
::newFatal( 'userexists' );
2727 $user->setName( $username );
2728 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2729 $expect = \Status
::newGood();
2730 $expect->warning( 'userexists' );
2731 $this->assertEquals( $expect, $ret );
2732 $this->assertNotEquals( 0, $user->getId() );
2733 $this->assertEquals( $username, $user->getName() );
2734 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2735 $this->assertSame( [
2736 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2737 [ LogLevel
::INFO
, '{username} already exists locally (race)' ],
2738 ], $logger->getBuffer() );
2739 $logger->clearBuffer();
2740 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2744 $username = self
::usernameForCreation();
2745 $user = \User
::newFromName( $username );
2746 $this->hook( 'AuthPluginAutoCreate', $this->once() )
2747 ->with( $callback );
2748 $this->hideDeprecated( 'AuthPluginAutoCreate hook (used in ' .
2749 get_class( $wgHooks['AuthPluginAutoCreate'][0] ) . '::onAuthPluginAutoCreate)' );
2750 $this->hook( 'LocalUserCreated', $this->once() )
2751 ->with( $callback, $this->equalTo( true ) );
2752 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2753 $this->unhook( 'LocalUserCreated' );
2754 $this->unhook( 'AuthPluginAutoCreate' );
2755 $this->assertEquals( \Status
::newGood(), $ret );
2756 $this->assertNotEquals( 0, $user->getId() );
2757 $this->assertEquals( $username, $user->getName() );
2758 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2759 $this->assertSame( [
2760 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2761 ], $logger->getBuffer() );
2762 $logger->clearBuffer();
2764 $dbw = wfGetDB( DB_MASTER
);
2765 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2767 $username = self
::usernameForCreation();
2768 $user = \User
::newFromName( $username );
2769 $this->hook( 'LocalUserCreated', $this->once() )
2770 ->with( $callback, $this->equalTo( true ) );
2771 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2772 $this->unhook( 'LocalUserCreated' );
2773 $this->assertEquals( \Status
::newGood(), $ret );
2774 $this->assertNotEquals( 0, $user->getId() );
2775 $this->assertEquals( $username, $user->getName() );
2776 $this->assertEquals( 0, $session->getUser()->getId() );
2777 $this->assertSame( [
2778 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2779 ], $logger->getBuffer() );
2780 $logger->clearBuffer();
2783 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2786 $this->config
->set( 'NewUserLog', true );
2788 $username = self
::usernameForCreation();
2789 $user = \User
::newFromName( $username );
2790 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2791 $this->assertEquals( \Status
::newGood(), $ret );
2792 $logger->clearBuffer();
2794 $data = \DatabaseLogEntry
::getSelectQueryData();
2795 $rows = iterator_to_array( $dbw->select(
2799 'log_id > ' . (int)$maxLogId,
2800 'log_type' => 'newusers'
2806 $this->assertCount( 1, $rows );
2807 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2809 $this->assertSame( 'autocreate', $entry->getSubtype() );
2810 $this->assertSame( $user->getId(), $entry->getPerformer()->getId() );
2811 $this->assertSame( $user->getName(), $entry->getPerformer()->getName() );
2812 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2813 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2815 $workaroundPHPUnitBug = true;
2819 * @dataProvider provideGetAuthenticationRequests
2820 * @param string $action
2821 * @param array $expect
2822 * @param array $state
2824 public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
2825 $makeReq = function ( $key ) use ( $action ) {
2826 $req = $this->createMock( AuthenticationRequest
::class );
2827 $req->expects( $this->any() )->method( 'getUniqueId' )
2828 ->will( $this->returnValue( $key ) );
2829 $req->action
= $action === AuthManager
::ACTION_UNLINK ? AuthManager
::ACTION_REMOVE
: $action;
2833 $cmpReqs = function ( $a, $b ) {
2834 $ret = strcmp( get_class( $a ), get_class( $b ) );
2836 $ret = strcmp( $a->key
, $b->key
);
2841 $good = StatusValue
::newGood();
2844 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2845 $class = ucfirst( $key ) . 'AuthenticationProvider';
2846 $mocks[$key] = $this->getMockForAbstractClass(
2847 "MediaWiki\\Auth\\$class", [], "Mock$class"
2849 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2850 ->will( $this->returnValue( $key ) );
2851 $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2852 ->will( $this->returnCallback( function ( $action ) use ( $key, $makeReq ) {
2853 return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
2855 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
2856 ->will( $this->returnValue( $good ) );
2861 PrimaryAuthenticationProvider
::TYPE_NONE
,
2862 PrimaryAuthenticationProvider
::TYPE_CREATE
,
2863 PrimaryAuthenticationProvider
::TYPE_LINK
2865 $class = 'PrimaryAuthenticationProvider';
2866 $mocks["primary-$type"] = $this->getMockForAbstractClass(
2867 "MediaWiki\\Auth\\$class", [], "Mock$class"
2869 $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
2870 ->will( $this->returnValue( "primary-$type" ) );
2871 $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
2872 ->will( $this->returnValue( $type ) );
2873 $mocks["primary-$type"]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2874 ->will( $this->returnCallback( function ( $action ) use ( $type, $makeReq ) {
2875 return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
2877 $mocks["primary-$type"]->expects( $this->any() )
2878 ->method( 'providerAllowsAuthenticationDataChange' )
2879 ->will( $this->returnValue( $good ) );
2880 $this->primaryauthMocks
[] = $mocks["primary-$type"];
2883 $mocks['primary2'] = $this->getMockForAbstractClass(
2884 PrimaryAuthenticationProvider
::class, [], "MockPrimaryAuthenticationProvider"
2886 $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
2887 ->will( $this->returnValue( 'primary2' ) );
2888 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
2889 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
2890 $mocks['primary2']->expects( $this->any() )->method( 'getAuthenticationRequests' )
2891 ->will( $this->returnValue( [] ) );
2892 $mocks['primary2']->expects( $this->any() )
2893 ->method( 'providerAllowsAuthenticationDataChange' )
2894 ->will( $this->returnCallback( function ( $req ) use ( $good ) {
2895 return $req->key
=== 'generic' ? StatusValue
::newFatal( 'no' ) : $good;
2897 $this->primaryauthMocks
[] = $mocks['primary2'];
2899 $this->preauthMocks
= [ $mocks['pre'] ];
2900 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2901 $this->initializeManager( true );
2904 if ( isset( $state['continueRequests'] ) ) {
2905 $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
2907 if ( $action === AuthManager
::ACTION_LOGIN_CONTINUE
) {
2908 $this->request
->getSession()->setSecret( 'AuthManager::authnState', $state );
2909 } elseif ( $action === AuthManager
::ACTION_CREATE_CONTINUE
) {
2910 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $state );
2911 } elseif ( $action === AuthManager
::ACTION_LINK_CONTINUE
) {
2912 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', $state );
2916 $expectReqs = array_map( $makeReq, $expect );
2917 if ( $action === AuthManager
::ACTION_LOGIN
) {
2918 $req = new RememberMeAuthenticationRequest
;
2919 $req->action
= $action;
2920 $req->required
= AuthenticationRequest
::REQUIRED
;
2921 $expectReqs[] = $req;
2922 } elseif ( $action === AuthManager
::ACTION_CREATE
) {
2923 $req = new UsernameAuthenticationRequest
;
2924 $req->action
= $action;
2925 $expectReqs[] = $req;
2926 $req = new UserDataAuthenticationRequest
;
2927 $req->action
= $action;
2928 $req->required
= AuthenticationRequest
::REQUIRED
;
2929 $expectReqs[] = $req;
2931 usort( $expectReqs, $cmpReqs );
2933 $actual = $this->manager
->getAuthenticationRequests( $action );
2934 foreach ( $actual as $req ) {
2935 // Don't test this here.
2936 $req->required
= AuthenticationRequest
::REQUIRED
;
2938 usort( $actual, $cmpReqs );
2940 $this->assertEquals( $expectReqs, $actual );
2942 // Test CreationReasonAuthenticationRequest gets returned
2943 if ( $action === AuthManager
::ACTION_CREATE
) {
2944 $req = new CreationReasonAuthenticationRequest
;
2945 $req->action
= $action;
2946 $req->required
= AuthenticationRequest
::REQUIRED
;
2947 $expectReqs[] = $req;
2948 usort( $expectReqs, $cmpReqs );
2950 $actual = $this->manager
->getAuthenticationRequests( $action, \User
::newFromName( 'UTSysop' ) );
2951 foreach ( $actual as $req ) {
2952 // Don't test this here.
2953 $req->required
= AuthenticationRequest
::REQUIRED
;
2955 usort( $actual, $cmpReqs );
2957 $this->assertEquals( $expectReqs, $actual );
2961 public static function provideGetAuthenticationRequests() {
2964 AuthManager
::ACTION_LOGIN
,
2965 [ 'pre-login', 'primary-none-login', 'primary-create-login',
2966 'primary-link-login', 'secondary-login', 'generic' ],
2969 AuthManager
::ACTION_CREATE
,
2970 [ 'pre-create', 'primary-none-create', 'primary-create-create',
2971 'primary-link-create', 'secondary-create', 'generic' ],
2974 AuthManager
::ACTION_LINK
,
2975 [ 'primary-link-link', 'generic' ],
2978 AuthManager
::ACTION_CHANGE
,
2979 [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
2980 'secondary-change' ],
2983 AuthManager
::ACTION_REMOVE
,
2984 [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
2985 'secondary-remove' ],
2988 AuthManager
::ACTION_UNLINK
,
2989 [ 'primary-link-remove' ],
2992 AuthManager
::ACTION_LOGIN_CONTINUE
,
2996 AuthManager
::ACTION_LOGIN_CONTINUE
,
2997 $reqs = [ 'continue-login', 'foo', 'bar' ],
2999 'continueRequests' => $reqs,
3003 AuthManager
::ACTION_CREATE_CONTINUE
,
3007 AuthManager
::ACTION_CREATE_CONTINUE
,
3008 $reqs = [ 'continue-create', 'foo', 'bar' ],
3010 'continueRequests' => $reqs,
3014 AuthManager
::ACTION_LINK_CONTINUE
,
3018 AuthManager
::ACTION_LINK_CONTINUE
,
3019 $reqs = [ 'continue-link', 'foo', 'bar' ],
3021 'continueRequests' => $reqs,
3027 public function testGetAuthenticationRequestsRequired() {
3028 $makeReq = function ( $key, $required ) {
3029 $req = $this->createMock( AuthenticationRequest
::class );
3030 $req->expects( $this->any() )->method( 'getUniqueId' )
3031 ->will( $this->returnValue( $key ) );
3032 $req->action
= AuthManager
::ACTION_LOGIN
;
3034 $req->required
= $required;
3037 $cmpReqs = function ( $a, $b ) {
3038 $ret = strcmp( get_class( $a ), get_class( $b ) );
3040 $ret = strcmp( $a->key
, $b->key
);
3045 $good = StatusValue
::newGood();
3047 $primary1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3048 $primary1->expects( $this->any() )->method( 'getUniqueId' )
3049 ->will( $this->returnValue( 'primary1' ) );
3050 $primary1->expects( $this->any() )->method( 'accountCreationType' )
3051 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3052 $primary1->expects( $this->any() )->method( 'getAuthenticationRequests' )
3053 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3055 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3056 $makeReq( "required", AuthenticationRequest
::REQUIRED
),
3057 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3058 $makeReq( "foo", AuthenticationRequest
::REQUIRED
),
3059 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3060 $makeReq( "baz", AuthenticationRequest
::OPTIONAL
),
3064 $primary2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3065 $primary2->expects( $this->any() )->method( 'getUniqueId' )
3066 ->will( $this->returnValue( 'primary2' ) );
3067 $primary2->expects( $this->any() )->method( 'accountCreationType' )
3068 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3069 $primary2->expects( $this->any() )->method( 'getAuthenticationRequests' )
3070 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3072 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3073 $makeReq( "required2", AuthenticationRequest
::REQUIRED
),
3074 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3078 $secondary = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3079 $secondary->expects( $this->any() )->method( 'getUniqueId' )
3080 ->will( $this->returnValue( 'secondary' ) );
3081 $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
3082 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3084 $makeReq( "foo", AuthenticationRequest
::OPTIONAL
),
3085 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3086 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3090 $rememberReq = new RememberMeAuthenticationRequest
;
3091 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
3093 $this->primaryauthMocks
= [ $primary1, $primary2 ];
3094 $this->secondaryauthMocks
= [ $secondary ];
3095 $this->initializeManager( true );
3097 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3100 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3101 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3102 $makeReq( "required2", AuthenticationRequest
::PRIMARY_REQUIRED
),
3103 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3104 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3105 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3106 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3107 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3109 usort( $actual, $cmpReqs );
3110 usort( $expected, $cmpReqs );
3111 $this->assertEquals( $expected, $actual );
3113 $this->primaryauthMocks
= [ $primary1 ];
3114 $this->secondaryauthMocks
= [ $secondary ];
3115 $this->initializeManager( true );
3117 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3120 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3121 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3122 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3123 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3124 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3125 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3127 usort( $actual, $cmpReqs );
3128 usort( $expected, $cmpReqs );
3129 $this->assertEquals( $expected, $actual );
3132 public function testAllowsPropertyChange() {
3134 foreach ( [ 'primary', 'secondary' ] as $key ) {
3135 $class = ucfirst( $key ) . 'AuthenticationProvider';
3136 $mocks[$key] = $this->getMockForAbstractClass(
3137 "MediaWiki\\Auth\\$class", [], "Mock$class"
3139 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3140 ->will( $this->returnValue( $key ) );
3141 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
3142 ->will( $this->returnCallback( function ( $prop ) use ( $key ) {
3143 return $prop !== $key;
3147 $this->primaryauthMocks
= [ $mocks['primary'] ];
3148 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
3149 $this->initializeManager( true );
3151 $this->assertTrue( $this->manager
->allowsPropertyChange( 'foo' ) );
3152 $this->assertFalse( $this->manager
->allowsPropertyChange( 'primary' ) );
3153 $this->assertFalse( $this->manager
->allowsPropertyChange( 'secondary' ) );
3156 public function testAutoCreateOnLogin() {
3157 $username = self
::usernameForCreation();
3159 $req = $this->createMock( AuthenticationRequest
::class );
3161 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3162 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3163 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3164 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3165 $mock->expects( $this->any() )->method( 'accountCreationType' )
3166 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3167 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3168 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3169 ->will( $this->returnValue( StatusValue
::newGood() ) );
3171 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3172 $mock2->expects( $this->any() )->method( 'getUniqueId' )
3173 ->will( $this->returnValue( 'secondary' ) );
3174 $mock2->expects( $this->any() )->method( 'beginSecondaryAuthentication' )->will(
3176 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) )
3179 $mock2->expects( $this->any() )->method( 'continueSecondaryAuthentication' )
3180 ->will( $this->returnValue( AuthenticationResponse
::newAbstain() ) );
3181 $mock2->expects( $this->any() )->method( 'testUserForCreation' )
3182 ->will( $this->returnValue( StatusValue
::newGood() ) );
3184 $this->primaryauthMocks
= [ $mock ];
3185 $this->secondaryauthMocks
= [ $mock2 ];
3186 $this->initializeManager( true );
3187 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3188 $session = $this->request
->getSession();
3191 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3194 $callback = $this->callback( function ( $user ) use ( $username ) {
3195 return $user->getName() === $username;
3198 $this->hook( 'UserLoggedIn', $this->never() );
3199 $this->hook( 'LocalUserCreated', $this->once() )->with( $callback, $this->equalTo( true ) );
3200 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3201 $this->unhook( 'LocalUserCreated' );
3202 $this->unhook( 'UserLoggedIn' );
3203 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
3205 $id = (int)\User
::newFromName( $username )->getId();
3206 $this->assertNotSame( 0, \User
::newFromName( $username )->getId() );
3207 $this->assertSame( 0, $session->getUser()->getId() );
3209 $this->hook( 'UserLoggedIn', $this->once() )->with( $callback );
3210 $this->hook( 'LocalUserCreated', $this->never() );
3211 $ret = $this->manager
->continueAuthentication( [] );
3212 $this->unhook( 'LocalUserCreated' );
3213 $this->unhook( 'UserLoggedIn' );
3214 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
3215 $this->assertSame( $username, $ret->username
);
3216 $this->assertSame( $id, $session->getUser()->getId() );
3219 public function testAutoCreateFailOnLogin() {
3220 $username = self
::usernameForCreation();
3222 $mock = $this->getMockForAbstractClass(
3223 PrimaryAuthenticationProvider
::class, [], "MockPrimaryAuthenticationProvider" );
3224 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3225 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3226 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3227 $mock->expects( $this->any() )->method( 'accountCreationType' )
3228 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3229 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3230 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3231 ->will( $this->returnValue( StatusValue
::newFatal( 'fail-from-primary' ) ) );
3233 $this->primaryauthMocks
= [ $mock ];
3234 $this->initializeManager( true );
3235 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3236 $session = $this->request
->getSession();
3239 $this->assertSame( 0, $session->getUser()->getId(),
3241 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3244 $this->hook( 'UserLoggedIn', $this->never() );
3245 $this->hook( 'LocalUserCreated', $this->never() );
3246 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3247 $this->unhook( 'LocalUserCreated' );
3248 $this->unhook( 'UserLoggedIn' );
3249 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3250 $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message
->getKey() );
3252 $this->assertSame( 0, \User
::newFromName( $username )->getId() );
3253 $this->assertSame( 0, $session->getUser()->getId() );
3256 public function testAuthenticationSessionData() {
3257 $this->initializeManager( true );
3259 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3260 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3261 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3262 $this->assertSame( 'foo!', $this->manager
->getAuthenticationSessionData( 'foo' ) );
3263 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3264 $this->manager
->removeAuthenticationSessionData( 'foo' );
3265 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3266 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3267 $this->manager
->removeAuthenticationSessionData( 'bar' );
3268 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3270 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3271 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3272 $this->manager
->removeAuthenticationSessionData( null );
3273 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3274 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3277 public function testCanLinkAccounts() {
3279 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
3280 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
3281 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
3284 foreach ( $types as $type => $can ) {
3285 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3286 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
3287 $mock->expects( $this->any() )->method( 'accountCreationType' )
3288 ->will( $this->returnValue( $type ) );
3289 $this->primaryauthMocks
= [ $mock ];
3290 $this->initializeManager( true );
3291 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
3295 public function testBeginAccountLink() {
3296 $user = \User
::newFromName( 'UTSysop' );
3297 $this->initializeManager();
3299 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', 'test' );
3301 $this->manager
->beginAccountLink( $user, [], 'http://localhost/' );
3302 $this->fail( 'Expected exception not thrown' );
3303 } catch ( \LogicException
$ex ) {
3304 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3306 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3308 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3309 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3310 $mock->expects( $this->any() )->method( 'accountCreationType' )
3311 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3312 $this->primaryauthMocks
= [ $mock ];
3313 $this->initializeManager( true );
3315 $ret = $this->manager
->beginAccountLink( new \User
, [], 'http://localhost/' );
3316 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3317 $this->assertSame( 'noname', $ret->message
->getKey() );
3319 $ret = $this->manager
->beginAccountLink(
3320 \User
::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
3322 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3323 $this->assertSame( 'authmanager-userdoesnotexist', $ret->message
->getKey() );
3326 public function testContinueAccountLink() {
3327 $user = \User
::newFromName( 'UTSysop' );
3328 $this->initializeManager();
3331 'userid' => $user->getId(),
3332 'username' => $user->getName(),
3337 $this->manager
->continueAccountLink( [] );
3338 $this->fail( 'Expected exception not thrown' );
3339 } catch ( \LogicException
$ex ) {
3340 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3343 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3344 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3345 $mock->expects( $this->any() )->method( 'accountCreationType' )
3346 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3347 $mock->expects( $this->any() )->method( 'beginPrimaryAccountLink' )->will(
3348 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
3350 $this->primaryauthMocks
= [ $mock ];
3351 $this->initializeManager( true );
3353 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', null );
3354 $ret = $this->manager
->continueAccountLink( [] );
3355 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3356 $this->assertSame( 'authmanager-link-not-in-progress', $ret->message
->getKey() );
3358 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3359 [ 'username' => $user->getName() . '<>' ] +
$session );
3360 $ret = $this->manager
->continueAccountLink( [] );
3361 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3362 $this->assertSame( 'noname', $ret->message
->getKey() );
3363 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3365 $id = $user->getId();
3366 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3367 [ 'userid' => $id +
1 ] +
$session );
3369 $ret = $this->manager
->continueAccountLink( [] );
3370 $this->fail( 'Expected exception not thrown' );
3371 } catch ( \UnexpectedValueException
$ex ) {
3372 $this->assertEquals(
3373 "User \"{$user->getName()}\" is valid, but ID $id != " . ( $id +
1 ) . '!',
3377 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3381 * @dataProvider provideAccountLink
3382 * @param StatusValue $preTest
3383 * @param array $primaryResponses
3384 * @param array $managerResponses
3386 public function testAccountLink(
3387 StatusValue
$preTest, array $primaryResponses, array $managerResponses
3389 $user = \User
::newFromName( 'UTSysop' );
3391 $this->initializeManager();
3393 // Set up lots of mocks...
3394 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3395 $req->primary
= $primaryResponses;
3398 foreach ( [ 'pre', 'primary' ] as $key ) {
3399 $class = ucfirst( $key ) . 'AuthenticationProvider';
3400 $mocks[$key] = $this->getMockForAbstractClass(
3401 "MediaWiki\\Auth\\$class", [], "Mock$class"
3403 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3404 ->will( $this->returnValue( $key ) );
3406 for ( $i = 2; $i <= 3; $i++
) {
3407 $mocks[$key . $i] = $this->getMockForAbstractClass(
3408 "MediaWiki\\Auth\\$class", [], "Mock$class"
3410 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
3411 ->will( $this->returnValue( $key . $i ) );
3415 $mocks['pre']->expects( $this->any() )->method( 'testForAccountLink' )
3416 ->will( $this->returnCallback(
3418 use ( $user, $preTest )
3420 $this->assertSame( $user->getId(), $u->getId() );
3421 $this->assertSame( $user->getName(), $u->getName() );
3426 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
3427 ->will( $this->returnValue( StatusValue
::newGood() ) );
3429 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
3430 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3431 $ct = count( $req->primary
);
3432 $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req ) {
3433 $this->assertSame( $user->getId(), $u->getId() );
3434 $this->assertSame( $user->getName(), $u->getName() );
3436 foreach ( $reqs as $r ) {
3437 $this->assertSame( $user->getName(), $r->username
);
3438 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
3440 $this->assertTrue( $foundReq, '$reqs contains $req' );
3441 return array_shift( $req->primary
);
3443 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
3444 ->method( 'beginPrimaryAccountLink' )
3445 ->will( $callback );
3446 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
3447 ->method( 'continuePrimaryAccountLink' )
3448 ->will( $callback );
3450 $abstain = AuthenticationResponse
::newAbstain();
3451 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
3452 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3453 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
3454 ->will( $this->returnValue( $abstain ) );
3455 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3456 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
3457 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3458 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
3459 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3461 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
3462 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
3463 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
3464 return $level === LogLevel
::DEBUG ?
null : $message;
3466 $this->initializeManager( true );
3468 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
3469 $this->equalTo( AuthenticationResponse
::PASS
),
3470 $this->equalTo( AuthenticationResponse
::FAIL
)
3472 $providers = array_merge( $this->preauthMocks
, $this->primaryauthMocks
);
3473 foreach ( $providers as $p ) {
3474 $p->postCalled
= false;
3475 $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
3476 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
3477 $this->assertInstanceOf( 'User', $user );
3478 $this->assertSame( 'UTSysop', $user->getName() );
3479 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
3480 $this->assertThat( $response->status
, $constraint );
3481 $p->postCalled
= $response->status
;
3488 foreach ( $managerResponses as $i => $response ) {
3489 if ( $response instanceof AuthenticationResponse
&&
3490 $response->status
=== AuthenticationResponse
::PASS
3492 $expectLog[] = [ LogLevel
::INFO
, 'Account linked to {user} by primary' ];
3498 $ret = $this->manager
->beginAccountLink( $user, [ $req ], 'http://localhost/' );
3500 $ret = $this->manager
->continueAccountLink( [ $req ] );
3502 if ( $response instanceof \Exception
) {
3503 $this->fail( 'Expected exception not thrown', "Response $i" );
3505 } catch ( \Exception
$ex ) {
3506 if ( !$response instanceof \Exception
) {
3509 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
3510 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3511 "Response $i, exception, session state" );
3515 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
3517 $ret->message
= $this->message( $ret->message
);
3518 $this->assertEquals( $response, $ret, "Response $i, response" );
3519 if ( $response->status
=== AuthenticationResponse
::PASS ||
3520 $response->status
=== AuthenticationResponse
::FAIL
3522 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3523 "Response $i, session state" );
3524 foreach ( $providers as $p ) {
3525 $this->assertSame( $response->status
, $p->postCalled
,
3526 "Response $i, post-auth callback called" );
3529 $this->assertNotNull(
3530 $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3531 "Response $i, session state"
3533 foreach ( $ret->neededRequests
as $neededReq ) {
3534 $this->assertEquals( AuthManager
::ACTION_LINK
, $neededReq->action
,
3535 "Response $i, neededRequest action" );
3537 $this->assertEquals(
3538 $ret->neededRequests
,
3539 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LINK_CONTINUE
),
3540 "Response $i, continuation check"
3542 foreach ( $providers as $p ) {
3543 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
3550 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
3553 public function provideAccountLink() {
3554 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3555 $good = StatusValue
::newGood();
3558 'Pre-link test fail in pre' => [
3559 StatusValue
::newFatal( 'fail-from-pre' ),
3562 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
3565 'Failure in primary' => [
3568 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
3572 'All primary abstain' => [
3575 AuthenticationResponse
::newAbstain(),
3578 AuthenticationResponse
::newFail( $this->message( 'authmanager-link-no-primary' ) )
3581 'Primary UI, then redirect, then fail' => [
3584 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3585 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
3586 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
3590 'Primary redirect, then abstain' => [
3593 $tmp = AuthenticationResponse
::newRedirect(
3594 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
3596 AuthenticationResponse
::newAbstain(),
3600 new \
DomainException(
3601 'MockPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
3605 'Primary UI, then pass' => [
3608 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3609 AuthenticationResponse
::newPass(),
3613 AuthenticationResponse
::newPass( '' ),
3619 AuthenticationResponse
::newPass( '' ),
3622 AuthenticationResponse
::newPass( '' ),