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;
38 * Sets a mock on a hook
40 * @param object $expect From $this->once(), $this->never(), etc.
41 * @return object $mock->expects( $expect )->method( ... ).
43 protected function hook( $hook, $expect ) {
44 $mock = $this->getMockBuilder( __CLASS__
)
45 ->setMethods( [ "on$hook" ] )
47 $this->setTemporaryHook( $hook, $mock );
48 return $mock->expects( $expect )->method( "on$hook" );
55 protected function unhook( $hook ) {
61 * Ensure a value is a clean Message object
62 * @param string|Message $key
63 * @param array $params
66 protected function message( $key, $params = [] ) {
67 if ( $key === null ) {
70 if ( $key instanceof \MessageSpecifier
) {
71 $params = $key->getParams();
72 $key = $key->getKey();
74 return new \
Message( $key, $params, \Language
::factory( 'en' ) );
78 * Test two AuthenticationResponses for equality. We don't want to use regular assertEquals
79 * because that recursively compares members, which leads to false negatives if e.g. Language
82 * @param AuthenticationResponse $response1
83 * @param AuthenticationResponse $response2
87 private function assertResponseEquals(
88 AuthenticationResponse
$expected, AuthenticationResponse
$actual, $msg = ''
90 foreach ( ( new \
ReflectionClass( $expected ) )->getProperties() as $prop ) {
91 $name = $prop->getName();
92 $usedMsg = ltrim( "$msg ($name)" );
93 if ( $name === 'message' && $expected->message
) {
94 $this->assertSame( $expected->message
->serialize(), $actual->message
->serialize(),
97 $this->assertEquals( $expected->$name, $actual->$name, $usedMsg );
103 * Initialize the AuthManagerConfig variable in $this->config
105 * Uses data from the various 'mocks' fields.
107 protected function initializeConfig() {
117 foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
118 $key = $type . 'Mocks';
119 foreach ( $this->$key as $mock ) {
120 $config[$type][$mock->getUniqueId()] = [ 'factory' => function () use ( $mock ) {
126 $this->config
->set( 'AuthManagerConfig', $config );
127 $this->config
->set( 'LanguageCode', 'en' );
128 $this->config
->set( 'NewUserLog', false );
132 * Initialize $this->manager
133 * @param bool $regen Force a call to $this->initializeConfig()
135 protected function initializeManager( $regen = false ) {
136 if ( $regen ||
!$this->config
) {
137 $this->config
= new \
HashConfig();
139 if ( $regen ||
!$this->request
) {
140 $this->request
= new \
FauxRequest();
142 if ( !$this->logger
) {
143 $this->logger
= new \
TestLogger();
146 if ( $regen ||
!$this->config
->has( 'AuthManagerConfig' ) ) {
147 $this->initializeConfig();
149 $this->manager
= new AuthManager( $this->request
, $this->config
);
150 $this->manager
->setLogger( $this->logger
);
151 $this->managerPriv
= TestingAccessWrapper
::newFromObject( $this->manager
);
155 * Setup SessionManager with a mock session provider
156 * @param bool|null $canChangeUser If non-null, canChangeUser will be mocked to return this
157 * @param array $methods Additional methods to mock
158 * @return array (MediaWiki\Session\SessionProvider, ScopedCallback)
160 protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
161 if ( !$this->config
) {
162 $this->config
= new \
HashConfig();
163 $this->initializeConfig();
165 $this->config
->set( 'ObjectCacheSessionExpiry', 100 );
167 $methods[] = '__toString';
168 $methods[] = 'describe';
169 if ( $canChangeUser !== null ) {
170 $methods[] = 'canChangeUser';
172 $provider = $this->getMockBuilder( \DummySessionProvider
::class )
173 ->setMethods( $methods )
175 $provider->expects( $this->any() )->method( '__toString' )
176 ->will( $this->returnValue( 'MockSessionProvider' ) );
177 $provider->expects( $this->any() )->method( 'describe' )
178 ->will( $this->returnValue( 'MockSessionProvider sessions' ) );
179 if ( $canChangeUser !== null ) {
180 $provider->expects( $this->any() )->method( 'canChangeUser' )
181 ->will( $this->returnValue( $canChangeUser ) );
183 $this->config
->set( 'SessionProviders', [
184 [ 'factory' => function () use ( $provider ) {
189 $manager = new \MediaWiki\Session\
SessionManager( [
190 'config' => $this->config
,
191 'logger' => new \Psr\Log\
NullLogger(),
192 'store' => new \
HashBagOStuff(),
194 TestingAccessWrapper
::newFromObject( $manager )->getProvider( (string)$provider );
196 $reset = \MediaWiki\Session\TestUtils
::setSessionManagerSingleton( $manager );
198 if ( $this->request
) {
199 $manager->getSessionForRequest( $this->request
);
202 return [ $provider, $reset ];
205 public function testSingleton() {
206 // Temporarily clear out the global singleton, if any, to test creating
208 $rProp = new \
ReflectionProperty( AuthManager
::class, 'instance' );
209 $rProp->setAccessible( true );
210 $old = $rProp->getValue();
211 $cb = new ScopedCallback( [ $rProp, 'setValue' ], [ $old ] );
212 $rProp->setValue( null );
214 $singleton = AuthManager
::singleton();
215 $this->assertInstanceOf( AuthManager
::class, AuthManager
::singleton() );
216 $this->assertSame( $singleton, AuthManager
::singleton() );
217 $this->assertSame( \RequestContext
::getMain()->getRequest(), $singleton->getRequest() );
219 \RequestContext
::getMain()->getConfig(),
220 TestingAccessWrapper
::newFromObject( $singleton )->config
224 public function testCanAuthenticateNow() {
225 $this->initializeManager();
227 list( $provider, $reset ) = $this->getMockSessionProvider( false );
228 $this->assertFalse( $this->manager
->canAuthenticateNow() );
229 ScopedCallback
::consume( $reset );
231 list( $provider, $reset ) = $this->getMockSessionProvider( true );
232 $this->assertTrue( $this->manager
->canAuthenticateNow() );
233 ScopedCallback
::consume( $reset );
236 public function testNormalizeUsername() {
238 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
239 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
240 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
241 $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
243 foreach ( $mocks as $key => $mock ) {
244 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
246 $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
247 ->with( $this->identicalTo( 'XYZ' ) )
248 ->willReturn( 'Foo' );
249 $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
250 ->with( $this->identicalTo( 'XYZ' ) )
251 ->willReturn( 'Foo' );
252 $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
253 ->with( $this->identicalTo( 'XYZ' ) )
254 ->willReturn( null );
255 $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
256 ->with( $this->identicalTo( 'XYZ' ) )
257 ->willReturn( 'Bar!' );
259 $this->primaryauthMocks
= $mocks;
261 $this->initializeManager();
263 $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager
->normalizeUsername( 'XYZ' ) );
267 * @dataProvider provideSecuritySensitiveOperationStatus
268 * @param bool $mutableSession
270 public function testSecuritySensitiveOperationStatus( $mutableSession ) {
271 $this->logger
= new \Psr\Log\
NullLogger();
272 $user = \User
::newFromName( 'UTSysop' );
274 $reauth = $mutableSession ? AuthManager
::SEC_REAUTH
: AuthManager
::SEC_FAIL
;
276 list( $provider, $reset ) = $this->getMockSessionProvider(
277 $mutableSession, [ 'provideSessionInfo' ]
279 $provider->expects( $this->any() )->method( 'provideSessionInfo' )
280 ->will( $this->returnCallback( function () use ( $provider, &$provideUser ) {
281 return new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
282 'provider' => $provider,
283 'id' => \DummySessionProvider
::ID
,
285 'userInfo' => UserInfo
::newFromUser( $provideUser, true )
288 $this->initializeManager();
290 $this->config
->set( 'ReauthenticateTime', [] );
291 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [] );
292 $provideUser = new \User
;
293 $session = $provider->getManager()->getSessionForRequest( $this->request
);
294 $this->assertSame( 0, $session->getUser()->getId(), 'sanity check' );
296 // Anonymous user => reauth
297 $session->set( 'AuthManager:lastAuthId', 0 );
298 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
299 $this->assertSame( $reauth, $this->manager
->securitySensitiveOperationStatus( 'foo' ) );
301 $provideUser = $user;
302 $session = $provider->getManager()->getSessionForRequest( $this->request
);
303 $this->assertSame( $user->getId(), $session->getUser()->getId(), 'sanity check' );
305 // Error for no default (only gets thrown for non-anonymous user)
306 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
307 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
309 $this->manager
->securitySensitiveOperationStatus( 'foo' );
310 $this->fail( 'Expected exception not thrown' );
311 } catch ( \UnexpectedValueException
$ex ) {
314 ?
'$wgReauthenticateTime lacks a default'
315 : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
320 if ( $mutableSession ) {
321 $this->config
->set( 'ReauthenticateTime', [
327 // Mismatched user ID
328 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
329 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
331 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
334 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
337 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
341 $session->set( 'AuthManager:lastAuthId', $user->getId() );
342 $session->set( 'AuthManager:lastAuthTimestamp', null );
344 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
347 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
350 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
353 // Recent enough to pass
354 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
356 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
359 // Not recent enough to pass
360 $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
362 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
364 // But recent enough for the 'test' operation
366 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test' )
369 $this->config
->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [
375 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
379 AuthManager
::SEC_FAIL
, $this->manager
->securitySensitiveOperationStatus( 'test' )
383 // Test hook, all three possible values
385 AuthManager
::SEC_OK
=> AuthManager
::SEC_OK
,
386 AuthManager
::SEC_REAUTH
=> $reauth,
387 AuthManager
::SEC_FAIL
=> AuthManager
::SEC_FAIL
,
388 ] as $hook => $expect ) {
389 $this->hook( 'SecuritySensitiveOperationStatus', $this->exactly( 2 ) )
393 $this->callback( function ( $s ) use ( $session ) {
394 return $s->getId() === $session->getId();
396 $mutableSession ?
$this->equalTo( 500, 1 ) : $this->equalTo( -1 )
398 ->will( $this->returnCallback( function ( &$v ) use ( $hook ) {
402 $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
404 $expect, $this->manager
->securitySensitiveOperationStatus( 'test' ), "hook $hook"
407 $expect, $this->manager
->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
409 $this->unhook( 'SecuritySensitiveOperationStatus' );
412 ScopedCallback
::consume( $reset );
415 public function onSecuritySensitiveOperationStatus( &$status, $operation, $session, $time ) {
418 public static function provideSecuritySensitiveOperationStatus() {
426 * @dataProvider provideUserCanAuthenticate
427 * @param bool $primary1Can
428 * @param bool $primary2Can
429 * @param bool $expect
431 public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
432 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
433 $mock1->expects( $this->any() )->method( 'getUniqueId' )
434 ->will( $this->returnValue( 'primary1' ) );
435 $mock1->expects( $this->any() )->method( 'testUserCanAuthenticate' )
436 ->with( $this->equalTo( 'UTSysop' ) )
437 ->will( $this->returnValue( $primary1Can ) );
438 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
439 $mock2->expects( $this->any() )->method( 'getUniqueId' )
440 ->will( $this->returnValue( 'primary2' ) );
441 $mock2->expects( $this->any() )->method( 'testUserCanAuthenticate' )
442 ->with( $this->equalTo( 'UTSysop' ) )
443 ->will( $this->returnValue( $primary2Can ) );
444 $this->primaryauthMocks
= [ $mock1, $mock2 ];
446 $this->initializeManager( true );
447 $this->assertSame( $expect, $this->manager
->userCanAuthenticate( 'UTSysop' ) );
450 public static function provideUserCanAuthenticate() {
452 [ false, false, false ],
453 [ true, false, true ],
454 [ false, true, true ],
455 [ true, true, true ],
459 public function testRevokeAccessForUser() {
460 $this->initializeManager();
462 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
463 $mock->expects( $this->any() )->method( 'getUniqueId' )
464 ->will( $this->returnValue( 'primary' ) );
465 $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
466 ->with( $this->equalTo( 'UTSysop' ) );
467 $this->primaryauthMocks
= [ $mock ];
469 $this->initializeManager( true );
470 $this->logger
->setCollect( true );
472 $this->manager
->revokeAccessForUser( 'UTSysop' );
475 [ LogLevel
::INFO
, 'Revoking access for {user}' ],
476 ], $this->logger
->getBuffer() );
479 public function testProviderCreation() {
481 'pre' => $this->getMockForAbstractClass( PreAuthenticationProvider
::class ),
482 'primary' => $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class ),
483 'secondary' => $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class ),
485 foreach ( $mocks as $key => $mock ) {
486 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
487 $mock->expects( $this->once() )->method( 'setLogger' );
488 $mock->expects( $this->once() )->method( 'setManager' );
489 $mock->expects( $this->once() )->method( 'setConfig' );
491 $this->preauthMocks
= [ $mocks['pre'] ];
492 $this->primaryauthMocks
= [ $mocks['primary'] ];
493 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
496 $this->initializeManager();
499 $this->managerPriv
->getAuthenticationProvider( 'primary' )
503 $this->managerPriv
->getAuthenticationProvider( 'secondary' )
507 $this->managerPriv
->getAuthenticationProvider( 'pre' )
510 [ 'pre' => $mocks['pre'] ],
511 $this->managerPriv
->getPreAuthenticationProviders()
514 [ 'primary' => $mocks['primary'] ],
515 $this->managerPriv
->getPrimaryAuthenticationProviders()
518 [ 'secondary' => $mocks['secondary'] ],
519 $this->managerPriv
->getSecondaryAuthenticationProviders()
523 $mock1 = $this->getMockForAbstractClass( PreAuthenticationProvider
::class );
524 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
525 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
526 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
527 $this->preauthMocks
= [ $mock1 ];
528 $this->primaryauthMocks
= [ $mock2 ];
529 $this->secondaryauthMocks
= [];
530 $this->initializeManager( true );
532 $this->managerPriv
->getAuthenticationProvider( 'Y' );
533 $this->fail( 'Expected exception not thrown' );
534 } catch ( \RuntimeException
$ex ) {
535 $class1 = get_class( $mock1 );
536 $class2 = get_class( $mock2 );
538 "Duplicate specifications for id X (classes $class1 and $class2)", $ex->getMessage()
543 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
544 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
545 $class = get_class( $mock );
546 $this->preauthMocks
= [ $mock ];
547 $this->primaryauthMocks
= [ $mock ];
548 $this->secondaryauthMocks
= [ $mock ];
549 $this->initializeManager( true );
551 $this->managerPriv
->getPreAuthenticationProviders();
552 $this->fail( 'Expected exception not thrown' );
553 } catch ( \RuntimeException
$ex ) {
555 "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
560 $this->managerPriv
->getPrimaryAuthenticationProviders();
561 $this->fail( 'Expected exception not thrown' );
562 } catch ( \RuntimeException
$ex ) {
564 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
569 $this->managerPriv
->getSecondaryAuthenticationProviders();
570 $this->fail( 'Expected exception not thrown' );
571 } catch ( \RuntimeException
$ex ) {
573 "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
579 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
580 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
581 $mock3 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
582 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
583 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
584 $mock3->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'C' ) );
585 $this->preauthMocks
= [];
586 $this->primaryauthMocks
= [ $mock1, $mock2, $mock3 ];
587 $this->secondaryauthMocks
= [];
588 $this->initializeConfig();
589 $config = $this->config
->get( 'AuthManagerConfig' );
591 $this->initializeManager( false );
593 [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
594 $this->managerPriv
->getPrimaryAuthenticationProviders(),
598 $config['primaryauth']['A']['sort'] = 100;
599 $config['primaryauth']['C']['sort'] = -1;
600 $this->config
->set( 'AuthManagerConfig', $config );
601 $this->initializeManager( false );
603 [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
604 $this->managerPriv
->getPrimaryAuthenticationProviders()
609 * @dataProvider provideSetDefaultUserOptions
611 public function testSetDefaultUserOptions(
612 $contLang, $useContextLang, $expectedLang, $expectedVariant
614 $this->initializeManager();
616 $this->setContentLang( $contLang );
617 $context = \RequestContext
::getMain();
618 $reset = new ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
619 $context->setLanguage( 'de' );
621 $user = \User
::newFromName( self
::usernameForCreation() );
622 $user->addToDatabase();
623 $oldToken = $user->getToken();
624 $this->managerPriv
->setDefaultUserOptions( $user, $useContextLang );
625 $user->saveSettings();
626 $this->assertNotEquals( $oldToken, $user->getToken() );
627 $this->assertSame( $expectedLang, $user->getOption( 'language' ) );
628 $this->assertSame( $expectedVariant, $user->getOption( 'variant' ) );
631 public function provideSetDefaultUserOptions() {
633 [ 'zh', false, 'zh', 'zh' ],
634 [ 'zh', true, 'de', 'zh' ],
635 [ 'fr', true, 'de', null ],
639 public function testForcePrimaryAuthenticationProviders() {
640 $mockA = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
641 $mockB = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
642 $mockB2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
643 $mockA->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
644 $mockB->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
645 $mockB2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
646 $this->primaryauthMocks
= [ $mockA ];
648 $this->logger
= new \
TestLogger( true );
650 // Test without first initializing the configured providers
651 $this->initializeManager();
652 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
654 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
656 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
657 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
659 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
660 ], $this->logger
->getBuffer() );
661 $this->logger
->clearBuffer();
663 // Test with first initializing the configured providers
664 $this->initializeManager();
665 $this->assertSame( $mockA, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
666 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
667 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
668 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
669 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
671 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
673 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
674 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
675 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
677 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
680 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
683 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
685 ], $this->logger
->getBuffer() );
686 $this->logger
->clearBuffer();
688 // Test duplicate IDs
689 $this->initializeManager();
691 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
692 $this->fail( 'Expected exception not thrown' );
693 } catch ( \RuntimeException
$ex ) {
694 $class1 = get_class( $mockB );
695 $class2 = get_class( $mockB2 );
697 "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
702 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
703 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
704 $class = get_class( $mock );
706 $this->manager
->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
707 $this->fail( 'Expected exception not thrown' );
708 } catch ( \RuntimeException
$ex ) {
710 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
716 public function testBeginAuthentication() {
717 $this->initializeManager();
720 list( $provider, $reset ) = $this->getMockSessionProvider( false );
721 $this->hook( 'UserLoggedIn', $this->never() );
722 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
724 $this->manager
->beginAuthentication( [], 'http://localhost/' );
725 $this->fail( 'Expected exception not thrown' );
726 } catch ( \LogicException
$ex ) {
727 $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
729 $this->unhook( 'UserLoggedIn' );
730 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
731 ScopedCallback
::consume( $reset );
732 $this->initializeManager( true );
734 // CreatedAccountAuthenticationRequest
735 $user = \User
::newFromName( 'UTSysop' );
737 new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
739 $this->hook( 'UserLoggedIn', $this->never() );
741 $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
742 $this->fail( 'Expected exception not thrown' );
743 } catch ( \LogicException
$ex ) {
745 'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
746 'that created the account',
750 $this->unhook( 'UserLoggedIn' );
752 $this->request
->getSession()->clear();
753 $this->request
->getSession()->setSecret( 'AuthManager::authnState', 'test' );
754 $this->managerPriv
->createdAccountAuthenticationRequests
= [ $reqs[0] ];
755 $this->hook( 'UserLoggedIn', $this->once() )
756 ->with( $this->callback( function ( $u ) use ( $user ) {
757 return $user->getId() === $u->getId() && $user->getName() === $u->getName();
759 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
760 $this->logger
->setCollect( true );
761 $ret = $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
762 $this->logger
->setCollect( false );
763 $this->unhook( 'UserLoggedIn' );
764 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
765 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
766 $this->assertSame( $user->getName(), $ret->username
);
767 $this->assertSame( $user->getId(), $this->request
->getSessionData( 'AuthManager:lastAuthId' ) );
769 time(), $this->request
->getSessionData( 'AuthManager:lastAuthTimestamp' ),
772 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::authnState' ) );
773 $this->assertSame( $user->getId(), $this->request
->getSession()->getUser()->getId() );
775 [ LogLevel
::INFO
, 'Logging in {user} after account creation' ],
776 ], $this->logger
->getBuffer() );
779 public function testCreateFromLogin() {
780 $user = \User
::newFromName( 'UTSysop' );
781 $req1 = $this->createMock( AuthenticationRequest
::class );
782 $req2 = $this->createMock( AuthenticationRequest
::class );
783 $req3 = $this->createMock( AuthenticationRequest
::class );
784 $userReq = new UsernameAuthenticationRequest
;
785 $userReq->username
= 'UTDummy';
787 $req1->returnToUrl
= 'http://localhost/';
788 $req2->returnToUrl
= 'http://localhost/';
789 $req3->returnToUrl
= 'http://localhost/';
790 $req3->username
= 'UTDummy';
791 $userReq->returnToUrl
= 'http://localhost/';
793 // Passing one into beginAuthentication(), and an immediate FAIL
794 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
795 $this->primaryauthMocks
= [ $primary ];
796 $this->initializeManager( true );
797 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
798 $res->createRequest
= $req1;
799 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
800 ->will( $this->returnValue( $res ) );
801 $createReq = new CreateFromLoginAuthenticationRequest(
802 null, [ $req2->getUniqueId() => $req2 ]
804 $this->logger
->setCollect( true );
805 $ret = $this->manager
->beginAuthentication( [ $createReq ], 'http://localhost/' );
806 $this->logger
->setCollect( false );
807 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
808 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
809 $this->assertSame( $req1, $ret->createRequest
->createRequest
);
810 $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest
->maybeLink
);
812 // UI, then FAIL in beginAuthentication()
813 $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider
::class )
814 ->setMethods( [ 'continuePrimaryAuthentication' ] )
815 ->getMockForAbstractClass();
816 $this->primaryauthMocks
= [ $primary ];
817 $this->initializeManager( true );
818 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
819 ->will( $this->returnValue(
820 AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) )
822 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
823 $res->createRequest
= $req2;
824 $primary->expects( $this->any() )->method( 'continuePrimaryAuthentication' )
825 ->will( $this->returnValue( $res ) );
826 $this->logger
->setCollect( true );
827 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
828 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
, 'sanity check' );
829 $ret = $this->manager
->continueAuthentication( [] );
830 $this->logger
->setCollect( false );
831 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
832 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
833 $this->assertSame( $req2, $ret->createRequest
->createRequest
);
834 $this->assertEquals( [], $ret->createRequest
->maybeLink
);
836 // Pass into beginAccountCreation(), see that maybeLink and createRequest get copied
837 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
838 $this->primaryauthMocks
= [ $primary ];
839 $this->initializeManager( true );
840 $createReq = new CreateFromLoginAuthenticationRequest( $req3, [ $req2 ] );
841 $createReq->returnToUrl
= 'http://localhost/';
842 $createReq->username
= 'UTDummy';
843 $res = AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) );
844 $primary->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
845 ->with( $this->anything(), $this->anything(), [ $userReq, $createReq, $req3 ] )
846 ->will( $this->returnValue( $res ) );
847 $primary->expects( $this->any() )->method( 'accountCreationType' )
848 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
849 $this->logger
->setCollect( true );
850 $ret = $this->manager
->beginAccountCreation(
851 $user, [ $userReq, $createReq ], 'http://localhost/'
853 $this->logger
->setCollect( false );
854 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
855 $state = $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' );
856 $this->assertNotNull( $state );
857 $this->assertEquals( [ $userReq, $createReq, $req3 ], $state['reqs'] );
858 $this->assertEquals( [ $req2 ], $state['maybeLink'] );
862 * @dataProvider provideAuthentication
863 * @param StatusValue $preResponse
864 * @param array $primaryResponses
865 * @param array $secondaryResponses
866 * @param array $managerResponses
867 * @param bool $link Whether the primary authentication provider is a "link" provider
869 public function testAuthentication(
870 StatusValue
$preResponse, array $primaryResponses, array $secondaryResponses,
871 array $managerResponses, $link = false
873 $this->initializeManager();
874 $user = \User
::newFromName( 'UTSysop' );
875 $id = $user->getId();
876 $name = $user->getName();
878 // Set up lots of mocks...
879 $req = new RememberMeAuthenticationRequest
;
880 $req->rememberMe
= (bool)rand( 0, 1 );
881 $req->pre
= $preResponse;
882 $req->primary
= $primaryResponses;
883 $req->secondary
= $secondaryResponses;
885 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
886 $class = ucfirst( $key ) . 'AuthenticationProvider';
887 $mocks[$key] = $this->getMockForAbstractClass(
888 "MediaWiki\\Auth\\$class", [], "Mock$class"
890 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
891 ->will( $this->returnValue( $key ) );
892 $mocks[$key . '2'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
893 $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
894 ->will( $this->returnValue( $key . '2' ) );
895 $mocks[$key . '3'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
896 $mocks[$key . '3']->expects( $this->any() )->method( 'getUniqueId' )
897 ->will( $this->returnValue( $key . '3' ) );
899 foreach ( $mocks as $mock ) {
900 $mock->expects( $this->any() )->method( 'getAuthenticationRequests' )
901 ->will( $this->returnValue( [] ) );
904 $mocks['pre']->expects( $this->once() )->method( 'testForAuthentication' )
905 ->will( $this->returnCallback( function ( $reqs ) use ( $req ) {
906 $this->assertContains( $req, $reqs );
910 $ct = count( $req->primary
);
911 $callback = $this->returnCallback( function ( $reqs ) use ( $req ) {
912 $this->assertContains( $req, $reqs );
913 return array_shift( $req->primary
);
915 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
916 ->method( 'beginPrimaryAuthentication' )
918 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
919 ->method( 'continuePrimaryAuthentication' )
922 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
923 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
926 $ct = count( $req->secondary
);
927 $callback = $this->returnCallback( function ( $user, $reqs ) use ( $id, $name, $req ) {
928 $this->assertSame( $id, $user->getId() );
929 $this->assertSame( $name, $user->getName() );
930 $this->assertContains( $req, $reqs );
931 return array_shift( $req->secondary
);
933 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
934 ->method( 'beginSecondaryAuthentication' )
936 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
937 ->method( 'continueSecondaryAuthentication' )
940 $abstain = AuthenticationResponse
::newAbstain();
941 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAuthentication' )
942 ->will( $this->returnValue( StatusValue
::newGood() ) );
943 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAuthentication' )
944 ->will( $this->returnValue( $abstain ) );
945 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAuthentication' );
946 $mocks['secondary2']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
947 ->will( $this->returnValue( $abstain ) );
948 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
949 $mocks['secondary3']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
950 ->will( $this->returnValue( $abstain ) );
951 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
953 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
954 $this->primaryauthMocks
= [ $mocks['primary'], $mocks['primary2'] ];
955 $this->secondaryauthMocks
= [
956 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2'],
957 // So linking happens
958 new ConfirmLinkSecondaryAuthenticationProvider
,
960 $this->initializeManager( true );
961 $this->logger
->setCollect( true );
963 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
964 $this->equalTo( AuthenticationResponse
::PASS
),
965 $this->equalTo( AuthenticationResponse
::FAIL
)
967 $providers = array_filter(
969 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
972 return is_callable( [ $p, 'expects' ] );
975 foreach ( $providers as $p ) {
976 $p->postCalled
= false;
977 $p->expects( $this->atMost( 1 ) )->method( 'postAuthentication' )
978 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
979 if ( $user !== null ) {
980 $this->assertInstanceOf( \User
::class, $user );
981 $this->assertSame( 'UTSysop', $user->getName() );
983 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
984 $this->assertThat( $response->status
, $constraint );
985 $p->postCalled
= $response->status
;
989 $session = $this->request
->getSession();
990 $session->setRememberUser( !$req->rememberMe
);
992 foreach ( $managerResponses as $i => $response ) {
993 $success = $response instanceof AuthenticationResponse
&&
994 $response->status
=== AuthenticationResponse
::PASS
;
996 $this->hook( 'UserLoggedIn', $this->once() )
997 ->with( $this->callback( function ( $user ) use ( $id, $name ) {
998 return $user->getId() === $id && $user->getName() === $name;
1001 $this->hook( 'UserLoggedIn', $this->never() );
1004 $response instanceof AuthenticationResponse
&&
1005 $response->status
=== AuthenticationResponse
::FAIL
&&
1006 $response->message
->getKey() !== 'authmanager-authn-not-in-progress' &&
1007 $response->message
->getKey() !== 'authmanager-authn-no-primary'
1010 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
1012 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->never() );
1018 $ret = $this->manager
->beginAuthentication( [ $req ], 'http://localhost/' );
1020 $ret = $this->manager
->continueAuthentication( [ $req ] );
1022 if ( $response instanceof \Exception
) {
1023 $this->fail( 'Expected exception not thrown', "Response $i" );
1025 } catch ( \Exception
$ex ) {
1026 if ( !$response instanceof \Exception
) {
1029 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
1030 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1031 "Response $i, exception, session state" );
1032 $this->unhook( 'UserLoggedIn' );
1033 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1037 $this->unhook( 'UserLoggedIn' );
1038 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1040 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
1042 $ret->message
= $this->message( $ret->message
);
1043 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
1045 $this->assertSame( $id, $session->getUser()->getId(),
1046 "Response $i, authn" );
1048 $this->assertSame( 0, $session->getUser()->getId(),
1049 "Response $i, authn" );
1051 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
1052 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1053 "Response $i, session state" );
1054 foreach ( $providers as $p ) {
1055 $this->assertSame( $response->status
, $p->postCalled
,
1056 "Response $i, post-auth callback called" );
1059 $this->assertNotNull( $session->getSecret( 'AuthManager::authnState' ),
1060 "Response $i, session state" );
1061 foreach ( $ret->neededRequests
as $neededReq ) {
1062 $this->assertEquals( AuthManager
::ACTION_LOGIN
, $neededReq->action
,
1063 "Response $i, neededRequest action" );
1065 $this->assertEquals(
1066 $ret->neededRequests
,
1067 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN_CONTINUE
),
1068 "Response $i, continuation check"
1070 foreach ( $providers as $p ) {
1071 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
1075 $state = $session->getSecret( 'AuthManager::authnState' );
1076 $maybeLink = $state['maybeLink'] ??
[];
1077 if ( $link && $response->status
=== AuthenticationResponse
::RESTART
) {
1078 $this->assertEquals(
1079 $response->createRequest
->maybeLink
,
1081 "Response $i, maybeLink"
1084 $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
1089 $this->assertSame( $req->rememberMe
, $session->shouldRememberUser(),
1090 'rememberMe checkbox had effect' );
1092 $this->assertNotSame( $req->rememberMe
, $session->shouldRememberUser(),
1093 'rememberMe checkbox wasn\'t applied' );
1097 public function provideAuthentication() {
1098 $rememberReq = new RememberMeAuthenticationRequest
;
1099 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
1101 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1102 $req->foobar
= 'baz';
1103 $restartResponse = AuthenticationResponse
::newRestart(
1104 $this->message( 'authmanager-authn-no-local-user' )
1106 $restartResponse->neededRequests
= [ $rememberReq ];
1108 $restartResponse2Pass = AuthenticationResponse
::newPass( null );
1109 $restartResponse2Pass->linkRequest
= $req;
1110 $restartResponse2 = AuthenticationResponse
::newRestart(
1111 $this->message( 'authmanager-authn-no-local-user-link' )
1113 $restartResponse2->createRequest
= new CreateFromLoginAuthenticationRequest(
1114 null, [ $req->getUniqueId() => $req ]
1116 $restartResponse2->createRequest
->action
= AuthManager
::ACTION_LOGIN
;
1117 $restartResponse2->neededRequests
= [ $rememberReq, $restartResponse2->createRequest
];
1119 $userName = 'UTSysop';
1122 'Failure in pre-auth' => [
1123 StatusValue
::newFatal( 'fail-from-pre' ),
1127 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
1128 AuthenticationResponse
::newFail(
1129 $this->message( 'authmanager-authn-not-in-progress' )
1133 'Failure in primary' => [
1134 StatusValue
::newGood(),
1136 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
1141 'All primary abstain' => [
1142 StatusValue
::newGood(),
1144 AuthenticationResponse
::newAbstain(),
1148 AuthenticationResponse
::newFail( $this->message( 'authmanager-authn-no-primary' ) )
1151 'Primary UI, then redirect, then fail' => [
1152 StatusValue
::newGood(),
1154 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1155 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
1156 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
1161 'Primary redirect, then abstain' => [
1162 StatusValue
::newGood(),
1164 $tmp = AuthenticationResponse
::newRedirect(
1165 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
1167 AuthenticationResponse
::newAbstain(),
1172 new \
DomainException(
1173 'MockPrimaryAuthenticationProvider::continuePrimaryAuthentication() returned ABSTAIN'
1177 'Primary UI, then pass with no local user' => [
1178 StatusValue
::newGood(),
1180 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1181 AuthenticationResponse
::newPass( null ),
1189 'Primary UI, then pass with no local user (link type)' => [
1190 StatusValue
::newGood(),
1192 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1193 $restartResponse2Pass,
1202 'Primary pass with invalid username' => [
1203 StatusValue
::newGood(),
1205 AuthenticationResponse
::newPass( '<>' ),
1209 new \
DomainException( 'MockPrimaryAuthenticationProvider returned an invalid username: <>' ),
1212 'Secondary fail' => [
1213 StatusValue
::newGood(),
1215 AuthenticationResponse
::newPass( $userName ),
1218 AuthenticationResponse
::newFail( $this->message( 'fail-in-secondary' ) ),
1222 'Secondary UI, then abstain' => [
1223 StatusValue
::newGood(),
1225 AuthenticationResponse
::newPass( $userName ),
1228 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1229 AuthenticationResponse
::newAbstain()
1233 AuthenticationResponse
::newPass( $userName ),
1236 'Secondary pass' => [
1237 StatusValue
::newGood(),
1239 AuthenticationResponse
::newPass( $userName ),
1242 AuthenticationResponse
::newPass()
1245 AuthenticationResponse
::newPass( $userName ),
1252 * @dataProvider provideUserExists
1253 * @param bool $primary1Exists
1254 * @param bool $primary2Exists
1255 * @param bool $expect
1257 public function testUserExists( $primary1Exists, $primary2Exists, $expect ) {
1258 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1259 $mock1->expects( $this->any() )->method( 'getUniqueId' )
1260 ->will( $this->returnValue( 'primary1' ) );
1261 $mock1->expects( $this->any() )->method( 'testUserExists' )
1262 ->with( $this->equalTo( 'UTSysop' ) )
1263 ->will( $this->returnValue( $primary1Exists ) );
1264 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1265 $mock2->expects( $this->any() )->method( 'getUniqueId' )
1266 ->will( $this->returnValue( 'primary2' ) );
1267 $mock2->expects( $this->any() )->method( 'testUserExists' )
1268 ->with( $this->equalTo( 'UTSysop' ) )
1269 ->will( $this->returnValue( $primary2Exists ) );
1270 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1272 $this->initializeManager( true );
1273 $this->assertSame( $expect, $this->manager
->userExists( 'UTSysop' ) );
1276 public static function provideUserExists() {
1278 [ false, false, false ],
1279 [ true, false, true ],
1280 [ false, true, true ],
1281 [ true, true, true ],
1286 * @dataProvider provideAllowsAuthenticationDataChange
1287 * @param StatusValue $primaryReturn
1288 * @param StatusValue $secondaryReturn
1289 * @param Status $expect
1291 public function testAllowsAuthenticationDataChange( $primaryReturn, $secondaryReturn, $expect ) {
1292 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1294 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1295 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1296 $mock1->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1297 ->with( $this->equalTo( $req ) )
1298 ->will( $this->returnValue( $primaryReturn ) );
1299 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
1300 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1301 $mock2->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1302 ->with( $this->equalTo( $req ) )
1303 ->will( $this->returnValue( $secondaryReturn ) );
1305 $this->primaryauthMocks
= [ $mock1 ];
1306 $this->secondaryauthMocks
= [ $mock2 ];
1307 $this->initializeManager( true );
1308 $this->assertEquals( $expect, $this->manager
->allowsAuthenticationDataChange( $req ) );
1311 public static function provideAllowsAuthenticationDataChange() {
1312 $ignored = \Status
::newGood( 'ignored' );
1313 $ignored->warning( 'authmanager-change-not-supported' );
1315 $okFromPrimary = StatusValue
::newGood();
1316 $okFromPrimary->warning( 'warning-from-primary' );
1317 $okFromSecondary = StatusValue
::newGood();
1318 $okFromSecondary->warning( 'warning-from-secondary' );
1322 StatusValue
::newGood(),
1323 StatusValue
::newGood(),
1327 StatusValue
::newGood(),
1328 StatusValue
::newGood( 'ignore' ),
1332 StatusValue
::newGood( 'ignored' ),
1333 StatusValue
::newGood(),
1337 StatusValue
::newGood( 'ignored' ),
1338 StatusValue
::newGood( 'ignored' ),
1342 StatusValue
::newFatal( 'fail from primary' ),
1343 StatusValue
::newGood(),
1344 \Status
::newFatal( 'fail from primary' ),
1348 StatusValue
::newGood(),
1349 \Status
::wrap( $okFromPrimary ),
1352 StatusValue
::newGood(),
1353 StatusValue
::newFatal( 'fail from secondary' ),
1354 \Status
::newFatal( 'fail from secondary' ),
1357 StatusValue
::newGood(),
1359 \Status
::wrap( $okFromSecondary ),
1364 public function testChangeAuthenticationData() {
1365 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1366 $req->username
= 'UTSysop';
1368 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1369 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1370 $mock1->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1371 ->with( $this->equalTo( $req ) );
1372 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1373 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1374 $mock2->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1375 ->with( $this->equalTo( $req ) );
1377 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1378 $this->initializeManager( true );
1379 $this->logger
->setCollect( true );
1380 $this->manager
->changeAuthenticationData( $req );
1381 $this->assertSame( [
1382 [ LogLevel
::INFO
, 'Changing authentication data for {user} class {what}' ],
1383 ], $this->logger
->getBuffer() );
1386 public function testCanCreateAccounts() {
1388 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
1389 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
1390 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
1393 foreach ( $types as $type => $can ) {
1394 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1395 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
1396 $mock->expects( $this->any() )->method( 'accountCreationType' )
1397 ->will( $this->returnValue( $type ) );
1398 $this->primaryauthMocks
= [ $mock ];
1399 $this->initializeManager( true );
1400 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
1404 public function testCheckAccountCreatePermissions() {
1405 $this->initializeManager( true );
1407 $this->setGroupPermissions( '*', 'createaccount', true );
1408 $this->assertEquals(
1410 $this->manager
->checkAccountCreatePermissions( new \User
)
1413 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1414 $readOnlyMode->setReason( 'Because' );
1415 $this->assertEquals(
1416 \Status
::newFatal( wfMessage( 'readonlytext', 'Because' ) ),
1417 $this->manager
->checkAccountCreatePermissions( new \User
)
1419 $readOnlyMode->setReason( false );
1421 $this->setGroupPermissions( '*', 'createaccount', false );
1422 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1423 $this->assertFalse( $status->isOK() );
1424 $this->assertTrue( $status->hasMessage( 'badaccess-groups' ) );
1425 $this->setGroupPermissions( '*', 'createaccount', true );
1427 $user = \User
::newFromName( 'UTBlockee' );
1428 if ( $user->getID() == 0 ) {
1429 $user->addToDatabase();
1430 \TestUser
::setPasswordForUser( $user, 'UTBlockeePassword' );
1431 $user->saveSettings();
1433 $oldBlock = \Block
::newFromTarget( 'UTBlockee' );
1435 // An old block will prevent our new one from saving.
1436 $oldBlock->delete();
1439 'address' => 'UTBlockee',
1440 'user' => $user->getID(),
1441 'by' => $this->getTestSysop()->getUser()->getId(),
1442 'reason' => __METHOD__
,
1443 'expiry' => time() +
100500,
1444 'createAccount' => true,
1446 $block = new \
Block( $blockOptions );
1448 $status = $this->manager
->checkAccountCreatePermissions( $user );
1449 $this->assertFalse( $status->isOK() );
1450 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
1453 'address' => '127.0.0.0/24',
1454 'by' => $this->getTestSysop()->getUser()->getId(),
1455 'reason' => __METHOD__
,
1456 'expiry' => time() +
100500,
1457 'createAccount' => true,
1459 $block = new \
Block( $blockOptions );
1461 $scopeVariable = new ScopedCallback( [ $block, 'delete' ] );
1462 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1463 $this->assertFalse( $status->isOK() );
1464 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
1465 ScopedCallback
::consume( $scopeVariable );
1467 $this->setMwGlobals( [
1468 'wgEnableDnsBlacklist' => true,
1469 'wgDnsBlacklistUrls' => [
1470 'local.wmftest.net', // This will resolve for every subdomain, which works to test "listed?"
1472 'wgProxyWhitelist' => [],
1474 $this->overrideMwServices();
1475 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1476 $this->assertFalse( $status->isOK() );
1477 $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
1478 $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
1479 $this->overrideMwServices();
1480 $status = $this->manager
->checkAccountCreatePermissions( new \User
);
1481 $this->assertTrue( $status->isGood() );
1485 * @param string $uniq
1488 private static function usernameForCreation( $uniq = '' ) {
1491 $username = "UTAuthManagerTestAccountCreation" . $uniq . ++
$i;
1492 } while ( \User
::newFromName( $username )->getId() !== 0 );
1496 public function testCanCreateAccount() {
1497 $username = self
::usernameForCreation();
1498 $this->initializeManager();
1500 $this->assertEquals(
1501 \Status
::newFatal( 'authmanager-create-disabled' ),
1502 $this->manager
->canCreateAccount( $username )
1505 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1506 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1507 $mock->expects( $this->any() )->method( 'accountCreationType' )
1508 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1509 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1510 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1511 ->will( $this->returnValue( StatusValue
::newGood() ) );
1512 $this->primaryauthMocks
= [ $mock ];
1513 $this->initializeManager( true );
1515 $this->assertEquals(
1516 \Status
::newFatal( 'userexists' ),
1517 $this->manager
->canCreateAccount( $username )
1520 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1521 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1522 $mock->expects( $this->any() )->method( 'accountCreationType' )
1523 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1524 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1525 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1526 ->will( $this->returnValue( StatusValue
::newGood() ) );
1527 $this->primaryauthMocks
= [ $mock ];
1528 $this->initializeManager( true );
1530 $this->assertEquals(
1531 \Status
::newFatal( 'noname' ),
1532 $this->manager
->canCreateAccount( $username . '<>' )
1535 $this->assertEquals(
1536 \Status
::newFatal( 'userexists' ),
1537 $this->manager
->canCreateAccount( 'UTSysop' )
1540 $this->assertEquals(
1542 $this->manager
->canCreateAccount( $username )
1545 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1546 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1547 $mock->expects( $this->any() )->method( 'accountCreationType' )
1548 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1549 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1550 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1551 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1552 $this->primaryauthMocks
= [ $mock ];
1553 $this->initializeManager( true );
1555 $this->assertEquals(
1556 \Status
::newFatal( 'fail' ),
1557 $this->manager
->canCreateAccount( $username )
1561 public function testBeginAccountCreation() {
1562 $creator = \User
::newFromName( 'UTSysop' );
1563 $userReq = new UsernameAuthenticationRequest
;
1564 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1565 return $level === LogLevel
::DEBUG ?
null : $message;
1567 $this->initializeManager();
1569 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
1570 $this->hook( 'LocalUserCreated', $this->never() );
1572 $this->manager
->beginAccountCreation(
1573 $creator, [], 'http://localhost/'
1575 $this->fail( 'Expected exception not thrown' );
1576 } catch ( \LogicException
$ex ) {
1577 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1579 $this->unhook( 'LocalUserCreated' );
1581 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1584 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1585 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1586 $mock->expects( $this->any() )->method( 'accountCreationType' )
1587 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1588 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1589 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1590 ->will( $this->returnValue( StatusValue
::newGood() ) );
1591 $this->primaryauthMocks
= [ $mock ];
1592 $this->initializeManager( true );
1594 $this->hook( 'LocalUserCreated', $this->never() );
1595 $ret = $this->manager
->beginAccountCreation( $creator, [], 'http://localhost/' );
1596 $this->unhook( 'LocalUserCreated' );
1597 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1598 $this->assertSame( 'noname', $ret->message
->getKey() );
1600 $this->hook( 'LocalUserCreated', $this->never() );
1601 $userReq->username
= self
::usernameForCreation();
1602 $userReq2 = new UsernameAuthenticationRequest
;
1603 $userReq2->username
= $userReq->username
. 'X';
1604 $ret = $this->manager
->beginAccountCreation(
1605 $creator, [ $userReq, $userReq2 ], 'http://localhost/'
1607 $this->unhook( 'LocalUserCreated' );
1608 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1609 $this->assertSame( 'noname', $ret->message
->getKey() );
1611 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1612 $readOnlyMode->setReason( 'Because' );
1613 $this->hook( 'LocalUserCreated', $this->never() );
1614 $userReq->username
= self
::usernameForCreation();
1615 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1616 $this->unhook( 'LocalUserCreated' );
1617 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1618 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
1619 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1620 $readOnlyMode->setReason( false );
1622 $this->hook( 'LocalUserCreated', $this->never() );
1623 $userReq->username
= self
::usernameForCreation();
1624 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1625 $this->unhook( 'LocalUserCreated' );
1626 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1627 $this->assertSame( 'userexists', $ret->message
->getKey() );
1629 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1630 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1631 $mock->expects( $this->any() )->method( 'accountCreationType' )
1632 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1633 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1634 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1635 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1636 $this->primaryauthMocks
= [ $mock ];
1637 $this->initializeManager( true );
1639 $this->hook( 'LocalUserCreated', $this->never() );
1640 $userReq->username
= self
::usernameForCreation();
1641 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1642 $this->unhook( 'LocalUserCreated' );
1643 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1644 $this->assertSame( 'fail', $ret->message
->getKey() );
1646 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1647 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1648 $mock->expects( $this->any() )->method( 'accountCreationType' )
1649 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1650 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1651 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1652 ->will( $this->returnValue( StatusValue
::newGood() ) );
1653 $this->primaryauthMocks
= [ $mock ];
1654 $this->initializeManager( true );
1656 $this->hook( 'LocalUserCreated', $this->never() );
1657 $userReq->username
= self
::usernameForCreation() . '<>';
1658 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1659 $this->unhook( 'LocalUserCreated' );
1660 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1661 $this->assertSame( 'noname', $ret->message
->getKey() );
1663 $this->hook( 'LocalUserCreated', $this->never() );
1664 $userReq->username
= $creator->getName();
1665 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1666 $this->unhook( 'LocalUserCreated' );
1667 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1668 $this->assertSame( 'userexists', $ret->message
->getKey() );
1670 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1671 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1672 $mock->expects( $this->any() )->method( 'accountCreationType' )
1673 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1674 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1675 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1676 ->will( $this->returnValue( StatusValue
::newGood() ) );
1677 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
1678 ->will( $this->returnValue( StatusValue
::newFatal( 'fail' ) ) );
1679 $this->primaryauthMocks
= [ $mock ];
1680 $this->initializeManager( true );
1682 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1683 ->setMethods( [ 'populateUser' ] )
1685 $req->expects( $this->any() )->method( 'populateUser' )
1686 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1687 $userReq->username
= self
::usernameForCreation();
1688 $ret = $this->manager
->beginAccountCreation(
1689 $creator, [ $userReq, $req ], 'http://localhost/'
1691 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1692 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1694 $req = new UserDataAuthenticationRequest
;
1695 $userReq->username
= self
::usernameForCreation();
1697 $ret = $this->manager
->beginAccountCreation(
1698 $creator, [ $userReq, $req ], 'http://localhost/'
1700 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1701 $this->assertSame( 'fail', $ret->message
->getKey() );
1703 $this->manager
->beginAccountCreation(
1704 \User
::newFromName( $userReq->username
), [ $userReq, $req ], 'http://localhost/'
1706 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1707 $this->assertSame( 'fail', $ret->message
->getKey() );
1710 public function testContinueAccountCreation() {
1711 $creator = \User
::newFromName( 'UTSysop' );
1712 $username = self
::usernameForCreation();
1713 $this->logger
= new \
TestLogger( false, function ( $message, $level ) {
1714 return $level === LogLevel
::DEBUG ?
null : $message;
1716 $this->initializeManager();
1720 'username' => $username,
1722 'creatorname' => $username,
1725 'primaryResponse' => null,
1727 'ranPreTests' => true,
1730 $this->hook( 'LocalUserCreated', $this->never() );
1732 $this->manager
->continueAccountCreation( [] );
1733 $this->fail( 'Expected exception not thrown' );
1734 } catch ( \LogicException
$ex ) {
1735 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1737 $this->unhook( 'LocalUserCreated' );
1739 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
1740 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1741 $mock->expects( $this->any() )->method( 'accountCreationType' )
1742 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1743 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1744 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )->will(
1745 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
1747 $this->primaryauthMocks
= [ $mock ];
1748 $this->initializeManager( true );
1750 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', null );
1751 $this->hook( 'LocalUserCreated', $this->never() );
1752 $ret = $this->manager
->continueAccountCreation( [] );
1753 $this->unhook( 'LocalUserCreated' );
1754 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1755 $this->assertSame( 'authmanager-create-not-in-progress', $ret->message
->getKey() );
1757 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1758 [ 'username' => "$username<>" ] +
$session );
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( 'noname', $ret->message
->getKey() );
1765 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1768 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $session );
1769 $this->hook( 'LocalUserCreated', $this->never() );
1770 $cache = \ObjectCache
::getLocalClusterInstance();
1771 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1772 $ret = $this->manager
->continueAccountCreation( [] );
1774 $this->unhook( 'LocalUserCreated' );
1775 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1776 $this->assertSame( 'usernameinprogress', $ret->message
->getKey() );
1777 // This error shouldn't remove the existing session, because the
1778 // raced-with process "owns" it.
1780 $session, $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1783 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1784 [ 'username' => $creator->getName() ] +
$session );
1785 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1786 $readOnlyMode->setReason( 'Because' );
1787 $this->hook( 'LocalUserCreated', $this->never() );
1788 $ret = $this->manager
->continueAccountCreation( [] );
1789 $this->unhook( 'LocalUserCreated' );
1790 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1791 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
1792 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
1793 $readOnlyMode->setReason( false );
1795 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1796 [ 'username' => $creator->getName() ] +
$session );
1797 $this->hook( 'LocalUserCreated', $this->never() );
1798 $ret = $this->manager
->continueAccountCreation( [] );
1799 $this->unhook( 'LocalUserCreated' );
1800 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1801 $this->assertSame( 'userexists', $ret->message
->getKey() );
1803 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1806 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1807 [ 'userid' => $creator->getId() ] +
$session );
1808 $this->hook( 'LocalUserCreated', $this->never() );
1810 $ret = $this->manager
->continueAccountCreation( [] );
1811 $this->fail( 'Expected exception not thrown' );
1812 } catch ( \UnexpectedValueException
$ex ) {
1813 $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
1815 $this->unhook( 'LocalUserCreated' );
1817 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1820 $id = $creator->getId();
1821 $name = $creator->getName();
1822 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1823 [ 'username' => $name, 'userid' => $id +
1 ] +
$session );
1824 $this->hook( 'LocalUserCreated', $this->never() );
1826 $ret = $this->manager
->continueAccountCreation( [] );
1827 $this->fail( 'Expected exception not thrown' );
1828 } catch ( \UnexpectedValueException
$ex ) {
1829 $this->assertEquals(
1830 "User \"{$name}\" exists, but ID $id !== " . ( $id +
1 ) . '!', $ex->getMessage()
1833 $this->unhook( 'LocalUserCreated' );
1835 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1838 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
1839 ->setMethods( [ 'populateUser' ] )
1841 $req->expects( $this->any() )->method( 'populateUser' )
1842 ->willReturn( \StatusValue
::newFatal( 'populatefail' ) );
1843 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState',
1844 [ 'reqs' => [ $req ] ] +
$session );
1845 $ret = $this->manager
->continueAccountCreation( [] );
1846 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1847 $this->assertSame( 'populatefail', $ret->message
->getKey() );
1849 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' )
1854 * @dataProvider provideAccountCreation
1855 * @param StatusValue $preTest
1856 * @param StatusValue $primaryTest
1857 * @param StatusValue $secondaryTest
1858 * @param array $primaryResponses
1859 * @param array $secondaryResponses
1860 * @param array $managerResponses
1862 public function testAccountCreation(
1863 StatusValue
$preTest, $primaryTest, $secondaryTest,
1864 array $primaryResponses, array $secondaryResponses, array $managerResponses
1866 $creator = \User
::newFromName( 'UTSysop' );
1867 $username = self
::usernameForCreation();
1869 $this->initializeManager();
1871 // Set up lots of mocks...
1872 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1873 $req->preTest
= $preTest;
1874 $req->primaryTest
= $primaryTest;
1875 $req->secondaryTest
= $secondaryTest;
1876 $req->primary
= $primaryResponses;
1877 $req->secondary
= $secondaryResponses;
1879 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
1880 $class = ucfirst( $key ) . 'AuthenticationProvider';
1881 $mocks[$key] = $this->getMockForAbstractClass(
1882 "MediaWiki\\Auth\\$class", [], "Mock$class"
1884 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
1885 ->will( $this->returnValue( $key ) );
1886 $mocks[$key]->expects( $this->any() )->method( 'testUserForCreation' )
1887 ->will( $this->returnValue( StatusValue
::newGood() ) );
1888 $mocks[$key]->expects( $this->any() )->method( 'testForAccountCreation' )
1889 ->will( $this->returnCallback(
1890 function ( $user, $creatorIn, $reqs )
1891 use ( $username, $creator, $req, $key )
1893 $this->assertSame( $username, $user->getName() );
1894 $this->assertSame( $creator->getId(), $creatorIn->getId() );
1895 $this->assertSame( $creator->getName(), $creatorIn->getName() );
1897 foreach ( $reqs as $r ) {
1898 $this->assertSame( $username, $r->username
);
1899 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1901 $this->assertTrue( $foundReq, '$reqs contains $req' );
1907 for ( $i = 2; $i <= 3; $i++
) {
1908 $mocks[$key . $i] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
1909 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
1910 ->will( $this->returnValue( $key . $i ) );
1911 $mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' )
1912 ->will( $this->returnValue( StatusValue
::newGood() ) );
1913 $mocks[$key . $i]->expects( $this->atMost( 1 ) )->method( 'testForAccountCreation' )
1914 ->will( $this->returnValue( StatusValue
::newGood() ) );
1918 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
1919 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
1920 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
1921 ->will( $this->returnValue( false ) );
1922 $ct = count( $req->primary
);
1923 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1924 $this->assertSame( $username, $user->getName() );
1925 $this->assertSame( 'UTSysop', $creator->getName() );
1927 foreach ( $reqs as $r ) {
1928 $this->assertSame( $username, $r->username
);
1929 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1931 $this->assertTrue( $foundReq, '$reqs contains $req' );
1932 return array_shift( $req->primary
);
1934 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
1935 ->method( 'beginPrimaryAccountCreation' )
1936 ->will( $callback );
1937 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1938 ->method( 'continuePrimaryAccountCreation' )
1939 ->will( $callback );
1941 $ct = count( $req->secondary
);
1942 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1943 $this->assertSame( $username, $user->getName() );
1944 $this->assertSame( 'UTSysop', $creator->getName() );
1946 foreach ( $reqs as $r ) {
1947 $this->assertSame( $username, $r->username
);
1948 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
1950 $this->assertTrue( $foundReq, '$reqs contains $req' );
1951 return array_shift( $req->secondary
);
1953 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
1954 ->method( 'beginSecondaryAccountCreation' )
1955 ->will( $callback );
1956 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1957 ->method( 'continueSecondaryAccountCreation' )
1958 ->will( $callback );
1960 $abstain = AuthenticationResponse
::newAbstain();
1961 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
1962 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
1963 $mocks['primary2']->expects( $this->any() )->method( 'testUserExists' )
1964 ->will( $this->returnValue( false ) );
1965 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountCreation' )
1966 ->will( $this->returnValue( $abstain ) );
1967 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1968 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
1969 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_NONE
) );
1970 $mocks['primary3']->expects( $this->any() )->method( 'testUserExists' )
1971 ->will( $this->returnValue( false ) );
1972 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountCreation' );
1973 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1974 $mocks['secondary2']->expects( $this->atMost( 1 ) )
1975 ->method( 'beginSecondaryAccountCreation' )
1976 ->will( $this->returnValue( $abstain ) );
1977 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1978 $mocks['secondary3']->expects( $this->atMost( 1 ) )
1979 ->method( 'beginSecondaryAccountCreation' )
1980 ->will( $this->returnValue( $abstain ) );
1981 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1983 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
1984 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary'], $mocks['primary2'] ];
1985 $this->secondaryauthMocks
= [
1986 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2']
1989 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
1990 return $level === LogLevel
::DEBUG ?
null : $message;
1993 $this->initializeManager( true );
1995 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
1996 $this->equalTo( AuthenticationResponse
::PASS
),
1997 $this->equalTo( AuthenticationResponse
::FAIL
)
1999 $providers = array_merge(
2000 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
2002 foreach ( $providers as $p ) {
2003 $p->postCalled
= false;
2004 $p->expects( $this->atMost( 1 ) )->method( 'postAccountCreation' )
2005 ->willReturnCallback( function ( $user, $creator, $response )
2006 use ( $constraint, $p, $username )
2008 $this->assertInstanceOf( \User
::class, $user );
2009 $this->assertSame( $username, $user->getName() );
2010 $this->assertSame( 'UTSysop', $creator->getName() );
2011 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
2012 $this->assertThat( $response->status
, $constraint );
2013 $p->postCalled
= $response->status
;
2017 // We're testing with $wgNewUserLog = false, so assert that it worked
2018 $dbw = wfGetDB( DB_MASTER
);
2019 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2023 foreach ( $managerResponses as $i => $response ) {
2024 $success = $response instanceof AuthenticationResponse
&&
2025 $response->status
=== AuthenticationResponse
::PASS
;
2026 if ( $i === 'created' ) {
2028 $this->hook( 'LocalUserCreated', $this->once() )
2030 $this->callback( function ( $user ) use ( $username ) {
2031 return $user->getName() === $username;
2033 $this->equalTo( false )
2035 $expectLog[] = [ LogLevel
::INFO
, "Creating user {user} during account creation" ];
2037 $this->hook( 'LocalUserCreated', $this->never() );
2043 $userReq = new UsernameAuthenticationRequest
;
2044 $userReq->username
= $username;
2045 $ret = $this->manager
->beginAccountCreation(
2046 $creator, [ $userReq, $req ], 'http://localhost/'
2049 $ret = $this->manager
->continueAccountCreation( [ $req ] );
2051 if ( $response instanceof \Exception
) {
2052 $this->fail( 'Expected exception not thrown', "Response $i" );
2054 } catch ( \Exception
$ex ) {
2055 if ( !$response instanceof \Exception
) {
2058 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
2060 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2061 "Response $i, exception, session state"
2063 $this->unhook( 'LocalUserCreated' );
2067 $this->unhook( 'LocalUserCreated' );
2069 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
2072 $this->assertNotNull( $ret->loginRequest
, "Response $i, login marker" );
2073 $this->assertContains(
2074 $ret->loginRequest
, $this->managerPriv
->createdAccountAuthenticationRequests
,
2075 "Response $i, login marker"
2080 "MediaWiki\Auth\AuthManager::continueAccountCreation: Account creation succeeded for {user}"
2083 // Set some fields in the expected $response that we couldn't
2084 // know in provideAccountCreation().
2085 $response->username
= $username;
2086 $response->loginRequest
= $ret->loginRequest
;
2088 $this->assertNull( $ret->loginRequest
, "Response $i, login marker" );
2089 $this->assertSame( [], $this->managerPriv
->createdAccountAuthenticationRequests
,
2090 "Response $i, login marker" );
2092 $ret->message
= $this->message( $ret->message
);
2093 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
2094 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
2096 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2097 "Response $i, session state"
2099 foreach ( $providers as $p ) {
2100 $this->assertSame( $response->status
, $p->postCalled
,
2101 "Response $i, post-auth callback called" );
2104 $this->assertNotNull(
2105 $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2106 "Response $i, session state"
2108 foreach ( $ret->neededRequests
as $neededReq ) {
2109 $this->assertEquals( AuthManager
::ACTION_CREATE
, $neededReq->action
,
2110 "Response $i, neededRequest action" );
2112 $this->assertEquals(
2113 $ret->neededRequests
,
2114 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_CREATE_CONTINUE
),
2115 "Response $i, continuation check"
2117 foreach ( $providers as $p ) {
2118 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
2123 $this->assertNotEquals( 0, \User
::idFromName( $username ) );
2125 $this->assertEquals( 0, \User
::idFromName( $username ) );
2131 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
2135 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2139 public function provideAccountCreation() {
2140 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
2141 $good = StatusValue
::newGood();
2144 'Pre-creation test fail in pre' => [
2145 StatusValue
::newFatal( 'fail-from-pre' ), $good, $good,
2149 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
2152 'Pre-creation test fail in primary' => [
2153 $good, StatusValue
::newFatal( 'fail-from-primary' ), $good,
2157 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2160 'Pre-creation test fail in secondary' => [
2161 $good, $good, StatusValue
::newFatal( 'fail-from-secondary' ),
2165 AuthenticationResponse
::newFail( $this->message( 'fail-from-secondary' ) ),
2168 'Failure in primary' => [
2169 $good, $good, $good,
2171 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2176 'All primary abstain' => [
2177 $good, $good, $good,
2179 AuthenticationResponse
::newAbstain(),
2183 AuthenticationResponse
::newFail( $this->message( 'authmanager-create-no-primary' ) )
2186 'Primary UI, then redirect, then fail' => [
2187 $good, $good, $good,
2189 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2190 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
2191 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
2196 'Primary redirect, then abstain' => [
2197 $good, $good, $good,
2199 $tmp = AuthenticationResponse
::newRedirect(
2200 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
2202 AuthenticationResponse
::newAbstain(),
2207 new \
DomainException(
2208 'MockPrimaryAuthenticationProvider::continuePrimaryAccountCreation() returned ABSTAIN'
2212 'Primary UI, then pass; secondary abstain' => [
2213 $good, $good, $good,
2215 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2216 AuthenticationResponse
::newPass(),
2219 AuthenticationResponse
::newAbstain(),
2223 'created' => AuthenticationResponse
::newPass( '' ),
2226 'Primary pass; secondary UI then pass' => [
2227 $good, $good, $good,
2229 AuthenticationResponse
::newPass( '' ),
2232 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2233 AuthenticationResponse
::newPass( '' ),
2237 AuthenticationResponse
::newPass( '' ),
2240 'Primary pass; secondary fail' => [
2241 $good, $good, $good,
2243 AuthenticationResponse
::newPass(),
2246 AuthenticationResponse
::newFail( $this->message( '...' ) ),
2249 'created' => new \
DomainException(
2250 'MockSecondaryAuthenticationProvider::beginSecondaryAccountCreation() returned FAIL. ' .
2251 'Secondary providers are not allowed to fail account creation, ' .
2252 'that should have been done via testForAccountCreation().'
2260 * @dataProvider provideAccountCreationLogging
2261 * @param bool $isAnon
2262 * @param string|null $logSubtype
2264 public function testAccountCreationLogging( $isAnon, $logSubtype ) {
2265 $creator = $isAnon ?
new \User
: \User
::newFromName( 'UTSysop' );
2266 $username = self
::usernameForCreation();
2268 $this->initializeManager();
2270 // Set up lots of mocks...
2271 $mock = $this->getMockForAbstractClass(
2272 \MediaWiki\Auth\PrimaryAuthenticationProvider
::class, []
2274 $mock->expects( $this->any() )->method( 'getUniqueId' )
2275 ->will( $this->returnValue( 'primary' ) );
2276 $mock->expects( $this->any() )->method( 'testUserForCreation' )
2277 ->will( $this->returnValue( StatusValue
::newGood() ) );
2278 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
2279 ->will( $this->returnValue( StatusValue
::newGood() ) );
2280 $mock->expects( $this->any() )->method( 'accountCreationType' )
2281 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2282 $mock->expects( $this->any() )->method( 'testUserExists' )
2283 ->will( $this->returnValue( false ) );
2284 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
2285 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
2286 $mock->expects( $this->any() )->method( 'finishAccountCreation' )
2287 ->will( $this->returnValue( $logSubtype ) );
2289 $this->primaryauthMocks
= [ $mock ];
2290 $this->initializeManager( true );
2291 $this->logger
->setCollect( true );
2293 $this->config
->set( 'NewUserLog', true );
2295 $dbw = wfGetDB( DB_MASTER
);
2296 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2298 $userReq = new UsernameAuthenticationRequest
;
2299 $userReq->username
= $username;
2300 $reasonReq = new CreationReasonAuthenticationRequest
;
2301 $reasonReq->reason
= $this->toString();
2302 $ret = $this->manager
->beginAccountCreation(
2303 $creator, [ $userReq, $reasonReq ], 'http://localhost/'
2306 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
2308 $user = \User
::newFromName( $username );
2309 $this->assertNotEquals( 0, $user->getId(), 'sanity check' );
2310 $this->assertNotEquals( $creator->getId(), $user->getId(), 'sanity check' );
2312 $data = \DatabaseLogEntry
::getSelectQueryData();
2313 $rows = iterator_to_array( $dbw->select(
2317 'log_id > ' . (int)$maxLogId,
2318 'log_type' => 'newusers'
2324 $this->assertCount( 1, $rows );
2325 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2327 $this->assertSame( $logSubtype ?
: ( $isAnon ?
'create' : 'create2' ), $entry->getSubtype() );
2329 $isAnon ?
$user->getId() : $creator->getId(),
2330 $entry->getPerformer()->getId()
2333 $isAnon ?
$user->getName() : $creator->getName(),
2334 $entry->getPerformer()->getName()
2336 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2337 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2338 $this->assertSame( $this->toString(), $entry->getComment() );
2341 public static function provideAccountCreationLogging() {
2346 [ false, 'byemail' ],
2350 public function testAutoAccountCreation() {
2351 // PHPUnit seems to have a bug where it will call the ->with()
2352 // callbacks for our hooks again after the test is run (WTF?), which
2353 // breaks here because $username no longer matches $user by the end of
2355 $workaroundPHPUnitBug = false;
2357 $username = self
::usernameForCreation();
2358 $expectedSource = AuthManager
::AUTOCREATE_SOURCE_SESSION
;
2359 $this->initializeManager();
2361 $this->setGroupPermissions( '*', 'createaccount', true );
2362 $this->setGroupPermissions( '*', 'autocreateaccount', false );
2364 $this->mergeMwGlobalArrayValue( 'wgObjectCaches',
2365 [ __METHOD__
=> [ 'class' => 'HashBagOStuff' ] ] );
2366 $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__
] );
2368 // Set up lots of mocks...
2370 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2371 $class = ucfirst( $key ) . 'AuthenticationProvider';
2372 $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
2373 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2374 ->will( $this->returnValue( $key ) );
2377 $good = StatusValue
::newGood();
2378 $ok = StatusValue
::newFatal( 'ok' );
2379 $callback = $this->callback( function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
2380 return $workaroundPHPUnitBug ||
$user->getName() === $username;
2382 $callback2 = $this->callback(
2383 function ( $source ) use ( &$expectedSource, &$workaroundPHPUnitBug ) {
2384 return $workaroundPHPUnitBug ||
$source === $expectedSource;
2388 $mocks['pre']->expects( $this->exactly( 13 ) )->method( 'testUserForCreation' )
2389 ->with( $callback, $callback2 )
2390 ->will( $this->onConsecutiveCalls(
2391 $ok, $ok, $ok, // For testing permissions
2392 StatusValue
::newFatal( 'fail-in-pre' ), $good, $good,
2393 $good, // backoff test
2394 $good, // addToDatabase fails test
2395 $good, // addToDatabase throws test
2396 $good, // addToDatabase exists test
2397 $good, $good, $good // success
2400 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
2401 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
2402 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
2403 ->will( $this->returnValue( true ) );
2404 $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
2405 ->with( $callback, $callback2 )
2406 ->will( $this->onConsecutiveCalls(
2407 StatusValue
::newFatal( 'fail-in-primary' ), $good,
2408 $good, // backoff test
2409 $good, // addToDatabase fails test
2410 $good, // addToDatabase throws test
2411 $good, // addToDatabase exists test
2414 $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2415 ->with( $callback, $callback2 );
2417 $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
2418 ->with( $callback, $callback2 )
2419 ->will( $this->onConsecutiveCalls(
2420 StatusValue
::newFatal( 'fail-in-secondary' ),
2421 $good, // backoff test
2422 $good, // addToDatabase fails test
2423 $good, // addToDatabase throws test
2424 $good, // addToDatabase exists test
2427 $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2428 ->with( $callback, $callback2 );
2430 $this->preauthMocks
= [ $mocks['pre'] ];
2431 $this->primaryauthMocks
= [ $mocks['primary'] ];
2432 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2433 $this->initializeManager( true );
2434 $session = $this->request
->getSession();
2436 $logger = new \
TestLogger( true, function ( $m ) {
2437 $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
2440 $this->manager
->setLogger( $logger );
2443 $user = \User
::newFromName( 'UTSysop' );
2444 $this->manager
->autoCreateUser( $user, 'InvalidSource', true );
2445 $this->fail( 'Expected exception not thrown' );
2446 } catch ( \InvalidArgumentException
$ex ) {
2447 $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
2450 // First, check an existing user
2452 $user = \User
::newFromName( 'UTSysop' );
2453 $this->hook( 'LocalUserCreated', $this->never() );
2454 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2455 $this->unhook( 'LocalUserCreated' );
2456 $expect = \Status
::newGood();
2457 $expect->warning( 'userexists' );
2458 $this->assertEquals( $expect, $ret );
2459 $this->assertNotEquals( 0, $user->getId() );
2460 $this->assertSame( 'UTSysop', $user->getName() );
2461 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2462 $this->assertSame( [
2463 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2464 ], $logger->getBuffer() );
2465 $logger->clearBuffer();
2468 $user = \User
::newFromName( 'UTSysop' );
2469 $this->hook( 'LocalUserCreated', $this->never() );
2470 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2471 $this->unhook( 'LocalUserCreated' );
2472 $expect = \Status
::newGood();
2473 $expect->warning( 'userexists' );
2474 $this->assertEquals( $expect, $ret );
2475 $this->assertNotEquals( 0, $user->getId() );
2476 $this->assertSame( 'UTSysop', $user->getName() );
2477 $this->assertEquals( 0, $session->getUser()->getId() );
2478 $this->assertSame( [
2479 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2480 ], $logger->getBuffer() );
2481 $logger->clearBuffer();
2483 // Wiki is read-only
2485 $readOnlyMode = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
2486 $readOnlyMode->setReason( 'Because' );
2487 $user = \User
::newFromName( $username );
2488 $this->hook( 'LocalUserCreated', $this->never() );
2489 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2490 $this->unhook( 'LocalUserCreated' );
2491 $this->assertEquals( \Status
::newFatal( wfMessage( 'readonlytext', 'Because' ) ), $ret );
2492 $this->assertEquals( 0, $user->getId() );
2493 $this->assertNotEquals( $username, $user->getName() );
2494 $this->assertEquals( 0, $session->getUser()->getId() );
2495 $this->assertSame( [
2496 [ LogLevel
::DEBUG
, 'denied by wfReadOnly(): {reason}' ],
2497 ], $logger->getBuffer() );
2498 $logger->clearBuffer();
2499 $readOnlyMode->setReason( false );
2501 // Session blacklisted
2503 $session->set( 'AuthManager::AutoCreateBlacklist', 'test' );
2504 $user = \User
::newFromName( $username );
2505 $this->hook( 'LocalUserCreated', $this->never() );
2506 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2507 $this->unhook( 'LocalUserCreated' );
2508 $this->assertEquals( \Status
::newFatal( 'test' ), $ret );
2509 $this->assertEquals( 0, $user->getId() );
2510 $this->assertNotEquals( $username, $user->getName() );
2511 $this->assertEquals( 0, $session->getUser()->getId() );
2512 $this->assertSame( [
2513 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2514 ], $logger->getBuffer() );
2515 $logger->clearBuffer();
2518 $session->set( 'AuthManager::AutoCreateBlacklist', StatusValue
::newFatal( 'test2' ) );
2519 $user = \User
::newFromName( $username );
2520 $this->hook( 'LocalUserCreated', $this->never() );
2521 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2522 $this->unhook( 'LocalUserCreated' );
2523 $this->assertEquals( \Status
::newFatal( 'test2' ), $ret );
2524 $this->assertEquals( 0, $user->getId() );
2525 $this->assertNotEquals( $username, $user->getName() );
2526 $this->assertEquals( 0, $session->getUser()->getId() );
2527 $this->assertSame( [
2528 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2529 ], $logger->getBuffer() );
2530 $logger->clearBuffer();
2534 $user = \User
::newFromName( $username . '@' );
2535 $this->hook( 'LocalUserCreated', $this->never() );
2536 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2537 $this->unhook( 'LocalUserCreated' );
2538 $this->assertEquals( \Status
::newFatal( 'noname' ), $ret );
2539 $this->assertEquals( 0, $user->getId() );
2540 $this->assertNotEquals( $username . '@', $user->getId() );
2541 $this->assertEquals( 0, $session->getUser()->getId() );
2542 $this->assertSame( [
2543 [ LogLevel
::DEBUG
, 'name "{username}" is not creatable' ],
2544 ], $logger->getBuffer() );
2545 $logger->clearBuffer();
2546 $this->assertSame( 'noname', $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2548 // IP unable to create accounts
2549 $this->setGroupPermissions( '*', 'createaccount', false );
2550 $this->setGroupPermissions( '*', 'autocreateaccount', false );
2552 $user = \User
::newFromName( $username );
2553 $this->hook( 'LocalUserCreated', $this->never() );
2554 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2555 $this->unhook( 'LocalUserCreated' );
2556 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-noperm' ), $ret );
2557 $this->assertEquals( 0, $user->getId() );
2558 $this->assertNotEquals( $username, $user->getName() );
2559 $this->assertEquals( 0, $session->getUser()->getId() );
2560 $this->assertSame( [
2561 [ LogLevel
::DEBUG
, 'IP lacks the ability to create or autocreate accounts' ],
2562 ], $logger->getBuffer() );
2563 $logger->clearBuffer();
2565 'authmanager-autocreate-noperm', $session->get( 'AuthManager::AutoCreateBlacklist' )
2568 // maintenance scripts always work
2569 $expectedSource = AuthManager
::AUTOCREATE_SOURCE_MAINT
;
2570 $this->setGroupPermissions( '*', 'createaccount', false );
2571 $this->setGroupPermissions( '*', 'autocreateaccount', false );
2573 $user = \User
::newFromName( $username );
2574 $this->hook( 'LocalUserCreated', $this->never() );
2575 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_MAINT
, true );
2576 $this->unhook( 'LocalUserCreated' );
2577 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2579 // Test that both permutations of permissions are allowed
2580 // (this hits the two "ok" entries in $mocks['pre'])
2581 $expectedSource = AuthManager
::AUTOCREATE_SOURCE_SESSION
;
2582 $this->setGroupPermissions( '*', 'createaccount', false );
2583 $this->setGroupPermissions( '*', 'autocreateaccount', true );
2585 $user = \User
::newFromName( $username );
2586 $this->hook( 'LocalUserCreated', $this->never() );
2587 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2588 $this->unhook( 'LocalUserCreated' );
2589 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2591 $this->setGroupPermissions( '*', 'createaccount', true );
2592 $this->setGroupPermissions( '*', 'autocreateaccount', false );
2594 $user = \User
::newFromName( $username );
2595 $this->hook( 'LocalUserCreated', $this->never() );
2596 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2597 $this->unhook( 'LocalUserCreated' );
2598 $this->assertEquals( \Status
::newFatal( 'ok' ), $ret );
2599 $logger->clearBuffer();
2603 $user = \User
::newFromName( $username );
2604 $this->hook( 'LocalUserCreated', $this->never() );
2605 $cache = \ObjectCache
::getLocalClusterInstance();
2606 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2607 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2609 $this->unhook( 'LocalUserCreated' );
2610 $this->assertEquals( \Status
::newFatal( 'usernameinprogress' ), $ret );
2611 $this->assertEquals( 0, $user->getId() );
2612 $this->assertNotEquals( $username, $user->getName() );
2613 $this->assertEquals( 0, $session->getUser()->getId() );
2614 $this->assertSame( [
2615 [ LogLevel
::DEBUG
, 'Could not acquire account creation lock' ],
2616 ], $logger->getBuffer() );
2617 $logger->clearBuffer();
2619 // Test pre-authentication provider fail
2621 $user = \User
::newFromName( $username );
2622 $this->hook( 'LocalUserCreated', $this->never() );
2623 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2624 $this->unhook( 'LocalUserCreated' );
2625 $this->assertEquals( \Status
::newFatal( 'fail-in-pre' ), $ret );
2626 $this->assertEquals( 0, $user->getId() );
2627 $this->assertNotEquals( $username, $user->getName() );
2628 $this->assertEquals( 0, $session->getUser()->getId() );
2629 $this->assertSame( [
2630 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2631 ], $logger->getBuffer() );
2632 $logger->clearBuffer();
2633 $this->assertEquals(
2634 StatusValue
::newFatal( 'fail-in-pre' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2638 $user = \User
::newFromName( $username );
2639 $this->hook( 'LocalUserCreated', $this->never() );
2640 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2641 $this->unhook( 'LocalUserCreated' );
2642 $this->assertEquals( \Status
::newFatal( 'fail-in-primary' ), $ret );
2643 $this->assertEquals( 0, $user->getId() );
2644 $this->assertNotEquals( $username, $user->getName() );
2645 $this->assertEquals( 0, $session->getUser()->getId() );
2646 $this->assertSame( [
2647 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2648 ], $logger->getBuffer() );
2649 $logger->clearBuffer();
2650 $this->assertEquals(
2651 StatusValue
::newFatal( 'fail-in-primary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2655 $user = \User
::newFromName( $username );
2656 $this->hook( 'LocalUserCreated', $this->never() );
2657 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2658 $this->unhook( 'LocalUserCreated' );
2659 $this->assertEquals( \Status
::newFatal( 'fail-in-secondary' ), $ret );
2660 $this->assertEquals( 0, $user->getId() );
2661 $this->assertNotEquals( $username, $user->getName() );
2662 $this->assertEquals( 0, $session->getUser()->getId() );
2663 $this->assertSame( [
2664 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
2665 ], $logger->getBuffer() );
2666 $logger->clearBuffer();
2667 $this->assertEquals(
2668 StatusValue
::newFatal( 'fail-in-secondary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2672 $cache = \ObjectCache
::getLocalClusterInstance();
2673 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2674 $cache->set( $backoffKey, true );
2676 $user = \User
::newFromName( $username );
2677 $this->hook( 'LocalUserCreated', $this->never() );
2678 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2679 $this->unhook( 'LocalUserCreated' );
2680 $this->assertEquals( \Status
::newFatal( 'authmanager-autocreate-exception' ), $ret );
2681 $this->assertEquals( 0, $user->getId() );
2682 $this->assertNotEquals( $username, $user->getName() );
2683 $this->assertEquals( 0, $session->getUser()->getId() );
2684 $this->assertSame( [
2685 [ LogLevel
::DEBUG
, '{username} denied by prior creation attempt failures' ],
2686 ], $logger->getBuffer() );
2687 $logger->clearBuffer();
2688 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2689 $cache->delete( $backoffKey );
2691 // Test addToDatabase fails
2693 $user = $this->getMockBuilder( \User
::class )
2694 ->setMethods( [ 'addToDatabase' ] )->getMock();
2695 $user->expects( $this->once() )->method( 'addToDatabase' )
2696 ->will( $this->returnValue( \Status
::newFatal( 'because' ) ) );
2697 $user->setName( $username );
2698 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2699 $this->assertEquals( \Status
::newFatal( 'because' ), $ret );
2700 $this->assertEquals( 0, $user->getId() );
2701 $this->assertNotEquals( $username, $user->getName() );
2702 $this->assertEquals( 0, $session->getUser()->getId() );
2703 $this->assertSame( [
2704 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2705 [ LogLevel
::ERROR
, '{username} failed with message {msg}' ],
2706 ], $logger->getBuffer() );
2707 $logger->clearBuffer();
2708 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2710 // Test addToDatabase throws an exception
2711 $cache = \ObjectCache
::getLocalClusterInstance();
2712 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2713 $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
2715 $user = $this->getMockBuilder( \User
::class )
2716 ->setMethods( [ 'addToDatabase' ] )->getMock();
2717 $user->expects( $this->once() )->method( 'addToDatabase' )
2718 ->will( $this->throwException( new \
Exception( 'Excepted' ) ) );
2719 $user->setName( $username );
2721 $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2722 $this->fail( 'Expected exception not thrown' );
2723 } catch ( \Exception
$ex ) {
2724 $this->assertSame( 'Excepted', $ex->getMessage() );
2726 $this->assertEquals( 0, $user->getId() );
2727 $this->assertEquals( 0, $session->getUser()->getId() );
2728 $this->assertSame( [
2729 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2730 [ LogLevel
::ERROR
, '{username} failed with exception {exception}' ],
2731 ], $logger->getBuffer() );
2732 $logger->clearBuffer();
2733 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2734 $this->assertNotEquals( false, $cache->get( $backoffKey ) );
2735 $cache->delete( $backoffKey );
2737 // Test addToDatabase fails because the user already exists.
2739 $user = $this->getMockBuilder( \User
::class )
2740 ->setMethods( [ 'addToDatabase' ] )->getMock();
2741 $user->expects( $this->once() )->method( 'addToDatabase' )
2742 ->will( $this->returnCallback( function () use ( $username, &$user ) {
2743 $oldUser = \User
::newFromName( $username );
2744 $status = $oldUser->addToDatabase();
2745 $this->assertTrue( $status->isOK(), 'sanity check' );
2746 $user->setId( $oldUser->getId() );
2747 return \Status
::newFatal( 'userexists' );
2749 $user->setName( $username );
2750 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2751 $expect = \Status
::newGood();
2752 $expect->warning( 'userexists' );
2753 $this->assertEquals( $expect, $ret );
2754 $this->assertNotEquals( 0, $user->getId() );
2755 $this->assertEquals( $username, $user->getName() );
2756 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2757 $this->assertSame( [
2758 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2759 [ LogLevel
::INFO
, '{username} already exists locally (race)' ],
2760 ], $logger->getBuffer() );
2761 $logger->clearBuffer();
2762 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2766 $username = self
::usernameForCreation();
2767 $user = \User
::newFromName( $username );
2768 $this->hook( 'LocalUserCreated', $this->once() )
2769 ->with( $callback, $this->equalTo( true ) );
2770 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true );
2771 $this->unhook( 'LocalUserCreated' );
2772 $this->assertEquals( \Status
::newGood(), $ret );
2773 $this->assertNotEquals( 0, $user->getId() );
2774 $this->assertEquals( $username, $user->getName() );
2775 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2776 $this->assertSame( [
2777 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2778 ], $logger->getBuffer() );
2779 $logger->clearBuffer();
2781 $dbw = wfGetDB( DB_MASTER
);
2782 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2784 $username = self
::usernameForCreation();
2785 $user = \User
::newFromName( $username );
2786 $this->hook( 'LocalUserCreated', $this->once() )
2787 ->with( $callback, $this->equalTo( true ) );
2788 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2789 $this->unhook( 'LocalUserCreated' );
2790 $this->assertEquals( \Status
::newGood(), $ret );
2791 $this->assertNotEquals( 0, $user->getId() );
2792 $this->assertEquals( $username, $user->getName() );
2793 $this->assertEquals( 0, $session->getUser()->getId() );
2794 $this->assertSame( [
2795 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
2796 ], $logger->getBuffer() );
2797 $logger->clearBuffer();
2800 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2803 $this->config
->set( 'NewUserLog', true );
2805 $username = self
::usernameForCreation();
2806 $user = \User
::newFromName( $username );
2807 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false );
2808 $this->assertEquals( \Status
::newGood(), $ret );
2809 $logger->clearBuffer();
2811 $data = \DatabaseLogEntry
::getSelectQueryData();
2812 $rows = iterator_to_array( $dbw->select(
2816 'log_id > ' . (int)$maxLogId,
2817 'log_type' => 'newusers'
2823 $this->assertCount( 1, $rows );
2824 $entry = \DatabaseLogEntry
::newFromRow( reset( $rows ) );
2826 $this->assertSame( 'autocreate', $entry->getSubtype() );
2827 $this->assertSame( $user->getId(), $entry->getPerformer()->getId() );
2828 $this->assertSame( $user->getName(), $entry->getPerformer()->getName() );
2829 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2830 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2832 $workaroundPHPUnitBug = true;
2836 * @dataProvider provideGetAuthenticationRequests
2837 * @param string $action
2838 * @param array $expect
2839 * @param array $state
2841 public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
2842 $makeReq = function ( $key ) use ( $action ) {
2843 $req = $this->createMock( AuthenticationRequest
::class );
2844 $req->expects( $this->any() )->method( 'getUniqueId' )
2845 ->will( $this->returnValue( $key ) );
2846 $req->action
= $action === AuthManager
::ACTION_UNLINK ? AuthManager
::ACTION_REMOVE
: $action;
2850 $cmpReqs = function ( $a, $b ) {
2851 $ret = strcmp( get_class( $a ), get_class( $b ) );
2853 $ret = strcmp( $a->key
, $b->key
);
2858 $good = StatusValue
::newGood();
2861 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2862 $class = ucfirst( $key ) . 'AuthenticationProvider';
2863 $mocks[$key] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" )
2865 'getUniqueId', 'getAuthenticationRequests', 'providerAllowsAuthenticationDataChange',
2867 ->getMockForAbstractClass();
2868 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2869 ->will( $this->returnValue( $key ) );
2870 $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2871 ->will( $this->returnCallback( function ( $action ) use ( $key, $makeReq ) {
2872 return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
2874 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
2875 ->will( $this->returnValue( $good ) );
2880 PrimaryAuthenticationProvider
::TYPE_NONE
,
2881 PrimaryAuthenticationProvider
::TYPE_CREATE
,
2882 PrimaryAuthenticationProvider
::TYPE_LINK
2884 $class = 'PrimaryAuthenticationProvider';
2885 $mocks["primary-$type"] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" )
2887 'getUniqueId', 'accountCreationType', 'getAuthenticationRequests',
2888 'providerAllowsAuthenticationDataChange',
2890 ->getMockForAbstractClass();
2891 $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
2892 ->will( $this->returnValue( "primary-$type" ) );
2893 $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
2894 ->will( $this->returnValue( $type ) );
2895 $mocks["primary-$type"]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2896 ->will( $this->returnCallback( function ( $action ) use ( $type, $makeReq ) {
2897 return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
2899 $mocks["primary-$type"]->expects( $this->any() )
2900 ->method( 'providerAllowsAuthenticationDataChange' )
2901 ->will( $this->returnValue( $good ) );
2902 $this->primaryauthMocks
[] = $mocks["primary-$type"];
2905 $mocks['primary2'] = $this->getMockBuilder( PrimaryAuthenticationProvider
::class )
2907 'getUniqueId', 'accountCreationType', 'getAuthenticationRequests',
2908 'providerAllowsAuthenticationDataChange',
2910 ->getMockForAbstractClass();
2911 $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
2912 ->will( $this->returnValue( 'primary2' ) );
2913 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
2914 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
2915 $mocks['primary2']->expects( $this->any() )->method( 'getAuthenticationRequests' )
2916 ->will( $this->returnValue( [] ) );
2917 $mocks['primary2']->expects( $this->any() )
2918 ->method( 'providerAllowsAuthenticationDataChange' )
2919 ->will( $this->returnCallback( function ( $req ) use ( $good ) {
2920 return $req->key
=== 'generic' ? StatusValue
::newFatal( 'no' ) : $good;
2922 $this->primaryauthMocks
[] = $mocks['primary2'];
2924 $this->preauthMocks
= [ $mocks['pre'] ];
2925 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2926 $this->initializeManager( true );
2929 if ( isset( $state['continueRequests'] ) ) {
2930 $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
2932 if ( $action === AuthManager
::ACTION_LOGIN_CONTINUE
) {
2933 $this->request
->getSession()->setSecret( 'AuthManager::authnState', $state );
2934 } elseif ( $action === AuthManager
::ACTION_CREATE_CONTINUE
) {
2935 $this->request
->getSession()->setSecret( 'AuthManager::accountCreationState', $state );
2936 } elseif ( $action === AuthManager
::ACTION_LINK_CONTINUE
) {
2937 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', $state );
2941 $expectReqs = array_map( $makeReq, $expect );
2942 if ( $action === AuthManager
::ACTION_LOGIN
) {
2943 $req = new RememberMeAuthenticationRequest
;
2944 $req->action
= $action;
2945 $req->required
= AuthenticationRequest
::REQUIRED
;
2946 $expectReqs[] = $req;
2947 } elseif ( $action === AuthManager
::ACTION_CREATE
) {
2948 $req = new UsernameAuthenticationRequest
;
2949 $req->action
= $action;
2950 $expectReqs[] = $req;
2951 $req = new UserDataAuthenticationRequest
;
2952 $req->action
= $action;
2953 $req->required
= AuthenticationRequest
::REQUIRED
;
2954 $expectReqs[] = $req;
2956 usort( $expectReqs, $cmpReqs );
2958 $actual = $this->manager
->getAuthenticationRequests( $action );
2959 foreach ( $actual as $req ) {
2960 // Don't test this here.
2961 $req->required
= AuthenticationRequest
::REQUIRED
;
2963 usort( $actual, $cmpReqs );
2965 $this->assertEquals( $expectReqs, $actual );
2967 // Test CreationReasonAuthenticationRequest gets returned
2968 if ( $action === AuthManager
::ACTION_CREATE
) {
2969 $req = new CreationReasonAuthenticationRequest
;
2970 $req->action
= $action;
2971 $req->required
= AuthenticationRequest
::REQUIRED
;
2972 $expectReqs[] = $req;
2973 usort( $expectReqs, $cmpReqs );
2975 $actual = $this->manager
->getAuthenticationRequests( $action, \User
::newFromName( 'UTSysop' ) );
2976 foreach ( $actual as $req ) {
2977 // Don't test this here.
2978 $req->required
= AuthenticationRequest
::REQUIRED
;
2980 usort( $actual, $cmpReqs );
2982 $this->assertEquals( $expectReqs, $actual );
2986 public static function provideGetAuthenticationRequests() {
2989 AuthManager
::ACTION_LOGIN
,
2990 [ 'pre-login', 'primary-none-login', 'primary-create-login',
2991 'primary-link-login', 'secondary-login', 'generic' ],
2994 AuthManager
::ACTION_CREATE
,
2995 [ 'pre-create', 'primary-none-create', 'primary-create-create',
2996 'primary-link-create', 'secondary-create', 'generic' ],
2999 AuthManager
::ACTION_LINK
,
3000 [ 'primary-link-link', 'generic' ],
3003 AuthManager
::ACTION_CHANGE
,
3004 [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
3005 'secondary-change' ],
3008 AuthManager
::ACTION_REMOVE
,
3009 [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
3010 'secondary-remove' ],
3013 AuthManager
::ACTION_UNLINK
,
3014 [ 'primary-link-remove' ],
3017 AuthManager
::ACTION_LOGIN_CONTINUE
,
3021 AuthManager
::ACTION_LOGIN_CONTINUE
,
3022 $reqs = [ 'continue-login', 'foo', 'bar' ],
3024 'continueRequests' => $reqs,
3028 AuthManager
::ACTION_CREATE_CONTINUE
,
3032 AuthManager
::ACTION_CREATE_CONTINUE
,
3033 $reqs = [ 'continue-create', 'foo', 'bar' ],
3035 'continueRequests' => $reqs,
3039 AuthManager
::ACTION_LINK_CONTINUE
,
3043 AuthManager
::ACTION_LINK_CONTINUE
,
3044 $reqs = [ 'continue-link', 'foo', 'bar' ],
3046 'continueRequests' => $reqs,
3052 public function testGetAuthenticationRequestsRequired() {
3053 $makeReq = function ( $key, $required ) {
3054 $req = $this->createMock( AuthenticationRequest
::class );
3055 $req->expects( $this->any() )->method( 'getUniqueId' )
3056 ->will( $this->returnValue( $key ) );
3057 $req->action
= AuthManager
::ACTION_LOGIN
;
3059 $req->required
= $required;
3062 $cmpReqs = function ( $a, $b ) {
3063 $ret = strcmp( get_class( $a ), get_class( $b ) );
3065 $ret = strcmp( $a->key
, $b->key
);
3070 $good = StatusValue
::newGood();
3072 $primary1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3073 $primary1->expects( $this->any() )->method( 'getUniqueId' )
3074 ->will( $this->returnValue( 'primary1' ) );
3075 $primary1->expects( $this->any() )->method( 'accountCreationType' )
3076 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3077 $primary1->expects( $this->any() )->method( 'getAuthenticationRequests' )
3078 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3080 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3081 $makeReq( "required", AuthenticationRequest
::REQUIRED
),
3082 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3083 $makeReq( "foo", AuthenticationRequest
::REQUIRED
),
3084 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3085 $makeReq( "baz", AuthenticationRequest
::OPTIONAL
),
3089 $primary2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3090 $primary2->expects( $this->any() )->method( 'getUniqueId' )
3091 ->will( $this->returnValue( 'primary2' ) );
3092 $primary2->expects( $this->any() )->method( 'accountCreationType' )
3093 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3094 $primary2->expects( $this->any() )->method( 'getAuthenticationRequests' )
3095 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3097 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3098 $makeReq( "required2", AuthenticationRequest
::REQUIRED
),
3099 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3103 $secondary = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3104 $secondary->expects( $this->any() )->method( 'getUniqueId' )
3105 ->will( $this->returnValue( 'secondary' ) );
3106 $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
3107 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3109 $makeReq( "foo", AuthenticationRequest
::OPTIONAL
),
3110 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3111 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3115 $rememberReq = new RememberMeAuthenticationRequest
;
3116 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
3118 $this->primaryauthMocks
= [ $primary1, $primary2 ];
3119 $this->secondaryauthMocks
= [ $secondary ];
3120 $this->initializeManager( true );
3122 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3125 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3126 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3127 $makeReq( "required2", AuthenticationRequest
::PRIMARY_REQUIRED
),
3128 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3129 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3130 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3131 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3132 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3134 usort( $actual, $cmpReqs );
3135 usort( $expected, $cmpReqs );
3136 $this->assertEquals( $expected, $actual );
3138 $this->primaryauthMocks
= [ $primary1 ];
3139 $this->secondaryauthMocks
= [ $secondary ];
3140 $this->initializeManager( true );
3142 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3145 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3146 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3147 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3148 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3149 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3150 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3152 usort( $actual, $cmpReqs );
3153 usort( $expected, $cmpReqs );
3154 $this->assertEquals( $expected, $actual );
3157 public function testAllowsPropertyChange() {
3159 foreach ( [ 'primary', 'secondary' ] as $key ) {
3160 $class = ucfirst( $key ) . 'AuthenticationProvider';
3161 $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
3162 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3163 ->will( $this->returnValue( $key ) );
3164 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
3165 ->will( $this->returnCallback( function ( $prop ) use ( $key ) {
3166 return $prop !== $key;
3170 $this->primaryauthMocks
= [ $mocks['primary'] ];
3171 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
3172 $this->initializeManager( true );
3174 $this->assertTrue( $this->manager
->allowsPropertyChange( 'foo' ) );
3175 $this->assertFalse( $this->manager
->allowsPropertyChange( 'primary' ) );
3176 $this->assertFalse( $this->manager
->allowsPropertyChange( 'secondary' ) );
3179 public function testAutoCreateOnLogin() {
3180 $username = self
::usernameForCreation();
3182 $req = $this->createMock( AuthenticationRequest
::class );
3184 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3185 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3186 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3187 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3188 $mock->expects( $this->any() )->method( 'accountCreationType' )
3189 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3190 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3191 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3192 ->will( $this->returnValue( StatusValue
::newGood() ) );
3194 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider
::class );
3195 $mock2->expects( $this->any() )->method( 'getUniqueId' )
3196 ->will( $this->returnValue( 'secondary' ) );
3197 $mock2->expects( $this->any() )->method( 'beginSecondaryAuthentication' )->will(
3199 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) )
3202 $mock2->expects( $this->any() )->method( 'continueSecondaryAuthentication' )
3203 ->will( $this->returnValue( AuthenticationResponse
::newAbstain() ) );
3204 $mock2->expects( $this->any() )->method( 'testUserForCreation' )
3205 ->will( $this->returnValue( StatusValue
::newGood() ) );
3207 $this->primaryauthMocks
= [ $mock ];
3208 $this->secondaryauthMocks
= [ $mock2 ];
3209 $this->initializeManager( true );
3210 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3211 $session = $this->request
->getSession();
3214 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3217 $callback = $this->callback( function ( $user ) use ( $username ) {
3218 return $user->getName() === $username;
3221 $this->hook( 'UserLoggedIn', $this->never() );
3222 $this->hook( 'LocalUserCreated', $this->once() )->with( $callback, $this->equalTo( true ) );
3223 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3224 $this->unhook( 'LocalUserCreated' );
3225 $this->unhook( 'UserLoggedIn' );
3226 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
3228 $id = (int)\User
::newFromName( $username )->getId();
3229 $this->assertNotSame( 0, \User
::newFromName( $username )->getId() );
3230 $this->assertSame( 0, $session->getUser()->getId() );
3232 $this->hook( 'UserLoggedIn', $this->once() )->with( $callback );
3233 $this->hook( 'LocalUserCreated', $this->never() );
3234 $ret = $this->manager
->continueAuthentication( [] );
3235 $this->unhook( 'LocalUserCreated' );
3236 $this->unhook( 'UserLoggedIn' );
3237 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
3238 $this->assertSame( $username, $ret->username
);
3239 $this->assertSame( $id, $session->getUser()->getId() );
3242 public function testAutoCreateFailOnLogin() {
3243 $username = self
::usernameForCreation();
3245 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3246 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3247 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3248 ->will( $this->returnValue( AuthenticationResponse
::newPass( $username ) ) );
3249 $mock->expects( $this->any() )->method( 'accountCreationType' )
3250 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3251 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3252 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3253 ->will( $this->returnValue( StatusValue
::newFatal( 'fail-from-primary' ) ) );
3255 $this->primaryauthMocks
= [ $mock ];
3256 $this->initializeManager( true );
3257 $this->manager
->setLogger( new \Psr\Log\
NullLogger() );
3258 $session = $this->request
->getSession();
3261 $this->assertSame( 0, $session->getUser()->getId(),
3263 $this->assertSame( 0, \User
::newFromName( $username )->getId(),
3266 $this->hook( 'UserLoggedIn', $this->never() );
3267 $this->hook( 'LocalUserCreated', $this->never() );
3268 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3269 $this->unhook( 'LocalUserCreated' );
3270 $this->unhook( 'UserLoggedIn' );
3271 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3272 $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message
->getKey() );
3274 $this->assertSame( 0, \User
::newFromName( $username )->getId() );
3275 $this->assertSame( 0, $session->getUser()->getId() );
3278 public function testAuthenticationSessionData() {
3279 $this->initializeManager( true );
3281 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3282 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3283 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3284 $this->assertSame( 'foo!', $this->manager
->getAuthenticationSessionData( 'foo' ) );
3285 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3286 $this->manager
->removeAuthenticationSessionData( 'foo' );
3287 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3288 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3289 $this->manager
->removeAuthenticationSessionData( 'bar' );
3290 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3292 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3293 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3294 $this->manager
->removeAuthenticationSessionData( null );
3295 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3296 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3299 public function testCanLinkAccounts() {
3301 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
3302 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
3303 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
3306 foreach ( $types as $type => $can ) {
3307 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3308 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
3309 $mock->expects( $this->any() )->method( 'accountCreationType' )
3310 ->will( $this->returnValue( $type ) );
3311 $this->primaryauthMocks
= [ $mock ];
3312 $this->initializeManager( true );
3313 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
3317 public function testBeginAccountLink() {
3318 $user = \User
::newFromName( 'UTSysop' );
3319 $this->initializeManager();
3321 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', 'test' );
3323 $this->manager
->beginAccountLink( $user, [], 'http://localhost/' );
3324 $this->fail( 'Expected exception not thrown' );
3325 } catch ( \LogicException
$ex ) {
3326 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3328 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3330 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3331 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3332 $mock->expects( $this->any() )->method( 'accountCreationType' )
3333 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3334 $this->primaryauthMocks
= [ $mock ];
3335 $this->initializeManager( true );
3337 $ret = $this->manager
->beginAccountLink( new \User
, [], 'http://localhost/' );
3338 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3339 $this->assertSame( 'noname', $ret->message
->getKey() );
3341 $ret = $this->manager
->beginAccountLink(
3342 \User
::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
3344 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3345 $this->assertSame( 'authmanager-userdoesnotexist', $ret->message
->getKey() );
3348 public function testContinueAccountLink() {
3349 $user = \User
::newFromName( 'UTSysop' );
3350 $this->initializeManager();
3353 'userid' => $user->getId(),
3354 'username' => $user->getName(),
3359 $this->manager
->continueAccountLink( [] );
3360 $this->fail( 'Expected exception not thrown' );
3361 } catch ( \LogicException
$ex ) {
3362 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3365 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider
::class );
3366 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3367 $mock->expects( $this->any() )->method( 'accountCreationType' )
3368 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3369 $mock->expects( $this->any() )->method( 'beginPrimaryAccountLink' )->will(
3370 $this->returnValue( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) )
3372 $this->primaryauthMocks
= [ $mock ];
3373 $this->initializeManager( true );
3375 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState', null );
3376 $ret = $this->manager
->continueAccountLink( [] );
3377 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3378 $this->assertSame( 'authmanager-link-not-in-progress', $ret->message
->getKey() );
3380 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3381 [ 'username' => $user->getName() . '<>' ] +
$session );
3382 $ret = $this->manager
->continueAccountLink( [] );
3383 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3384 $this->assertSame( 'noname', $ret->message
->getKey() );
3385 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3387 $id = $user->getId();
3388 $this->request
->getSession()->setSecret( 'AuthManager::accountLinkState',
3389 [ 'userid' => $id +
1 ] +
$session );
3391 $ret = $this->manager
->continueAccountLink( [] );
3392 $this->fail( 'Expected exception not thrown' );
3393 } catch ( \UnexpectedValueException
$ex ) {
3394 $this->assertEquals(
3395 "User \"{$user->getName()}\" is valid, but ID $id !== " . ( $id +
1 ) . '!',
3399 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3403 * @dataProvider provideAccountLink
3404 * @param StatusValue $preTest
3405 * @param array $primaryResponses
3406 * @param array $managerResponses
3408 public function testAccountLink(
3409 StatusValue
$preTest, array $primaryResponses, array $managerResponses
3411 $user = \User
::newFromName( 'UTSysop' );
3413 $this->initializeManager();
3415 // Set up lots of mocks...
3416 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3417 $req->primary
= $primaryResponses;
3420 foreach ( [ 'pre', 'primary' ] as $key ) {
3421 $class = ucfirst( $key ) . 'AuthenticationProvider';
3422 $mocks[$key] = $this->getMockForAbstractClass(
3423 "MediaWiki\\Auth\\$class", [], "Mock$class"
3425 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3426 ->will( $this->returnValue( $key ) );
3428 for ( $i = 2; $i <= 3; $i++
) {
3429 $mocks[$key . $i] = $this->getMockForAbstractClass(
3430 "MediaWiki\\Auth\\$class", [], "Mock$class"
3432 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
3433 ->will( $this->returnValue( $key . $i ) );
3437 $mocks['pre']->expects( $this->any() )->method( 'testForAccountLink' )
3438 ->will( $this->returnCallback(
3440 use ( $user, $preTest )
3442 $this->assertSame( $user->getId(), $u->getId() );
3443 $this->assertSame( $user->getName(), $u->getName() );
3448 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
3449 ->will( $this->returnValue( StatusValue
::newGood() ) );
3451 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
3452 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3453 $ct = count( $req->primary
);
3454 $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req ) {
3455 $this->assertSame( $user->getId(), $u->getId() );
3456 $this->assertSame( $user->getName(), $u->getName() );
3458 foreach ( $reqs as $r ) {
3459 $this->assertSame( $user->getName(), $r->username
);
3460 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
3462 $this->assertTrue( $foundReq, '$reqs contains $req' );
3463 return array_shift( $req->primary
);
3465 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
3466 ->method( 'beginPrimaryAccountLink' )
3467 ->will( $callback );
3468 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
3469 ->method( 'continuePrimaryAccountLink' )
3470 ->will( $callback );
3472 $abstain = AuthenticationResponse
::newAbstain();
3473 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
3474 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_LINK
) );
3475 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
3476 ->will( $this->returnValue( $abstain ) );
3477 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3478 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
3479 ->will( $this->returnValue( PrimaryAuthenticationProvider
::TYPE_CREATE
) );
3480 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
3481 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3483 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
3484 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
3485 $this->logger
= new \
TestLogger( true, function ( $message, $level ) {
3486 return $level === LogLevel
::DEBUG ?
null : $message;
3488 $this->initializeManager( true );
3490 $constraint = \PHPUnit_Framework_Assert
::logicalOr(
3491 $this->equalTo( AuthenticationResponse
::PASS
),
3492 $this->equalTo( AuthenticationResponse
::FAIL
)
3494 $providers = array_merge( $this->preauthMocks
, $this->primaryauthMocks
);
3495 foreach ( $providers as $p ) {
3496 $p->postCalled
= false;
3497 $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
3498 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
3499 $this->assertInstanceOf( \User
::class, $user );
3500 $this->assertSame( 'UTSysop', $user->getName() );
3501 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
3502 $this->assertThat( $response->status
, $constraint );
3503 $p->postCalled
= $response->status
;
3510 foreach ( $managerResponses as $i => $response ) {
3511 if ( $response instanceof AuthenticationResponse
&&
3512 $response->status
=== AuthenticationResponse
::PASS
3514 $expectLog[] = [ LogLevel
::INFO
, 'Account linked to {user} by primary' ];
3520 $ret = $this->manager
->beginAccountLink( $user, [ $req ], 'http://localhost/' );
3522 $ret = $this->manager
->continueAccountLink( [ $req ] );
3524 if ( $response instanceof \Exception
) {
3525 $this->fail( 'Expected exception not thrown', "Response $i" );
3527 } catch ( \Exception
$ex ) {
3528 if ( !$response instanceof \Exception
) {
3531 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
3532 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3533 "Response $i, exception, session state" );
3537 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
3539 $ret->message
= $this->message( $ret->message
);
3540 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
3541 if ( $response->status
=== AuthenticationResponse
::PASS ||
3542 $response->status
=== AuthenticationResponse
::FAIL
3544 $this->assertNull( $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3545 "Response $i, session state" );
3546 foreach ( $providers as $p ) {
3547 $this->assertSame( $response->status
, $p->postCalled
,
3548 "Response $i, post-auth callback called" );
3551 $this->assertNotNull(
3552 $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3553 "Response $i, session state"
3555 foreach ( $ret->neededRequests
as $neededReq ) {
3556 $this->assertEquals( AuthManager
::ACTION_LINK
, $neededReq->action
,
3557 "Response $i, neededRequest action" );
3559 $this->assertEquals(
3560 $ret->neededRequests
,
3561 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LINK_CONTINUE
),
3562 "Response $i, continuation check"
3564 foreach ( $providers as $p ) {
3565 $this->assertFalse( $p->postCalled
, "Response $i, post-auth callback not called" );
3572 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
3575 public function provideAccountLink() {
3576 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3577 $good = StatusValue
::newGood();
3580 'Pre-link test fail in pre' => [
3581 StatusValue
::newFatal( 'fail-from-pre' ),
3584 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
3587 'Failure in primary' => [
3590 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
3594 'All primary abstain' => [
3597 AuthenticationResponse
::newAbstain(),
3600 AuthenticationResponse
::newFail( $this->message( 'authmanager-link-no-primary' ) )
3603 'Primary UI, then redirect, then fail' => [
3606 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3607 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
3608 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
3612 'Primary redirect, then abstain' => [
3615 $tmp = AuthenticationResponse
::newRedirect(
3616 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
3618 AuthenticationResponse
::newAbstain(),
3622 new \
DomainException(
3623 'MockPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
3627 'Primary UI, then pass' => [
3630 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
3631 AuthenticationResponse
::newPass(),
3635 AuthenticationResponse
::newPass( '' ),
3641 AuthenticationResponse
::newPass( '' ),
3644 AuthenticationResponse
::newPass( '' ),