3 namespace MediaWiki\Auth
;
6 use MediaWiki\Session\SessionInfo
;
7 use MediaWiki\Session\UserInfo
;
8 use Psr\Log\LoggerInterface
;
12 use Wikimedia\ScopedCallback
;
13 use Wikimedia\TestingAccessWrapper
;
18 * @covers MediaWiki\Auth\AuthManager
20 class AuthManagerTest
extends \MediaWikiTestCase
{
21 /** @var WebRequest */
25 /** @var LoggerInterface */
28 protected $preauthMocks = [];
29 protected $primaryauthMocks = [];
30 protected $secondaryauthMocks = [];
32 /** @var AuthManager */
34 /** @var TestingAccessWrapper */
35 protected $managerPriv;
37 protected function setUp() {
40 $this->setMwGlobals( [ 'wgAuth' => null ] );
41 $this->stashMwGlobals( [ 'wgHooks' ] );
45 * Sets a mock on a hook
47 * @param object $expect From $this->once(), $this->never(), etc.
48 * @return object $mock->expects( $expect )->method( ... ).
50 protected function hook( $hook, $expect ) {
52 $mock = $this->getMockBuilder( __CLASS__
)
53 ->setMethods( [ "on$hook" ] )
55 $wgHooks[$hook] = [ $mock ];
56 return $mock->expects( $expect )->method( "on$hook" );
63 protected function unhook( $hook ) {
69 * Ensure a value is a clean Message object
70 * @param string|Message $key
71 * @param array $params
74 protected function message( $key, $params = [] ) {
75 if ( $key === null ) {
78 if ( $key instanceof \MessageSpecifier
) {
79 $params = $key->getParams();
80 $key = $key->getKey();
82 return new \
Message( $key, $params, \Language
::factory( 'en' ) );
86 * Test two AuthenticationResponses for equality. We don't want to use regular assertEquals
87 * because that recursively compares members, which leads to false negatives if e.g. Language
90 * @param AuthenticationResponse $response1
91 * @param AuthenticationResponse $response2
95 private function assertResponseEquals(
96 AuthenticationResponse
$expected, AuthenticationResponse
$actual, $msg = ''
98 foreach ( ( new \
ReflectionClass( $expected ) )->getProperties() as $prop ) {
99 $name = $prop->getName();
100 $usedMsg = ltrim( "$msg ($name)" );
101 if ( $name === 'message' && $expected->message
) {
102 $this->assertSame( $expected->message
->serialize(), $actual->message
->serialize(),
105 $this->assertEquals( $expected->$name, $actual->$name, $usedMsg );
111 * Initialize the AuthManagerConfig variable in $this->config
113 * Uses data from the various 'mocks' fields.
115 protected function initializeConfig() {
125 foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
126 $key = $type . 'Mocks';
127 foreach ( $this->$key as $mock ) {
128 $config[$type][$mock->getUniqueId()] = [ 'factory' => function () use ( $mock ) {
134 $this->config
->set( 'AuthManagerConfig', $config );
135 $this->config
->set( 'LanguageCode', 'en' );
136 $this->config
->set( 'NewUserLog', false );
140 * Initialize $this->manager
141 * @param bool $regen Force a call to $this->initializeConfig()
143 protected function initializeManager( $regen = false ) {
144 if ( $regen ||
!$this->config
) {
145 $this->config
= new \
HashConfig();
147 if ( $regen ||
!$this->request
) {
148 $this->request
= new \
FauxRequest();
150 if ( !$this->logger
) {
151 $this->logger
= new \
TestLogger();
154 if ( $regen ||
!$this->config
->has( 'AuthManagerConfig' ) ) {
155 $this->initializeConfig();
157 $this->manager
= new AuthManager( $this->request
, $this->config
);
158 $this->manager
->setLogger( $this->logger
);
159 $this->managerPriv
= TestingAccessWrapper
::newFromObject( $this->manager
);
163 * Setup SessionManager with a mock session provider
164 * @param bool|null $canChangeUser If non-null, canChangeUser will be mocked to return this
165 * @param array $methods Additional methods to mock
166 * @return array (MediaWiki\Session\SessionProvider, ScopedCallback)
168 protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
169 if ( !$this->config
) {
170 $this->config
= new \
HashConfig();
171 $this->initializeConfig();
173 $this->config
->set( 'ObjectCacheSessionExpiry', 100 );
175 $methods[] = '__toString';
176 $methods[] = 'describe';
177 if ( $canChangeUser !== null ) {
178 $methods[] = 'canChangeUser';
180 $provider = $this->getMockBuilder( \DummySessionProvider
::class )
181 ->setMethods( $methods )
183 $provider->expects( $this->any() )->method( '__toString' )
184 ->will( $this->returnValue( 'MockSessionProvider' ) );
185 $provider->expects( $this->any() )->method( 'describe' )
186 ->will( $this->returnValue( 'MockSessionProvider sessions' ) );
187 if ( $canChangeUser !== null ) {
188 $provider->expects( $this->any() )->method( 'canChangeUser' )
189 ->will( $this->returnValue( $canChangeUser ) );
191 $this->config
->set( 'SessionProviders', [
192 [ 'factory' => function () use ( $provider ) {
197 $manager = new \MediaWiki\Session\
SessionManager( [
198 'config' => $this->config
,
199 'logger' => new \Psr\Log\
NullLogger(),
200 'store' => new \
HashBagOStuff(),
202 TestingAccessWrapper
::newFromObject( $manager )->getProvider( (string)$provider );
204 $reset = \MediaWiki\Session\TestUtils
::setSessionManagerSingleton( $manager );
206 if ( $this->request
) {
207 $manager->getSessionForRequest( $this->request
);
210 return [ $provider, $reset ];
213 public function testSingleton() {
214 // Temporarily clear out the global singleton, if any, to test creating
216 $rProp = new \
ReflectionProperty( AuthManager
::class, 'instance' );
217 $rProp->setAccessible( true );
218 $old = $rProp->getValue();
219 $cb = new ScopedCallback( [ $rProp, 'setValue' ], [ $old ] );
220 $rProp->setValue( null );
222 $singleton = AuthManager
::singleton();
223 $this->assertInstanceOf( AuthManager
::class, AuthManager
::singleton() );
224 $this->assertSame( $singleton, AuthManager
::singleton() );
225 $this->assertSame( \RequestContext
::getMain()->getRequest(), $singleton->getRequest() );
227 \RequestContext
::getMain()->getConfig(),
228 TestingAccessWrapper
::newFromObject( $singleton )->config
232 public function testCanAuthenticateNow() {
233 $this->initializeManager();
235 list( $provider, $reset ) = $this->getMockSessionProvider( false );
236 $this->assertFalse( $this->manager
->canAuthenticateNow() );
237 ScopedCallback
::consume( $reset );
239 list( $provider, $reset ) = $this->getMockSessionProvider( true );
240 $this->assertTrue( $this->manager
->canAuthenticateNow() );
241 ScopedCallback
::consume( $reset );
244 public function testNormalizeUsername() {
246 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
247 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
248 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
249 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
251 foreach ( $mocks as $key => $mock ) {
252 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
254 $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
255 ->with( $this->identicalTo( 'XYZ' ) )
256 ->willReturn( 'Foo' );
257 $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
258 ->with( $this->identicalTo( 'XYZ' ) )
259 ->willReturn( 'Foo' );
260 $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
261 ->with( $this->identicalTo( 'XYZ' ) )
262 ->willReturn( null );
263 $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
264 ->with( $this->identicalTo( 'XYZ' ) )
265 ->willReturn( 'Bar!' );
267 $this->primaryauthMocks
= $mocks;
269 $this->initializeManager();
271 $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager
->normalizeUsername( 'XYZ' ) );
275 * @dataProvider provideSecuritySensitiveOperationStatus
276 * @param bool $mutableSession
278 public function testSecuritySensitiveOperationStatus( $mutableSession ) {
279 $this->logger
= new \Psr\Log\
NullLogger();
280 $user = \User
::newFromName( 'UTSysop' );
282 $reauth = $mutableSession ? AuthManager
::SEC_REAUTH
: AuthManager
::SEC_FAIL
;
284 list( $provider, $reset ) = $this->getMockSessionProvider(
285 $mutableSession, [ 'provideSessionInfo' ]
287 $provider->expects( $this->any() )->method( 'provideSessionInfo' )
288 ->will( $this->returnCallback( function () use ( $provider, &$provideUser ) {
289 return new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
290 'provider' => $provider,
291 'id' => \DummySessionProvider
::ID
,
293 'userInfo' => UserInfo
::newFromUser( $provideUser, true )
296 $this->initializeManager();
298 $this->config
->set( 'ReauthenticateTime', [] );
299 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [] );
300 $provideUser = new \User
;
301 $session = $provider->getManager()->getSessionForRequest( $this->request
);
302 $this->assertSame( 0, $session->getUser()->getId(), 'sanity check' );
304 // Anonymous user => reauth
305 $session->set( 'AuthManager:lastAuthId', 0 );
306 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
307 $this->assertSame( $reauth, $this->manager
->securitySensitiveOperationStatus( 'foo' ) );
309 $provideUser = $user;
310 $session = $provider->getManager()->getSessionForRequest( $this->request
);
311 $this->assertSame( $user->getId(), $session->getUser()->getId(), 'sanity check' );
313 // Error for no default (only gets thrown for non-anonymous user)
314 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
315 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
317 $this->manager
->securitySensitiveOperationStatus( 'foo' );
318 $this->fail( 'Expected exception not thrown' );
319 } catch ( \UnexpectedValueException
$ex ) {
322 ?
'$wgReauthenticateTime lacks a default'
323 : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
328 if ( $mutableSession ) {
329 $this->config
->set( 'ReauthenticateTime', [
335 // Mismatched user ID
336 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
337 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
339 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
342 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
345 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
349 $session->set( 'AuthManager:lastAuthId', $user->getId() );
350 $session->set( 'AuthManager:lastAuthTimestamp', null );
352 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
355 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
358 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
361 // Recent enough to pass
362 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
364 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
367 // Not recent enough to pass
368 $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
370 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
372 // But recent enough for the 'test' operation
374 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test' )
377 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [
383 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
387 AuthManager
::SEC_FAIL
, $this->manager
->securitySensitiveOperationStatus( 'test' )
391 // Test hook, all three possible values
393 AuthManager
::SEC_OK
=> AuthManager
::SEC_OK
,
394 AuthManager
::SEC_REAUTH
=> $reauth,
395 AuthManager
::SEC_FAIL
=> AuthManager
::SEC_FAIL
,
396 ] as $hook => $expect ) {
397 $this->hook( 'SecuritySensitiveOperationStatus', $this->exactly( 2 ) )
401 $this->callback( function ( $s ) use ( $session ) {
402 return $s->getId() === $session->getId();
404 $mutableSession ?
$this->equalTo( 500, 1 ) : $this->equalTo( -1 )
406 ->will( $this->returnCallback( function ( &$v ) use ( $hook ) {
410 $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
412 $expect, $this->manager
->securitySensitiveOperationStatus( 'test' ), "hook $hook"
415 $expect, $this->manager
->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
417 $this->unhook( 'SecuritySensitiveOperationStatus' );
420 ScopedCallback
::consume( $reset );
423 public function onSecuritySensitiveOperationStatus( &$status, $operation, $session, $time ) {
426 public static function provideSecuritySensitiveOperationStatus() {
434 * @dataProvider provideUserCanAuthenticate
435 * @param bool $primary1Can
436 * @param bool $primary2Can
437 * @param bool $expect
439 public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
440 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
441 $mock1->expects( $this->any() )->method( 'getUniqueId' )
442 ->will( $this->returnValue( 'primary1' ) );
443 $mock1->expects( $this->any() )->method( 'testUserCanAuthenticate' )
444 ->with( $this->equalTo( 'UTSysop' ) )
445 ->will( $this->returnValue( $primary1Can ) );
446 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
447 $mock2->expects( $this->any() )->method( 'getUniqueId' )
448 ->will( $this->returnValue( 'primary2' ) );
449 $mock2->expects( $this->any() )->method( 'testUserCanAuthenticate' )
450 ->with( $this->equalTo( 'UTSysop' ) )
451 ->will( $this->returnValue( $primary2Can ) );
452 $this->primaryauthMocks
= [ $mock1, $mock2 ];
454 $this->initializeManager( true );
455 $this->assertSame( $expect, $this->manager
->userCanAuthenticate( 'UTSysop' ) );
458 public static function provideUserCanAuthenticate() {
460 [ false, false, false ],
461 [ true, false, true ],
462 [ false, true, true ],
463 [ true, true, true ],
467 public function testRevokeAccessForUser() {
468 $this->initializeManager();
470 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
471 $mock->expects( $this->any() )->method( 'getUniqueId' )
472 ->will( $this->returnValue( 'primary' ) );
473 $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
474 ->with( $this->equalTo( 'UTSysop' ) );
475 $this->primaryauthMocks
= [ $mock ];
477 $this->initializeManager( true );
478 $this->logger
->setCollect( true );
480 $this->manager
->revokeAccessForUser( 'UTSysop' );
483 [ LogLevel
::INFO
, 'Revoking access for {user}' ],
484 ], $this->logger
->getBuffer() );
487 public function testProviderCreation() {
489 'pre' => $this->getMockForAbstractClass( PreAuthenticationProvider
::class ),
490 'primary' => $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
491 'secondary' => $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class ),
493 foreach ( $mocks as $key => $mock ) {
494 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
495 $mock->expects( $this->once() )->method( 'setLogger' );
496 $mock->expects( $this->once() )->method( 'setManager' );
497 $mock->expects( $this->once() )->method( 'setConfig' );
499 $this->preauthMocks
= [ $mocks['pre'] ];
500 $this->primaryauthMocks
= [ $mocks['primary'] ];
501 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
504 $this->initializeManager();
507 $this->managerPriv
->getAuthenticationProvider( 'primary' )
511 $this->managerPriv
->getAuthenticationProvider( 'secondary' )
515 $this->managerPriv
->getAuthenticationProvider( 'pre' )
518 [ 'pre' => $mocks['pre'] ],
519 $this->managerPriv
->getPreAuthenticationProviders()
522 [ 'primary' => $mocks['primary'] ],
523 $this->managerPriv
->getPrimaryAuthenticationProviders()
526 [ 'secondary' => $mocks['secondary'] ],
527 $this->managerPriv
->getSecondaryAuthenticationProviders()
531 $mock1 = $this->getMockForAbstractClass( PreAuthenticationProvider
::class );
532 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
533 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
534 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
535 $this->preauthMocks
= [ $mock1 ];
536 $this->primaryauthMocks
= [ $mock2 ];
537 $this->secondaryauthMocks
= [];
538 $this->initializeManager( true );
540 $this->managerPriv
->getAuthenticationProvider( 'Y' );
541 $this->fail( 'Expected exception not thrown' );
542 } catch ( \RuntimeException
$ex ) {
543 $class1 = get_class( $mock1 );
544 $class2 = get_class( $mock2 );
546 "Duplicate specifications for id X (classes $class1 and $class2)", $ex->getMessage()
551 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
552 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
553 $class = get_class( $mock );
554 $this->preauthMocks
= [ $mock ];
555 $this->primaryauthMocks
= [ $mock ];
556 $this->secondaryauthMocks
= [ $mock ];
557 $this->initializeManager( true );
559 $this->managerPriv
->getPreAuthenticationProviders();
560 $this->fail( 'Expected exception not thrown' );
561 } catch ( \RuntimeException
$ex ) {
563 "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
568 $this->managerPriv
->getPrimaryAuthenticationProviders();
569 $this->fail( 'Expected exception not thrown' );
570 } catch ( \RuntimeException
$ex ) {
572 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
577 $this->managerPriv
->getSecondaryAuthenticationProviders();
578 $this->fail( 'Expected exception not thrown' );
579 } catch ( \RuntimeException
$ex ) {
581 "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
587 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
588 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
589 $mock3 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
590 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
591 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
592 $mock3->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'C' ) );
593 $this->preauthMocks
= [];
594 $this->primaryauthMocks
= [ $mock1, $mock2, $mock3 ];
595 $this->secondaryauthMocks
= [];
596 $this->initializeConfig();
597 $config = $this->config
->get( 'AuthManagerConfig' );
599 $this->initializeManager( false );
601 [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
602 $this->managerPriv
->getPrimaryAuthenticationProviders(),
606 $config['primaryauth']['A']['sort'] = 100;
607 $config['primaryauth']['C']['sort'] = -1;
608 $this->config
->set( 'AuthManagerConfig', $config );
609 $this->initializeManager( false );
611 [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
612 $this->managerPriv
->getPrimaryAuthenticationProviders()
616 public function testSetDefaultUserOptions() {
617 $this->initializeManager();
619 $context = \RequestContext
::getMain();
620 $reset = new ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
621 $context->setLanguage( 'de' );
622 $this->setContentLang( 'zh' );
624 $user = \User
::newFromName( self
::usernameForCreation() );
625 $user->addToDatabase();
626 $oldToken = $user->getToken();
627 $this->managerPriv
->setDefaultUserOptions( $user, false );
628 $user->saveSettings();
629 $this->assertNotEquals( $oldToken, $user->getToken() );
630 $this->assertSame( 'zh', $user->getOption( 'language' ) );
631 $this->assertSame( 'zh', $user->getOption( 'variant' ) );
633 $user = \User
::newFromName( self
::usernameForCreation() );
634 $user->addToDatabase();
635 $oldToken = $user->getToken();
636 $this->managerPriv
->setDefaultUserOptions( $user, true );
637 $user->saveSettings();
638 $this->assertNotEquals( $oldToken, $user->getToken() );
639 $this->assertSame( 'de', $user->getOption( 'language' ) );
640 $this->assertSame( 'zh', $user->getOption( 'variant' ) );
642 $this->setContentLang( 'fr' );
644 $user = \User
::newFromName( self
::usernameForCreation() );
645 $user->addToDatabase();
646 $oldToken = $user->getToken();
647 $this->managerPriv
->setDefaultUserOptions( $user, true );
648 $user->saveSettings();
649 $this->assertNotEquals( $oldToken, $user->getToken() );
650 $this->assertSame( 'de', $user->getOption( 'language' ) );
651 $this->assertSame( null, $user->getOption( 'variant' ) );
654 public function testForcePrimaryAuthenticationProviders() {
655 $mockA = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
656 $mockB = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
657 $mockB2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
658 $mockA->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
659 $mockB->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
660 $mockB2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
661 $this->primaryauthMocks
= [ $mockA ];
663 $this->logger
= new \
TestLogger( true );
665 // Test without first initializing the configured providers
666 $this->initializeManager();
667 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
669 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
671 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
672 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
674 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
675 ], $this->logger
->getBuffer() );
676 $this->logger
->clearBuffer();
678 // Test with first initializing the configured providers
679 $this->initializeManager();
680 $this->assertSame( $mockA, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
681 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
682 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
683 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
684 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
686 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
688 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
689 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
690 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
692 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
695 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
698 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
700 ], $this->logger
->getBuffer() );
701 $this->logger
->clearBuffer();
703 // Test duplicate IDs
704 $this->initializeManager();
706 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
707 $this->fail( 'Expected exception not thrown' );
708 } catch ( \RuntimeException
$ex ) {
709 $class1 = get_class( $mockB );
710 $class2 = get_class( $mockB2 );
712 "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
717 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
718 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
719 $class = get_class( $mock );
721 $this->manager
->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
722 $this->fail( 'Expected exception not thrown' );
723 } catch ( \RuntimeException
$ex ) {
725 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
731 public function testBeginAuthentication() {
732 $this->initializeManager();
735 list( $provider, $reset ) = $this->getMockSessionProvider( false );
736 $this->hook( 'UserLoggedIn', $this->never() );
737 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
739 $this->manager
->beginAuthentication( [], 'http://localhost/' );
740 $this->fail( 'Expected exception not thrown' );
741 } catch ( \LogicException
$ex ) {
742 $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
744 $this->unhook( 'UserLoggedIn' );
745 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
746 ScopedCallback
::consume( $reset );
747 $this->initializeManager( true );
749 // CreatedAccountAuthenticationRequest
750 $user = \User
::newFromName( 'UTSysop' );
752 new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
754 $this->hook( 'UserLoggedIn', $this->never() );
756 $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
757 $this->fail( 'Expected exception not thrown' );
758 } catch ( \LogicException
$ex ) {
760 'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
761 'that created the account',
765 $this->unhook( 'UserLoggedIn' );
767 $this->request
->getSession()->clear();
768 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
769 $this->managerPriv
->createdAccountAuthenticationRequests
= [ $reqs[0] ];
770 $this->hook( 'UserLoggedIn', $this->once() )
771 ->with( $this->callback( function ( $u ) use ( $user ) {
772 return $user->getId() === $u->getId() && $user->getName() === $u->getName();
774 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
775 $this->logger
->setCollect( true );
776 $ret = $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
777 $this->logger
->setCollect( false );
778 $this->unhook( 'UserLoggedIn' );
779 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
780 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
781 $this->assertSame( $user->getName(), $ret->username
);
782 $this->assertSame( $user->getId(), $this->request
->getSessionData( 'AuthManager:lastAuthId' ) );
784 time(), $this->request
->getSessionData( 'AuthManager:lastAuthTimestamp' ),
787 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
788 $this->assertSame( $user->getId(), $this->request
->getSession()->getUser()->getId() );
790 [ LogLevel
::INFO
, 'Logging in {user} after account creation' ],
791 ], $this->logger
->getBuffer() );
794 public function testCreateFromLogin() {
795 $user = \User
::newFromName( 'UTSysop' );
796 $req1 = $this->createMock( AuthenticationRequest
::class );
797 $req2 = $this->createMock( AuthenticationRequest
::class );
798 $req3 = $this->createMock( AuthenticationRequest
::class );
799 $userReq = new UsernameAuthenticationRequest
;
800 $userReq->username
= 'UTDummy';
802 $req1->returnToUrl
= 'http://localhost/';
803 $req2->returnToUrl
= 'http://localhost/';
804 $req3->returnToUrl
= 'http://localhost/';
805 $req3->username
= 'UTDummy';
806 $userReq->returnToUrl
= 'http://localhost/';
808 // Passing one into beginAuthentication(), and an immediate FAIL
809 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
810 $this->primaryauthMocks
= [ $primary ];
811 $this->initializeManager( true );
812 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
813 $res->createRequest
= $req1;
814 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
815 ->will( $this->returnValue( $res ) );
816 $createReq = new CreateFromLoginAuthenticationRequest(
817 null, [ $req2->getUniqueId() => $req2 ]
819 $this->logger
->setCollect( true );
820 $ret = $this->manager
->beginAuthentication( [ $createReq ], 'http://localhost/' );
821 $this->logger
->setCollect( false );
822 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
823 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
824 $this->assertSame( $req1, $ret->createRequest
->createRequest
);
825 $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest
->maybeLink
);
827 // UI, then FAIL in beginAuthentication()
828 $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider
::class )
829 ->setMethods( [ 'continuePrimaryAuthentication' ] )
830 ->getMockForAbstractClass();
831 $this->primaryauthMocks
= [ $primary ];
832 $this->initializeManager( true );
833 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
834 ->will( $this->returnValue(
835 AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) )
837 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
838 $res->createRequest
= $req2;
839 $primary->expects( $this->any() )->method( 'continuePrimaryAuthentication' )
840 ->will( $this->returnValue( $res ) );
841 $this->logger
->setCollect( true );
842 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
843 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
, 'sanity check' );
844 $ret = $this->manager
->continueAuthentication( [] );
845 $this->logger
->setCollect( false );
846 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
847 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
848 $this->assertSame( $req2, $ret->createRequest
->createRequest
);
849 $this->assertEquals( [], $ret->createRequest
->maybeLink
);
851 // Pass into beginAccountCreation(), see that maybeLink and createRequest get copied
852 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
853 $this->primaryauthMocks
= [ $primary ];
854 $this->initializeManager( true );
855 $createReq = new CreateFromLoginAuthenticationRequest( $req3, [ $req2 ] );
856 $createReq->returnToUrl
= 'http://localhost/';
857 $createReq->username
= 'UTDummy';
858 $res = AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) );
859 $primary->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
860 ->with( $this->anything(), $this->anything(), [ $userReq, $createReq, $req3 ] )
861 ->will( $this->returnValue( $res ) );
862 $primary->expects( $this->any() )->method( 'accountCreationType' )
863 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
864 $this->logger
->setCollect( true );
865 $ret = $this->manager
->beginAccountCreation(
866 $user, [ $userReq, $createReq ], 'http://localhost/'
868 $this->logger
->setCollect( false );
869 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
870 $state = $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' );
871 $this->assertNotNull( $state );
872 $this->assertEquals( [ $userReq, $createReq, $req3 ], $state['reqs'] );
873 $this->assertEquals( [ $req2 ], $state['maybeLink'] );
877 * @dataProvider provideAuthentication
878 * @param StatusValue $preResponse
879 * @param array $primaryResponses
880 * @param array $secondaryResponses
881 * @param array $managerResponses
882 * @param bool $link Whether the primary authentication provider is a "link" provider
884 public function testAuthentication(
885 StatusValue
$preResponse, array $primaryResponses, array $secondaryResponses,
886 array $managerResponses, $link = false
888 $this->initializeManager();
889 $user = \User
::newFromName( 'UTSysop' );
890 $id = $user->getId();
891 $name = $user->getName();
893 // Set up lots of mocks...
894 $req = new RememberMeAuthenticationRequest
;
895 $req->rememberMe
= (bool)rand( 0, 1 );
896 $req->pre
= $preResponse;
897 $req->primary
= $primaryResponses;
898 $req->secondary
= $secondaryResponses;
900 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
901 $class = ucfirst( $key ) . 'AuthenticationProvider';
902 $mocks[$key] = $this->getMockForAbstractClass(
903 "MediaWiki\\Auth\\$class", [], "Mock$class"
905 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
906 ->will( $this->returnValue( $key ) );
907 $mocks[$key . '2'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
908 $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
909 ->will( $this->returnValue( $key . '2' ) );
910 $mocks[$key . '3'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
911 $mocks[$key . '3']->expects( $this->any() )->method( 'getUniqueId' )
912 ->will( $this->returnValue( $key . '3' ) );
914 foreach ( $mocks as $mock ) {
915 $mock->expects( $this->any() )->method( 'getAuthenticationRequests' )
916 ->will( $this->returnValue( [] ) );
919 $mocks['pre']->expects( $this->once() )->method( 'testForAuthentication' )
920 ->will( $this->returnCallback( function ( $reqs ) use ( $req ) {
921 $this->assertContains( $req, $reqs );
925 $ct = count( $req->primary
);
926 $callback = $this->returnCallback( function ( $reqs ) use ( $req ) {
927 $this->assertContains( $req, $reqs );
928 return array_shift( $req->primary
);
930 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
931 ->method( 'beginPrimaryAuthentication' )
933 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
934 ->method( 'continuePrimaryAuthentication' )
937 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
938 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
941 $ct = count( $req->secondary
);
942 $callback = $this->returnCallback( function ( $user, $reqs ) use ( $id, $name, $req ) {
943 $this->assertSame( $id, $user->getId() );
944 $this->assertSame( $name, $user->getName() );
945 $this->assertContains( $req, $reqs );
946 return array_shift( $req->secondary
);
948 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
949 ->method( 'beginSecondaryAuthentication' )
951 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
952 ->method( 'continueSecondaryAuthentication' )
955 $abstain = AuthenticationResponse
::newAbstain();
956 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAuthentication' )
957 ->will( $this->returnValue( StatusValue
::newGood() ) );
958 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAuthentication' )
959 ->will( $this->returnValue( $abstain ) );
960 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAuthentication' );
961 $mocks['secondary2']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
962 ->will( $this->returnValue( $abstain ) );
963 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
964 $mocks['secondary3']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
965 ->will( $this->returnValue( $abstain ) );
966 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
968 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
969 $this->primaryauthMocks
= [ $mocks['primary'], $mocks['primary2'] ];
970 $this->secondaryauthMocks
= [
971 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2'],
972 // So linking happens
973 new ConfirmLinkSecondaryAuthenticationProvider
,
975 $this->initializeManager( true );
976 $this->logger
->setCollect( true );
978 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
979 $this->equalTo( AuthenticationResponse
::PASS
),
980 $this->equalTo( AuthenticationResponse
::FAIL
)
982 $providers = array_filter(
984 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
987 return is_callable( [ $p, 'expects' ] );
990 foreach ( $providers as $p ) {
991 $p->postCalled
= false;
992 $p->expects( $this->atMost( 1 ) )->method( 'postAuthentication' )
993 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
994 if ( $user !== null ) {
995 $this->assertInstanceOf( \User
::class, $user );
996 $this->assertSame( 'UTSysop', $user->getName() );
998 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
999 $this->assertThat( $response->status
, $constraint );
1000 $p->postCalled
= $response->status
;
1004 $session = $this->request
->getSession();
1005 $session->setRememberUser( !$req->rememberMe
);
1007 foreach ( $managerResponses as $i => $response ) {
1008 $success = $response instanceof AuthenticationResponse
&&
1009 $response->status
=== AuthenticationResponse
::PASS
;
1011 $this->hook( 'UserLoggedIn', $this->once() )
1012 ->with( $this->callback( function ( $user ) use ( $id, $name ) {
1013 return $user->getId() === $id && $user->getName() === $name;
1016 $this->hook( 'UserLoggedIn', $this->never() );
1019 $response instanceof AuthenticationResponse
&&
1020 $response->status
=== AuthenticationResponse
::FAIL
&&
1021 $response->message
->getKey() !== 'authmanager-authn-not-in-progress' &&
1022 $response->message
->getKey() !== 'authmanager-authn-no-primary'
1025 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
1027 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->never() );
1033 $ret = $this->manager
->beginAuthentication( [ $req ], 'http://localhost/' );
1035 $ret = $this->manager
->continueAuthentication( [ $req ] );
1037 if ( $response instanceof \Exception
) {
1038 $this->fail( 'Expected exception not thrown', "Response $i" );
1040 } catch ( \Exception
$ex ) {
1041 if ( !$response instanceof \Exception
) {
1044 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
1045 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1046 "Response $i, exception, session state" );
1047 $this->unhook( 'UserLoggedIn' );
1048 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1052 $this->unhook( 'UserLoggedIn' );
1053 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1055 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
1057 $ret->message
= $this->message( $ret->message
);
1058 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
1060 $this->assertSame( $id, $session->getUser()->getId(),
1061 "Response $i, authn" );
1063 $this->assertSame( 0, $session->getUser()->getId(),
1064 "Response $i, authn" );
1066 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
1067 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1068 "Response $i, session state" );
1069 foreach ( $providers as $p ) {
1070 $this->assertSame( $response->status
, $p->postCalled
,
1071 "Response $i, post-auth callback called" );
1074 $this->assertNotNull( $session->getSecret( 'AuthManager::authnState' ),
1075 "Response $i, session state" );
1076 foreach ( $ret->neededRequests
as $neededReq ) {
1077 $this->assertEquals( AuthManager
::ACTION_LOGIN
, $neededReq->action
,
1078 "Response $i, neededRequest action" );
1080 $this->assertEquals(
1081 $ret->neededRequests
,
1082 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN_CONTINUE
),
1083 "Response $i, continuation check"
1085 foreach ( $providers as $p ) {
1086 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
1090 $state = $session->getSecret( 'AuthManager::authnState' );
1091 $maybeLink = $state['maybeLink'] ??
[];
1092 if ( $link && $response->status
=== AuthenticationResponse
::RESTART
) {
1093 $this->assertEquals(
1094 $response->createRequest
->maybeLink
,
1096 "Response $i, maybeLink"
1099 $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
1104 $this->assertSame( $req->rememberMe
, $session->shouldRememberUser(),
1105 'rememberMe checkbox had effect' );
1107 $this->assertNotSame( $req->rememberMe
, $session->shouldRememberUser(),
1108 'rememberMe checkbox wasn\'t applied' );
1112 public function provideAuthentication() {
1113 $rememberReq = new RememberMeAuthenticationRequest
;
1114 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
1116 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1117 $req->foobar
= 'baz';
1118 $restartResponse = AuthenticationResponse
::newRestart(
1119 $this->message( 'authmanager-authn-no-local-user' )
1121 $restartResponse->neededRequests
= [ $rememberReq ];
1123 $restartResponse2Pass = AuthenticationResponse
::newPass( null );
1124 $restartResponse2Pass->linkRequest
= $req;
1125 $restartResponse2 = AuthenticationResponse
::newRestart(
1126 $this->message( 'authmanager-authn-no-local-user-link' )
1128 $restartResponse2->createRequest
= new CreateFromLoginAuthenticationRequest(
1129 null, [ $req->getUniqueId() => $req ]
1131 $restartResponse2->createRequest
->action
= AuthManager
::ACTION_LOGIN
;
1132 $restartResponse2->neededRequests
= [ $rememberReq, $restartResponse2->createRequest
];
1134 $userName = 'UTSysop';
1137 'Failure in pre-auth' => [
1138 StatusValue
::newFatal( 'fail-from-pre' ),
1142 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
1143 AuthenticationResponse
::newFail(
1144 $this->message( 'authmanager-authn-not-in-progress' )
1148 'Failure in primary' => [
1149 StatusValue
::newGood(),
1151 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
1156 'All primary abstain' => [
1157 StatusValue
::newGood(),
1159 AuthenticationResponse
::newAbstain(),
1163 AuthenticationResponse
::newFail( $this->message( 'authmanager-authn-no-primary' ) )
1166 'Primary UI, then redirect, then fail' => [
1167 StatusValue
::newGood(),
1169 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1170 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
1171 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
1176 'Primary redirect, then abstain' => [
1177 StatusValue
::newGood(),
1179 $tmp = AuthenticationResponse
::newRedirect(
1180 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
1182 AuthenticationResponse
::newAbstain(),
1187 new \
DomainException(
1188 'MockPrimaryAuthenticationProvider::continuePrimaryAuthentication() returned ABSTAIN'
1192 'Primary UI, then pass with no local user' => [
1193 StatusValue
::newGood(),
1195 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1196 AuthenticationResponse
::newPass( null ),
1204 'Primary UI, then pass with no local user (link type)' => [
1205 StatusValue
::newGood(),
1207 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1208 $restartResponse2Pass,
1217 'Primary pass with invalid username' => [
1218 StatusValue
::newGood(),
1220 AuthenticationResponse
::newPass( '<>' ),
1224 new \
DomainException( 'MockPrimaryAuthenticationProvider returned an invalid username: <>' ),
1227 'Secondary fail' => [
1228 StatusValue
::newGood(),
1230 AuthenticationResponse
::newPass( $userName ),
1233 AuthenticationResponse
::newFail( $this->message( 'fail-in-secondary' ) ),
1237 'Secondary UI, then abstain' => [
1238 StatusValue
::newGood(),
1240 AuthenticationResponse
::newPass( $userName ),
1243 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1244 AuthenticationResponse
::newAbstain()
1248 AuthenticationResponse
::newPass( $userName ),
1251 'Secondary pass' => [
1252 StatusValue
::newGood(),
1254 AuthenticationResponse
::newPass( $userName ),
1257 AuthenticationResponse
::newPass()
1260 AuthenticationResponse
::newPass( $userName ),
1267 * @dataProvider provideUserExists
1268 * @param bool $primary1Exists
1269 * @param bool $primary2Exists
1270 * @param bool $expect
1272 public function testUserExists( $primary1Exists, $primary2Exists, $expect ) {
1273 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1274 $mock1->expects( $this->any() )->method( 'getUniqueId' )
1275 ->will( $this->returnValue( 'primary1' ) );
1276 $mock1->expects( $this->any() )->method( 'testUserExists' )
1277 ->with( $this->equalTo( 'UTSysop' ) )
1278 ->will( $this->returnValue( $primary1Exists ) );
1279 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1280 $mock2->expects( $this->any() )->method( 'getUniqueId' )
1281 ->will( $this->returnValue( 'primary2' ) );
1282 $mock2->expects( $this->any() )->method( 'testUserExists' )
1283 ->with( $this->equalTo( 'UTSysop' ) )
1284 ->will( $this->returnValue( $primary2Exists ) );
1285 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1287 $this->initializeManager( true );
1288 $this->assertSame( $expect, $this->manager
->userExists( 'UTSysop' ) );
1291 public static function provideUserExists() {
1293 [ false, false, false ],
1294 [ true, false, true ],
1295 [ false, true, true ],
1296 [ true, true, true ],
1301 * @dataProvider provideAllowsAuthenticationDataChange
1302 * @param StatusValue $primaryReturn
1303 * @param StatusValue $secondaryReturn
1304 * @param Status $expect
1306 public function testAllowsAuthenticationDataChange( $primaryReturn, $secondaryReturn, $expect ) {
1307 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1309 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1310 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1311 $mock1->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1312 ->with( $this->equalTo( $req ) )
1313 ->will( $this->returnValue( $primaryReturn ) );
1314 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
1315 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1316 $mock2->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1317 ->with( $this->equalTo( $req ) )
1318 ->will( $this->returnValue( $secondaryReturn ) );
1320 $this->primaryauthMocks
= [ $mock1 ];
1321 $this->secondaryauthMocks
= [ $mock2 ];
1322 $this->initializeManager( true );
1323 $this->assertEquals( $expect, $this->manager
->allowsAuthenticationDataChange( $req ) );
1326 public static function provideAllowsAuthenticationDataChange() {
1327 $ignored = \Status
::newGood( 'ignored' );
1328 $ignored->warning( 'authmanager-change-not-supported' );
1330 $okFromPrimary = StatusValue
::newGood();
1331 $okFromPrimary->warning( 'warning-from-primary' );
1332 $okFromSecondary = StatusValue
::newGood();
1333 $okFromSecondary->warning( 'warning-from-secondary' );
1337 StatusValue
::newGood(),
1338 StatusValue
::newGood(),
1342 StatusValue
::newGood(),
1343 StatusValue
::newGood( 'ignore' ),
1347 StatusValue
::newGood( 'ignored' ),
1348 StatusValue
::newGood(),
1352 StatusValue
::newGood( 'ignored' ),
1353 StatusValue
::newGood( 'ignored' ),
1357 StatusValue
::newFatal( 'fail from primary' ),
1358 StatusValue
::newGood(),
1359 \Status
::newFatal( 'fail from primary' ),
1363 StatusValue
::newGood(),
1364 \Status
::wrap( $okFromPrimary ),
1367 StatusValue
::newGood(),
1368 StatusValue
::newFatal( 'fail from secondary' ),
1369 \Status
::newFatal( 'fail from secondary' ),
1372 StatusValue
::newGood(),
1374 \Status
::wrap( $okFromSecondary ),
1379 public function testChangeAuthenticationData() {
1380 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1381 $req->username
= 'UTSysop';
1383 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1384 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1385 $mock1->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1386 ->with( $this->equalTo( $req ) );
1387 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1388 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1389 $mock2->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1390 ->with( $this->equalTo( $req ) );
1392 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1393 $this->initializeManager( true );
1394 $this->logger
->setCollect( true );
1395 $this->manager
->changeAuthenticationData( $req );
1396 $this->assertSame( [
1397 [ LogLevel
::INFO
, 'Changing authentication data for {user} class {what}' ],
1398 ], $this->logger
->getBuffer() );
1401 public function testCanCreateAccounts() {
1403 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
1404 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
1405 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
1408 foreach ( $types as $type => $can ) {
1409 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1410 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
1411 $mock->expects( $this->any() )->method( 'accountCreationType' )
1412 ->will( $this->returnValue( $type ) );
1413 $this->primaryauthMocks
= [ $mock ];
1414 $this->initializeManager( true );
1415 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
1419 public function testCheckAccountCreatePermissions() {
1420 global $wgGroupPermissions;
1422 $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
1424 $this->initializeManager( true );
1426 $wgGroupPermissions['*']['createaccount'] = true;
1427 $this->assertEquals(
1429 $this->manager
->checkAccountCreatePermissions( new \User
)
1432 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1433 $readOnlyMode->setReason( 'Because' );
1434 $this->assertEquals(
1435 \Status
::newFatal( wfMessage( 'readonlytext', 'Because' ) ),
1436 $this->manager
->checkAccountCreatePermissions( new \User
)
1438 $readOnlyMode->setReason( false );
1440 $wgGroupPermissions['*']['createaccount'] = false;
1441 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1442 $this->assertFalse( $status->isOK() );
1443 $this->assertTrue( $status->hasMessage( 'badaccess-groups' ) );
1444 $wgGroupPermissions['*']['createaccount'] = true;
1446 $user = \User
::newFromName( 'UTBlockee' );
1447 if ( $user->getID() == 0 ) {
1448 $user->addToDatabase();
1449 \TestUser
::setPasswordForUser( $user, 'UTBlockeePassword' );
1450 $user->saveSettings();
1452 $oldBlock = \Block
::newFromTarget( 'UTBlockee' );
1454 // An old block will prevent our new one from saving.
1455 $oldBlock->delete();
1458 'address' => 'UTBlockee',
1459 'user' => $user->getID(),
1460 'by' => $this->getTestSysop()->getUser()->getId(),
1461 'reason' => __METHOD__
,
1462 'expiry' => time() +
100500,
1463 'createAccount' => true,
1465 $block = new \
Block( $blockOptions );
1467 $status = $this->manager
->checkAccountCreatePermissions( $user );
1468 $this->assertFalse( $status->isOK() );
1469 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
1472 'address' => '127.0.0.0/24',
1473 'by' => $this->getTestSysop()->getUser()->getId(),
1474 'reason' => __METHOD__
,
1475 'expiry' => time() +
100500,
1476 'createAccount' => true,
1478 $block = new \
Block( $blockOptions );
1480 $scopeVariable = new ScopedCallback( [ $block, 'delete' ] );
1481 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1482 $this->assertFalse( $status->isOK() );
1483 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
1484 ScopedCallback
::consume( $scopeVariable );
1486 $this->setMwGlobals( [
1487 'wgEnableDnsBlacklist' => true,
1488 'wgDnsBlacklistUrls' => [
1489 'local.wmftest.net', // This will resolve for every subdomain, which works to test "listed?"
1491 'wgProxyWhitelist' => [],
1493 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1494 $this->assertFalse( $status->isOK() );
1495 $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
1496 $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
1497 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1498 $this->assertTrue( $status->isGood() );
1502 * @param string $uniq
1505 private static function usernameForCreation( $uniq = '' ) {
1508 $username = "UTAuthManagerTestAccountCreation" . $uniq . ++
$i;
1509 } while ( \User
::newFromName( $username )->getId() !== 0 );
1513 public function testCanCreateAccount() {
1514 $username = self
::usernameForCreation();
1515 $this->initializeManager();
1517 $this->assertEquals(
1518 \Status
::newFatal( 'authmanager-create-disabled' ),
1519 $this->manager
->canCreateAccount( $username )
1522 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1523 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1524 $mock->expects( $this->any() )->method( 'accountCreationType' )
1525 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1526 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1527 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1528 ->will( $this->returnValue( StatusValue
::newGood() ) );
1529 $this->primaryauthMocks
= [ $mock ];
1530 $this->initializeManager( true );
1532 $this->assertEquals(
1533 \Status
::newFatal( 'userexists' ),
1534 $this->manager
->canCreateAccount( $username )
1537 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1538 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1539 $mock->expects( $this->any() )->method( 'accountCreationType' )
1540 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1541 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1542 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1543 ->will( $this->returnValue( StatusValue
::newGood() ) );
1544 $this->primaryauthMocks
= [ $mock ];
1545 $this->initializeManager( true );
1547 $this->assertEquals(
1548 \Status
::newFatal( 'noname' ),
1549 $this->manager
->canCreateAccount( $username . '<>' )
1552 $this->assertEquals(
1553 \Status
::newFatal( 'userexists' ),
1554 $this->manager
->canCreateAccount( 'UTSysop' )
1557 $this->assertEquals(
1559 $this->manager
->canCreateAccount( $username )
1562 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1563 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1564 $mock->expects( $this->any() )->method( 'accountCreationType' )
1565 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1566 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1567 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1568 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1569 $this->primaryauthMocks
= [ $mock ];
1570 $this->initializeManager( true );
1572 $this->assertEquals(
1573 \Status
::newFatal( 'fail' ),
1574 $this->manager
->canCreateAccount( $username )
1578 public function testBeginAccountCreation() {
1579 $creator = \User
::newFromName( 'UTSysop' );
1580 $userReq = new UsernameAuthenticationRequest
;
1581 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1582 return $level === LogLevel
::DEBUG ?
null : $message;
1584 $this->initializeManager();
1586 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
1587 $this->hook( 'LocalUserCreated', $this->never() );
1589 $this->manager
->beginAccountCreation(
1590 $creator, [], 'http://localhost/'
1592 $this->fail( 'Expected exception not thrown' );
1593 } catch ( \LogicException
$ex ) {
1594 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1596 $this->unhook( 'LocalUserCreated' );
1598 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1601 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1602 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1603 $mock->expects( $this->any() )->method( 'accountCreationType' )
1604 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1605 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1606 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1607 ->will( $this->returnValue( StatusValue
::newGood() ) );
1608 $this->primaryauthMocks
= [ $mock ];
1609 $this->initializeManager( true );
1611 $this->hook( 'LocalUserCreated', $this->never() );
1612 $ret = $this->manager
->beginAccountCreation( $creator, [], 'http://localhost/' );
1613 $this->unhook( 'LocalUserCreated' );
1614 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1615 $this->assertSame( 'noname', $ret->message
->getKey() );
1617 $this->hook( 'LocalUserCreated', $this->never() );
1618 $userReq->username
= self
::usernameForCreation();
1619 $userReq2 = new UsernameAuthenticationRequest
;
1620 $userReq2->username
= $userReq->username
. 'X';
1621 $ret = $this->manager
->beginAccountCreation(
1622 $creator, [ $userReq, $userReq2 ], 'http://localhost/'
1624 $this->unhook( 'LocalUserCreated' );
1625 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1626 $this->assertSame( 'noname', $ret->message
->getKey() );
1628 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1629 $readOnlyMode->setReason( 'Because' );
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( 'readonlytext', $ret->message
->getKey() );
1636 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1637 $readOnlyMode->setReason( false );
1639 $this->hook( 'LocalUserCreated', $this->never() );
1640 $userReq->username
= self
::usernameForCreation();
1641 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1642 $this->unhook( 'LocalUserCreated' );
1643 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1644 $this->assertSame( 'userexists', $ret->message
->getKey() );
1646 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1647 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1648 $mock->expects( $this->any() )->method( 'accountCreationType' )
1649 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1650 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1651 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1652 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1653 $this->primaryauthMocks
= [ $mock ];
1654 $this->initializeManager( true );
1656 $this->hook( 'LocalUserCreated', $this->never() );
1657 $userReq->username
= self
::usernameForCreation();
1658 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1659 $this->unhook( 'LocalUserCreated' );
1660 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1661 $this->assertSame( 'fail', $ret->message
->getKey() );
1663 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1664 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1665 $mock->expects( $this->any() )->method( 'accountCreationType' )
1666 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1667 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1668 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1669 ->will( $this->returnValue( StatusValue
::newGood() ) );
1670 $this->primaryauthMocks
= [ $mock ];
1671 $this->initializeManager( true );
1673 $this->hook( 'LocalUserCreated', $this->never() );
1674 $userReq->username
= self
::usernameForCreation() . '<>';
1675 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1676 $this->unhook( 'LocalUserCreated' );
1677 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1678 $this->assertSame( 'noname', $ret->message
->getKey() );
1680 $this->hook( 'LocalUserCreated', $this->never() );
1681 $userReq->username
= $creator->getName();
1682 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1683 $this->unhook( 'LocalUserCreated' );
1684 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1685 $this->assertSame( 'userexists', $ret->message
->getKey() );
1687 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1688 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1689 $mock->expects( $this->any() )->method( 'accountCreationType' )
1690 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1691 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1692 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1693 ->will( $this->returnValue( StatusValue
::newGood() ) );
1694 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
1695 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1696 $this->primaryauthMocks
= [ $mock ];
1697 $this->initializeManager( true );
1699 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1700 ->setMethods( [ 'populateUser' ] )
1702 $req->expects( $this->any() )->method( 'populateUser' )
1703 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1704 $userReq->username
= self
::usernameForCreation();
1705 $ret = $this->manager
->beginAccountCreation(
1706 $creator, [ $userReq, $req ], 'http://localhost/'
1708 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1709 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1711 $req = new UserDataAuthenticationRequest
;
1712 $userReq->username
= self
::usernameForCreation();
1714 $ret = $this->manager
->beginAccountCreation(
1715 $creator, [ $userReq, $req ], 'http://localhost/'
1717 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1718 $this->assertSame( 'fail', $ret->message
->getKey() );
1720 $this->manager
->beginAccountCreation(
1721 \User
::newFromName( $userReq->username
), [ $userReq, $req ], 'http://localhost/'
1723 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1724 $this->assertSame( 'fail', $ret->message
->getKey() );
1727 public function testContinueAccountCreation() {
1728 $creator = \User
::newFromName( 'UTSysop' );
1729 $username = self
::usernameForCreation();
1730 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1731 return $level === LogLevel
::DEBUG ?
null : $message;
1733 $this->initializeManager();
1737 'username' => $username,
1739 'creatorname' => $username,
1742 'primaryResponse' => null,
1744 'ranPreTests' => true,
1747 $this->hook( 'LocalUserCreated', $this->never() );
1749 $this->manager
->continueAccountCreation( [] );
1750 $this->fail( 'Expected exception not thrown' );
1751 } catch ( \LogicException
$ex ) {
1752 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1754 $this->unhook( 'LocalUserCreated' );
1756 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1757 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1758 $mock->expects( $this->any() )->method( 'accountCreationType' )
1759 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1760 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1761 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )->will(
1762 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
1764 $this->primaryauthMocks
= [ $mock ];
1765 $this->initializeManager( true );
1767 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', null );
1768 $this->hook( 'LocalUserCreated', $this->never() );
1769 $ret = $this->manager
->continueAccountCreation( [] );
1770 $this->unhook( 'LocalUserCreated' );
1771 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1772 $this->assertSame( 'authmanager-create-not-in-progress', $ret->message
->getKey() );
1774 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1775 [ 'username' => "$username<>" ] +
$session );
1776 $this->hook( 'LocalUserCreated', $this->never() );
1777 $ret = $this->manager
->continueAccountCreation( [] );
1778 $this->unhook( 'LocalUserCreated' );
1779 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1780 $this->assertSame( 'noname', $ret->message
->getKey() );
1782 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1785 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $session );
1786 $this->hook( 'LocalUserCreated', $this->never() );
1787 $cache = \ObjectCache
::getLocalClusterInstance();
1788 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1789 $ret = $this->manager
->continueAccountCreation( [] );
1791 $this->unhook( 'LocalUserCreated' );
1792 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1793 $this->assertSame( 'usernameinprogress', $ret->message
->getKey() );
1794 // This error shouldn't remove the existing session, because the
1795 // raced-with process "owns" it.
1797 $session, $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1800 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1801 [ 'username' => $creator->getName() ] +
$session );
1802 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1803 $readOnlyMode->setReason( 'Because' );
1804 $this->hook( 'LocalUserCreated', $this->never() );
1805 $ret = $this->manager
->continueAccountCreation( [] );
1806 $this->unhook( 'LocalUserCreated' );
1807 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1808 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
1809 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1810 $readOnlyMode->setReason( false );
1812 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1813 [ 'username' => $creator->getName() ] +
$session );
1814 $this->hook( 'LocalUserCreated', $this->never() );
1815 $ret = $this->manager
->continueAccountCreation( [] );
1816 $this->unhook( 'LocalUserCreated' );
1817 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1818 $this->assertSame( 'userexists', $ret->message
->getKey() );
1820 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1823 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1824 [ 'userid' => $creator->getId() ] +
$session );
1825 $this->hook( 'LocalUserCreated', $this->never() );
1827 $ret = $this->manager
->continueAccountCreation( [] );
1828 $this->fail( 'Expected exception not thrown' );
1829 } catch ( \UnexpectedValueException
$ex ) {
1830 $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
1832 $this->unhook( 'LocalUserCreated' );
1834 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1837 $id = $creator->getId();
1838 $name = $creator->getName();
1839 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1840 [ 'username' => $name, 'userid' => $id +
1 ] +
$session );
1841 $this->hook( 'LocalUserCreated', $this->never() );
1843 $ret = $this->manager
->continueAccountCreation( [] );
1844 $this->fail( 'Expected exception not thrown' );
1845 } catch ( \UnexpectedValueException
$ex ) {
1846 $this->assertEquals(
1847 "User \"{$name}\" exists, but ID $id != " . ( $id +
1 ) . '!', $ex->getMessage()
1850 $this->unhook( 'LocalUserCreated' );
1852 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1855 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1856 ->setMethods( [ 'populateUser' ] )
1858 $req->expects( $this->any() )->method( 'populateUser' )
1859 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1860 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1861 [ 'reqs' => [ $req ] ] +
$session );
1862 $ret = $this->manager
->continueAccountCreation( [] );
1863 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1864 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1866 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1871 * @dataProvider provideAccountCreation
1872 * @param StatusValue $preTest
1873 * @param StatusValue $primaryTest
1874 * @param StatusValue $secondaryTest
1875 * @param array $primaryResponses
1876 * @param array $secondaryResponses
1877 * @param array $managerResponses
1879 public function testAccountCreation(
1880 StatusValue
$preTest, $primaryTest, $secondaryTest,
1881 array $primaryResponses, array $secondaryResponses, array $managerResponses
1883 $creator = \User
::newFromName( 'UTSysop' );
1884 $username = self
::usernameForCreation();
1886 $this->initializeManager();
1888 // Set up lots of mocks...
1889 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1890 $req->preTest
= $preTest;
1891 $req->primaryTest
= $primaryTest;
1892 $req->secondaryTest
= $secondaryTest;
1893 $req->primary
= $primaryResponses;
1894 $req->secondary
= $secondaryResponses;
1896 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
1897 $class = ucfirst( $key ) . 'AuthenticationProvider';
1898 $mocks[$key] = $this->getMockForAbstractClass(
1899 "MediaWiki\\Auth\\$class", [], "Mock$class"
1901 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
1902 ->will( $this->returnValue( $key ) );
1903 $mocks[$key]->expects( $this->any() )->method( 'testUserForCreation' )
1904 ->will( $this->returnValue( StatusValue
::newGood() ) );
1905 $mocks[$key]->expects( $this->any() )->method( 'testForAccountCreation' )
1906 ->will( $this->returnCallback(
1907 function ( $user, $creatorIn, $reqs )
1908 use ( $username, $creator, $req, $key )
1910 $this->assertSame( $username, $user->getName() );
1911 $this->assertSame( $creator->getId(), $creatorIn->getId() );
1912 $this->assertSame( $creator->getName(), $creatorIn->getName() );
1914 foreach ( $reqs as $r ) {
1915 $this->assertSame( $username, $r->username
);
1916 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1918 $this->assertTrue( $foundReq, '$reqs contains $req' );
1924 for ( $i = 2; $i <= 3; $i++
) {
1925 $mocks[$key . $i] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
1926 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
1927 ->will( $this->returnValue( $key . $i ) );
1928 $mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' )
1929 ->will( $this->returnValue( StatusValue
::newGood() ) );
1930 $mocks[$key . $i]->expects( $this->atMost( 1 ) )->method( 'testForAccountCreation' )
1931 ->will( $this->returnValue( StatusValue
::newGood() ) );
1935 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
1936 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1937 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
1938 ->will( $this->returnValue( false ) );
1939 $ct = count( $req->primary
);
1940 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1941 $this->assertSame( $username, $user->getName() );
1942 $this->assertSame( 'UTSysop', $creator->getName() );
1944 foreach ( $reqs as $r ) {
1945 $this->assertSame( $username, $r->username
);
1946 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1948 $this->assertTrue( $foundReq, '$reqs contains $req' );
1949 return array_shift( $req->primary
);
1951 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
1952 ->method( 'beginPrimaryAccountCreation' )
1953 ->will( $callback );
1954 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1955 ->method( 'continuePrimaryAccountCreation' )
1956 ->will( $callback );
1958 $ct = count( $req->secondary
);
1959 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1960 $this->assertSame( $username, $user->getName() );
1961 $this->assertSame( 'UTSysop', $creator->getName() );
1963 foreach ( $reqs as $r ) {
1964 $this->assertSame( $username, $r->username
);
1965 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1967 $this->assertTrue( $foundReq, '$reqs contains $req' );
1968 return array_shift( $req->secondary
);
1970 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
1971 ->method( 'beginSecondaryAccountCreation' )
1972 ->will( $callback );
1973 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1974 ->method( 'continueSecondaryAccountCreation' )
1975 ->will( $callback );
1977 $abstain = AuthenticationResponse
::newAbstain();
1978 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
1979 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
1980 $mocks['primary2']->expects( $this->any() )->method( 'testUserExists' )
1981 ->will( $this->returnValue( false ) );
1982 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountCreation' )
1983 ->will( $this->returnValue( $abstain ) );
1984 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1985 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
1986 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_NONE
) );
1987 $mocks['primary3']->expects( $this->any() )->method( 'testUserExists' )
1988 ->will( $this->returnValue( false ) );
1989 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountCreation' );
1990 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1991 $mocks['secondary2']->expects( $this->atMost( 1 ) )
1992 ->method( 'beginSecondaryAccountCreation' )
1993 ->will( $this->returnValue( $abstain ) );
1994 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1995 $mocks['secondary3']->expects( $this->atMost( 1 ) )
1996 ->method( 'beginSecondaryAccountCreation' )
1997 ->will( $this->returnValue( $abstain ) );
1998 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
2000 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
2001 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary'], $mocks['primary2'] ];
2002 $this->secondaryauthMocks
= [
2003 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2']
2006 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
2007 return $level === LogLevel
::DEBUG ?
null : $message;
2010 $this->initializeManager( true );
2012 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
2013 $this->equalTo( AuthenticationResponse
::PASS
),
2014 $this->equalTo( AuthenticationResponse
::FAIL
)
2016 $providers = array_merge(
2017 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
2019 foreach ( $providers as $p ) {
2020 $p->postCalled
= false;
2021 $p->expects( $this->atMost( 1 ) )->method( 'postAccountCreation' )
2022 ->willReturnCallback( function ( $user, $creator, $response )
2023 use ( $constraint, $p, $username )
2025 $this->assertInstanceOf( \User
::class, $user );
2026 $this->assertSame( $username, $user->getName() );
2027 $this->assertSame( 'UTSysop', $creator->getName() );
2028 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
2029 $this->assertThat( $response->status
, $constraint );
2030 $p->postCalled
= $response->status
;
2034 // We're testing with $wgNewUserLog = false, so assert that it worked
2035 $dbw = wfGetDB( DB_MASTER
);
2036 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2040 foreach ( $managerResponses as $i => $response ) {
2041 $success = $response instanceof AuthenticationResponse
&&
2042 $response->status
=== AuthenticationResponse
::PASS
;
2043 if ( $i === 'created' ) {
2045 $this->hook( 'LocalUserCreated', $this->once() )
2047 $this->callback( function ( $user ) use ( $username ) {
2048 return $user->getName() === $username;
2050 $this->equalTo( false )
2052 $expectLog[] = [ LogLevel
::INFO
, "Creating user {user} during account creation" ];
2054 $this->hook( 'LocalUserCreated', $this->never() );
2060 $userReq = new UsernameAuthenticationRequest
;
2061 $userReq->username
= $username;
2062 $ret = $this->manager
->beginAccountCreation(
2063 $creator, [ $userReq, $req ], 'http://localhost/'
2066 $ret = $this->manager
->continueAccountCreation( [ $req ] );
2068 if ( $response instanceof \Exception
) {
2069 $this->fail( 'Expected exception not thrown', "Response $i" );
2071 } catch ( \Exception
$ex ) {
2072 if ( !$response instanceof \Exception
) {
2075 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
2077 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2078 "Response $i, exception, session state"
2080 $this->unhook( 'LocalUserCreated' );
2084 $this->unhook( 'LocalUserCreated' );
2086 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
2089 $this->assertNotNull( $ret->loginRequest
, "Response $i, login marker" );
2090 $this->assertContains(
2091 $ret->loginRequest
, $this->managerPriv
->createdAccountAuthenticationRequests
,
2092 "Response $i, login marker"
2097 "MediaWiki\Auth\AuthManager::continueAccountCreation: Account creation succeeded for {user}"
2100 // Set some fields in the expected $response that we couldn't
2101 // know in provideAccountCreation().
2102 $response->username
= $username;
2103 $response->loginRequest
= $ret->loginRequest
;
2105 $this->assertNull( $ret->loginRequest
, "Response $i, login marker" );
2106 $this->assertSame( [], $this->managerPriv
->createdAccountAuthenticationRequests
,
2107 "Response $i, login marker" );
2109 $ret->message
= $this->message( $ret->message
);
2110 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
2111 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
2113 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2114 "Response $i, session state"
2116 foreach ( $providers as $p ) {
2117 $this->assertSame( $response->status
, $p->postCalled
,
2118 "Response $i, post-auth callback called" );
2121 $this->assertNotNull(
2122 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2123 "Response $i, session state"
2125 foreach ( $ret->neededRequests
as $neededReq ) {
2126 $this->assertEquals( AuthManager
::ACTION_CREATE
, $neededReq->action
,
2127 "Response $i, neededRequest action" );
2129 $this->assertEquals(
2130 $ret->neededRequests
,
2131 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_CREATE_CONTINUE
),
2132 "Response $i, continuation check"
2134 foreach ( $providers as $p ) {
2135 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
2140 $this->assertNotEquals( 0, \User
::idFromName( $username ) );
2142 $this->assertEquals( 0, \User
::idFromName( $username ) );
2148 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
2152 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2156 public function provideAccountCreation() {
2157 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
2158 $good = StatusValue
::newGood();
2161 'Pre-creation test fail in pre' => [
2162 StatusValue
::newFatal( 'fail-from-pre' ), $good, $good,
2166 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
2169 'Pre-creation test fail in primary' => [
2170 $good, StatusValue
::newFatal( 'fail-from-primary' ), $good,
2174 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2177 'Pre-creation test fail in secondary' => [
2178 $good, $good, StatusValue
::newFatal( 'fail-from-secondary' ),
2182 AuthenticationResponse
::newFail( $this->message( 'fail-from-secondary' ) ),
2185 'Failure in primary' => [
2186 $good, $good, $good,
2188 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2193 'All primary abstain' => [
2194 $good, $good, $good,
2196 AuthenticationResponse
::newAbstain(),
2200 AuthenticationResponse
::newFail( $this->message( 'authmanager-create-no-primary' ) )
2203 'Primary UI, then redirect, then fail' => [
2204 $good, $good, $good,
2206 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2207 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
2208 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
2213 'Primary redirect, then abstain' => [
2214 $good, $good, $good,
2216 $tmp = AuthenticationResponse
::newRedirect(
2217 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
2219 AuthenticationResponse
::newAbstain(),
2224 new \
DomainException(
2225 'MockPrimaryAuthenticationProvider::continuePrimaryAccountCreation() returned ABSTAIN'
2229 'Primary UI, then pass; secondary abstain' => [
2230 $good, $good, $good,
2232 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2233 AuthenticationResponse
::newPass(),
2236 AuthenticationResponse
::newAbstain(),
2240 'created' => AuthenticationResponse
::newPass( '' ),
2243 'Primary pass; secondary UI then pass' => [
2244 $good, $good, $good,
2246 AuthenticationResponse
::newPass( '' ),
2249 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2250 AuthenticationResponse
::newPass( '' ),
2254 AuthenticationResponse
::newPass( '' ),
2257 'Primary pass; secondary fail' => [
2258 $good, $good, $good,
2260 AuthenticationResponse
::newPass(),
2263 AuthenticationResponse
::newFail( $this->message( '...' ) ),
2266 'created' => new \
DomainException(
2267 'MockSecondaryAuthenticationProvider::beginSecondaryAccountCreation() returned FAIL. ' .
2268 'Secondary providers are not allowed to fail account creation, ' .
2269 'that should have been done via testForAccountCreation().'
2277 * @dataProvider provideAccountCreationLogging
2278 * @param bool $isAnon
2279 * @param string|null $logSubtype
2281 public function testAccountCreationLogging( $isAnon, $logSubtype ) {
2282 $creator = $isAnon ?
new \User
: \User
::newFromName( 'UTSysop' );
2283 $username = self
::usernameForCreation();
2285 $this->initializeManager();
2287 // Set up lots of mocks...
2288 $mock = $this->getMockForAbstractClass(
2289 \MediaWiki\Auth\PrimaryAuthenticationProvider
::class, []
2291 $mock->expects( $this->any() )->method( 'getUniqueId' )
2292 ->will( $this->returnValue( 'primary' ) );
2293 $mock->expects( $this->any() )->method( 'testUserForCreation' )
2294 ->will( $this->returnValue( StatusValue
::newGood() ) );
2295 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
2296 ->will( $this->returnValue( StatusValue
::newGood() ) );
2297 $mock->expects( $this->any() )->method( 'accountCreationType' )
2298 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2299 $mock->expects( $this->any() )->method( 'testUserExists' )
2300 ->will( $this->returnValue( false ) );
2301 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
2302 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
2303 $mock->expects( $this->any() )->method( 'finishAccountCreation' )
2304 ->will( $this->returnValue( $logSubtype ) );
2306 $this->primaryauthMocks
= [ $mock ];
2307 $this->initializeManager( true );
2308 $this->logger
->setCollect( true );
2310 $this->config
->set( 'NewUserLog', true );
2312 $dbw = wfGetDB( DB_MASTER
);
2313 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2315 $userReq = new UsernameAuthenticationRequest
;
2316 $userReq->username
= $username;
2317 $reasonReq = new CreationReasonAuthenticationRequest
;
2318 $reasonReq->reason
= $this->toString();
2319 $ret = $this->manager
->beginAccountCreation(
2320 $creator, [ $userReq, $reasonReq ], 'http://localhost/'
2323 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
2325 $user = \User
::newFromName( $username );
2326 $this->assertNotEquals( 0, $user->getId(), 'sanity check' );
2327 $this->assertNotEquals( $creator->getId(), $user->getId(), 'sanity check' );
2329 $data = \DatabaseLogEntry
::getSelectQueryData();
2330 $rows = iterator_to_array( $dbw->select(
2334 'log_id > ' . (int)$maxLogId,
2335 'log_type' => 'newusers'
2341 $this->assertCount( 1, $rows );
2342 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2344 $this->assertSame( $logSubtype ?
: ( $isAnon ?
'create' : 'create2' ), $entry->getSubtype() );
2346 $isAnon ?
$user->getId() : $creator->getId(),
2347 $entry->getPerformer()->getId()
2350 $isAnon ?
$user->getName() : $creator->getName(),
2351 $entry->getPerformer()->getName()
2353 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2354 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2355 $this->assertSame( $this->toString(), $entry->getComment() );
2358 public static function provideAccountCreationLogging() {
2363 [ false, 'byemail' ],
2367 public function testAutoAccountCreation() {
2368 global $wgGroupPermissions, $wgHooks;
2370 // PHPUnit seems to have a bug where it will call the ->with()
2371 // callbacks for our hooks again after the test is run (WTF?), which
2372 // breaks here because $username no longer matches $user by the end of
2374 $workaroundPHPUnitBug = false;
2376 $username = self
::usernameForCreation();
2377 $this->initializeManager();
2379 $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
2380 $wgGroupPermissions['*']['createaccount'] = true;
2381 $wgGroupPermissions['*']['autocreateaccount'] = false;
2383 \ObjectCache
::$instances[__METHOD__
] = new \
HashBagOStuff();
2384 $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__
] );
2386 // Set up lots of mocks...
2388 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2389 $class = ucfirst( $key ) . 'AuthenticationProvider';
2390 $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
2391 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2392 ->will( $this->returnValue( $key ) );
2395 $good = StatusValue
::newGood();
2396 $callback = $this->callback( function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
2397 return $workaroundPHPUnitBug ||
$user->getName() === $username;
2400 $mocks['pre']->expects( $this->exactly( 12 ) )->method( 'testUserForCreation' )
2401 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2402 ->will( $this->onConsecutiveCalls(
2403 StatusValue
::newFatal( 'ok' ), StatusValue
::newFatal( 'ok' ), // For testing permissions
2404 StatusValue
::newFatal( 'fail-in-pre' ), $good, $good,
2405 $good, // backoff test
2406 $good, // addToDatabase fails test
2407 $good, // addToDatabase throws test
2408 $good, // addToDatabase exists test
2409 $good, $good, $good // success
2412 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
2413 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2414 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
2415 ->will( $this->returnValue( true ) );
2416 $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
2417 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2418 ->will( $this->onConsecutiveCalls(
2419 StatusValue
::newFatal( 'fail-in-primary' ), $good,
2420 $good, // backoff test
2421 $good, // addToDatabase fails test
2422 $good, // addToDatabase throws test
2423 $good, // addToDatabase exists test
2426 $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2427 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) );
2429 $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
2430 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2431 ->will( $this->onConsecutiveCalls(
2432 StatusValue
::newFatal( 'fail-in-secondary' ),
2433 $good, // backoff test
2434 $good, // addToDatabase fails test
2435 $good, // addToDatabase throws test
2436 $good, // addToDatabase exists test
2439 $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2440 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) );
2442 $this->preauthMocks
= [ $mocks['pre'] ];
2443 $this->primaryauthMocks
= [ $mocks['primary'] ];
2444 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2445 $this->initializeManager( true );
2446 $session = $this->request
->getSession();
2448 $logger = new \
TestLogger( true, function ( $m ) {
2449 $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
2452 $this->manager
->setLogger( $logger );
2455 $user = \User
::newFromName( 'UTSysop' );
2456 $this->manager
->autoCreateUser( $user, 'InvalidSource', true );
2457 $this->fail( 'Expected exception not thrown' );
2458 } catch ( \InvalidArgumentException
$ex ) {
2459 $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
2462 // First, check an existing user
2464 $user = \User
::newFromName( 'UTSysop' );
2465 $this->hook( 'LocalUserCreated', $this->never() );
2466 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2467 $this->unhook( 'LocalUserCreated' );
2468 $expect = \Status
::newGood();
2469 $expect->warning( 'userexists' );
2470 $this->assertEquals( $expect, $ret );
2471 $this->assertNotEquals( 0, $user->getId() );
2472 $this->assertSame( 'UTSysop', $user->getName() );
2473 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2474 $this->assertSame( [
2475 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2476 ], $logger->getBuffer() );
2477 $logger->clearBuffer();
2480 $user = \User
::newFromName( 'UTSysop' );
2481 $this->hook( 'LocalUserCreated', $this->never() );
2482 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2483 $this->unhook( 'LocalUserCreated' );
2484 $expect = \Status
::newGood();
2485 $expect->warning( 'userexists' );
2486 $this->assertEquals( $expect, $ret );
2487 $this->assertNotEquals( 0, $user->getId() );
2488 $this->assertSame( 'UTSysop', $user->getName() );
2489 $this->assertEquals( 0, $session->getUser()->getId() );
2490 $this->assertSame( [
2491 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2492 ], $logger->getBuffer() );
2493 $logger->clearBuffer();
2495 // Wiki is read-only
2497 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
2498 $readOnlyMode->setReason( 'Because' );
2499 $user = \User
::newFromName( $username );
2500 $this->hook( 'LocalUserCreated', $this->never() );
2501 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2502 $this->unhook( 'LocalUserCreated' );
2503 $this->assertEquals( \Status
::newFatal( wfMessage( 'readonlytext', 'Because' ) ), $ret );
2504 $this->assertEquals( 0, $user->getId() );
2505 $this->assertNotEquals( $username, $user->getName() );
2506 $this->assertEquals( 0, $session->getUser()->getId() );
2507 $this->assertSame( [
2508 [ LogLevel
::DEBUG
, 'denied by wfReadOnly(): {reason}' ],
2509 ], $logger->getBuffer() );
2510 $logger->clearBuffer();
2511 $readOnlyMode->setReason( false );
2513 // Session blacklisted
2515 $session->set( 'AuthManager::AutoCreateBlacklist', 'test' );
2516 $user = \User
::newFromName( $username );
2517 $this->hook( 'LocalUserCreated', $this->never() );
2518 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2519 $this->unhook( 'LocalUserCreated' );
2520 $this->assertEquals( \Status
::newFatal( 'test' ), $ret );
2521 $this->assertEquals( 0, $user->getId() );
2522 $this->assertNotEquals( $username, $user->getName() );
2523 $this->assertEquals( 0, $session->getUser()->getId() );
2524 $this->assertSame( [
2525 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2526 ], $logger->getBuffer() );
2527 $logger->clearBuffer();
2530 $session->set( 'AuthManager::AutoCreateBlacklist', StatusValue
::newFatal( 'test2' ) );
2531 $user = \User
::newFromName( $username );
2532 $this->hook( 'LocalUserCreated', $this->never() );
2533 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2534 $this->unhook( 'LocalUserCreated' );
2535 $this->assertEquals( \Status
::newFatal( 'test2' ), $ret );
2536 $this->assertEquals( 0, $user->getId() );
2537 $this->assertNotEquals( $username, $user->getName() );
2538 $this->assertEquals( 0, $session->getUser()->getId() );
2539 $this->assertSame( [
2540 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2541 ], $logger->getBuffer() );
2542 $logger->clearBuffer();
2546 $user = \User
::newFromName( $username . '@' );
2547 $this->hook( 'LocalUserCreated', $this->never() );
2548 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2549 $this->unhook( 'LocalUserCreated' );
2550 $this->assertEquals( \Status
::newFatal( 'noname' ), $ret );
2551 $this->assertEquals( 0, $user->getId() );
2552 $this->assertNotEquals( $username . '@', $user->getId() );
2553 $this->assertEquals( 0, $session->getUser()->getId() );
2554 $this->assertSame( [
2555 [ LogLevel
::DEBUG
, 'name "{username}" is not creatable' ],
2556 ], $logger->getBuffer() );
2557 $logger->clearBuffer();
2558 $this->assertSame( 'noname', $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2560 // IP unable to create accounts
2561 $wgGroupPermissions['*']['createaccount'] = false;
2562 $wgGroupPermissions['*']['autocreateaccount'] = false;
2564 $user = \User
::newFromName( $username );
2565 $this->hook( 'LocalUserCreated', $this->never() );
2566 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2567 $this->unhook( 'LocalUserCreated' );
2568 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-noperm' ), $ret );
2569 $this->assertEquals( 0, $user->getId() );
2570 $this->assertNotEquals( $username, $user->getName() );
2571 $this->assertEquals( 0, $session->getUser()->getId() );
2572 $this->assertSame( [
2573 [ LogLevel
::DEBUG
, 'IP lacks the ability to create or autocreate accounts' ],
2574 ], $logger->getBuffer() );
2575 $logger->clearBuffer();
2577 'authmanager-autocreate-noperm', $session->get( 'AuthManager::AutoCreateBlacklist' )
2580 // Test that both permutations of permissions are allowed
2581 // (this hits the two "ok" entries in $mocks['pre'])
2582 $wgGroupPermissions['*']['createaccount'] = false;
2583 $wgGroupPermissions['*']['autocreateaccount'] = true;
2585 $user = \User
::newFromName( $username );
2586 $this->hook( 'LocalUserCreated', $this->never() );
2587 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2588 $this->unhook( 'LocalUserCreated' );
2589 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2591 $wgGroupPermissions['*']['createaccount'] = true;
2592 $wgGroupPermissions['*']['autocreateaccount'] = false;
2594 $user = \User
::newFromName( $username );
2595 $this->hook( 'LocalUserCreated', $this->never() );
2596 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2597 $this->unhook( 'LocalUserCreated' );
2598 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2599 $logger->clearBuffer();
2603 $user = \User
::newFromName( $username );
2604 $this->hook( 'LocalUserCreated', $this->never() );
2605 $cache = \ObjectCache
::getLocalClusterInstance();
2606 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2607 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2609 $this->unhook( 'LocalUserCreated' );
2610 $this->assertEquals( \Status
::newFatal( 'usernameinprogress' ), $ret );
2611 $this->assertEquals( 0, $user->getId() );
2612 $this->assertNotEquals( $username, $user->getName() );
2613 $this->assertEquals( 0, $session->getUser()->getId() );
2614 $this->assertSame( [
2615 [ LogLevel
::DEBUG
, 'Could not acquire account creation lock' ],
2616 ], $logger->getBuffer() );
2617 $logger->clearBuffer();
2619 // Test pre-authentication provider fail
2621 $user = \User
::newFromName( $username );
2622 $this->hook( 'LocalUserCreated', $this->never() );
2623 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2624 $this->unhook( 'LocalUserCreated' );
2625 $this->assertEquals( \Status
::newFatal( 'fail-in-pre' ), $ret );
2626 $this->assertEquals( 0, $user->getId() );
2627 $this->assertNotEquals( $username, $user->getName() );
2628 $this->assertEquals( 0, $session->getUser()->getId() );
2629 $this->assertSame( [
2630 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2631 ], $logger->getBuffer() );
2632 $logger->clearBuffer();
2633 $this->assertEquals(
2634 StatusValue
::newFatal( 'fail-in-pre' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2638 $user = \User
::newFromName( $username );
2639 $this->hook( 'LocalUserCreated', $this->never() );
2640 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2641 $this->unhook( 'LocalUserCreated' );
2642 $this->assertEquals( \Status
::newFatal( 'fail-in-primary' ), $ret );
2643 $this->assertEquals( 0, $user->getId() );
2644 $this->assertNotEquals( $username, $user->getName() );
2645 $this->assertEquals( 0, $session->getUser()->getId() );
2646 $this->assertSame( [
2647 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2648 ], $logger->getBuffer() );
2649 $logger->clearBuffer();
2650 $this->assertEquals(
2651 StatusValue
::newFatal( 'fail-in-primary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2655 $user = \User
::newFromName( $username );
2656 $this->hook( 'LocalUserCreated', $this->never() );
2657 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2658 $this->unhook( 'LocalUserCreated' );
2659 $this->assertEquals( \Status
::newFatal( 'fail-in-secondary' ), $ret );
2660 $this->assertEquals( 0, $user->getId() );
2661 $this->assertNotEquals( $username, $user->getName() );
2662 $this->assertEquals( 0, $session->getUser()->getId() );
2663 $this->assertSame( [
2664 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2665 ], $logger->getBuffer() );
2666 $logger->clearBuffer();
2667 $this->assertEquals(
2668 StatusValue
::newFatal( 'fail-in-secondary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2672 $cache = \ObjectCache
::getLocalClusterInstance();
2673 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2674 $cache->set( $backoffKey, true );
2676 $user = \User
::newFromName( $username );
2677 $this->hook( 'LocalUserCreated', $this->never() );
2678 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2679 $this->unhook( 'LocalUserCreated' );
2680 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-exception' ), $ret );
2681 $this->assertEquals( 0, $user->getId() );
2682 $this->assertNotEquals( $username, $user->getName() );
2683 $this->assertEquals( 0, $session->getUser()->getId() );
2684 $this->assertSame( [
2685 [ LogLevel
::DEBUG
, '{username} denied by prior creation attempt failures' ],
2686 ], $logger->getBuffer() );
2687 $logger->clearBuffer();
2688 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2689 $cache->delete( $backoffKey );
2691 // Test addToDatabase fails
2693 $user = $this->getMockBuilder( \User
::class )
2694 ->setMethods( [ 'addToDatabase' ] )->getMock();
2695 $user->expects( $this->once() )->method( 'addToDatabase' )
2696 ->will( $this->returnValue( \Status
::newFatal( 'because' ) ) );
2697 $user->setName( $username );
2698 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2699 $this->assertEquals( \Status
::newFatal( 'because' ), $ret );
2700 $this->assertEquals( 0, $user->getId() );
2701 $this->assertNotEquals( $username, $user->getName() );
2702 $this->assertEquals( 0, $session->getUser()->getId() );
2703 $this->assertSame( [
2704 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2705 [ LogLevel
::ERROR
, '{username} failed with message {msg}' ],
2706 ], $logger->getBuffer() );
2707 $logger->clearBuffer();
2708 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2710 // Test addToDatabase throws an exception
2711 $cache = \ObjectCache
::getLocalClusterInstance();
2712 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2713 $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
2715 $user = $this->getMockBuilder( \User
::class )
2716 ->setMethods( [ 'addToDatabase' ] )->getMock();
2717 $user->expects( $this->once() )->method( 'addToDatabase' )
2718 ->will( $this->throwException( new \
Exception( 'Excepted' ) ) );
2719 $user->setName( $username );
2721 $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2722 $this->fail( 'Expected exception not thrown' );
2723 } catch ( \Exception
$ex ) {
2724 $this->assertSame( 'Excepted', $ex->getMessage() );
2726 $this->assertEquals( 0, $user->getId() );
2727 $this->assertEquals( 0, $session->getUser()->getId() );
2728 $this->assertSame( [
2729 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2730 [ LogLevel
::ERROR
, '{username} failed with exception {exception}' ],
2731 ], $logger->getBuffer() );
2732 $logger->clearBuffer();
2733 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2734 $this->assertNotEquals( false, $cache->get( $backoffKey ) );
2735 $cache->delete( $backoffKey );
2737 // Test addToDatabase fails because the user already exists.
2739 $user = $this->getMockBuilder( \User
::class )
2740 ->setMethods( [ 'addToDatabase' ] )->getMock();
2741 $user->expects( $this->once() )->method( 'addToDatabase' )
2742 ->will( $this->returnCallback( function () use ( $username, &$user ) {
2743 $oldUser = \User
::newFromName( $username );
2744 $status = $oldUser->addToDatabase();
2745 $this->assertTrue( $status->isOK(), 'sanity check' );
2746 $user->setId( $oldUser->getId() );
2747 return \Status
::newFatal( 'userexists' );
2749 $user->setName( $username );
2750 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2751 $expect = \Status
::newGood();
2752 $expect->warning( 'userexists' );
2753 $this->assertEquals( $expect, $ret );
2754 $this->assertNotEquals( 0, $user->getId() );
2755 $this->assertEquals( $username, $user->getName() );
2756 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2757 $this->assertSame( [
2758 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2759 [ LogLevel
::INFO
, '{username} already exists locally (race)' ],
2760 ], $logger->getBuffer() );
2761 $logger->clearBuffer();
2762 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2766 $username = self
::usernameForCreation();
2767 $user = \User
::newFromName( $username );
2768 $this->hook( 'AuthPluginAutoCreate', $this->once() )
2769 ->with( $callback );
2770 $this->hideDeprecated( 'AuthPluginAutoCreate hook (used in ' .
2771 get_class( $wgHooks['AuthPluginAutoCreate'][0] ) . '::onAuthPluginAutoCreate)' );
2772 $this->hook( 'LocalUserCreated', $this->once() )
2773 ->with( $callback, $this->equalTo( true ) );
2774 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2775 $this->unhook( 'LocalUserCreated' );
2776 $this->unhook( 'AuthPluginAutoCreate' );
2777 $this->assertEquals( \Status
::newGood(), $ret );
2778 $this->assertNotEquals( 0, $user->getId() );
2779 $this->assertEquals( $username, $user->getName() );
2780 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2781 $this->assertSame( [
2782 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2783 ], $logger->getBuffer() );
2784 $logger->clearBuffer();
2786 $dbw = wfGetDB( DB_MASTER
);
2787 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2789 $username = self
::usernameForCreation();
2790 $user = \User
::newFromName( $username );
2791 $this->hook( 'LocalUserCreated', $this->once() )
2792 ->with( $callback, $this->equalTo( true ) );
2793 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2794 $this->unhook( 'LocalUserCreated' );
2795 $this->assertEquals( \Status
::newGood(), $ret );
2796 $this->assertNotEquals( 0, $user->getId() );
2797 $this->assertEquals( $username, $user->getName() );
2798 $this->assertEquals( 0, $session->getUser()->getId() );
2799 $this->assertSame( [
2800 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2801 ], $logger->getBuffer() );
2802 $logger->clearBuffer();
2805 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2808 $this->config
->set( 'NewUserLog', true );
2810 $username = self
::usernameForCreation();
2811 $user = \User
::newFromName( $username );
2812 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2813 $this->assertEquals( \Status
::newGood(), $ret );
2814 $logger->clearBuffer();
2816 $data = \DatabaseLogEntry
::getSelectQueryData();
2817 $rows = iterator_to_array( $dbw->select(
2821 'log_id > ' . (int)$maxLogId,
2822 'log_type' => 'newusers'
2828 $this->assertCount( 1, $rows );
2829 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2831 $this->assertSame( 'autocreate', $entry->getSubtype() );
2832 $this->assertSame( $user->getId(), $entry->getPerformer()->getId() );
2833 $this->assertSame( $user->getName(), $entry->getPerformer()->getName() );
2834 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2835 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2837 $workaroundPHPUnitBug = true;
2841 * @dataProvider provideGetAuthenticationRequests
2842 * @param string $action
2843 * @param array $expect
2844 * @param array $state
2846 public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
2847 $makeReq = function ( $key ) use ( $action ) {
2848 $req = $this->createMock( AuthenticationRequest
::class );
2849 $req->expects( $this->any() )->method( 'getUniqueId' )
2850 ->will( $this->returnValue( $key ) );
2851 $req->action
= $action === AuthManager
::ACTION_UNLINK ? AuthManager
::ACTION_REMOVE
: $action;
2855 $cmpReqs = function ( $a, $b ) {
2856 $ret = strcmp( get_class( $a ), get_class( $b ) );
2858 $ret = strcmp( $a->key
, $b->key
);
2863 $good = StatusValue
::newGood();
2866 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2867 $class = ucfirst( $key ) . 'AuthenticationProvider';
2868 $mocks[$key] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" )
2870 'getUniqueId', 'getAuthenticationRequests', 'providerAllowsAuthenticationDataChange',
2872 ->getMockForAbstractClass();
2873 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2874 ->will( $this->returnValue( $key ) );
2875 $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2876 ->will( $this->returnCallback( function ( $action ) use ( $key, $makeReq ) {
2877 return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
2879 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
2880 ->will( $this->returnValue( $good ) );
2885 PrimaryAuthenticationProvider
::TYPE_NONE
,
2886 PrimaryAuthenticationProvider
::TYPE_CREATE
,
2887 PrimaryAuthenticationProvider
::TYPE_LINK
2889 $class = 'PrimaryAuthenticationProvider';
2890 $mocks["primary-$type"] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" )
2892 'getUniqueId', 'accountCreationType', 'getAuthenticationRequests',
2893 'providerAllowsAuthenticationDataChange',
2895 ->getMockForAbstractClass();
2896 $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
2897 ->will( $this->returnValue( "primary-$type" ) );
2898 $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
2899 ->will( $this->returnValue( $type ) );
2900 $mocks["primary-$type"]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2901 ->will( $this->returnCallback( function ( $action ) use ( $type, $makeReq ) {
2902 return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
2904 $mocks["primary-$type"]->expects( $this->any() )
2905 ->method( 'providerAllowsAuthenticationDataChange' )
2906 ->will( $this->returnValue( $good ) );
2907 $this->primaryauthMocks
[] = $mocks["primary-$type"];
2910 $mocks['primary2'] = $this->getMockBuilder( PrimaryAuthenticationProvider
::class )
2912 'getUniqueId', 'accountCreationType', 'getAuthenticationRequests',
2913 'providerAllowsAuthenticationDataChange',
2915 ->getMockForAbstractClass();
2916 $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
2917 ->will( $this->returnValue( 'primary2' ) );
2918 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
2919 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
2920 $mocks['primary2']->expects( $this->any() )->method( 'getAuthenticationRequests' )
2921 ->will( $this->returnValue( [] ) );
2922 $mocks['primary2']->expects( $this->any() )
2923 ->method( 'providerAllowsAuthenticationDataChange' )
2924 ->will( $this->returnCallback( function ( $req ) use ( $good ) {
2925 return $req->key
=== 'generic' ? StatusValue
::newFatal( 'no' ) : $good;
2927 $this->primaryauthMocks
[] = $mocks['primary2'];
2929 $this->preauthMocks
= [ $mocks['pre'] ];
2930 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2931 $this->initializeManager( true );
2934 if ( isset( $state['continueRequests'] ) ) {
2935 $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
2937 if ( $action === AuthManager
::ACTION_LOGIN_CONTINUE
) {
2938 $this->request
->getSession()->setSecret( 'AuthManager::authnState', $state );
2939 } elseif ( $action === AuthManager
::ACTION_CREATE_CONTINUE
) {
2940 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $state );
2941 } elseif ( $action === AuthManager
::ACTION_LINK_CONTINUE
) {
2942 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', $state );
2946 $expectReqs = array_map( $makeReq, $expect );
2947 if ( $action === AuthManager
::ACTION_LOGIN
) {
2948 $req = new RememberMeAuthenticationRequest
;
2949 $req->action
= $action;
2950 $req->required
= AuthenticationRequest
::REQUIRED
;
2951 $expectReqs[] = $req;
2952 } elseif ( $action === AuthManager
::ACTION_CREATE
) {
2953 $req = new UsernameAuthenticationRequest
;
2954 $req->action
= $action;
2955 $expectReqs[] = $req;
2956 $req = new UserDataAuthenticationRequest
;
2957 $req->action
= $action;
2958 $req->required
= AuthenticationRequest
::REQUIRED
;
2959 $expectReqs[] = $req;
2961 usort( $expectReqs, $cmpReqs );
2963 $actual = $this->manager
->getAuthenticationRequests( $action );
2964 foreach ( $actual as $req ) {
2965 // Don't test this here.
2966 $req->required
= AuthenticationRequest
::REQUIRED
;
2968 usort( $actual, $cmpReqs );
2970 $this->assertEquals( $expectReqs, $actual );
2972 // Test CreationReasonAuthenticationRequest gets returned
2973 if ( $action === AuthManager
::ACTION_CREATE
) {
2974 $req = new CreationReasonAuthenticationRequest
;
2975 $req->action
= $action;
2976 $req->required
= AuthenticationRequest
::REQUIRED
;
2977 $expectReqs[] = $req;
2978 usort( $expectReqs, $cmpReqs );
2980 $actual = $this->manager
->getAuthenticationRequests( $action, \User
::newFromName( 'UTSysop' ) );
2981 foreach ( $actual as $req ) {
2982 // Don't test this here.
2983 $req->required
= AuthenticationRequest
::REQUIRED
;
2985 usort( $actual, $cmpReqs );
2987 $this->assertEquals( $expectReqs, $actual );
2991 public static function provideGetAuthenticationRequests() {
2994 AuthManager
::ACTION_LOGIN
,
2995 [ 'pre-login', 'primary-none-login', 'primary-create-login',
2996 'primary-link-login', 'secondary-login', 'generic' ],
2999 AuthManager
::ACTION_CREATE
,
3000 [ 'pre-create', 'primary-none-create', 'primary-create-create',
3001 'primary-link-create', 'secondary-create', 'generic' ],
3004 AuthManager
::ACTION_LINK
,
3005 [ 'primary-link-link', 'generic' ],
3008 AuthManager
::ACTION_CHANGE
,
3009 [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
3010 'secondary-change' ],
3013 AuthManager
::ACTION_REMOVE
,
3014 [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
3015 'secondary-remove' ],
3018 AuthManager
::ACTION_UNLINK
,
3019 [ 'primary-link-remove' ],
3022 AuthManager
::ACTION_LOGIN_CONTINUE
,
3026 AuthManager
::ACTION_LOGIN_CONTINUE
,
3027 $reqs = [ 'continue-login', 'foo', 'bar' ],
3029 'continueRequests' => $reqs,
3033 AuthManager
::ACTION_CREATE_CONTINUE
,
3037 AuthManager
::ACTION_CREATE_CONTINUE
,
3038 $reqs = [ 'continue-create', 'foo', 'bar' ],
3040 'continueRequests' => $reqs,
3044 AuthManager
::ACTION_LINK_CONTINUE
,
3048 AuthManager
::ACTION_LINK_CONTINUE
,
3049 $reqs = [ 'continue-link', 'foo', 'bar' ],
3051 'continueRequests' => $reqs,
3057 public function testGetAuthenticationRequestsRequired() {
3058 $makeReq = function ( $key, $required ) {
3059 $req = $this->createMock( AuthenticationRequest
::class );
3060 $req->expects( $this->any() )->method( 'getUniqueId' )
3061 ->will( $this->returnValue( $key ) );
3062 $req->action
= AuthManager
::ACTION_LOGIN
;
3064 $req->required
= $required;
3067 $cmpReqs = function ( $a, $b ) {
3068 $ret = strcmp( get_class( $a ), get_class( $b ) );
3070 $ret = strcmp( $a->key
, $b->key
);
3075 $good = StatusValue
::newGood();
3077 $primary1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3078 $primary1->expects( $this->any() )->method( 'getUniqueId' )
3079 ->will( $this->returnValue( 'primary1' ) );
3080 $primary1->expects( $this->any() )->method( 'accountCreationType' )
3081 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3082 $primary1->expects( $this->any() )->method( 'getAuthenticationRequests' )
3083 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3085 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3086 $makeReq( "required", AuthenticationRequest
::REQUIRED
),
3087 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3088 $makeReq( "foo", AuthenticationRequest
::REQUIRED
),
3089 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3090 $makeReq( "baz", AuthenticationRequest
::OPTIONAL
),
3094 $primary2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3095 $primary2->expects( $this->any() )->method( 'getUniqueId' )
3096 ->will( $this->returnValue( 'primary2' ) );
3097 $primary2->expects( $this->any() )->method( 'accountCreationType' )
3098 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3099 $primary2->expects( $this->any() )->method( 'getAuthenticationRequests' )
3100 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3102 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3103 $makeReq( "required2", AuthenticationRequest
::REQUIRED
),
3104 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3108 $secondary = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3109 $secondary->expects( $this->any() )->method( 'getUniqueId' )
3110 ->will( $this->returnValue( 'secondary' ) );
3111 $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
3112 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3114 $makeReq( "foo", AuthenticationRequest
::OPTIONAL
),
3115 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3116 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3120 $rememberReq = new RememberMeAuthenticationRequest
;
3121 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
3123 $this->primaryauthMocks
= [ $primary1, $primary2 ];
3124 $this->secondaryauthMocks
= [ $secondary ];
3125 $this->initializeManager( true );
3127 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3130 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3131 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3132 $makeReq( "required2", AuthenticationRequest
::PRIMARY_REQUIRED
),
3133 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3134 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3135 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3136 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3137 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3139 usort( $actual, $cmpReqs );
3140 usort( $expected, $cmpReqs );
3141 $this->assertEquals( $expected, $actual );
3143 $this->primaryauthMocks
= [ $primary1 ];
3144 $this->secondaryauthMocks
= [ $secondary ];
3145 $this->initializeManager( true );
3147 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3150 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3151 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3152 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3153 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3154 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3155 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3157 usort( $actual, $cmpReqs );
3158 usort( $expected, $cmpReqs );
3159 $this->assertEquals( $expected, $actual );
3162 public function testAllowsPropertyChange() {
3164 foreach ( [ 'primary', 'secondary' ] as $key ) {
3165 $class = ucfirst( $key ) . 'AuthenticationProvider';
3166 $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
3167 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3168 ->will( $this->returnValue( $key ) );
3169 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
3170 ->will( $this->returnCallback( function ( $prop ) use ( $key ) {
3171 return $prop !== $key;
3175 $this->primaryauthMocks
= [ $mocks['primary'] ];
3176 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
3177 $this->initializeManager( true );
3179 $this->assertTrue( $this->manager
->allowsPropertyChange( 'foo' ) );
3180 $this->assertFalse( $this->manager
->allowsPropertyChange( 'primary' ) );
3181 $this->assertFalse( $this->manager
->allowsPropertyChange( 'secondary' ) );
3184 public function testAutoCreateOnLogin() {
3185 $username = self
::usernameForCreation();
3187 $req = $this->createMock( AuthenticationRequest
::class );
3189 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3190 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3191 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3192 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3193 $mock->expects( $this->any() )->method( 'accountCreationType' )
3194 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3195 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3196 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3197 ->will( $this->returnValue( StatusValue
::newGood() ) );
3199 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3200 $mock2->expects( $this->any() )->method( 'getUniqueId' )
3201 ->will( $this->returnValue( 'secondary' ) );
3202 $mock2->expects( $this->any() )->method( 'beginSecondaryAuthentication' )->will(
3204 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) )
3207 $mock2->expects( $this->any() )->method( 'continueSecondaryAuthentication' )
3208 ->will( $this->returnValue( AuthenticationResponse
::newAbstain() ) );
3209 $mock2->expects( $this->any() )->method( 'testUserForCreation' )
3210 ->will( $this->returnValue( StatusValue
::newGood() ) );
3212 $this->primaryauthMocks
= [ $mock ];
3213 $this->secondaryauthMocks
= [ $mock2 ];
3214 $this->initializeManager( true );
3215 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3216 $session = $this->request
->getSession();
3219 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3222 $callback = $this->callback( function ( $user ) use ( $username ) {
3223 return $user->getName() === $username;
3226 $this->hook( 'UserLoggedIn', $this->never() );
3227 $this->hook( 'LocalUserCreated', $this->once() )->with( $callback, $this->equalTo( true ) );
3228 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3229 $this->unhook( 'LocalUserCreated' );
3230 $this->unhook( 'UserLoggedIn' );
3231 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
3233 $id = (int)\User
::newFromName( $username )->getId();
3234 $this->assertNotSame( 0, \User
::newFromName( $username )->getId() );
3235 $this->assertSame( 0, $session->getUser()->getId() );
3237 $this->hook( 'UserLoggedIn', $this->once() )->with( $callback );
3238 $this->hook( 'LocalUserCreated', $this->never() );
3239 $ret = $this->manager
->continueAuthentication( [] );
3240 $this->unhook( 'LocalUserCreated' );
3241 $this->unhook( 'UserLoggedIn' );
3242 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
3243 $this->assertSame( $username, $ret->username
);
3244 $this->assertSame( $id, $session->getUser()->getId() );
3247 public function testAutoCreateFailOnLogin() {
3248 $username = self
::usernameForCreation();
3250 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3251 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3252 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3253 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3254 $mock->expects( $this->any() )->method( 'accountCreationType' )
3255 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3256 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3257 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3258 ->will( $this->returnValue( StatusValue
::newFatal( 'fail-from-primary' ) ) );
3260 $this->primaryauthMocks
= [ $mock ];
3261 $this->initializeManager( true );
3262 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3263 $session = $this->request
->getSession();
3266 $this->assertSame( 0, $session->getUser()->getId(),
3268 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3271 $this->hook( 'UserLoggedIn', $this->never() );
3272 $this->hook( 'LocalUserCreated', $this->never() );
3273 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3274 $this->unhook( 'LocalUserCreated' );
3275 $this->unhook( 'UserLoggedIn' );
3276 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3277 $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message
->getKey() );
3279 $this->assertSame( 0, \User
::newFromName( $username )->getId() );
3280 $this->assertSame( 0, $session->getUser()->getId() );
3283 public function testAuthenticationSessionData() {
3284 $this->initializeManager( true );
3286 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3287 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3288 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3289 $this->assertSame( 'foo!', $this->manager
->getAuthenticationSessionData( 'foo' ) );
3290 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3291 $this->manager
->removeAuthenticationSessionData( 'foo' );
3292 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3293 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3294 $this->manager
->removeAuthenticationSessionData( 'bar' );
3295 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3297 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3298 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3299 $this->manager
->removeAuthenticationSessionData( null );
3300 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3301 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3304 public function testCanLinkAccounts() {
3306 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
3307 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
3308 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
3311 foreach ( $types as $type => $can ) {
3312 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3313 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
3314 $mock->expects( $this->any() )->method( 'accountCreationType' )
3315 ->will( $this->returnValue( $type ) );
3316 $this->primaryauthMocks
= [ $mock ];
3317 $this->initializeManager( true );
3318 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
3322 public function testBeginAccountLink() {
3323 $user = \User
::newFromName( 'UTSysop' );
3324 $this->initializeManager();
3326 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', 'test' );
3328 $this->manager
->beginAccountLink( $user, [], 'http://localhost/' );
3329 $this->fail( 'Expected exception not thrown' );
3330 } catch ( \LogicException
$ex ) {
3331 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3333 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3335 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3336 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3337 $mock->expects( $this->any() )->method( 'accountCreationType' )
3338 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3339 $this->primaryauthMocks
= [ $mock ];
3340 $this->initializeManager( true );
3342 $ret = $this->manager
->beginAccountLink( new \User
, [], 'http://localhost/' );
3343 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3344 $this->assertSame( 'noname', $ret->message
->getKey() );
3346 $ret = $this->manager
->beginAccountLink(
3347 \User
::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
3349 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3350 $this->assertSame( 'authmanager-userdoesnotexist', $ret->message
->getKey() );
3353 public function testContinueAccountLink() {
3354 $user = \User
::newFromName( 'UTSysop' );
3355 $this->initializeManager();
3358 'userid' => $user->getId(),
3359 'username' => $user->getName(),
3364 $this->manager
->continueAccountLink( [] );
3365 $this->fail( 'Expected exception not thrown' );
3366 } catch ( \LogicException
$ex ) {
3367 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3370 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3371 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3372 $mock->expects( $this->any() )->method( 'accountCreationType' )
3373 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3374 $mock->expects( $this->any() )->method( 'beginPrimaryAccountLink' )->will(
3375 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
3377 $this->primaryauthMocks
= [ $mock ];
3378 $this->initializeManager( true );
3380 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', null );
3381 $ret = $this->manager
->continueAccountLink( [] );
3382 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3383 $this->assertSame( 'authmanager-link-not-in-progress', $ret->message
->getKey() );
3385 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3386 [ 'username' => $user->getName() . '<>' ] +
$session );
3387 $ret = $this->manager
->continueAccountLink( [] );
3388 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3389 $this->assertSame( 'noname', $ret->message
->getKey() );
3390 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3392 $id = $user->getId();
3393 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3394 [ 'userid' => $id +
1 ] +
$session );
3396 $ret = $this->manager
->continueAccountLink( [] );
3397 $this->fail( 'Expected exception not thrown' );
3398 } catch ( \UnexpectedValueException
$ex ) {
3399 $this->assertEquals(
3400 "User \"{$user->getName()}\" is valid, but ID $id != " . ( $id +
1 ) . '!',
3404 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3408 * @dataProvider provideAccountLink
3409 * @param StatusValue $preTest
3410 * @param array $primaryResponses
3411 * @param array $managerResponses
3413 public function testAccountLink(
3414 StatusValue
$preTest, array $primaryResponses, array $managerResponses
3416 $user = \User
::newFromName( 'UTSysop' );
3418 $this->initializeManager();
3420 // Set up lots of mocks...
3421 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3422 $req->primary
= $primaryResponses;
3425 foreach ( [ 'pre', 'primary' ] as $key ) {
3426 $class = ucfirst( $key ) . 'AuthenticationProvider';
3427 $mocks[$key] = $this->getMockForAbstractClass(
3428 "MediaWiki\\Auth\\$class", [], "Mock$class"
3430 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3431 ->will( $this->returnValue( $key ) );
3433 for ( $i = 2; $i <= 3; $i++
) {
3434 $mocks[$key . $i] = $this->getMockForAbstractClass(
3435 "MediaWiki\\Auth\\$class", [], "Mock$class"
3437 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
3438 ->will( $this->returnValue( $key . $i ) );
3442 $mocks['pre']->expects( $this->any() )->method( 'testForAccountLink' )
3443 ->will( $this->returnCallback(
3445 use ( $user, $preTest )
3447 $this->assertSame( $user->getId(), $u->getId() );
3448 $this->assertSame( $user->getName(), $u->getName() );
3453 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
3454 ->will( $this->returnValue( StatusValue
::newGood() ) );
3456 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
3457 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3458 $ct = count( $req->primary
);
3459 $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req ) {
3460 $this->assertSame( $user->getId(), $u->getId() );
3461 $this->assertSame( $user->getName(), $u->getName() );
3463 foreach ( $reqs as $r ) {
3464 $this->assertSame( $user->getName(), $r->username
);
3465 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
3467 $this->assertTrue( $foundReq, '$reqs contains $req' );
3468 return array_shift( $req->primary
);
3470 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
3471 ->method( 'beginPrimaryAccountLink' )
3472 ->will( $callback );
3473 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
3474 ->method( 'continuePrimaryAccountLink' )
3475 ->will( $callback );
3477 $abstain = AuthenticationResponse
::newAbstain();
3478 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
3479 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3480 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
3481 ->will( $this->returnValue( $abstain ) );
3482 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3483 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
3484 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3485 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
3486 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3488 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
3489 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
3490 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
3491 return $level === LogLevel
::DEBUG ?
null : $message;
3493 $this->initializeManager( true );
3495 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
3496 $this->equalTo( AuthenticationResponse
::PASS
),
3497 $this->equalTo( AuthenticationResponse
::FAIL
)
3499 $providers = array_merge( $this->preauthMocks
, $this->primaryauthMocks
);
3500 foreach ( $providers as $p ) {
3501 $p->postCalled
= false;
3502 $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
3503 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
3504 $this->assertInstanceOf( \User
::class, $user );
3505 $this->assertSame( 'UTSysop', $user->getName() );
3506 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
3507 $this->assertThat( $response->status
, $constraint );
3508 $p->postCalled
= $response->status
;
3515 foreach ( $managerResponses as $i => $response ) {
3516 if ( $response instanceof AuthenticationResponse
&&
3517 $response->status
=== AuthenticationResponse
::PASS
3519 $expectLog[] = [ LogLevel
::INFO
, 'Account linked to {user} by primary' ];
3525 $ret = $this->manager
->beginAccountLink( $user, [ $req ], 'http://localhost/' );
3527 $ret = $this->manager
->continueAccountLink( [ $req ] );
3529 if ( $response instanceof \Exception
) {
3530 $this->fail( 'Expected exception not thrown', "Response $i" );
3532 } catch ( \Exception
$ex ) {
3533 if ( !$response instanceof \Exception
) {
3536 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
3537 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3538 "Response $i, exception, session state" );
3542 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
3544 $ret->message
= $this->message( $ret->message
);
3545 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
3546 if ( $response->status
=== AuthenticationResponse
::PASS ||
3547 $response->status
=== AuthenticationResponse
::FAIL
3549 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3550 "Response $i, session state" );
3551 foreach ( $providers as $p ) {
3552 $this->assertSame( $response->status
, $p->postCalled
,
3553 "Response $i, post-auth callback called" );
3556 $this->assertNotNull(
3557 $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3558 "Response $i, session state"
3560 foreach ( $ret->neededRequests
as $neededReq ) {
3561 $this->assertEquals( AuthManager
::ACTION_LINK
, $neededReq->action
,
3562 "Response $i, neededRequest action" );
3564 $this->assertEquals(
3565 $ret->neededRequests
,
3566 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LINK_CONTINUE
),
3567 "Response $i, continuation check"
3569 foreach ( $providers as $p ) {
3570 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
3577 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
3580 public function provideAccountLink() {
3581 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3582 $good = StatusValue
::newGood();
3585 'Pre-link test fail in pre' => [
3586 StatusValue
::newFatal( 'fail-from-pre' ),
3589 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
3592 'Failure in primary' => [
3595 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
3599 'All primary abstain' => [
3602 AuthenticationResponse
::newAbstain(),
3605 AuthenticationResponse
::newFail( $this->message( 'authmanager-link-no-primary' ) )
3608 'Primary UI, then redirect, then fail' => [
3611 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3612 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
3613 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
3617 'Primary redirect, then abstain' => [
3620 $tmp = AuthenticationResponse
::newRedirect(
3621 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
3623 AuthenticationResponse
::newAbstain(),
3627 new \
DomainException(
3628 'MockPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
3632 'Primary UI, then pass' => [
3635 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3636 AuthenticationResponse
::newPass(),
3640 AuthenticationResponse
::newPass( '' ),
3646 AuthenticationResponse
::newPass( '' ),
3649 AuthenticationResponse
::newPass( '' ),