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 ] );
44 * Sets a mock on a hook
46 * @param object $expect From $this->once(), $this->never(), etc.
47 * @return object $mock->expects( $expect )->method( ... ).
49 protected function hook( $hook, $expect ) {
50 $mock = $this->getMockBuilder( __CLASS__
)
51 ->setMethods( [ "on$hook" ] )
53 $this->setTemporaryHook( $hook, $mock );
54 return $mock->expects( $expect )->method( "on$hook" );
61 protected function unhook( $hook ) {
67 * Ensure a value is a clean Message object
68 * @param string|Message $key
69 * @param array $params
72 protected function message( $key, $params = [] ) {
73 if ( $key === null ) {
76 if ( $key instanceof \MessageSpecifier
) {
77 $params = $key->getParams();
78 $key = $key->getKey();
80 return new \
Message( $key, $params, \Language
::factory( 'en' ) );
84 * Test two AuthenticationResponses for equality. We don't want to use regular assertEquals
85 * because that recursively compares members, which leads to false negatives if e.g. Language
88 * @param AuthenticationResponse $response1
89 * @param AuthenticationResponse $response2
93 private function assertResponseEquals(
94 AuthenticationResponse
$expected, AuthenticationResponse
$actual, $msg = ''
96 foreach ( ( new \
ReflectionClass( $expected ) )->getProperties() as $prop ) {
97 $name = $prop->getName();
98 $usedMsg = ltrim( "$msg ($name)" );
99 if ( $name === 'message' && $expected->message
) {
100 $this->assertSame( $expected->message
->serialize(), $actual->message
->serialize(),
103 $this->assertEquals( $expected->$name, $actual->$name, $usedMsg );
109 * Initialize the AuthManagerConfig variable in $this->config
111 * Uses data from the various 'mocks' fields.
113 protected function initializeConfig() {
123 foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
124 $key = $type . 'Mocks';
125 foreach ( $this->$key as $mock ) {
126 $config[$type][$mock->getUniqueId()] = [ 'factory' => function () use ( $mock ) {
132 $this->config
->set( 'AuthManagerConfig', $config );
133 $this->config
->set( 'LanguageCode', 'en' );
134 $this->config
->set( 'NewUserLog', false );
138 * Initialize $this->manager
139 * @param bool $regen Force a call to $this->initializeConfig()
141 protected function initializeManager( $regen = false ) {
142 if ( $regen ||
!$this->config
) {
143 $this->config
= new \
HashConfig();
145 if ( $regen ||
!$this->request
) {
146 $this->request
= new \
FauxRequest();
148 if ( !$this->logger
) {
149 $this->logger
= new \
TestLogger();
152 if ( $regen ||
!$this->config
->has( 'AuthManagerConfig' ) ) {
153 $this->initializeConfig();
155 $this->manager
= new AuthManager( $this->request
, $this->config
);
156 $this->manager
->setLogger( $this->logger
);
157 $this->managerPriv
= TestingAccessWrapper
::newFromObject( $this->manager
);
161 * Setup SessionManager with a mock session provider
162 * @param bool|null $canChangeUser If non-null, canChangeUser will be mocked to return this
163 * @param array $methods Additional methods to mock
164 * @return array (MediaWiki\Session\SessionProvider, ScopedCallback)
166 protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
167 if ( !$this->config
) {
168 $this->config
= new \
HashConfig();
169 $this->initializeConfig();
171 $this->config
->set( 'ObjectCacheSessionExpiry', 100 );
173 $methods[] = '__toString';
174 $methods[] = 'describe';
175 if ( $canChangeUser !== null ) {
176 $methods[] = 'canChangeUser';
178 $provider = $this->getMockBuilder( \DummySessionProvider
::class )
179 ->setMethods( $methods )
181 $provider->expects( $this->any() )->method( '__toString' )
182 ->will( $this->returnValue( 'MockSessionProvider' ) );
183 $provider->expects( $this->any() )->method( 'describe' )
184 ->will( $this->returnValue( 'MockSessionProvider sessions' ) );
185 if ( $canChangeUser !== null ) {
186 $provider->expects( $this->any() )->method( 'canChangeUser' )
187 ->will( $this->returnValue( $canChangeUser ) );
189 $this->config
->set( 'SessionProviders', [
190 [ 'factory' => function () use ( $provider ) {
195 $manager = new \MediaWiki\Session\
SessionManager( [
196 'config' => $this->config
,
197 'logger' => new \Psr\Log\
NullLogger(),
198 'store' => new \
HashBagOStuff(),
200 TestingAccessWrapper
::newFromObject( $manager )->getProvider( (string)$provider );
202 $reset = \MediaWiki\Session\TestUtils
::setSessionManagerSingleton( $manager );
204 if ( $this->request
) {
205 $manager->getSessionForRequest( $this->request
);
208 return [ $provider, $reset ];
211 public function testSingleton() {
212 // Temporarily clear out the global singleton, if any, to test creating
214 $rProp = new \
ReflectionProperty( AuthManager
::class, 'instance' );
215 $rProp->setAccessible( true );
216 $old = $rProp->getValue();
217 $cb = new ScopedCallback( [ $rProp, 'setValue' ], [ $old ] );
218 $rProp->setValue( null );
220 $singleton = AuthManager
::singleton();
221 $this->assertInstanceOf( AuthManager
::class, AuthManager
::singleton() );
222 $this->assertSame( $singleton, AuthManager
::singleton() );
223 $this->assertSame( \RequestContext
::getMain()->getRequest(), $singleton->getRequest() );
225 \RequestContext
::getMain()->getConfig(),
226 TestingAccessWrapper
::newFromObject( $singleton )->config
230 public function testCanAuthenticateNow() {
231 $this->initializeManager();
233 list( $provider, $reset ) = $this->getMockSessionProvider( false );
234 $this->assertFalse( $this->manager
->canAuthenticateNow() );
235 ScopedCallback
::consume( $reset );
237 list( $provider, $reset ) = $this->getMockSessionProvider( true );
238 $this->assertTrue( $this->manager
->canAuthenticateNow() );
239 ScopedCallback
::consume( $reset );
242 public function testNormalizeUsername() {
244 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
245 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
246 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
247 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
249 foreach ( $mocks as $key => $mock ) {
250 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
252 $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
253 ->with( $this->identicalTo( 'XYZ' ) )
254 ->willReturn( 'Foo' );
255 $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
256 ->with( $this->identicalTo( 'XYZ' ) )
257 ->willReturn( 'Foo' );
258 $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
259 ->with( $this->identicalTo( 'XYZ' ) )
260 ->willReturn( null );
261 $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
262 ->with( $this->identicalTo( 'XYZ' ) )
263 ->willReturn( 'Bar!' );
265 $this->primaryauthMocks
= $mocks;
267 $this->initializeManager();
269 $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager
->normalizeUsername( 'XYZ' ) );
273 * @dataProvider provideSecuritySensitiveOperationStatus
274 * @param bool $mutableSession
276 public function testSecuritySensitiveOperationStatus( $mutableSession ) {
277 $this->logger
= new \Psr\Log\
NullLogger();
278 $user = \User
::newFromName( 'UTSysop' );
280 $reauth = $mutableSession ? AuthManager
::SEC_REAUTH
: AuthManager
::SEC_FAIL
;
282 list( $provider, $reset ) = $this->getMockSessionProvider(
283 $mutableSession, [ 'provideSessionInfo' ]
285 $provider->expects( $this->any() )->method( 'provideSessionInfo' )
286 ->will( $this->returnCallback( function () use ( $provider, &$provideUser ) {
287 return new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
288 'provider' => $provider,
289 'id' => \DummySessionProvider
::ID
,
291 'userInfo' => UserInfo
::newFromUser( $provideUser, true )
294 $this->initializeManager();
296 $this->config
->set( 'ReauthenticateTime', [] );
297 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [] );
298 $provideUser = new \User
;
299 $session = $provider->getManager()->getSessionForRequest( $this->request
);
300 $this->assertSame( 0, $session->getUser()->getId(), 'sanity check' );
302 // Anonymous user => reauth
303 $session->set( 'AuthManager:lastAuthId', 0 );
304 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
305 $this->assertSame( $reauth, $this->manager
->securitySensitiveOperationStatus( 'foo' ) );
307 $provideUser = $user;
308 $session = $provider->getManager()->getSessionForRequest( $this->request
);
309 $this->assertSame( $user->getId(), $session->getUser()->getId(), 'sanity check' );
311 // Error for no default (only gets thrown for non-anonymous user)
312 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
313 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
315 $this->manager
->securitySensitiveOperationStatus( 'foo' );
316 $this->fail( 'Expected exception not thrown' );
317 } catch ( \UnexpectedValueException
$ex ) {
320 ?
'$wgReauthenticateTime lacks a default'
321 : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
326 if ( $mutableSession ) {
327 $this->config
->set( 'ReauthenticateTime', [
333 // Mismatched user ID
334 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
335 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
337 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
340 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
343 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
347 $session->set( 'AuthManager:lastAuthId', $user->getId() );
348 $session->set( 'AuthManager:lastAuthTimestamp', null );
350 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
353 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
356 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
359 // Recent enough to pass
360 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
362 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
365 // Not recent enough to pass
366 $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
368 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
370 // But recent enough for the 'test' operation
372 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test' )
375 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [
381 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
385 AuthManager
::SEC_FAIL
, $this->manager
->securitySensitiveOperationStatus( 'test' )
389 // Test hook, all three possible values
391 AuthManager
::SEC_OK
=> AuthManager
::SEC_OK
,
392 AuthManager
::SEC_REAUTH
=> $reauth,
393 AuthManager
::SEC_FAIL
=> AuthManager
::SEC_FAIL
,
394 ] as $hook => $expect ) {
395 $this->hook( 'SecuritySensitiveOperationStatus', $this->exactly( 2 ) )
399 $this->callback( function ( $s ) use ( $session ) {
400 return $s->getId() === $session->getId();
402 $mutableSession ?
$this->equalTo( 500, 1 ) : $this->equalTo( -1 )
404 ->will( $this->returnCallback( function ( &$v ) use ( $hook ) {
408 $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
410 $expect, $this->manager
->securitySensitiveOperationStatus( 'test' ), "hook $hook"
413 $expect, $this->manager
->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
415 $this->unhook( 'SecuritySensitiveOperationStatus' );
418 ScopedCallback
::consume( $reset );
421 public function onSecuritySensitiveOperationStatus( &$status, $operation, $session, $time ) {
424 public static function provideSecuritySensitiveOperationStatus() {
432 * @dataProvider provideUserCanAuthenticate
433 * @param bool $primary1Can
434 * @param bool $primary2Can
435 * @param bool $expect
437 public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
438 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
439 $mock1->expects( $this->any() )->method( 'getUniqueId' )
440 ->will( $this->returnValue( 'primary1' ) );
441 $mock1->expects( $this->any() )->method( 'testUserCanAuthenticate' )
442 ->with( $this->equalTo( 'UTSysop' ) )
443 ->will( $this->returnValue( $primary1Can ) );
444 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
445 $mock2->expects( $this->any() )->method( 'getUniqueId' )
446 ->will( $this->returnValue( 'primary2' ) );
447 $mock2->expects( $this->any() )->method( 'testUserCanAuthenticate' )
448 ->with( $this->equalTo( 'UTSysop' ) )
449 ->will( $this->returnValue( $primary2Can ) );
450 $this->primaryauthMocks
= [ $mock1, $mock2 ];
452 $this->initializeManager( true );
453 $this->assertSame( $expect, $this->manager
->userCanAuthenticate( 'UTSysop' ) );
456 public static function provideUserCanAuthenticate() {
458 [ false, false, false ],
459 [ true, false, true ],
460 [ false, true, true ],
461 [ true, true, true ],
465 public function testRevokeAccessForUser() {
466 $this->initializeManager();
468 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
469 $mock->expects( $this->any() )->method( 'getUniqueId' )
470 ->will( $this->returnValue( 'primary' ) );
471 $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
472 ->with( $this->equalTo( 'UTSysop' ) );
473 $this->primaryauthMocks
= [ $mock ];
475 $this->initializeManager( true );
476 $this->logger
->setCollect( true );
478 $this->manager
->revokeAccessForUser( 'UTSysop' );
481 [ LogLevel
::INFO
, 'Revoking access for {user}' ],
482 ], $this->logger
->getBuffer() );
485 public function testProviderCreation() {
487 'pre' => $this->getMockForAbstractClass( PreAuthenticationProvider
::class ),
488 'primary' => $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
489 'secondary' => $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class ),
491 foreach ( $mocks as $key => $mock ) {
492 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
493 $mock->expects( $this->once() )->method( 'setLogger' );
494 $mock->expects( $this->once() )->method( 'setManager' );
495 $mock->expects( $this->once() )->method( 'setConfig' );
497 $this->preauthMocks
= [ $mocks['pre'] ];
498 $this->primaryauthMocks
= [ $mocks['primary'] ];
499 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
502 $this->initializeManager();
505 $this->managerPriv
->getAuthenticationProvider( 'primary' )
509 $this->managerPriv
->getAuthenticationProvider( 'secondary' )
513 $this->managerPriv
->getAuthenticationProvider( 'pre' )
516 [ 'pre' => $mocks['pre'] ],
517 $this->managerPriv
->getPreAuthenticationProviders()
520 [ 'primary' => $mocks['primary'] ],
521 $this->managerPriv
->getPrimaryAuthenticationProviders()
524 [ 'secondary' => $mocks['secondary'] ],
525 $this->managerPriv
->getSecondaryAuthenticationProviders()
529 $mock1 = $this->getMockForAbstractClass( PreAuthenticationProvider
::class );
530 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
531 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
532 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
533 $this->preauthMocks
= [ $mock1 ];
534 $this->primaryauthMocks
= [ $mock2 ];
535 $this->secondaryauthMocks
= [];
536 $this->initializeManager( true );
538 $this->managerPriv
->getAuthenticationProvider( 'Y' );
539 $this->fail( 'Expected exception not thrown' );
540 } catch ( \RuntimeException
$ex ) {
541 $class1 = get_class( $mock1 );
542 $class2 = get_class( $mock2 );
544 "Duplicate specifications for id X (classes $class1 and $class2)", $ex->getMessage()
549 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
550 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
551 $class = get_class( $mock );
552 $this->preauthMocks
= [ $mock ];
553 $this->primaryauthMocks
= [ $mock ];
554 $this->secondaryauthMocks
= [ $mock ];
555 $this->initializeManager( true );
557 $this->managerPriv
->getPreAuthenticationProviders();
558 $this->fail( 'Expected exception not thrown' );
559 } catch ( \RuntimeException
$ex ) {
561 "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
566 $this->managerPriv
->getPrimaryAuthenticationProviders();
567 $this->fail( 'Expected exception not thrown' );
568 } catch ( \RuntimeException
$ex ) {
570 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
575 $this->managerPriv
->getSecondaryAuthenticationProviders();
576 $this->fail( 'Expected exception not thrown' );
577 } catch ( \RuntimeException
$ex ) {
579 "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
585 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
586 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
587 $mock3 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
588 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
589 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
590 $mock3->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'C' ) );
591 $this->preauthMocks
= [];
592 $this->primaryauthMocks
= [ $mock1, $mock2, $mock3 ];
593 $this->secondaryauthMocks
= [];
594 $this->initializeConfig();
595 $config = $this->config
->get( 'AuthManagerConfig' );
597 $this->initializeManager( false );
599 [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
600 $this->managerPriv
->getPrimaryAuthenticationProviders(),
604 $config['primaryauth']['A']['sort'] = 100;
605 $config['primaryauth']['C']['sort'] = -1;
606 $this->config
->set( 'AuthManagerConfig', $config );
607 $this->initializeManager( false );
609 [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
610 $this->managerPriv
->getPrimaryAuthenticationProviders()
615 * @dataProvider provideSetDefaultUserOptions
617 public function testSetDefaultUserOptions(
618 $contLang, $useContextLang, $expectedLang, $expectedVariant
620 $this->initializeManager();
622 $this->setContentLang( $contLang );
623 $context = \RequestContext
::getMain();
624 $reset = new ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
625 $context->setLanguage( 'de' );
627 $user = \User
::newFromName( self
::usernameForCreation() );
628 $user->addToDatabase();
629 $oldToken = $user->getToken();
630 $this->managerPriv
->setDefaultUserOptions( $user, $useContextLang );
631 $user->saveSettings();
632 $this->assertNotEquals( $oldToken, $user->getToken() );
633 $this->assertSame( $expectedLang, $user->getOption( 'language' ) );
634 $this->assertSame( $expectedVariant, $user->getOption( 'variant' ) );
637 public function provideSetDefaultUserOptions() {
639 [ 'zh', false, 'zh', 'zh' ],
640 [ 'zh', true, 'de', 'zh' ],
641 [ 'fr', true, 'de', null ],
645 public function testForcePrimaryAuthenticationProviders() {
646 $mockA = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
647 $mockB = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
648 $mockB2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
649 $mockA->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
650 $mockB->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
651 $mockB2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
652 $this->primaryauthMocks
= [ $mockA ];
654 $this->logger
= new \
TestLogger( true );
656 // Test without first initializing the configured providers
657 $this->initializeManager();
658 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
660 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
662 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
663 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
665 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
666 ], $this->logger
->getBuffer() );
667 $this->logger
->clearBuffer();
669 // Test with first initializing the configured providers
670 $this->initializeManager();
671 $this->assertSame( $mockA, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
672 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
673 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
674 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
675 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
677 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
679 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
680 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
681 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
683 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
686 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
689 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
691 ], $this->logger
->getBuffer() );
692 $this->logger
->clearBuffer();
694 // Test duplicate IDs
695 $this->initializeManager();
697 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
698 $this->fail( 'Expected exception not thrown' );
699 } catch ( \RuntimeException
$ex ) {
700 $class1 = get_class( $mockB );
701 $class2 = get_class( $mockB2 );
703 "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
708 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
709 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
710 $class = get_class( $mock );
712 $this->manager
->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
713 $this->fail( 'Expected exception not thrown' );
714 } catch ( \RuntimeException
$ex ) {
716 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
722 public function testBeginAuthentication() {
723 $this->initializeManager();
726 list( $provider, $reset ) = $this->getMockSessionProvider( false );
727 $this->hook( 'UserLoggedIn', $this->never() );
728 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
730 $this->manager
->beginAuthentication( [], 'http://localhost/' );
731 $this->fail( 'Expected exception not thrown' );
732 } catch ( \LogicException
$ex ) {
733 $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
735 $this->unhook( 'UserLoggedIn' );
736 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
737 ScopedCallback
::consume( $reset );
738 $this->initializeManager( true );
740 // CreatedAccountAuthenticationRequest
741 $user = \User
::newFromName( 'UTSysop' );
743 new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
745 $this->hook( 'UserLoggedIn', $this->never() );
747 $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
748 $this->fail( 'Expected exception not thrown' );
749 } catch ( \LogicException
$ex ) {
751 'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
752 'that created the account',
756 $this->unhook( 'UserLoggedIn' );
758 $this->request
->getSession()->clear();
759 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
760 $this->managerPriv
->createdAccountAuthenticationRequests
= [ $reqs[0] ];
761 $this->hook( 'UserLoggedIn', $this->once() )
762 ->with( $this->callback( function ( $u ) use ( $user ) {
763 return $user->getId() === $u->getId() && $user->getName() === $u->getName();
765 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
766 $this->logger
->setCollect( true );
767 $ret = $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
768 $this->logger
->setCollect( false );
769 $this->unhook( 'UserLoggedIn' );
770 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
771 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
772 $this->assertSame( $user->getName(), $ret->username
);
773 $this->assertSame( $user->getId(), $this->request
->getSessionData( 'AuthManager:lastAuthId' ) );
775 time(), $this->request
->getSessionData( 'AuthManager:lastAuthTimestamp' ),
778 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
779 $this->assertSame( $user->getId(), $this->request
->getSession()->getUser()->getId() );
781 [ LogLevel
::INFO
, 'Logging in {user} after account creation' ],
782 ], $this->logger
->getBuffer() );
785 public function testCreateFromLogin() {
786 $user = \User
::newFromName( 'UTSysop' );
787 $req1 = $this->createMock( AuthenticationRequest
::class );
788 $req2 = $this->createMock( AuthenticationRequest
::class );
789 $req3 = $this->createMock( AuthenticationRequest
::class );
790 $userReq = new UsernameAuthenticationRequest
;
791 $userReq->username
= 'UTDummy';
793 $req1->returnToUrl
= 'http://localhost/';
794 $req2->returnToUrl
= 'http://localhost/';
795 $req3->returnToUrl
= 'http://localhost/';
796 $req3->username
= 'UTDummy';
797 $userReq->returnToUrl
= 'http://localhost/';
799 // Passing one into beginAuthentication(), and an immediate FAIL
800 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
801 $this->primaryauthMocks
= [ $primary ];
802 $this->initializeManager( true );
803 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
804 $res->createRequest
= $req1;
805 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
806 ->will( $this->returnValue( $res ) );
807 $createReq = new CreateFromLoginAuthenticationRequest(
808 null, [ $req2->getUniqueId() => $req2 ]
810 $this->logger
->setCollect( true );
811 $ret = $this->manager
->beginAuthentication( [ $createReq ], 'http://localhost/' );
812 $this->logger
->setCollect( false );
813 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
814 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
815 $this->assertSame( $req1, $ret->createRequest
->createRequest
);
816 $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest
->maybeLink
);
818 // UI, then FAIL in beginAuthentication()
819 $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider
::class )
820 ->setMethods( [ 'continuePrimaryAuthentication' ] )
821 ->getMockForAbstractClass();
822 $this->primaryauthMocks
= [ $primary ];
823 $this->initializeManager( true );
824 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
825 ->will( $this->returnValue(
826 AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) )
828 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
829 $res->createRequest
= $req2;
830 $primary->expects( $this->any() )->method( 'continuePrimaryAuthentication' )
831 ->will( $this->returnValue( $res ) );
832 $this->logger
->setCollect( true );
833 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
834 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
, 'sanity check' );
835 $ret = $this->manager
->continueAuthentication( [] );
836 $this->logger
->setCollect( false );
837 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
838 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
839 $this->assertSame( $req2, $ret->createRequest
->createRequest
);
840 $this->assertEquals( [], $ret->createRequest
->maybeLink
);
842 // Pass into beginAccountCreation(), see that maybeLink and createRequest get copied
843 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
844 $this->primaryauthMocks
= [ $primary ];
845 $this->initializeManager( true );
846 $createReq = new CreateFromLoginAuthenticationRequest( $req3, [ $req2 ] );
847 $createReq->returnToUrl
= 'http://localhost/';
848 $createReq->username
= 'UTDummy';
849 $res = AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) );
850 $primary->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
851 ->with( $this->anything(), $this->anything(), [ $userReq, $createReq, $req3 ] )
852 ->will( $this->returnValue( $res ) );
853 $primary->expects( $this->any() )->method( 'accountCreationType' )
854 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
855 $this->logger
->setCollect( true );
856 $ret = $this->manager
->beginAccountCreation(
857 $user, [ $userReq, $createReq ], 'http://localhost/'
859 $this->logger
->setCollect( false );
860 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
861 $state = $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' );
862 $this->assertNotNull( $state );
863 $this->assertEquals( [ $userReq, $createReq, $req3 ], $state['reqs'] );
864 $this->assertEquals( [ $req2 ], $state['maybeLink'] );
868 * @dataProvider provideAuthentication
869 * @param StatusValue $preResponse
870 * @param array $primaryResponses
871 * @param array $secondaryResponses
872 * @param array $managerResponses
873 * @param bool $link Whether the primary authentication provider is a "link" provider
875 public function testAuthentication(
876 StatusValue
$preResponse, array $primaryResponses, array $secondaryResponses,
877 array $managerResponses, $link = false
879 $this->initializeManager();
880 $user = \User
::newFromName( 'UTSysop' );
881 $id = $user->getId();
882 $name = $user->getName();
884 // Set up lots of mocks...
885 $req = new RememberMeAuthenticationRequest
;
886 $req->rememberMe
= (bool)rand( 0, 1 );
887 $req->pre
= $preResponse;
888 $req->primary
= $primaryResponses;
889 $req->secondary
= $secondaryResponses;
891 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
892 $class = ucfirst( $key ) . 'AuthenticationProvider';
893 $mocks[$key] = $this->getMockForAbstractClass(
894 "MediaWiki\\Auth\\$class", [], "Mock$class"
896 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
897 ->will( $this->returnValue( $key ) );
898 $mocks[$key . '2'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
899 $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
900 ->will( $this->returnValue( $key . '2' ) );
901 $mocks[$key . '3'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
902 $mocks[$key . '3']->expects( $this->any() )->method( 'getUniqueId' )
903 ->will( $this->returnValue( $key . '3' ) );
905 foreach ( $mocks as $mock ) {
906 $mock->expects( $this->any() )->method( 'getAuthenticationRequests' )
907 ->will( $this->returnValue( [] ) );
910 $mocks['pre']->expects( $this->once() )->method( 'testForAuthentication' )
911 ->will( $this->returnCallback( function ( $reqs ) use ( $req ) {
912 $this->assertContains( $req, $reqs );
916 $ct = count( $req->primary
);
917 $callback = $this->returnCallback( function ( $reqs ) use ( $req ) {
918 $this->assertContains( $req, $reqs );
919 return array_shift( $req->primary
);
921 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
922 ->method( 'beginPrimaryAuthentication' )
924 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
925 ->method( 'continuePrimaryAuthentication' )
928 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
929 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
932 $ct = count( $req->secondary
);
933 $callback = $this->returnCallback( function ( $user, $reqs ) use ( $id, $name, $req ) {
934 $this->assertSame( $id, $user->getId() );
935 $this->assertSame( $name, $user->getName() );
936 $this->assertContains( $req, $reqs );
937 return array_shift( $req->secondary
);
939 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
940 ->method( 'beginSecondaryAuthentication' )
942 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
943 ->method( 'continueSecondaryAuthentication' )
946 $abstain = AuthenticationResponse
::newAbstain();
947 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAuthentication' )
948 ->will( $this->returnValue( StatusValue
::newGood() ) );
949 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAuthentication' )
950 ->will( $this->returnValue( $abstain ) );
951 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAuthentication' );
952 $mocks['secondary2']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
953 ->will( $this->returnValue( $abstain ) );
954 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
955 $mocks['secondary3']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
956 ->will( $this->returnValue( $abstain ) );
957 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
959 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
960 $this->primaryauthMocks
= [ $mocks['primary'], $mocks['primary2'] ];
961 $this->secondaryauthMocks
= [
962 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2'],
963 // So linking happens
964 new ConfirmLinkSecondaryAuthenticationProvider
,
966 $this->initializeManager( true );
967 $this->logger
->setCollect( true );
969 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
970 $this->equalTo( AuthenticationResponse
::PASS
),
971 $this->equalTo( AuthenticationResponse
::FAIL
)
973 $providers = array_filter(
975 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
978 return is_callable( [ $p, 'expects' ] );
981 foreach ( $providers as $p ) {
982 $p->postCalled
= false;
983 $p->expects( $this->atMost( 1 ) )->method( 'postAuthentication' )
984 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
985 if ( $user !== null ) {
986 $this->assertInstanceOf( \User
::class, $user );
987 $this->assertSame( 'UTSysop', $user->getName() );
989 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
990 $this->assertThat( $response->status
, $constraint );
991 $p->postCalled
= $response->status
;
995 $session = $this->request
->getSession();
996 $session->setRememberUser( !$req->rememberMe
);
998 foreach ( $managerResponses as $i => $response ) {
999 $success = $response instanceof AuthenticationResponse
&&
1000 $response->status
=== AuthenticationResponse
::PASS
;
1002 $this->hook( 'UserLoggedIn', $this->once() )
1003 ->with( $this->callback( function ( $user ) use ( $id, $name ) {
1004 return $user->getId() === $id && $user->getName() === $name;
1007 $this->hook( 'UserLoggedIn', $this->never() );
1010 $response instanceof AuthenticationResponse
&&
1011 $response->status
=== AuthenticationResponse
::FAIL
&&
1012 $response->message
->getKey() !== 'authmanager-authn-not-in-progress' &&
1013 $response->message
->getKey() !== 'authmanager-authn-no-primary'
1016 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
1018 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->never() );
1024 $ret = $this->manager
->beginAuthentication( [ $req ], 'http://localhost/' );
1026 $ret = $this->manager
->continueAuthentication( [ $req ] );
1028 if ( $response instanceof \Exception
) {
1029 $this->fail( 'Expected exception not thrown', "Response $i" );
1031 } catch ( \Exception
$ex ) {
1032 if ( !$response instanceof \Exception
) {
1035 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
1036 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1037 "Response $i, exception, session state" );
1038 $this->unhook( 'UserLoggedIn' );
1039 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1043 $this->unhook( 'UserLoggedIn' );
1044 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1046 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
1048 $ret->message
= $this->message( $ret->message
);
1049 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
1051 $this->assertSame( $id, $session->getUser()->getId(),
1052 "Response $i, authn" );
1054 $this->assertSame( 0, $session->getUser()->getId(),
1055 "Response $i, authn" );
1057 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
1058 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1059 "Response $i, session state" );
1060 foreach ( $providers as $p ) {
1061 $this->assertSame( $response->status
, $p->postCalled
,
1062 "Response $i, post-auth callback called" );
1065 $this->assertNotNull( $session->getSecret( 'AuthManager::authnState' ),
1066 "Response $i, session state" );
1067 foreach ( $ret->neededRequests
as $neededReq ) {
1068 $this->assertEquals( AuthManager
::ACTION_LOGIN
, $neededReq->action
,
1069 "Response $i, neededRequest action" );
1071 $this->assertEquals(
1072 $ret->neededRequests
,
1073 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN_CONTINUE
),
1074 "Response $i, continuation check"
1076 foreach ( $providers as $p ) {
1077 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
1081 $state = $session->getSecret( 'AuthManager::authnState' );
1082 $maybeLink = $state['maybeLink'] ??
[];
1083 if ( $link && $response->status
=== AuthenticationResponse
::RESTART
) {
1084 $this->assertEquals(
1085 $response->createRequest
->maybeLink
,
1087 "Response $i, maybeLink"
1090 $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
1095 $this->assertSame( $req->rememberMe
, $session->shouldRememberUser(),
1096 'rememberMe checkbox had effect' );
1098 $this->assertNotSame( $req->rememberMe
, $session->shouldRememberUser(),
1099 'rememberMe checkbox wasn\'t applied' );
1103 public function provideAuthentication() {
1104 $rememberReq = new RememberMeAuthenticationRequest
;
1105 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
1107 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1108 $req->foobar
= 'baz';
1109 $restartResponse = AuthenticationResponse
::newRestart(
1110 $this->message( 'authmanager-authn-no-local-user' )
1112 $restartResponse->neededRequests
= [ $rememberReq ];
1114 $restartResponse2Pass = AuthenticationResponse
::newPass( null );
1115 $restartResponse2Pass->linkRequest
= $req;
1116 $restartResponse2 = AuthenticationResponse
::newRestart(
1117 $this->message( 'authmanager-authn-no-local-user-link' )
1119 $restartResponse2->createRequest
= new CreateFromLoginAuthenticationRequest(
1120 null, [ $req->getUniqueId() => $req ]
1122 $restartResponse2->createRequest
->action
= AuthManager
::ACTION_LOGIN
;
1123 $restartResponse2->neededRequests
= [ $rememberReq, $restartResponse2->createRequest
];
1125 $userName = 'UTSysop';
1128 'Failure in pre-auth' => [
1129 StatusValue
::newFatal( 'fail-from-pre' ),
1133 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
1134 AuthenticationResponse
::newFail(
1135 $this->message( 'authmanager-authn-not-in-progress' )
1139 'Failure in primary' => [
1140 StatusValue
::newGood(),
1142 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
1147 'All primary abstain' => [
1148 StatusValue
::newGood(),
1150 AuthenticationResponse
::newAbstain(),
1154 AuthenticationResponse
::newFail( $this->message( 'authmanager-authn-no-primary' ) )
1157 'Primary UI, then redirect, then fail' => [
1158 StatusValue
::newGood(),
1160 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1161 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
1162 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
1167 'Primary redirect, then abstain' => [
1168 StatusValue
::newGood(),
1170 $tmp = AuthenticationResponse
::newRedirect(
1171 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
1173 AuthenticationResponse
::newAbstain(),
1178 new \
DomainException(
1179 'MockPrimaryAuthenticationProvider::continuePrimaryAuthentication() returned ABSTAIN'
1183 'Primary UI, then pass with no local user' => [
1184 StatusValue
::newGood(),
1186 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1187 AuthenticationResponse
::newPass( null ),
1195 'Primary UI, then pass with no local user (link type)' => [
1196 StatusValue
::newGood(),
1198 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1199 $restartResponse2Pass,
1208 'Primary pass with invalid username' => [
1209 StatusValue
::newGood(),
1211 AuthenticationResponse
::newPass( '<>' ),
1215 new \
DomainException( 'MockPrimaryAuthenticationProvider returned an invalid username: <>' ),
1218 'Secondary fail' => [
1219 StatusValue
::newGood(),
1221 AuthenticationResponse
::newPass( $userName ),
1224 AuthenticationResponse
::newFail( $this->message( 'fail-in-secondary' ) ),
1228 'Secondary UI, then abstain' => [
1229 StatusValue
::newGood(),
1231 AuthenticationResponse
::newPass( $userName ),
1234 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1235 AuthenticationResponse
::newAbstain()
1239 AuthenticationResponse
::newPass( $userName ),
1242 'Secondary pass' => [
1243 StatusValue
::newGood(),
1245 AuthenticationResponse
::newPass( $userName ),
1248 AuthenticationResponse
::newPass()
1251 AuthenticationResponse
::newPass( $userName ),
1258 * @dataProvider provideUserExists
1259 * @param bool $primary1Exists
1260 * @param bool $primary2Exists
1261 * @param bool $expect
1263 public function testUserExists( $primary1Exists, $primary2Exists, $expect ) {
1264 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1265 $mock1->expects( $this->any() )->method( 'getUniqueId' )
1266 ->will( $this->returnValue( 'primary1' ) );
1267 $mock1->expects( $this->any() )->method( 'testUserExists' )
1268 ->with( $this->equalTo( 'UTSysop' ) )
1269 ->will( $this->returnValue( $primary1Exists ) );
1270 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1271 $mock2->expects( $this->any() )->method( 'getUniqueId' )
1272 ->will( $this->returnValue( 'primary2' ) );
1273 $mock2->expects( $this->any() )->method( 'testUserExists' )
1274 ->with( $this->equalTo( 'UTSysop' ) )
1275 ->will( $this->returnValue( $primary2Exists ) );
1276 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1278 $this->initializeManager( true );
1279 $this->assertSame( $expect, $this->manager
->userExists( 'UTSysop' ) );
1282 public static function provideUserExists() {
1284 [ false, false, false ],
1285 [ true, false, true ],
1286 [ false, true, true ],
1287 [ true, true, true ],
1292 * @dataProvider provideAllowsAuthenticationDataChange
1293 * @param StatusValue $primaryReturn
1294 * @param StatusValue $secondaryReturn
1295 * @param Status $expect
1297 public function testAllowsAuthenticationDataChange( $primaryReturn, $secondaryReturn, $expect ) {
1298 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1300 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1301 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1302 $mock1->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1303 ->with( $this->equalTo( $req ) )
1304 ->will( $this->returnValue( $primaryReturn ) );
1305 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
1306 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1307 $mock2->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1308 ->with( $this->equalTo( $req ) )
1309 ->will( $this->returnValue( $secondaryReturn ) );
1311 $this->primaryauthMocks
= [ $mock1 ];
1312 $this->secondaryauthMocks
= [ $mock2 ];
1313 $this->initializeManager( true );
1314 $this->assertEquals( $expect, $this->manager
->allowsAuthenticationDataChange( $req ) );
1317 public static function provideAllowsAuthenticationDataChange() {
1318 $ignored = \Status
::newGood( 'ignored' );
1319 $ignored->warning( 'authmanager-change-not-supported' );
1321 $okFromPrimary = StatusValue
::newGood();
1322 $okFromPrimary->warning( 'warning-from-primary' );
1323 $okFromSecondary = StatusValue
::newGood();
1324 $okFromSecondary->warning( 'warning-from-secondary' );
1328 StatusValue
::newGood(),
1329 StatusValue
::newGood(),
1333 StatusValue
::newGood(),
1334 StatusValue
::newGood( 'ignore' ),
1338 StatusValue
::newGood( 'ignored' ),
1339 StatusValue
::newGood(),
1343 StatusValue
::newGood( 'ignored' ),
1344 StatusValue
::newGood( 'ignored' ),
1348 StatusValue
::newFatal( 'fail from primary' ),
1349 StatusValue
::newGood(),
1350 \Status
::newFatal( 'fail from primary' ),
1354 StatusValue
::newGood(),
1355 \Status
::wrap( $okFromPrimary ),
1358 StatusValue
::newGood(),
1359 StatusValue
::newFatal( 'fail from secondary' ),
1360 \Status
::newFatal( 'fail from secondary' ),
1363 StatusValue
::newGood(),
1365 \Status
::wrap( $okFromSecondary ),
1370 public function testChangeAuthenticationData() {
1371 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1372 $req->username
= 'UTSysop';
1374 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1375 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1376 $mock1->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1377 ->with( $this->equalTo( $req ) );
1378 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1379 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1380 $mock2->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1381 ->with( $this->equalTo( $req ) );
1383 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1384 $this->initializeManager( true );
1385 $this->logger
->setCollect( true );
1386 $this->manager
->changeAuthenticationData( $req );
1387 $this->assertSame( [
1388 [ LogLevel
::INFO
, 'Changing authentication data for {user} class {what}' ],
1389 ], $this->logger
->getBuffer() );
1392 public function testCanCreateAccounts() {
1394 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
1395 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
1396 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
1399 foreach ( $types as $type => $can ) {
1400 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1401 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
1402 $mock->expects( $this->any() )->method( 'accountCreationType' )
1403 ->will( $this->returnValue( $type ) );
1404 $this->primaryauthMocks
= [ $mock ];
1405 $this->initializeManager( true );
1406 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
1410 public function testCheckAccountCreatePermissions() {
1411 global $wgGroupPermissions;
1413 $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
1415 $this->initializeManager( true );
1417 $wgGroupPermissions['*']['createaccount'] = true;
1418 $this->assertEquals(
1420 $this->manager
->checkAccountCreatePermissions( new \User
)
1423 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1424 $readOnlyMode->setReason( 'Because' );
1425 $this->assertEquals(
1426 \Status
::newFatal( wfMessage( 'readonlytext', 'Because' ) ),
1427 $this->manager
->checkAccountCreatePermissions( new \User
)
1429 $readOnlyMode->setReason( false );
1431 $wgGroupPermissions['*']['createaccount'] = false;
1432 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1433 $this->assertFalse( $status->isOK() );
1434 $this->assertTrue( $status->hasMessage( 'badaccess-groups' ) );
1435 $wgGroupPermissions['*']['createaccount'] = true;
1437 $user = \User
::newFromName( 'UTBlockee' );
1438 if ( $user->getID() == 0 ) {
1439 $user->addToDatabase();
1440 \TestUser
::setPasswordForUser( $user, 'UTBlockeePassword' );
1441 $user->saveSettings();
1443 $oldBlock = \Block
::newFromTarget( 'UTBlockee' );
1445 // An old block will prevent our new one from saving.
1446 $oldBlock->delete();
1449 'address' => 'UTBlockee',
1450 'user' => $user->getID(),
1451 'by' => $this->getTestSysop()->getUser()->getId(),
1452 'reason' => __METHOD__
,
1453 'expiry' => time() +
100500,
1454 'createAccount' => true,
1456 $block = new \
Block( $blockOptions );
1458 $status = $this->manager
->checkAccountCreatePermissions( $user );
1459 $this->assertFalse( $status->isOK() );
1460 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
1463 'address' => '127.0.0.0/24',
1464 'by' => $this->getTestSysop()->getUser()->getId(),
1465 'reason' => __METHOD__
,
1466 'expiry' => time() +
100500,
1467 'createAccount' => true,
1469 $block = new \
Block( $blockOptions );
1471 $scopeVariable = new ScopedCallback( [ $block, 'delete' ] );
1472 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1473 $this->assertFalse( $status->isOK() );
1474 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
1475 ScopedCallback
::consume( $scopeVariable );
1477 $this->setMwGlobals( [
1478 'wgEnableDnsBlacklist' => true,
1479 'wgDnsBlacklistUrls' => [
1480 'local.wmftest.net', // This will resolve for every subdomain, which works to test "listed?"
1482 'wgProxyWhitelist' => [],
1484 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1485 $this->assertFalse( $status->isOK() );
1486 $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
1487 $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
1488 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1489 $this->assertTrue( $status->isGood() );
1493 * @param string $uniq
1496 private static function usernameForCreation( $uniq = '' ) {
1499 $username = "UTAuthManagerTestAccountCreation" . $uniq . ++
$i;
1500 } while ( \User
::newFromName( $username )->getId() !== 0 );
1504 public function testCanCreateAccount() {
1505 $username = self
::usernameForCreation();
1506 $this->initializeManager();
1508 $this->assertEquals(
1509 \Status
::newFatal( 'authmanager-create-disabled' ),
1510 $this->manager
->canCreateAccount( $username )
1513 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1514 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1515 $mock->expects( $this->any() )->method( 'accountCreationType' )
1516 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1517 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1518 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1519 ->will( $this->returnValue( StatusValue
::newGood() ) );
1520 $this->primaryauthMocks
= [ $mock ];
1521 $this->initializeManager( true );
1523 $this->assertEquals(
1524 \Status
::newFatal( 'userexists' ),
1525 $this->manager
->canCreateAccount( $username )
1528 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1529 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1530 $mock->expects( $this->any() )->method( 'accountCreationType' )
1531 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1532 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1533 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1534 ->will( $this->returnValue( StatusValue
::newGood() ) );
1535 $this->primaryauthMocks
= [ $mock ];
1536 $this->initializeManager( true );
1538 $this->assertEquals(
1539 \Status
::newFatal( 'noname' ),
1540 $this->manager
->canCreateAccount( $username . '<>' )
1543 $this->assertEquals(
1544 \Status
::newFatal( 'userexists' ),
1545 $this->manager
->canCreateAccount( 'UTSysop' )
1548 $this->assertEquals(
1550 $this->manager
->canCreateAccount( $username )
1553 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1554 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1555 $mock->expects( $this->any() )->method( 'accountCreationType' )
1556 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1557 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1558 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1559 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1560 $this->primaryauthMocks
= [ $mock ];
1561 $this->initializeManager( true );
1563 $this->assertEquals(
1564 \Status
::newFatal( 'fail' ),
1565 $this->manager
->canCreateAccount( $username )
1569 public function testBeginAccountCreation() {
1570 $creator = \User
::newFromName( 'UTSysop' );
1571 $userReq = new UsernameAuthenticationRequest
;
1572 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1573 return $level === LogLevel
::DEBUG ?
null : $message;
1575 $this->initializeManager();
1577 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
1578 $this->hook( 'LocalUserCreated', $this->never() );
1580 $this->manager
->beginAccountCreation(
1581 $creator, [], 'http://localhost/'
1583 $this->fail( 'Expected exception not thrown' );
1584 } catch ( \LogicException
$ex ) {
1585 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1587 $this->unhook( 'LocalUserCreated' );
1589 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1592 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1593 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1594 $mock->expects( $this->any() )->method( 'accountCreationType' )
1595 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1596 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1597 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1598 ->will( $this->returnValue( StatusValue
::newGood() ) );
1599 $this->primaryauthMocks
= [ $mock ];
1600 $this->initializeManager( true );
1602 $this->hook( 'LocalUserCreated', $this->never() );
1603 $ret = $this->manager
->beginAccountCreation( $creator, [], 'http://localhost/' );
1604 $this->unhook( 'LocalUserCreated' );
1605 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1606 $this->assertSame( 'noname', $ret->message
->getKey() );
1608 $this->hook( 'LocalUserCreated', $this->never() );
1609 $userReq->username
= self
::usernameForCreation();
1610 $userReq2 = new UsernameAuthenticationRequest
;
1611 $userReq2->username
= $userReq->username
. 'X';
1612 $ret = $this->manager
->beginAccountCreation(
1613 $creator, [ $userReq, $userReq2 ], 'http://localhost/'
1615 $this->unhook( 'LocalUserCreated' );
1616 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1617 $this->assertSame( 'noname', $ret->message
->getKey() );
1619 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1620 $readOnlyMode->setReason( 'Because' );
1621 $this->hook( 'LocalUserCreated', $this->never() );
1622 $userReq->username
= self
::usernameForCreation();
1623 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1624 $this->unhook( 'LocalUserCreated' );
1625 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1626 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
1627 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1628 $readOnlyMode->setReason( false );
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( 'userexists', $ret->message
->getKey() );
1637 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1638 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1639 $mock->expects( $this->any() )->method( 'accountCreationType' )
1640 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1641 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1642 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1643 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1644 $this->primaryauthMocks
= [ $mock ];
1645 $this->initializeManager( true );
1647 $this->hook( 'LocalUserCreated', $this->never() );
1648 $userReq->username
= self
::usernameForCreation();
1649 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1650 $this->unhook( 'LocalUserCreated' );
1651 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1652 $this->assertSame( 'fail', $ret->message
->getKey() );
1654 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1655 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1656 $mock->expects( $this->any() )->method( 'accountCreationType' )
1657 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1658 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1659 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1660 ->will( $this->returnValue( StatusValue
::newGood() ) );
1661 $this->primaryauthMocks
= [ $mock ];
1662 $this->initializeManager( true );
1664 $this->hook( 'LocalUserCreated', $this->never() );
1665 $userReq->username
= self
::usernameForCreation() . '<>';
1666 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1667 $this->unhook( 'LocalUserCreated' );
1668 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1669 $this->assertSame( 'noname', $ret->message
->getKey() );
1671 $this->hook( 'LocalUserCreated', $this->never() );
1672 $userReq->username
= $creator->getName();
1673 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1674 $this->unhook( 'LocalUserCreated' );
1675 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1676 $this->assertSame( 'userexists', $ret->message
->getKey() );
1678 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1679 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1680 $mock->expects( $this->any() )->method( 'accountCreationType' )
1681 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1682 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1683 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1684 ->will( $this->returnValue( StatusValue
::newGood() ) );
1685 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
1686 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1687 $this->primaryauthMocks
= [ $mock ];
1688 $this->initializeManager( true );
1690 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1691 ->setMethods( [ 'populateUser' ] )
1693 $req->expects( $this->any() )->method( 'populateUser' )
1694 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1695 $userReq->username
= self
::usernameForCreation();
1696 $ret = $this->manager
->beginAccountCreation(
1697 $creator, [ $userReq, $req ], 'http://localhost/'
1699 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1700 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1702 $req = new UserDataAuthenticationRequest
;
1703 $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( 'fail', $ret->message
->getKey() );
1711 $this->manager
->beginAccountCreation(
1712 \User
::newFromName( $userReq->username
), [ $userReq, $req ], 'http://localhost/'
1714 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1715 $this->assertSame( 'fail', $ret->message
->getKey() );
1718 public function testContinueAccountCreation() {
1719 $creator = \User
::newFromName( 'UTSysop' );
1720 $username = self
::usernameForCreation();
1721 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1722 return $level === LogLevel
::DEBUG ?
null : $message;
1724 $this->initializeManager();
1728 'username' => $username,
1730 'creatorname' => $username,
1733 'primaryResponse' => null,
1735 'ranPreTests' => true,
1738 $this->hook( 'LocalUserCreated', $this->never() );
1740 $this->manager
->continueAccountCreation( [] );
1741 $this->fail( 'Expected exception not thrown' );
1742 } catch ( \LogicException
$ex ) {
1743 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1745 $this->unhook( 'LocalUserCreated' );
1747 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1748 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1749 $mock->expects( $this->any() )->method( 'accountCreationType' )
1750 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1751 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1752 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )->will(
1753 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
1755 $this->primaryauthMocks
= [ $mock ];
1756 $this->initializeManager( true );
1758 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', null );
1759 $this->hook( 'LocalUserCreated', $this->never() );
1760 $ret = $this->manager
->continueAccountCreation( [] );
1761 $this->unhook( 'LocalUserCreated' );
1762 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1763 $this->assertSame( 'authmanager-create-not-in-progress', $ret->message
->getKey() );
1765 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1766 [ 'username' => "$username<>" ] +
$session );
1767 $this->hook( 'LocalUserCreated', $this->never() );
1768 $ret = $this->manager
->continueAccountCreation( [] );
1769 $this->unhook( 'LocalUserCreated' );
1770 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1771 $this->assertSame( 'noname', $ret->message
->getKey() );
1773 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1776 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $session );
1777 $this->hook( 'LocalUserCreated', $this->never() );
1778 $cache = \ObjectCache
::getLocalClusterInstance();
1779 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1780 $ret = $this->manager
->continueAccountCreation( [] );
1782 $this->unhook( 'LocalUserCreated' );
1783 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1784 $this->assertSame( 'usernameinprogress', $ret->message
->getKey() );
1785 // This error shouldn't remove the existing session, because the
1786 // raced-with process "owns" it.
1788 $session, $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1791 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1792 [ 'username' => $creator->getName() ] +
$session );
1793 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1794 $readOnlyMode->setReason( 'Because' );
1795 $this->hook( 'LocalUserCreated', $this->never() );
1796 $ret = $this->manager
->continueAccountCreation( [] );
1797 $this->unhook( 'LocalUserCreated' );
1798 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1799 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
1800 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1801 $readOnlyMode->setReason( false );
1803 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1804 [ 'username' => $creator->getName() ] +
$session );
1805 $this->hook( 'LocalUserCreated', $this->never() );
1806 $ret = $this->manager
->continueAccountCreation( [] );
1807 $this->unhook( 'LocalUserCreated' );
1808 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1809 $this->assertSame( 'userexists', $ret->message
->getKey() );
1811 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1814 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1815 [ 'userid' => $creator->getId() ] +
$session );
1816 $this->hook( 'LocalUserCreated', $this->never() );
1818 $ret = $this->manager
->continueAccountCreation( [] );
1819 $this->fail( 'Expected exception not thrown' );
1820 } catch ( \UnexpectedValueException
$ex ) {
1821 $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
1823 $this->unhook( 'LocalUserCreated' );
1825 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1828 $id = $creator->getId();
1829 $name = $creator->getName();
1830 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1831 [ 'username' => $name, 'userid' => $id +
1 ] +
$session );
1832 $this->hook( 'LocalUserCreated', $this->never() );
1834 $ret = $this->manager
->continueAccountCreation( [] );
1835 $this->fail( 'Expected exception not thrown' );
1836 } catch ( \UnexpectedValueException
$ex ) {
1837 $this->assertEquals(
1838 "User \"{$name}\" exists, but ID $id != " . ( $id +
1 ) . '!', $ex->getMessage()
1841 $this->unhook( 'LocalUserCreated' );
1843 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1846 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1847 ->setMethods( [ 'populateUser' ] )
1849 $req->expects( $this->any() )->method( 'populateUser' )
1850 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1851 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1852 [ 'reqs' => [ $req ] ] +
$session );
1853 $ret = $this->manager
->continueAccountCreation( [] );
1854 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1855 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1857 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1862 * @dataProvider provideAccountCreation
1863 * @param StatusValue $preTest
1864 * @param StatusValue $primaryTest
1865 * @param StatusValue $secondaryTest
1866 * @param array $primaryResponses
1867 * @param array $secondaryResponses
1868 * @param array $managerResponses
1870 public function testAccountCreation(
1871 StatusValue
$preTest, $primaryTest, $secondaryTest,
1872 array $primaryResponses, array $secondaryResponses, array $managerResponses
1874 $creator = \User
::newFromName( 'UTSysop' );
1875 $username = self
::usernameForCreation();
1877 $this->initializeManager();
1879 // Set up lots of mocks...
1880 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1881 $req->preTest
= $preTest;
1882 $req->primaryTest
= $primaryTest;
1883 $req->secondaryTest
= $secondaryTest;
1884 $req->primary
= $primaryResponses;
1885 $req->secondary
= $secondaryResponses;
1887 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
1888 $class = ucfirst( $key ) . 'AuthenticationProvider';
1889 $mocks[$key] = $this->getMockForAbstractClass(
1890 "MediaWiki\\Auth\\$class", [], "Mock$class"
1892 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
1893 ->will( $this->returnValue( $key ) );
1894 $mocks[$key]->expects( $this->any() )->method( 'testUserForCreation' )
1895 ->will( $this->returnValue( StatusValue
::newGood() ) );
1896 $mocks[$key]->expects( $this->any() )->method( 'testForAccountCreation' )
1897 ->will( $this->returnCallback(
1898 function ( $user, $creatorIn, $reqs )
1899 use ( $username, $creator, $req, $key )
1901 $this->assertSame( $username, $user->getName() );
1902 $this->assertSame( $creator->getId(), $creatorIn->getId() );
1903 $this->assertSame( $creator->getName(), $creatorIn->getName() );
1905 foreach ( $reqs as $r ) {
1906 $this->assertSame( $username, $r->username
);
1907 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1909 $this->assertTrue( $foundReq, '$reqs contains $req' );
1915 for ( $i = 2; $i <= 3; $i++
) {
1916 $mocks[$key . $i] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
1917 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
1918 ->will( $this->returnValue( $key . $i ) );
1919 $mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' )
1920 ->will( $this->returnValue( StatusValue
::newGood() ) );
1921 $mocks[$key . $i]->expects( $this->atMost( 1 ) )->method( 'testForAccountCreation' )
1922 ->will( $this->returnValue( StatusValue
::newGood() ) );
1926 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
1927 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1928 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
1929 ->will( $this->returnValue( false ) );
1930 $ct = count( $req->primary
);
1931 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1932 $this->assertSame( $username, $user->getName() );
1933 $this->assertSame( 'UTSysop', $creator->getName() );
1935 foreach ( $reqs as $r ) {
1936 $this->assertSame( $username, $r->username
);
1937 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1939 $this->assertTrue( $foundReq, '$reqs contains $req' );
1940 return array_shift( $req->primary
);
1942 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
1943 ->method( 'beginPrimaryAccountCreation' )
1944 ->will( $callback );
1945 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1946 ->method( 'continuePrimaryAccountCreation' )
1947 ->will( $callback );
1949 $ct = count( $req->secondary
);
1950 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1951 $this->assertSame( $username, $user->getName() );
1952 $this->assertSame( 'UTSysop', $creator->getName() );
1954 foreach ( $reqs as $r ) {
1955 $this->assertSame( $username, $r->username
);
1956 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1958 $this->assertTrue( $foundReq, '$reqs contains $req' );
1959 return array_shift( $req->secondary
);
1961 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
1962 ->method( 'beginSecondaryAccountCreation' )
1963 ->will( $callback );
1964 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1965 ->method( 'continueSecondaryAccountCreation' )
1966 ->will( $callback );
1968 $abstain = AuthenticationResponse
::newAbstain();
1969 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
1970 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
1971 $mocks['primary2']->expects( $this->any() )->method( 'testUserExists' )
1972 ->will( $this->returnValue( false ) );
1973 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountCreation' )
1974 ->will( $this->returnValue( $abstain ) );
1975 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1976 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
1977 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_NONE
) );
1978 $mocks['primary3']->expects( $this->any() )->method( 'testUserExists' )
1979 ->will( $this->returnValue( false ) );
1980 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountCreation' );
1981 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1982 $mocks['secondary2']->expects( $this->atMost( 1 ) )
1983 ->method( 'beginSecondaryAccountCreation' )
1984 ->will( $this->returnValue( $abstain ) );
1985 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1986 $mocks['secondary3']->expects( $this->atMost( 1 ) )
1987 ->method( 'beginSecondaryAccountCreation' )
1988 ->will( $this->returnValue( $abstain ) );
1989 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1991 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
1992 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary'], $mocks['primary2'] ];
1993 $this->secondaryauthMocks
= [
1994 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2']
1997 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
1998 return $level === LogLevel
::DEBUG ?
null : $message;
2001 $this->initializeManager( true );
2003 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
2004 $this->equalTo( AuthenticationResponse
::PASS
),
2005 $this->equalTo( AuthenticationResponse
::FAIL
)
2007 $providers = array_merge(
2008 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
2010 foreach ( $providers as $p ) {
2011 $p->postCalled
= false;
2012 $p->expects( $this->atMost( 1 ) )->method( 'postAccountCreation' )
2013 ->willReturnCallback( function ( $user, $creator, $response )
2014 use ( $constraint, $p, $username )
2016 $this->assertInstanceOf( \User
::class, $user );
2017 $this->assertSame( $username, $user->getName() );
2018 $this->assertSame( 'UTSysop', $creator->getName() );
2019 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
2020 $this->assertThat( $response->status
, $constraint );
2021 $p->postCalled
= $response->status
;
2025 // We're testing with $wgNewUserLog = false, so assert that it worked
2026 $dbw = wfGetDB( DB_MASTER
);
2027 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2031 foreach ( $managerResponses as $i => $response ) {
2032 $success = $response instanceof AuthenticationResponse
&&
2033 $response->status
=== AuthenticationResponse
::PASS
;
2034 if ( $i === 'created' ) {
2036 $this->hook( 'LocalUserCreated', $this->once() )
2038 $this->callback( function ( $user ) use ( $username ) {
2039 return $user->getName() === $username;
2041 $this->equalTo( false )
2043 $expectLog[] = [ LogLevel
::INFO
, "Creating user {user} during account creation" ];
2045 $this->hook( 'LocalUserCreated', $this->never() );
2051 $userReq = new UsernameAuthenticationRequest
;
2052 $userReq->username
= $username;
2053 $ret = $this->manager
->beginAccountCreation(
2054 $creator, [ $userReq, $req ], 'http://localhost/'
2057 $ret = $this->manager
->continueAccountCreation( [ $req ] );
2059 if ( $response instanceof \Exception
) {
2060 $this->fail( 'Expected exception not thrown', "Response $i" );
2062 } catch ( \Exception
$ex ) {
2063 if ( !$response instanceof \Exception
) {
2066 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
2068 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2069 "Response $i, exception, session state"
2071 $this->unhook( 'LocalUserCreated' );
2075 $this->unhook( 'LocalUserCreated' );
2077 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
2080 $this->assertNotNull( $ret->loginRequest
, "Response $i, login marker" );
2081 $this->assertContains(
2082 $ret->loginRequest
, $this->managerPriv
->createdAccountAuthenticationRequests
,
2083 "Response $i, login marker"
2088 "MediaWiki\Auth\AuthManager::continueAccountCreation: Account creation succeeded for {user}"
2091 // Set some fields in the expected $response that we couldn't
2092 // know in provideAccountCreation().
2093 $response->username
= $username;
2094 $response->loginRequest
= $ret->loginRequest
;
2096 $this->assertNull( $ret->loginRequest
, "Response $i, login marker" );
2097 $this->assertSame( [], $this->managerPriv
->createdAccountAuthenticationRequests
,
2098 "Response $i, login marker" );
2100 $ret->message
= $this->message( $ret->message
);
2101 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
2102 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
2104 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2105 "Response $i, session state"
2107 foreach ( $providers as $p ) {
2108 $this->assertSame( $response->status
, $p->postCalled
,
2109 "Response $i, post-auth callback called" );
2112 $this->assertNotNull(
2113 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2114 "Response $i, session state"
2116 foreach ( $ret->neededRequests
as $neededReq ) {
2117 $this->assertEquals( AuthManager
::ACTION_CREATE
, $neededReq->action
,
2118 "Response $i, neededRequest action" );
2120 $this->assertEquals(
2121 $ret->neededRequests
,
2122 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_CREATE_CONTINUE
),
2123 "Response $i, continuation check"
2125 foreach ( $providers as $p ) {
2126 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
2131 $this->assertNotEquals( 0, \User
::idFromName( $username ) );
2133 $this->assertEquals( 0, \User
::idFromName( $username ) );
2139 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
2143 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2147 public function provideAccountCreation() {
2148 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
2149 $good = StatusValue
::newGood();
2152 'Pre-creation test fail in pre' => [
2153 StatusValue
::newFatal( 'fail-from-pre' ), $good, $good,
2157 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
2160 'Pre-creation test fail in primary' => [
2161 $good, StatusValue
::newFatal( 'fail-from-primary' ), $good,
2165 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2168 'Pre-creation test fail in secondary' => [
2169 $good, $good, StatusValue
::newFatal( 'fail-from-secondary' ),
2173 AuthenticationResponse
::newFail( $this->message( 'fail-from-secondary' ) ),
2176 'Failure in primary' => [
2177 $good, $good, $good,
2179 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2184 'All primary abstain' => [
2185 $good, $good, $good,
2187 AuthenticationResponse
::newAbstain(),
2191 AuthenticationResponse
::newFail( $this->message( 'authmanager-create-no-primary' ) )
2194 'Primary UI, then redirect, then fail' => [
2195 $good, $good, $good,
2197 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2198 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
2199 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
2204 'Primary redirect, then abstain' => [
2205 $good, $good, $good,
2207 $tmp = AuthenticationResponse
::newRedirect(
2208 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
2210 AuthenticationResponse
::newAbstain(),
2215 new \
DomainException(
2216 'MockPrimaryAuthenticationProvider::continuePrimaryAccountCreation() returned ABSTAIN'
2220 'Primary UI, then pass; secondary abstain' => [
2221 $good, $good, $good,
2223 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2224 AuthenticationResponse
::newPass(),
2227 AuthenticationResponse
::newAbstain(),
2231 'created' => AuthenticationResponse
::newPass( '' ),
2234 'Primary pass; secondary UI then pass' => [
2235 $good, $good, $good,
2237 AuthenticationResponse
::newPass( '' ),
2240 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2241 AuthenticationResponse
::newPass( '' ),
2245 AuthenticationResponse
::newPass( '' ),
2248 'Primary pass; secondary fail' => [
2249 $good, $good, $good,
2251 AuthenticationResponse
::newPass(),
2254 AuthenticationResponse
::newFail( $this->message( '...' ) ),
2257 'created' => new \
DomainException(
2258 'MockSecondaryAuthenticationProvider::beginSecondaryAccountCreation() returned FAIL. ' .
2259 'Secondary providers are not allowed to fail account creation, ' .
2260 'that should have been done via testForAccountCreation().'
2268 * @dataProvider provideAccountCreationLogging
2269 * @param bool $isAnon
2270 * @param string|null $logSubtype
2272 public function testAccountCreationLogging( $isAnon, $logSubtype ) {
2273 $creator = $isAnon ?
new \User
: \User
::newFromName( 'UTSysop' );
2274 $username = self
::usernameForCreation();
2276 $this->initializeManager();
2278 // Set up lots of mocks...
2279 $mock = $this->getMockForAbstractClass(
2280 \MediaWiki\Auth\PrimaryAuthenticationProvider
::class, []
2282 $mock->expects( $this->any() )->method( 'getUniqueId' )
2283 ->will( $this->returnValue( 'primary' ) );
2284 $mock->expects( $this->any() )->method( 'testUserForCreation' )
2285 ->will( $this->returnValue( StatusValue
::newGood() ) );
2286 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
2287 ->will( $this->returnValue( StatusValue
::newGood() ) );
2288 $mock->expects( $this->any() )->method( 'accountCreationType' )
2289 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2290 $mock->expects( $this->any() )->method( 'testUserExists' )
2291 ->will( $this->returnValue( false ) );
2292 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
2293 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
2294 $mock->expects( $this->any() )->method( 'finishAccountCreation' )
2295 ->will( $this->returnValue( $logSubtype ) );
2297 $this->primaryauthMocks
= [ $mock ];
2298 $this->initializeManager( true );
2299 $this->logger
->setCollect( true );
2301 $this->config
->set( 'NewUserLog', true );
2303 $dbw = wfGetDB( DB_MASTER
);
2304 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2306 $userReq = new UsernameAuthenticationRequest
;
2307 $userReq->username
= $username;
2308 $reasonReq = new CreationReasonAuthenticationRequest
;
2309 $reasonReq->reason
= $this->toString();
2310 $ret = $this->manager
->beginAccountCreation(
2311 $creator, [ $userReq, $reasonReq ], 'http://localhost/'
2314 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
2316 $user = \User
::newFromName( $username );
2317 $this->assertNotEquals( 0, $user->getId(), 'sanity check' );
2318 $this->assertNotEquals( $creator->getId(), $user->getId(), 'sanity check' );
2320 $data = \DatabaseLogEntry
::getSelectQueryData();
2321 $rows = iterator_to_array( $dbw->select(
2325 'log_id > ' . (int)$maxLogId,
2326 'log_type' => 'newusers'
2332 $this->assertCount( 1, $rows );
2333 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2335 $this->assertSame( $logSubtype ?
: ( $isAnon ?
'create' : 'create2' ), $entry->getSubtype() );
2337 $isAnon ?
$user->getId() : $creator->getId(),
2338 $entry->getPerformer()->getId()
2341 $isAnon ?
$user->getName() : $creator->getName(),
2342 $entry->getPerformer()->getName()
2344 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2345 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2346 $this->assertSame( $this->toString(), $entry->getComment() );
2349 public static function provideAccountCreationLogging() {
2354 [ false, 'byemail' ],
2358 public function testAutoAccountCreation() {
2359 global $wgGroupPermissions, $wgHooks;
2361 // PHPUnit seems to have a bug where it will call the ->with()
2362 // callbacks for our hooks again after the test is run (WTF?), which
2363 // breaks here because $username no longer matches $user by the end of
2365 $workaroundPHPUnitBug = false;
2367 $username = self
::usernameForCreation();
2368 $this->initializeManager();
2370 $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
2371 $wgGroupPermissions['*']['createaccount'] = true;
2372 $wgGroupPermissions['*']['autocreateaccount'] = false;
2374 $this->mergeMwGlobalArrayValue( 'wgObjectCaches',
2375 [ __METHOD__
=> [ 'class' => 'HashBagOStuff' ] ] );
2376 $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__
] );
2378 // Set up lots of mocks...
2380 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2381 $class = ucfirst( $key ) . 'AuthenticationProvider';
2382 $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
2383 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2384 ->will( $this->returnValue( $key ) );
2387 $good = StatusValue
::newGood();
2388 $callback = $this->callback( function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
2389 return $workaroundPHPUnitBug ||
$user->getName() === $username;
2392 $mocks['pre']->expects( $this->exactly( 12 ) )->method( 'testUserForCreation' )
2393 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2394 ->will( $this->onConsecutiveCalls(
2395 StatusValue
::newFatal( 'ok' ), StatusValue
::newFatal( 'ok' ), // For testing permissions
2396 StatusValue
::newFatal( 'fail-in-pre' ), $good, $good,
2397 $good, // backoff test
2398 $good, // addToDatabase fails test
2399 $good, // addToDatabase throws test
2400 $good, // addToDatabase exists test
2401 $good, $good, $good // success
2404 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
2405 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2406 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
2407 ->will( $this->returnValue( true ) );
2408 $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
2409 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2410 ->will( $this->onConsecutiveCalls(
2411 StatusValue
::newFatal( 'fail-in-primary' ), $good,
2412 $good, // backoff test
2413 $good, // addToDatabase fails test
2414 $good, // addToDatabase throws test
2415 $good, // addToDatabase exists test
2418 $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2419 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) );
2421 $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
2422 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) )
2423 ->will( $this->onConsecutiveCalls(
2424 StatusValue
::newFatal( 'fail-in-secondary' ),
2425 $good, // backoff test
2426 $good, // addToDatabase fails test
2427 $good, // addToDatabase throws test
2428 $good, // addToDatabase exists test
2431 $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2432 ->with( $callback, $this->identicalTo( AuthManager
::AUTOCREATE_SOURCE_SESSION
) );
2434 $this->preauthMocks
= [ $mocks['pre'] ];
2435 $this->primaryauthMocks
= [ $mocks['primary'] ];
2436 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2437 $this->initializeManager( true );
2438 $session = $this->request
->getSession();
2440 $logger = new \
TestLogger( true, function ( $m ) {
2441 $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
2444 $this->manager
->setLogger( $logger );
2447 $user = \User
::newFromName( 'UTSysop' );
2448 $this->manager
->autoCreateUser( $user, 'InvalidSource', true );
2449 $this->fail( 'Expected exception not thrown' );
2450 } catch ( \InvalidArgumentException
$ex ) {
2451 $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
2454 // First, check an existing user
2456 $user = \User
::newFromName( 'UTSysop' );
2457 $this->hook( 'LocalUserCreated', $this->never() );
2458 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2459 $this->unhook( 'LocalUserCreated' );
2460 $expect = \Status
::newGood();
2461 $expect->warning( 'userexists' );
2462 $this->assertEquals( $expect, $ret );
2463 $this->assertNotEquals( 0, $user->getId() );
2464 $this->assertSame( 'UTSysop', $user->getName() );
2465 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2466 $this->assertSame( [
2467 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2468 ], $logger->getBuffer() );
2469 $logger->clearBuffer();
2472 $user = \User
::newFromName( 'UTSysop' );
2473 $this->hook( 'LocalUserCreated', $this->never() );
2474 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2475 $this->unhook( 'LocalUserCreated' );
2476 $expect = \Status
::newGood();
2477 $expect->warning( 'userexists' );
2478 $this->assertEquals( $expect, $ret );
2479 $this->assertNotEquals( 0, $user->getId() );
2480 $this->assertSame( 'UTSysop', $user->getName() );
2481 $this->assertEquals( 0, $session->getUser()->getId() );
2482 $this->assertSame( [
2483 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2484 ], $logger->getBuffer() );
2485 $logger->clearBuffer();
2487 // Wiki is read-only
2489 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
2490 $readOnlyMode->setReason( 'Because' );
2491 $user = \User
::newFromName( $username );
2492 $this->hook( 'LocalUserCreated', $this->never() );
2493 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2494 $this->unhook( 'LocalUserCreated' );
2495 $this->assertEquals( \Status
::newFatal( wfMessage( 'readonlytext', 'Because' ) ), $ret );
2496 $this->assertEquals( 0, $user->getId() );
2497 $this->assertNotEquals( $username, $user->getName() );
2498 $this->assertEquals( 0, $session->getUser()->getId() );
2499 $this->assertSame( [
2500 [ LogLevel
::DEBUG
, 'denied by wfReadOnly(): {reason}' ],
2501 ], $logger->getBuffer() );
2502 $logger->clearBuffer();
2503 $readOnlyMode->setReason( false );
2505 // Session blacklisted
2507 $session->set( 'AuthManager::AutoCreateBlacklist', 'test' );
2508 $user = \User
::newFromName( $username );
2509 $this->hook( 'LocalUserCreated', $this->never() );
2510 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2511 $this->unhook( 'LocalUserCreated' );
2512 $this->assertEquals( \Status
::newFatal( 'test' ), $ret );
2513 $this->assertEquals( 0, $user->getId() );
2514 $this->assertNotEquals( $username, $user->getName() );
2515 $this->assertEquals( 0, $session->getUser()->getId() );
2516 $this->assertSame( [
2517 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2518 ], $logger->getBuffer() );
2519 $logger->clearBuffer();
2522 $session->set( 'AuthManager::AutoCreateBlacklist', StatusValue
::newFatal( 'test2' ) );
2523 $user = \User
::newFromName( $username );
2524 $this->hook( 'LocalUserCreated', $this->never() );
2525 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2526 $this->unhook( 'LocalUserCreated' );
2527 $this->assertEquals( \Status
::newFatal( 'test2' ), $ret );
2528 $this->assertEquals( 0, $user->getId() );
2529 $this->assertNotEquals( $username, $user->getName() );
2530 $this->assertEquals( 0, $session->getUser()->getId() );
2531 $this->assertSame( [
2532 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2533 ], $logger->getBuffer() );
2534 $logger->clearBuffer();
2538 $user = \User
::newFromName( $username . '@' );
2539 $this->hook( 'LocalUserCreated', $this->never() );
2540 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2541 $this->unhook( 'LocalUserCreated' );
2542 $this->assertEquals( \Status
::newFatal( 'noname' ), $ret );
2543 $this->assertEquals( 0, $user->getId() );
2544 $this->assertNotEquals( $username . '@', $user->getId() );
2545 $this->assertEquals( 0, $session->getUser()->getId() );
2546 $this->assertSame( [
2547 [ LogLevel
::DEBUG
, 'name "{username}" is not creatable' ],
2548 ], $logger->getBuffer() );
2549 $logger->clearBuffer();
2550 $this->assertSame( 'noname', $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2552 // IP unable to create accounts
2553 $wgGroupPermissions['*']['createaccount'] = false;
2554 $wgGroupPermissions['*']['autocreateaccount'] = false;
2556 $user = \User
::newFromName( $username );
2557 $this->hook( 'LocalUserCreated', $this->never() );
2558 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2559 $this->unhook( 'LocalUserCreated' );
2560 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-noperm' ), $ret );
2561 $this->assertEquals( 0, $user->getId() );
2562 $this->assertNotEquals( $username, $user->getName() );
2563 $this->assertEquals( 0, $session->getUser()->getId() );
2564 $this->assertSame( [
2565 [ LogLevel
::DEBUG
, 'IP lacks the ability to create or autocreate accounts' ],
2566 ], $logger->getBuffer() );
2567 $logger->clearBuffer();
2569 'authmanager-autocreate-noperm', $session->get( 'AuthManager::AutoCreateBlacklist' )
2572 // Test that both permutations of permissions are allowed
2573 // (this hits the two "ok" entries in $mocks['pre'])
2574 $wgGroupPermissions['*']['createaccount'] = false;
2575 $wgGroupPermissions['*']['autocreateaccount'] = true;
2577 $user = \User
::newFromName( $username );
2578 $this->hook( 'LocalUserCreated', $this->never() );
2579 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2580 $this->unhook( 'LocalUserCreated' );
2581 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2583 $wgGroupPermissions['*']['createaccount'] = true;
2584 $wgGroupPermissions['*']['autocreateaccount'] = false;
2586 $user = \User
::newFromName( $username );
2587 $this->hook( 'LocalUserCreated', $this->never() );
2588 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2589 $this->unhook( 'LocalUserCreated' );
2590 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2591 $logger->clearBuffer();
2595 $user = \User
::newFromName( $username );
2596 $this->hook( 'LocalUserCreated', $this->never() );
2597 $cache = \ObjectCache
::getLocalClusterInstance();
2598 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2599 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2601 $this->unhook( 'LocalUserCreated' );
2602 $this->assertEquals( \Status
::newFatal( 'usernameinprogress' ), $ret );
2603 $this->assertEquals( 0, $user->getId() );
2604 $this->assertNotEquals( $username, $user->getName() );
2605 $this->assertEquals( 0, $session->getUser()->getId() );
2606 $this->assertSame( [
2607 [ LogLevel
::DEBUG
, 'Could not acquire account creation lock' ],
2608 ], $logger->getBuffer() );
2609 $logger->clearBuffer();
2611 // Test pre-authentication provider fail
2613 $user = \User
::newFromName( $username );
2614 $this->hook( 'LocalUserCreated', $this->never() );
2615 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2616 $this->unhook( 'LocalUserCreated' );
2617 $this->assertEquals( \Status
::newFatal( 'fail-in-pre' ), $ret );
2618 $this->assertEquals( 0, $user->getId() );
2619 $this->assertNotEquals( $username, $user->getName() );
2620 $this->assertEquals( 0, $session->getUser()->getId() );
2621 $this->assertSame( [
2622 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2623 ], $logger->getBuffer() );
2624 $logger->clearBuffer();
2625 $this->assertEquals(
2626 StatusValue
::newFatal( 'fail-in-pre' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2630 $user = \User
::newFromName( $username );
2631 $this->hook( 'LocalUserCreated', $this->never() );
2632 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2633 $this->unhook( 'LocalUserCreated' );
2634 $this->assertEquals( \Status
::newFatal( 'fail-in-primary' ), $ret );
2635 $this->assertEquals( 0, $user->getId() );
2636 $this->assertNotEquals( $username, $user->getName() );
2637 $this->assertEquals( 0, $session->getUser()->getId() );
2638 $this->assertSame( [
2639 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2640 ], $logger->getBuffer() );
2641 $logger->clearBuffer();
2642 $this->assertEquals(
2643 StatusValue
::newFatal( 'fail-in-primary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2647 $user = \User
::newFromName( $username );
2648 $this->hook( 'LocalUserCreated', $this->never() );
2649 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2650 $this->unhook( 'LocalUserCreated' );
2651 $this->assertEquals( \Status
::newFatal( 'fail-in-secondary' ), $ret );
2652 $this->assertEquals( 0, $user->getId() );
2653 $this->assertNotEquals( $username, $user->getName() );
2654 $this->assertEquals( 0, $session->getUser()->getId() );
2655 $this->assertSame( [
2656 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2657 ], $logger->getBuffer() );
2658 $logger->clearBuffer();
2659 $this->assertEquals(
2660 StatusValue
::newFatal( 'fail-in-secondary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2664 $cache = \ObjectCache
::getLocalClusterInstance();
2665 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2666 $cache->set( $backoffKey, true );
2668 $user = \User
::newFromName( $username );
2669 $this->hook( 'LocalUserCreated', $this->never() );
2670 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2671 $this->unhook( 'LocalUserCreated' );
2672 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-exception' ), $ret );
2673 $this->assertEquals( 0, $user->getId() );
2674 $this->assertNotEquals( $username, $user->getName() );
2675 $this->assertEquals( 0, $session->getUser()->getId() );
2676 $this->assertSame( [
2677 [ LogLevel
::DEBUG
, '{username} denied by prior creation attempt failures' ],
2678 ], $logger->getBuffer() );
2679 $logger->clearBuffer();
2680 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2681 $cache->delete( $backoffKey );
2683 // Test addToDatabase fails
2685 $user = $this->getMockBuilder( \User
::class )
2686 ->setMethods( [ 'addToDatabase' ] )->getMock();
2687 $user->expects( $this->once() )->method( 'addToDatabase' )
2688 ->will( $this->returnValue( \Status
::newFatal( 'because' ) ) );
2689 $user->setName( $username );
2690 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2691 $this->assertEquals( \Status
::newFatal( 'because' ), $ret );
2692 $this->assertEquals( 0, $user->getId() );
2693 $this->assertNotEquals( $username, $user->getName() );
2694 $this->assertEquals( 0, $session->getUser()->getId() );
2695 $this->assertSame( [
2696 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2697 [ LogLevel
::ERROR
, '{username} failed with message {msg}' ],
2698 ], $logger->getBuffer() );
2699 $logger->clearBuffer();
2700 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2702 // Test addToDatabase throws an exception
2703 $cache = \ObjectCache
::getLocalClusterInstance();
2704 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2705 $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
2707 $user = $this->getMockBuilder( \User
::class )
2708 ->setMethods( [ 'addToDatabase' ] )->getMock();
2709 $user->expects( $this->once() )->method( 'addToDatabase' )
2710 ->will( $this->throwException( new \
Exception( 'Excepted' ) ) );
2711 $user->setName( $username );
2713 $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2714 $this->fail( 'Expected exception not thrown' );
2715 } catch ( \Exception
$ex ) {
2716 $this->assertSame( 'Excepted', $ex->getMessage() );
2718 $this->assertEquals( 0, $user->getId() );
2719 $this->assertEquals( 0, $session->getUser()->getId() );
2720 $this->assertSame( [
2721 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2722 [ LogLevel
::ERROR
, '{username} failed with exception {exception}' ],
2723 ], $logger->getBuffer() );
2724 $logger->clearBuffer();
2725 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2726 $this->assertNotEquals( false, $cache->get( $backoffKey ) );
2727 $cache->delete( $backoffKey );
2729 // Test addToDatabase fails because the user already exists.
2731 $user = $this->getMockBuilder( \User
::class )
2732 ->setMethods( [ 'addToDatabase' ] )->getMock();
2733 $user->expects( $this->once() )->method( 'addToDatabase' )
2734 ->will( $this->returnCallback( function () use ( $username, &$user ) {
2735 $oldUser = \User
::newFromName( $username );
2736 $status = $oldUser->addToDatabase();
2737 $this->assertTrue( $status->isOK(), 'sanity check' );
2738 $user->setId( $oldUser->getId() );
2739 return \Status
::newFatal( 'userexists' );
2741 $user->setName( $username );
2742 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2743 $expect = \Status
::newGood();
2744 $expect->warning( 'userexists' );
2745 $this->assertEquals( $expect, $ret );
2746 $this->assertNotEquals( 0, $user->getId() );
2747 $this->assertEquals( $username, $user->getName() );
2748 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2749 $this->assertSame( [
2750 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2751 [ LogLevel
::INFO
, '{username} already exists locally (race)' ],
2752 ], $logger->getBuffer() );
2753 $logger->clearBuffer();
2754 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2758 $username = self
::usernameForCreation();
2759 $user = \User
::newFromName( $username );
2760 $this->hook( 'AuthPluginAutoCreate', $this->once() )
2761 ->with( $callback );
2762 $this->hideDeprecated( 'AuthPluginAutoCreate hook (used in ' .
2763 get_class( $wgHooks['AuthPluginAutoCreate'][0] ) . '::onAuthPluginAutoCreate)' );
2764 $this->hook( 'LocalUserCreated', $this->once() )
2765 ->with( $callback, $this->equalTo( true ) );
2766 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2767 $this->unhook( 'LocalUserCreated' );
2768 $this->unhook( 'AuthPluginAutoCreate' );
2769 $this->assertEquals( \Status
::newGood(), $ret );
2770 $this->assertNotEquals( 0, $user->getId() );
2771 $this->assertEquals( $username, $user->getName() );
2772 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2773 $this->assertSame( [
2774 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2775 ], $logger->getBuffer() );
2776 $logger->clearBuffer();
2778 $dbw = wfGetDB( DB_MASTER
);
2779 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2781 $username = self
::usernameForCreation();
2782 $user = \User
::newFromName( $username );
2783 $this->hook( 'LocalUserCreated', $this->once() )
2784 ->with( $callback, $this->equalTo( true ) );
2785 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2786 $this->unhook( 'LocalUserCreated' );
2787 $this->assertEquals( \Status
::newGood(), $ret );
2788 $this->assertNotEquals( 0, $user->getId() );
2789 $this->assertEquals( $username, $user->getName() );
2790 $this->assertEquals( 0, $session->getUser()->getId() );
2791 $this->assertSame( [
2792 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2793 ], $logger->getBuffer() );
2794 $logger->clearBuffer();
2797 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2800 $this->config
->set( 'NewUserLog', true );
2802 $username = self
::usernameForCreation();
2803 $user = \User
::newFromName( $username );
2804 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2805 $this->assertEquals( \Status
::newGood(), $ret );
2806 $logger->clearBuffer();
2808 $data = \DatabaseLogEntry
::getSelectQueryData();
2809 $rows = iterator_to_array( $dbw->select(
2813 'log_id > ' . (int)$maxLogId,
2814 'log_type' => 'newusers'
2820 $this->assertCount( 1, $rows );
2821 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2823 $this->assertSame( 'autocreate', $entry->getSubtype() );
2824 $this->assertSame( $user->getId(), $entry->getPerformer()->getId() );
2825 $this->assertSame( $user->getName(), $entry->getPerformer()->getName() );
2826 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2827 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2829 $workaroundPHPUnitBug = true;
2833 * @dataProvider provideGetAuthenticationRequests
2834 * @param string $action
2835 * @param array $expect
2836 * @param array $state
2838 public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
2839 $makeReq = function ( $key ) use ( $action ) {
2840 $req = $this->createMock( AuthenticationRequest
::class );
2841 $req->expects( $this->any() )->method( 'getUniqueId' )
2842 ->will( $this->returnValue( $key ) );
2843 $req->action
= $action === AuthManager
::ACTION_UNLINK ? AuthManager
::ACTION_REMOVE
: $action;
2847 $cmpReqs = function ( $a, $b ) {
2848 $ret = strcmp( get_class( $a ), get_class( $b ) );
2850 $ret = strcmp( $a->key
, $b->key
);
2855 $good = StatusValue
::newGood();
2858 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2859 $class = ucfirst( $key ) . 'AuthenticationProvider';
2860 $mocks[$key] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" )
2862 'getUniqueId', 'getAuthenticationRequests', 'providerAllowsAuthenticationDataChange',
2864 ->getMockForAbstractClass();
2865 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2866 ->will( $this->returnValue( $key ) );
2867 $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2868 ->will( $this->returnCallback( function ( $action ) use ( $key, $makeReq ) {
2869 return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
2871 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
2872 ->will( $this->returnValue( $good ) );
2877 PrimaryAuthenticationProvider
::TYPE_NONE
,
2878 PrimaryAuthenticationProvider
::TYPE_CREATE
,
2879 PrimaryAuthenticationProvider
::TYPE_LINK
2881 $class = 'PrimaryAuthenticationProvider';
2882 $mocks["primary-$type"] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" )
2884 'getUniqueId', 'accountCreationType', 'getAuthenticationRequests',
2885 'providerAllowsAuthenticationDataChange',
2887 ->getMockForAbstractClass();
2888 $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
2889 ->will( $this->returnValue( "primary-$type" ) );
2890 $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
2891 ->will( $this->returnValue( $type ) );
2892 $mocks["primary-$type"]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2893 ->will( $this->returnCallback( function ( $action ) use ( $type, $makeReq ) {
2894 return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
2896 $mocks["primary-$type"]->expects( $this->any() )
2897 ->method( 'providerAllowsAuthenticationDataChange' )
2898 ->will( $this->returnValue( $good ) );
2899 $this->primaryauthMocks
[] = $mocks["primary-$type"];
2902 $mocks['primary2'] = $this->getMockBuilder( PrimaryAuthenticationProvider
::class )
2904 'getUniqueId', 'accountCreationType', 'getAuthenticationRequests',
2905 'providerAllowsAuthenticationDataChange',
2907 ->getMockForAbstractClass();
2908 $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
2909 ->will( $this->returnValue( 'primary2' ) );
2910 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
2911 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
2912 $mocks['primary2']->expects( $this->any() )->method( 'getAuthenticationRequests' )
2913 ->will( $this->returnValue( [] ) );
2914 $mocks['primary2']->expects( $this->any() )
2915 ->method( 'providerAllowsAuthenticationDataChange' )
2916 ->will( $this->returnCallback( function ( $req ) use ( $good ) {
2917 return $req->key
=== 'generic' ? StatusValue
::newFatal( 'no' ) : $good;
2919 $this->primaryauthMocks
[] = $mocks['primary2'];
2921 $this->preauthMocks
= [ $mocks['pre'] ];
2922 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2923 $this->initializeManager( true );
2926 if ( isset( $state['continueRequests'] ) ) {
2927 $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
2929 if ( $action === AuthManager
::ACTION_LOGIN_CONTINUE
) {
2930 $this->request
->getSession()->setSecret( 'AuthManager::authnState', $state );
2931 } elseif ( $action === AuthManager
::ACTION_CREATE_CONTINUE
) {
2932 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $state );
2933 } elseif ( $action === AuthManager
::ACTION_LINK_CONTINUE
) {
2934 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', $state );
2938 $expectReqs = array_map( $makeReq, $expect );
2939 if ( $action === AuthManager
::ACTION_LOGIN
) {
2940 $req = new RememberMeAuthenticationRequest
;
2941 $req->action
= $action;
2942 $req->required
= AuthenticationRequest
::REQUIRED
;
2943 $expectReqs[] = $req;
2944 } elseif ( $action === AuthManager
::ACTION_CREATE
) {
2945 $req = new UsernameAuthenticationRequest
;
2946 $req->action
= $action;
2947 $expectReqs[] = $req;
2948 $req = new UserDataAuthenticationRequest
;
2949 $req->action
= $action;
2950 $req->required
= AuthenticationRequest
::REQUIRED
;
2951 $expectReqs[] = $req;
2953 usort( $expectReqs, $cmpReqs );
2955 $actual = $this->manager
->getAuthenticationRequests( $action );
2956 foreach ( $actual as $req ) {
2957 // Don't test this here.
2958 $req->required
= AuthenticationRequest
::REQUIRED
;
2960 usort( $actual, $cmpReqs );
2962 $this->assertEquals( $expectReqs, $actual );
2964 // Test CreationReasonAuthenticationRequest gets returned
2965 if ( $action === AuthManager
::ACTION_CREATE
) {
2966 $req = new CreationReasonAuthenticationRequest
;
2967 $req->action
= $action;
2968 $req->required
= AuthenticationRequest
::REQUIRED
;
2969 $expectReqs[] = $req;
2970 usort( $expectReqs, $cmpReqs );
2972 $actual = $this->manager
->getAuthenticationRequests( $action, \User
::newFromName( 'UTSysop' ) );
2973 foreach ( $actual as $req ) {
2974 // Don't test this here.
2975 $req->required
= AuthenticationRequest
::REQUIRED
;
2977 usort( $actual, $cmpReqs );
2979 $this->assertEquals( $expectReqs, $actual );
2983 public static function provideGetAuthenticationRequests() {
2986 AuthManager
::ACTION_LOGIN
,
2987 [ 'pre-login', 'primary-none-login', 'primary-create-login',
2988 'primary-link-login', 'secondary-login', 'generic' ],
2991 AuthManager
::ACTION_CREATE
,
2992 [ 'pre-create', 'primary-none-create', 'primary-create-create',
2993 'primary-link-create', 'secondary-create', 'generic' ],
2996 AuthManager
::ACTION_LINK
,
2997 [ 'primary-link-link', 'generic' ],
3000 AuthManager
::ACTION_CHANGE
,
3001 [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
3002 'secondary-change' ],
3005 AuthManager
::ACTION_REMOVE
,
3006 [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
3007 'secondary-remove' ],
3010 AuthManager
::ACTION_UNLINK
,
3011 [ 'primary-link-remove' ],
3014 AuthManager
::ACTION_LOGIN_CONTINUE
,
3018 AuthManager
::ACTION_LOGIN_CONTINUE
,
3019 $reqs = [ 'continue-login', 'foo', 'bar' ],
3021 'continueRequests' => $reqs,
3025 AuthManager
::ACTION_CREATE_CONTINUE
,
3029 AuthManager
::ACTION_CREATE_CONTINUE
,
3030 $reqs = [ 'continue-create', 'foo', 'bar' ],
3032 'continueRequests' => $reqs,
3036 AuthManager
::ACTION_LINK_CONTINUE
,
3040 AuthManager
::ACTION_LINK_CONTINUE
,
3041 $reqs = [ 'continue-link', 'foo', 'bar' ],
3043 'continueRequests' => $reqs,
3049 public function testGetAuthenticationRequestsRequired() {
3050 $makeReq = function ( $key, $required ) {
3051 $req = $this->createMock( AuthenticationRequest
::class );
3052 $req->expects( $this->any() )->method( 'getUniqueId' )
3053 ->will( $this->returnValue( $key ) );
3054 $req->action
= AuthManager
::ACTION_LOGIN
;
3056 $req->required
= $required;
3059 $cmpReqs = function ( $a, $b ) {
3060 $ret = strcmp( get_class( $a ), get_class( $b ) );
3062 $ret = strcmp( $a->key
, $b->key
);
3067 $good = StatusValue
::newGood();
3069 $primary1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3070 $primary1->expects( $this->any() )->method( 'getUniqueId' )
3071 ->will( $this->returnValue( 'primary1' ) );
3072 $primary1->expects( $this->any() )->method( 'accountCreationType' )
3073 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3074 $primary1->expects( $this->any() )->method( 'getAuthenticationRequests' )
3075 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3077 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3078 $makeReq( "required", AuthenticationRequest
::REQUIRED
),
3079 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3080 $makeReq( "foo", AuthenticationRequest
::REQUIRED
),
3081 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3082 $makeReq( "baz", AuthenticationRequest
::OPTIONAL
),
3086 $primary2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3087 $primary2->expects( $this->any() )->method( 'getUniqueId' )
3088 ->will( $this->returnValue( 'primary2' ) );
3089 $primary2->expects( $this->any() )->method( 'accountCreationType' )
3090 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3091 $primary2->expects( $this->any() )->method( 'getAuthenticationRequests' )
3092 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3094 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3095 $makeReq( "required2", AuthenticationRequest
::REQUIRED
),
3096 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3100 $secondary = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3101 $secondary->expects( $this->any() )->method( 'getUniqueId' )
3102 ->will( $this->returnValue( 'secondary' ) );
3103 $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
3104 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3106 $makeReq( "foo", AuthenticationRequest
::OPTIONAL
),
3107 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3108 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3112 $rememberReq = new RememberMeAuthenticationRequest
;
3113 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
3115 $this->primaryauthMocks
= [ $primary1, $primary2 ];
3116 $this->secondaryauthMocks
= [ $secondary ];
3117 $this->initializeManager( true );
3119 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3122 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3123 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3124 $makeReq( "required2", AuthenticationRequest
::PRIMARY_REQUIRED
),
3125 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3126 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3127 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3128 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3129 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3131 usort( $actual, $cmpReqs );
3132 usort( $expected, $cmpReqs );
3133 $this->assertEquals( $expected, $actual );
3135 $this->primaryauthMocks
= [ $primary1 ];
3136 $this->secondaryauthMocks
= [ $secondary ];
3137 $this->initializeManager( true );
3139 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3142 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3143 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3144 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3145 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3146 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3147 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3149 usort( $actual, $cmpReqs );
3150 usort( $expected, $cmpReqs );
3151 $this->assertEquals( $expected, $actual );
3154 public function testAllowsPropertyChange() {
3156 foreach ( [ 'primary', 'secondary' ] as $key ) {
3157 $class = ucfirst( $key ) . 'AuthenticationProvider';
3158 $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
3159 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3160 ->will( $this->returnValue( $key ) );
3161 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
3162 ->will( $this->returnCallback( function ( $prop ) use ( $key ) {
3163 return $prop !== $key;
3167 $this->primaryauthMocks
= [ $mocks['primary'] ];
3168 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
3169 $this->initializeManager( true );
3171 $this->assertTrue( $this->manager
->allowsPropertyChange( 'foo' ) );
3172 $this->assertFalse( $this->manager
->allowsPropertyChange( 'primary' ) );
3173 $this->assertFalse( $this->manager
->allowsPropertyChange( 'secondary' ) );
3176 public function testAutoCreateOnLogin() {
3177 $username = self
::usernameForCreation();
3179 $req = $this->createMock( AuthenticationRequest
::class );
3181 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3182 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3183 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3184 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3185 $mock->expects( $this->any() )->method( 'accountCreationType' )
3186 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3187 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3188 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3189 ->will( $this->returnValue( StatusValue
::newGood() ) );
3191 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3192 $mock2->expects( $this->any() )->method( 'getUniqueId' )
3193 ->will( $this->returnValue( 'secondary' ) );
3194 $mock2->expects( $this->any() )->method( 'beginSecondaryAuthentication' )->will(
3196 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) )
3199 $mock2->expects( $this->any() )->method( 'continueSecondaryAuthentication' )
3200 ->will( $this->returnValue( AuthenticationResponse
::newAbstain() ) );
3201 $mock2->expects( $this->any() )->method( 'testUserForCreation' )
3202 ->will( $this->returnValue( StatusValue
::newGood() ) );
3204 $this->primaryauthMocks
= [ $mock ];
3205 $this->secondaryauthMocks
= [ $mock2 ];
3206 $this->initializeManager( true );
3207 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3208 $session = $this->request
->getSession();
3211 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3214 $callback = $this->callback( function ( $user ) use ( $username ) {
3215 return $user->getName() === $username;
3218 $this->hook( 'UserLoggedIn', $this->never() );
3219 $this->hook( 'LocalUserCreated', $this->once() )->with( $callback, $this->equalTo( true ) );
3220 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3221 $this->unhook( 'LocalUserCreated' );
3222 $this->unhook( 'UserLoggedIn' );
3223 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
3225 $id = (int)\User
::newFromName( $username )->getId();
3226 $this->assertNotSame( 0, \User
::newFromName( $username )->getId() );
3227 $this->assertSame( 0, $session->getUser()->getId() );
3229 $this->hook( 'UserLoggedIn', $this->once() )->with( $callback );
3230 $this->hook( 'LocalUserCreated', $this->never() );
3231 $ret = $this->manager
->continueAuthentication( [] );
3232 $this->unhook( 'LocalUserCreated' );
3233 $this->unhook( 'UserLoggedIn' );
3234 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
3235 $this->assertSame( $username, $ret->username
);
3236 $this->assertSame( $id, $session->getUser()->getId() );
3239 public function testAutoCreateFailOnLogin() {
3240 $username = self
::usernameForCreation();
3242 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3243 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3244 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3245 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3246 $mock->expects( $this->any() )->method( 'accountCreationType' )
3247 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3248 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3249 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3250 ->will( $this->returnValue( StatusValue
::newFatal( 'fail-from-primary' ) ) );
3252 $this->primaryauthMocks
= [ $mock ];
3253 $this->initializeManager( true );
3254 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3255 $session = $this->request
->getSession();
3258 $this->assertSame( 0, $session->getUser()->getId(),
3260 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3263 $this->hook( 'UserLoggedIn', $this->never() );
3264 $this->hook( 'LocalUserCreated', $this->never() );
3265 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3266 $this->unhook( 'LocalUserCreated' );
3267 $this->unhook( 'UserLoggedIn' );
3268 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3269 $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message
->getKey() );
3271 $this->assertSame( 0, \User
::newFromName( $username )->getId() );
3272 $this->assertSame( 0, $session->getUser()->getId() );
3275 public function testAuthenticationSessionData() {
3276 $this->initializeManager( true );
3278 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3279 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3280 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3281 $this->assertSame( 'foo!', $this->manager
->getAuthenticationSessionData( 'foo' ) );
3282 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3283 $this->manager
->removeAuthenticationSessionData( 'foo' );
3284 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3285 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3286 $this->manager
->removeAuthenticationSessionData( 'bar' );
3287 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3289 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3290 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3291 $this->manager
->removeAuthenticationSessionData( null );
3292 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3293 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3296 public function testCanLinkAccounts() {
3298 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
3299 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
3300 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
3303 foreach ( $types as $type => $can ) {
3304 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3305 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
3306 $mock->expects( $this->any() )->method( 'accountCreationType' )
3307 ->will( $this->returnValue( $type ) );
3308 $this->primaryauthMocks
= [ $mock ];
3309 $this->initializeManager( true );
3310 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
3314 public function testBeginAccountLink() {
3315 $user = \User
::newFromName( 'UTSysop' );
3316 $this->initializeManager();
3318 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', 'test' );
3320 $this->manager
->beginAccountLink( $user, [], 'http://localhost/' );
3321 $this->fail( 'Expected exception not thrown' );
3322 } catch ( \LogicException
$ex ) {
3323 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3325 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3327 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3328 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3329 $mock->expects( $this->any() )->method( 'accountCreationType' )
3330 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3331 $this->primaryauthMocks
= [ $mock ];
3332 $this->initializeManager( true );
3334 $ret = $this->manager
->beginAccountLink( new \User
, [], 'http://localhost/' );
3335 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3336 $this->assertSame( 'noname', $ret->message
->getKey() );
3338 $ret = $this->manager
->beginAccountLink(
3339 \User
::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
3341 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3342 $this->assertSame( 'authmanager-userdoesnotexist', $ret->message
->getKey() );
3345 public function testContinueAccountLink() {
3346 $user = \User
::newFromName( 'UTSysop' );
3347 $this->initializeManager();
3350 'userid' => $user->getId(),
3351 'username' => $user->getName(),
3356 $this->manager
->continueAccountLink( [] );
3357 $this->fail( 'Expected exception not thrown' );
3358 } catch ( \LogicException
$ex ) {
3359 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3362 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3363 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3364 $mock->expects( $this->any() )->method( 'accountCreationType' )
3365 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3366 $mock->expects( $this->any() )->method( 'beginPrimaryAccountLink' )->will(
3367 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
3369 $this->primaryauthMocks
= [ $mock ];
3370 $this->initializeManager( true );
3372 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', null );
3373 $ret = $this->manager
->continueAccountLink( [] );
3374 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3375 $this->assertSame( 'authmanager-link-not-in-progress', $ret->message
->getKey() );
3377 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3378 [ 'username' => $user->getName() . '<>' ] +
$session );
3379 $ret = $this->manager
->continueAccountLink( [] );
3380 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3381 $this->assertSame( 'noname', $ret->message
->getKey() );
3382 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3384 $id = $user->getId();
3385 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3386 [ 'userid' => $id +
1 ] +
$session );
3388 $ret = $this->manager
->continueAccountLink( [] );
3389 $this->fail( 'Expected exception not thrown' );
3390 } catch ( \UnexpectedValueException
$ex ) {
3391 $this->assertEquals(
3392 "User \"{$user->getName()}\" is valid, but ID $id != " . ( $id +
1 ) . '!',
3396 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3400 * @dataProvider provideAccountLink
3401 * @param StatusValue $preTest
3402 * @param array $primaryResponses
3403 * @param array $managerResponses
3405 public function testAccountLink(
3406 StatusValue
$preTest, array $primaryResponses, array $managerResponses
3408 $user = \User
::newFromName( 'UTSysop' );
3410 $this->initializeManager();
3412 // Set up lots of mocks...
3413 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3414 $req->primary
= $primaryResponses;
3417 foreach ( [ 'pre', 'primary' ] as $key ) {
3418 $class = ucfirst( $key ) . 'AuthenticationProvider';
3419 $mocks[$key] = $this->getMockForAbstractClass(
3420 "MediaWiki\\Auth\\$class", [], "Mock$class"
3422 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3423 ->will( $this->returnValue( $key ) );
3425 for ( $i = 2; $i <= 3; $i++
) {
3426 $mocks[$key . $i] = $this->getMockForAbstractClass(
3427 "MediaWiki\\Auth\\$class", [], "Mock$class"
3429 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
3430 ->will( $this->returnValue( $key . $i ) );
3434 $mocks['pre']->expects( $this->any() )->method( 'testForAccountLink' )
3435 ->will( $this->returnCallback(
3437 use ( $user, $preTest )
3439 $this->assertSame( $user->getId(), $u->getId() );
3440 $this->assertSame( $user->getName(), $u->getName() );
3445 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
3446 ->will( $this->returnValue( StatusValue
::newGood() ) );
3448 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
3449 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3450 $ct = count( $req->primary
);
3451 $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req ) {
3452 $this->assertSame( $user->getId(), $u->getId() );
3453 $this->assertSame( $user->getName(), $u->getName() );
3455 foreach ( $reqs as $r ) {
3456 $this->assertSame( $user->getName(), $r->username
);
3457 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
3459 $this->assertTrue( $foundReq, '$reqs contains $req' );
3460 return array_shift( $req->primary
);
3462 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
3463 ->method( 'beginPrimaryAccountLink' )
3464 ->will( $callback );
3465 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
3466 ->method( 'continuePrimaryAccountLink' )
3467 ->will( $callback );
3469 $abstain = AuthenticationResponse
::newAbstain();
3470 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
3471 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3472 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
3473 ->will( $this->returnValue( $abstain ) );
3474 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3475 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
3476 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3477 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
3478 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3480 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
3481 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
3482 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
3483 return $level === LogLevel
::DEBUG ?
null : $message;
3485 $this->initializeManager( true );
3487 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
3488 $this->equalTo( AuthenticationResponse
::PASS
),
3489 $this->equalTo( AuthenticationResponse
::FAIL
)
3491 $providers = array_merge( $this->preauthMocks
, $this->primaryauthMocks
);
3492 foreach ( $providers as $p ) {
3493 $p->postCalled
= false;
3494 $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
3495 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
3496 $this->assertInstanceOf( \User
::class, $user );
3497 $this->assertSame( 'UTSysop', $user->getName() );
3498 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
3499 $this->assertThat( $response->status
, $constraint );
3500 $p->postCalled
= $response->status
;
3507 foreach ( $managerResponses as $i => $response ) {
3508 if ( $response instanceof AuthenticationResponse
&&
3509 $response->status
=== AuthenticationResponse
::PASS
3511 $expectLog[] = [ LogLevel
::INFO
, 'Account linked to {user} by primary' ];
3517 $ret = $this->manager
->beginAccountLink( $user, [ $req ], 'http://localhost/' );
3519 $ret = $this->manager
->continueAccountLink( [ $req ] );
3521 if ( $response instanceof \Exception
) {
3522 $this->fail( 'Expected exception not thrown', "Response $i" );
3524 } catch ( \Exception
$ex ) {
3525 if ( !$response instanceof \Exception
) {
3528 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
3529 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3530 "Response $i, exception, session state" );
3534 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
3536 $ret->message
= $this->message( $ret->message
);
3537 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
3538 if ( $response->status
=== AuthenticationResponse
::PASS ||
3539 $response->status
=== AuthenticationResponse
::FAIL
3541 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3542 "Response $i, session state" );
3543 foreach ( $providers as $p ) {
3544 $this->assertSame( $response->status
, $p->postCalled
,
3545 "Response $i, post-auth callback called" );
3548 $this->assertNotNull(
3549 $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3550 "Response $i, session state"
3552 foreach ( $ret->neededRequests
as $neededReq ) {
3553 $this->assertEquals( AuthManager
::ACTION_LINK
, $neededReq->action
,
3554 "Response $i, neededRequest action" );
3556 $this->assertEquals(
3557 $ret->neededRequests
,
3558 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LINK_CONTINUE
),
3559 "Response $i, continuation check"
3561 foreach ( $providers as $p ) {
3562 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
3569 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
3572 public function provideAccountLink() {
3573 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3574 $good = StatusValue
::newGood();
3577 'Pre-link test fail in pre' => [
3578 StatusValue
::newFatal( 'fail-from-pre' ),
3581 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
3584 'Failure in primary' => [
3587 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
3591 'All primary abstain' => [
3594 AuthenticationResponse
::newAbstain(),
3597 AuthenticationResponse
::newFail( $this->message( 'authmanager-link-no-primary' ) )
3600 'Primary UI, then redirect, then fail' => [
3603 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3604 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
3605 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
3609 'Primary redirect, then abstain' => [
3612 $tmp = AuthenticationResponse
::newRedirect(
3613 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
3615 AuthenticationResponse
::newAbstain(),
3619 new \
DomainException(
3620 'MockPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
3624 'Primary UI, then pass' => [
3627 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3628 AuthenticationResponse
::newPass(),
3632 AuthenticationResponse
::newPass( '' ),
3638 AuthenticationResponse
::newPass( '' ),
3641 AuthenticationResponse
::newPass( '' ),