Merge "Speed up password-handling in the unit tests"
[lhc/web/wiklou.git] / tests / phpunit / includes / auth / AuthManagerTest.php
1 <?php
2
3 namespace MediaWiki\Auth;
4
5 use MediaWiki\Session\SessionInfo;
6 use MediaWiki\Session\UserInfo;
7 use Psr\Log\LogLevel;
8 use StatusValue;
9
10 /**
11 * @group AuthManager
12 * @group Database
13 * @covers MediaWiki\Auth\AuthManager
14 */
15 class AuthManagerTest extends \MediaWikiTestCase {
16 /** @var WebRequest */
17 protected $request;
18 /** @var Config */
19 protected $config;
20 /** @var \\Psr\\Log\\LoggerInterface */
21 protected $logger;
22
23 protected $preauthMocks = [];
24 protected $primaryauthMocks = [];
25 protected $secondaryauthMocks = [];
26
27 /** @var AuthManager */
28 protected $manager;
29 /** @var TestingAccessWrapper */
30 protected $managerPriv;
31
32 protected function setUp() {
33 global $wgDisableAuthManager;
34
35 parent::setUp();
36 if ( $wgDisableAuthManager ) {
37 $this->markTestSkipped( '$wgDisableAuthManager is set' );
38 }
39
40 $this->setMwGlobals( [ 'wgAuth' => null ] );
41 $this->stashMwGlobals( [ 'wgHooks' ] );
42 }
43
44 /**
45 * Sets a mock on a hook
46 * @param string $hook
47 * @param object $expect From $this->once(), $this->never(), etc.
48 * @return object $mock->expects( $expect )->method( ... ).
49 */
50 protected function hook( $hook, $expect ) {
51 global $wgHooks;
52 $mock = $this->getMock( __CLASS__, [ "on$hook" ] );
53 $wgHooks[$hook] = [ $mock ];
54 return $mock->expects( $expect )->method( "on$hook" );
55 }
56
57 /**
58 * Unsets a hook
59 * @param string $hook
60 */
61 protected function unhook( $hook ) {
62 global $wgHooks;
63 $wgHooks[$hook] = [];
64 }
65
66 /**
67 * Ensure a value is a clean Message object
68 * @param string|Message $key
69 * @param array $params
70 * @return Message
71 */
72 protected function message( $key, $params = [] ) {
73 if ( $key === null ) {
74 return null;
75 }
76 if ( $key instanceof \MessageSpecifier ) {
77 $params = $key->getParams();
78 $key = $key->getKey();
79 }
80 return new \Message( $key, $params, \Language::factory( 'en' ) );
81 }
82
83 /**
84 * Initialize the AuthManagerConfig variable in $this->config
85 *
86 * Uses data from the various 'mocks' fields.
87 */
88 protected function initializeConfig() {
89 $config = [
90 'preauth' => [
91 ],
92 'primaryauth' => [
93 ],
94 'secondaryauth' => [
95 ],
96 ];
97
98 foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
99 $key = $type . 'Mocks';
100 foreach ( $this->$key as $mock ) {
101 $config[$type][$mock->getUniqueId()] = [ 'factory' => function () use ( $mock ) {
102 return $mock;
103 } ];
104 }
105 }
106
107 $this->config->set( 'AuthManagerConfig', $config );
108 $this->config->set( 'LanguageCode', 'en' );
109 $this->config->set( 'NewUserLog', false );
110 }
111
112 /**
113 * Initialize $this->manager
114 * @param bool $regen Force a call to $this->initializeConfig()
115 */
116 protected function initializeManager( $regen = false ) {
117 if ( $regen || !$this->config ) {
118 $this->config = new \HashConfig();
119 }
120 if ( $regen || !$this->request ) {
121 $this->request = new \FauxRequest();
122 }
123 if ( !$this->logger ) {
124 $this->logger = new \TestLogger();
125 }
126
127 if ( $regen || !$this->config->has( 'AuthManagerConfig' ) ) {
128 $this->initializeConfig();
129 }
130 $this->manager = new AuthManager( $this->request, $this->config );
131 $this->manager->setLogger( $this->logger );
132 $this->managerPriv = \TestingAccessWrapper::newFromObject( $this->manager );
133 }
134
135 /**
136 * Setup SessionManager with a mock session provider
137 * @param bool|null $canChangeUser If non-null, canChangeUser will be mocked to return this
138 * @param array $methods Additional methods to mock
139 * @return array (MediaWiki\Session\SessionProvider, ScopedCallback)
140 */
141 protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
142 if ( !$this->config ) {
143 $this->config = new \HashConfig();
144 $this->initializeConfig();
145 }
146 $this->config->set( 'ObjectCacheSessionExpiry', 100 );
147
148 $methods[] = '__toString';
149 $methods[] = 'describe';
150 if ( $canChangeUser !== null ) {
151 $methods[] = 'canChangeUser';
152 }
153 $provider = $this->getMockBuilder( 'DummySessionProvider' )
154 ->setMethods( $methods )
155 ->getMock();
156 $provider->expects( $this->any() )->method( '__toString' )
157 ->will( $this->returnValue( 'MockSessionProvider' ) );
158 $provider->expects( $this->any() )->method( 'describe' )
159 ->will( $this->returnValue( 'MockSessionProvider sessions' ) );
160 if ( $canChangeUser !== null ) {
161 $provider->expects( $this->any() )->method( 'canChangeUser' )
162 ->will( $this->returnValue( $canChangeUser ) );
163 }
164 $this->config->set( 'SessionProviders', [
165 [ 'factory' => function () use ( $provider ) {
166 return $provider;
167 } ],
168 ] );
169
170 $manager = new \MediaWiki\Session\SessionManager( [
171 'config' => $this->config,
172 'logger' => new \Psr\Log\NullLogger(),
173 'store' => new \HashBagOStuff(),
174 ] );
175 \TestingAccessWrapper::newFromObject( $manager )->getProvider( (string)$provider );
176
177 $reset = \MediaWiki\Session\TestUtils::setSessionManagerSingleton( $manager );
178
179 if ( $this->request ) {
180 $manager->getSessionForRequest( $this->request );
181 }
182
183 return [ $provider, $reset ];
184 }
185
186 public function testSingleton() {
187 // Temporarily clear out the global singleton, if any, to test creating
188 // one.
189 $rProp = new \ReflectionProperty( AuthManager::class, 'instance' );
190 $rProp->setAccessible( true );
191 $old = $rProp->getValue();
192 $cb = new \ScopedCallback( [ $rProp, 'setValue' ], [ $old ] );
193 $rProp->setValue( null );
194
195 $singleton = AuthManager::singleton();
196 $this->assertInstanceOf( AuthManager::class, AuthManager::singleton() );
197 $this->assertSame( $singleton, AuthManager::singleton() );
198 $this->assertSame( \RequestContext::getMain()->getRequest(), $singleton->getRequest() );
199 $this->assertSame(
200 \RequestContext::getMain()->getConfig(),
201 \TestingAccessWrapper::newFromObject( $singleton )->config
202 );
203
204 $this->setMwGlobals( [ 'wgDisableAuthManager' => true ] );
205 try {
206 AuthManager::singleton();
207 $this->fail( 'Expected exception not thrown' );
208 } catch ( \BadMethodCallException $ex ) {
209 $this->assertSame( '$wgDisableAuthManager is set', $ex->getMessage() );
210 }
211 }
212
213 public function testCanAuthenticateNow() {
214 $this->initializeManager();
215
216 list( $provider, $reset ) = $this->getMockSessionProvider( false );
217 $this->assertFalse( $this->manager->canAuthenticateNow() );
218 \ScopedCallback::consume( $reset );
219
220 list( $provider, $reset ) = $this->getMockSessionProvider( true );
221 $this->assertTrue( $this->manager->canAuthenticateNow() );
222 \ScopedCallback::consume( $reset );
223 }
224
225 public function testNormalizeUsername() {
226 $mocks = [
227 $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
228 $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
229 $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
230 $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
231 ];
232 foreach ( $mocks as $key => $mock ) {
233 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
234 }
235 $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
236 ->with( $this->identicalTo( 'XYZ' ) )
237 ->willReturn( 'Foo' );
238 $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
239 ->with( $this->identicalTo( 'XYZ' ) )
240 ->willReturn( 'Foo' );
241 $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
242 ->with( $this->identicalTo( 'XYZ' ) )
243 ->willReturn( null );
244 $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
245 ->with( $this->identicalTo( 'XYZ' ) )
246 ->willReturn( 'Bar!' );
247
248 $this->primaryauthMocks = $mocks;
249
250 $this->initializeManager();
251
252 $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager->normalizeUsername( 'XYZ' ) );
253 }
254
255 /**
256 * @dataProvider provideSecuritySensitiveOperationStatus
257 * @param bool $mutableSession
258 */
259 public function testSecuritySensitiveOperationStatus( $mutableSession ) {
260 $this->logger = new \Psr\Log\NullLogger();
261 $user = \User::newFromName( 'UTSysop' );
262 $provideUser = null;
263 $reauth = $mutableSession ? AuthManager::SEC_REAUTH : AuthManager::SEC_FAIL;
264
265 list( $provider, $reset ) = $this->getMockSessionProvider(
266 $mutableSession, [ 'provideSessionInfo' ]
267 );
268 $provider->expects( $this->any() )->method( 'provideSessionInfo' )
269 ->will( $this->returnCallback( function () use ( $provider, &$provideUser ) {
270 return new SessionInfo( SessionInfo::MIN_PRIORITY, [
271 'provider' => $provider,
272 'id' => \DummySessionProvider::ID,
273 'persisted' => true,
274 'userInfo' => UserInfo::newFromUser( $provideUser, true )
275 ] );
276 } ) );
277 $this->initializeManager();
278
279 $this->config->set( 'ReauthenticateTime', [] );
280 $this->config->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [] );
281 $provideUser = new \User;
282 $session = $provider->getManager()->getSessionForRequest( $this->request );
283 $this->assertSame( 0, $session->getUser()->getId(), 'sanity check' );
284
285 // Anonymous user => reauth
286 $session->set( 'AuthManager:lastAuthId', 0 );
287 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
288 $this->assertSame( $reauth, $this->manager->securitySensitiveOperationStatus( 'foo' ) );
289
290 $provideUser = $user;
291 $session = $provider->getManager()->getSessionForRequest( $this->request );
292 $this->assertSame( $user->getId(), $session->getUser()->getId(), 'sanity check' );
293
294 // Error for no default (only gets thrown for non-anonymous user)
295 $session->set( 'AuthManager:lastAuthId', $user->getId() + 1 );
296 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
297 try {
298 $this->manager->securitySensitiveOperationStatus( 'foo' );
299 $this->fail( 'Expected exception not thrown' );
300 } catch ( \UnexpectedValueException $ex ) {
301 $this->assertSame(
302 $mutableSession
303 ? '$wgReauthenticateTime lacks a default'
304 : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
305 $ex->getMessage()
306 );
307 }
308
309 if ( $mutableSession ) {
310 $this->config->set( 'ReauthenticateTime', [
311 'test' => 100,
312 'test2' => -1,
313 'default' => 10,
314 ] );
315
316 // Mismatched user ID
317 $session->set( 'AuthManager:lastAuthId', $user->getId() + 1 );
318 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
319 $this->assertSame(
320 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
321 );
322 $this->assertSame(
323 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
324 );
325 $this->assertSame(
326 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
327 );
328
329 // Missing time
330 $session->set( 'AuthManager:lastAuthId', $user->getId() );
331 $session->set( 'AuthManager:lastAuthTimestamp', null );
332 $this->assertSame(
333 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
334 );
335 $this->assertSame(
336 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
337 );
338 $this->assertSame(
339 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
340 );
341
342 // Recent enough to pass
343 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
344 $this->assertSame(
345 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
346 );
347
348 // Not recent enough to pass
349 $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
350 $this->assertSame(
351 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
352 );
353 // But recent enough for the 'test' operation
354 $this->assertSame(
355 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test' )
356 );
357 } else {
358 $this->config->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [
359 'test' => false,
360 'default' => true,
361 ] );
362
363 $this->assertEquals(
364 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
365 );
366
367 $this->assertEquals(
368 AuthManager::SEC_FAIL, $this->manager->securitySensitiveOperationStatus( 'test' )
369 );
370 }
371
372 // Test hook, all three possible values
373 foreach ( [
374 AuthManager::SEC_OK => AuthManager::SEC_OK,
375 AuthManager::SEC_REAUTH => $reauth,
376 AuthManager::SEC_FAIL => AuthManager::SEC_FAIL,
377 ] as $hook => $expect ) {
378 $this->hook( 'SecuritySensitiveOperationStatus', $this->exactly( 2 ) )
379 ->with(
380 $this->anything(),
381 $this->anything(),
382 $this->callback( function ( $s ) use ( $session ) {
383 return $s->getId() === $session->getId();
384 } ),
385 $mutableSession ? $this->equalTo( 500, 1 ) : $this->equalTo( -1 )
386 )
387 ->will( $this->returnCallback( function ( &$v ) use ( $hook ) {
388 $v = $hook;
389 return true;
390 } ) );
391 $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
392 $this->assertEquals(
393 $expect, $this->manager->securitySensitiveOperationStatus( 'test' ), "hook $hook"
394 );
395 $this->assertEquals(
396 $expect, $this->manager->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
397 );
398 $this->unhook( 'SecuritySensitiveOperationStatus' );
399 }
400
401 \ScopedCallback::consume( $reset );
402 }
403
404 public function onSecuritySensitiveOperationStatus( &$status, $operation, $session, $time ) {
405 }
406
407 public static function provideSecuritySensitiveOperationStatus() {
408 return [
409 [ true ],
410 [ false ],
411 ];
412 }
413
414 /**
415 * @dataProvider provideUserCanAuthenticate
416 * @param bool $primary1Can
417 * @param bool $primary2Can
418 * @param bool $expect
419 */
420 public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
421 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
422 $mock1->expects( $this->any() )->method( 'getUniqueId' )
423 ->will( $this->returnValue( 'primary1' ) );
424 $mock1->expects( $this->any() )->method( 'testUserCanAuthenticate' )
425 ->with( $this->equalTo( 'UTSysop' ) )
426 ->will( $this->returnValue( $primary1Can ) );
427 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
428 $mock2->expects( $this->any() )->method( 'getUniqueId' )
429 ->will( $this->returnValue( 'primary2' ) );
430 $mock2->expects( $this->any() )->method( 'testUserCanAuthenticate' )
431 ->with( $this->equalTo( 'UTSysop' ) )
432 ->will( $this->returnValue( $primary2Can ) );
433 $this->primaryauthMocks = [ $mock1, $mock2 ];
434
435 $this->initializeManager( true );
436 $this->assertSame( $expect, $this->manager->userCanAuthenticate( 'UTSysop' ) );
437 }
438
439 public static function provideUserCanAuthenticate() {
440 return [
441 [ false, false, false ],
442 [ true, false, true ],
443 [ false, true, true ],
444 [ true, true, true ],
445 ];
446 }
447
448 public function testRevokeAccessForUser() {
449 $this->initializeManager();
450
451 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
452 $mock->expects( $this->any() )->method( 'getUniqueId' )
453 ->will( $this->returnValue( 'primary' ) );
454 $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
455 ->with( $this->equalTo( 'UTSysop' ) );
456 $this->primaryauthMocks = [ $mock ];
457
458 $this->initializeManager( true );
459 $this->logger->setCollect( true );
460
461 $this->manager->revokeAccessForUser( 'UTSysop' );
462
463 $this->assertSame( [
464 [ LogLevel::INFO, 'Revoking access for {user}' ],
465 ], $this->logger->getBuffer() );
466 }
467
468 public function testProviderCreation() {
469 $mocks = [
470 'pre' => $this->getMockForAbstractClass( PreAuthenticationProvider::class ),
471 'primary' => $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
472 'secondary' => $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class ),
473 ];
474 foreach ( $mocks as $key => $mock ) {
475 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
476 $mock->expects( $this->once() )->method( 'setLogger' );
477 $mock->expects( $this->once() )->method( 'setManager' );
478 $mock->expects( $this->once() )->method( 'setConfig' );
479 }
480 $this->preauthMocks = [ $mocks['pre'] ];
481 $this->primaryauthMocks = [ $mocks['primary'] ];
482 $this->secondaryauthMocks = [ $mocks['secondary'] ];
483
484 // Normal operation
485 $this->initializeManager();
486 $this->assertSame(
487 $mocks['primary'],
488 $this->managerPriv->getAuthenticationProvider( 'primary' )
489 );
490 $this->assertSame(
491 $mocks['secondary'],
492 $this->managerPriv->getAuthenticationProvider( 'secondary' )
493 );
494 $this->assertSame(
495 $mocks['pre'],
496 $this->managerPriv->getAuthenticationProvider( 'pre' )
497 );
498 $this->assertSame(
499 [ 'pre' => $mocks['pre'] ],
500 $this->managerPriv->getPreAuthenticationProviders()
501 );
502 $this->assertSame(
503 [ 'primary' => $mocks['primary'] ],
504 $this->managerPriv->getPrimaryAuthenticationProviders()
505 );
506 $this->assertSame(
507 [ 'secondary' => $mocks['secondary'] ],
508 $this->managerPriv->getSecondaryAuthenticationProviders()
509 );
510
511 // Duplicate IDs
512 $mock1 = $this->getMockForAbstractClass( PreAuthenticationProvider::class );
513 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
514 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
515 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
516 $this->preauthMocks = [ $mock1 ];
517 $this->primaryauthMocks = [ $mock2 ];
518 $this->secondaryauthMocks = [];
519 $this->initializeManager( true );
520 try {
521 $this->managerPriv->getAuthenticationProvider( 'Y' );
522 $this->fail( 'Expected exception not thrown' );
523 } catch ( \RuntimeException $ex ) {
524 $class1 = get_class( $mock1 );
525 $class2 = get_class( $mock2 );
526 $this->assertSame(
527 "Duplicate specifications for id X (classes $class1 and $class2)", $ex->getMessage()
528 );
529 }
530
531 // Wrong classes
532 $mock = $this->getMockForAbstractClass( AuthenticationProvider::class );
533 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
534 $class = get_class( $mock );
535 $this->preauthMocks = [ $mock ];
536 $this->primaryauthMocks = [ $mock ];
537 $this->secondaryauthMocks = [ $mock ];
538 $this->initializeManager( true );
539 try {
540 $this->managerPriv->getPreAuthenticationProviders();
541 $this->fail( 'Expected exception not thrown' );
542 } catch ( \RuntimeException $ex ) {
543 $this->assertSame(
544 "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
545 $ex->getMessage()
546 );
547 }
548 try {
549 $this->managerPriv->getPrimaryAuthenticationProviders();
550 $this->fail( 'Expected exception not thrown' );
551 } catch ( \RuntimeException $ex ) {
552 $this->assertSame(
553 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
554 $ex->getMessage()
555 );
556 }
557 try {
558 $this->managerPriv->getSecondaryAuthenticationProviders();
559 $this->fail( 'Expected exception not thrown' );
560 } catch ( \RuntimeException $ex ) {
561 $this->assertSame(
562 "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
563 $ex->getMessage()
564 );
565 }
566
567 // Sorting
568 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
569 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
570 $mock3 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
571 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
572 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
573 $mock3->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'C' ) );
574 $this->preauthMocks = [];
575 $this->primaryauthMocks = [ $mock1, $mock2, $mock3 ];
576 $this->secondaryauthMocks = [];
577 $this->initializeConfig();
578 $config = $this->config->get( 'AuthManagerConfig' );
579
580 $this->initializeManager( false );
581 $this->assertSame(
582 [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
583 $this->managerPriv->getPrimaryAuthenticationProviders(),
584 'sanity check'
585 );
586
587 $config['primaryauth']['A']['sort'] = 100;
588 $config['primaryauth']['C']['sort'] = -1;
589 $this->config->set( 'AuthManagerConfig', $config );
590 $this->initializeManager( false );
591 $this->assertSame(
592 [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
593 $this->managerPriv->getPrimaryAuthenticationProviders()
594 );
595 }
596
597 public function testSetDefaultUserOptions() {
598 $this->initializeManager();
599
600 $context = \RequestContext::getMain();
601 $reset = new \ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
602 $context->setLanguage( 'de' );
603 $this->setMwGlobals( 'wgContLang', \Language::factory( 'zh' ) );
604
605 $user = \User::newFromName( self::usernameForCreation() );
606 $user->addToDatabase();
607 $oldToken = $user->getToken();
608 $this->managerPriv->setDefaultUserOptions( $user, false );
609 $user->saveSettings();
610 $this->assertNotEquals( $oldToken, $user->getToken() );
611 $this->assertSame( 'zh', $user->getOption( 'language' ) );
612 $this->assertSame( 'zh', $user->getOption( 'variant' ) );
613
614 $user = \User::newFromName( self::usernameForCreation() );
615 $user->addToDatabase();
616 $oldToken = $user->getToken();
617 $this->managerPriv->setDefaultUserOptions( $user, true );
618 $user->saveSettings();
619 $this->assertNotEquals( $oldToken, $user->getToken() );
620 $this->assertSame( 'de', $user->getOption( 'language' ) );
621 $this->assertSame( 'zh', $user->getOption( 'variant' ) );
622
623 $this->setMwGlobals( 'wgContLang', \Language::factory( 'en' ) );
624
625 $user = \User::newFromName( self::usernameForCreation() );
626 $user->addToDatabase();
627 $oldToken = $user->getToken();
628 $this->managerPriv->setDefaultUserOptions( $user, true );
629 $user->saveSettings();
630 $this->assertNotEquals( $oldToken, $user->getToken() );
631 $this->assertSame( 'de', $user->getOption( 'language' ) );
632 $this->assertSame( null, $user->getOption( 'variant' ) );
633 }
634
635 public function testForcePrimaryAuthenticationProviders() {
636 $mockA = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
637 $mockB = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
638 $mockB2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
639 $mockA->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
640 $mockB->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
641 $mockB2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
642 $this->primaryauthMocks = [ $mockA ];
643
644 $this->logger = new \TestLogger( true );
645
646 // Test without first initializing the configured providers
647 $this->initializeManager();
648 $this->manager->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
649 $this->assertSame(
650 [ 'B' => $mockB ], $this->managerPriv->getPrimaryAuthenticationProviders()
651 );
652 $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'A' ) );
653 $this->assertSame( $mockB, $this->managerPriv->getAuthenticationProvider( 'B' ) );
654 $this->assertSame( [
655 [ LogLevel::WARNING, 'Overriding AuthManager primary authn because testing' ],
656 ], $this->logger->getBuffer() );
657 $this->logger->clearBuffer();
658
659 // Test with first initializing the configured providers
660 $this->initializeManager();
661 $this->assertSame( $mockA, $this->managerPriv->getAuthenticationProvider( 'A' ) );
662 $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'B' ) );
663 $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
664 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
665 $this->manager->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
666 $this->assertSame(
667 [ 'B' => $mockB ], $this->managerPriv->getPrimaryAuthenticationProviders()
668 );
669 $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'A' ) );
670 $this->assertSame( $mockB, $this->managerPriv->getAuthenticationProvider( 'B' ) );
671 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
672 $this->assertNull(
673 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
674 );
675 $this->assertSame( [
676 [ LogLevel::WARNING, 'Overriding AuthManager primary authn because testing' ],
677 [
678 LogLevel::WARNING,
679 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
680 ],
681 ], $this->logger->getBuffer() );
682 $this->logger->clearBuffer();
683
684 // Test duplicate IDs
685 $this->initializeManager();
686 try {
687 $this->manager->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
688 $this->fail( 'Expected exception not thrown' );
689 } catch ( \RuntimeException $ex ) {
690 $class1 = get_class( $mockB );
691 $class2 = get_class( $mockB2 );
692 $this->assertSame(
693 "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
694 );
695 }
696
697 // Wrong classes
698 $mock = $this->getMockForAbstractClass( AuthenticationProvider::class );
699 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
700 $class = get_class( $mock );
701 try {
702 $this->manager->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
703 $this->fail( 'Expected exception not thrown' );
704 } catch ( \RuntimeException $ex ) {
705 $this->assertSame(
706 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
707 $ex->getMessage()
708 );
709 }
710
711 }
712
713 public function testBeginAuthentication() {
714 $this->initializeManager();
715
716 // Immutable session
717 list( $provider, $reset ) = $this->getMockSessionProvider( false );
718 $this->hook( 'UserLoggedIn', $this->never() );
719 $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
720 try {
721 $this->manager->beginAuthentication( [], 'http://localhost/' );
722 $this->fail( 'Expected exception not thrown' );
723 } catch ( \LogicException $ex ) {
724 $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
725 }
726 $this->unhook( 'UserLoggedIn' );
727 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
728 \ScopedCallback::consume( $reset );
729 $this->initializeManager( true );
730
731 // CreatedAccountAuthenticationRequest
732 $user = \User::newFromName( 'UTSysop' );
733 $reqs = [
734 new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
735 ];
736 $this->hook( 'UserLoggedIn', $this->never() );
737 try {
738 $this->manager->beginAuthentication( $reqs, 'http://localhost/' );
739 $this->fail( 'Expected exception not thrown' );
740 } catch ( \LogicException $ex ) {
741 $this->assertSame(
742 'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
743 'that created the account',
744 $ex->getMessage()
745 );
746 }
747 $this->unhook( 'UserLoggedIn' );
748
749 $this->request->getSession()->clear();
750 $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
751 $this->managerPriv->createdAccountAuthenticationRequests = [ $reqs[0] ];
752 $this->hook( 'UserLoggedIn', $this->once() )
753 ->with( $this->callback( function ( $u ) use ( $user ) {
754 return $user->getId() === $u->getId() && $user->getName() === $u->getName();
755 } ) );
756 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
757 $this->logger->setCollect( true );
758 $ret = $this->manager->beginAuthentication( $reqs, 'http://localhost/' );
759 $this->logger->setCollect( false );
760 $this->unhook( 'UserLoggedIn' );
761 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
762 $this->assertSame( AuthenticationResponse::PASS, $ret->status );
763 $this->assertSame( $user->getName(), $ret->username );
764 $this->assertSame( $user->getId(), $this->request->getSessionData( 'AuthManager:lastAuthId' ) );
765 $this->assertEquals(
766 time(), $this->request->getSessionData( 'AuthManager:lastAuthTimestamp' ),
767 'timestamp ±1', 1
768 );
769 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
770 $this->assertSame( $user->getId(), $this->request->getSession()->getUser()->getId() );
771 $this->assertSame( [
772 [ LogLevel::INFO, 'Logging in {user} after account creation' ],
773 ], $this->logger->getBuffer() );
774 }
775
776 public function testCreateFromLogin() {
777 $user = \User::newFromName( 'UTSysop' );
778 $req1 = $this->getMock( AuthenticationRequest::class );
779 $req2 = $this->getMock( AuthenticationRequest::class );
780 $req3 = $this->getMock( AuthenticationRequest::class );
781 $userReq = new UsernameAuthenticationRequest;
782 $userReq->username = 'UTDummy';
783
784 // Passing one into beginAuthentication(), and an immediate FAIL
785 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
786 $this->primaryauthMocks = [ $primary ];
787 $this->initializeManager( true );
788 $res = AuthenticationResponse::newFail( wfMessage( 'foo' ) );
789 $res->createRequest = $req1;
790 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
791 ->will( $this->returnValue( $res ) );
792 $createReq = new CreateFromLoginAuthenticationRequest(
793 null, [ $req2->getUniqueId() => $req2 ]
794 );
795 $this->logger->setCollect( true );
796 $ret = $this->manager->beginAuthentication( [ $createReq ], 'http://localhost/' );
797 $this->logger->setCollect( false );
798 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
799 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->createRequest );
800 $this->assertSame( $req1, $ret->createRequest->createRequest );
801 $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest->maybeLink );
802
803 // UI, then FAIL in beginAuthentication()
804 $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider::class )
805 ->setMethods( [ 'continuePrimaryAuthentication' ] )
806 ->getMockForAbstractClass();
807 $this->primaryauthMocks = [ $primary ];
808 $this->initializeManager( true );
809 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
810 ->will( $this->returnValue(
811 AuthenticationResponse::newUI( [ $req1 ], wfMessage( 'foo' ) )
812 ) );
813 $res = AuthenticationResponse::newFail( wfMessage( 'foo' ) );
814 $res->createRequest = $req2;
815 $primary->expects( $this->any() )->method( 'continuePrimaryAuthentication' )
816 ->will( $this->returnValue( $res ) );
817 $this->logger->setCollect( true );
818 $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
819 $this->assertSame( AuthenticationResponse::UI, $ret->status, 'sanity check' );
820 $ret = $this->manager->continueAuthentication( [] );
821 $this->logger->setCollect( false );
822 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
823 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->createRequest );
824 $this->assertSame( $req2, $ret->createRequest->createRequest );
825 $this->assertEquals( [], $ret->createRequest->maybeLink );
826
827 // Pass into beginAccountCreation(), no createRequest, primary needs reqs
828 $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider::class )
829 ->setMethods( [ 'testForAccountCreation' ] )
830 ->getMockForAbstractClass();
831 $this->primaryauthMocks = [ $primary ];
832 $this->initializeManager( true );
833 $primary->expects( $this->any() )->method( 'accountCreationType' )
834 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
835 $primary->expects( $this->any() )->method( 'getAuthenticationRequests' )
836 ->will( $this->returnValue( [ $req1 ] ) );
837 $primary->expects( $this->any() )->method( 'testForAccountCreation' )
838 ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
839 $createReq = new CreateFromLoginAuthenticationRequest(
840 null, [ $req2->getUniqueId() => $req2 ]
841 );
842 $this->logger->setCollect( true );
843 $ret = $this->manager->beginAccountCreation(
844 $user, [ $userReq, $createReq ], 'http://localhost/'
845 );
846 $this->logger->setCollect( false );
847 $this->assertSame( AuthenticationResponse::UI, $ret->status );
848 $this->assertCount( 4, $ret->neededRequests );
849 $this->assertSame( $req1, $ret->neededRequests[0] );
850 $this->assertInstanceOf( UsernameAuthenticationRequest::class, $ret->neededRequests[1] );
851 $this->assertInstanceOf( UserDataAuthenticationRequest::class, $ret->neededRequests[2] );
852 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->neededRequests[3] );
853 $this->assertSame( null, $ret->neededRequests[3]->createRequest );
854 $this->assertEquals( [], $ret->neededRequests[3]->maybeLink );
855
856 // Pass into beginAccountCreation(), with createRequest, primary needs reqs
857 $createReq = new CreateFromLoginAuthenticationRequest( $req2, [] );
858 $this->logger->setCollect( true );
859 $ret = $this->manager->beginAccountCreation(
860 $user, [ $userReq, $createReq ], 'http://localhost/'
861 );
862 $this->logger->setCollect( false );
863 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
864 $this->assertSame( 'fail', $ret->message->getKey() );
865
866 // Again, with a secondary needing reqs too
867 $secondary = $this->getMockBuilder( AbstractSecondaryAuthenticationProvider::class )
868 ->getMockForAbstractClass();
869 $this->secondaryauthMocks = [ $secondary ];
870 $this->initializeManager( true );
871 $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
872 ->will( $this->returnValue( [ $req3 ] ) );
873 $createReq = new CreateFromLoginAuthenticationRequest( $req2, [] );
874 $this->logger->setCollect( true );
875 $ret = $this->manager->beginAccountCreation(
876 $user, [ $userReq, $createReq ], 'http://localhost/'
877 );
878 $this->logger->setCollect( false );
879 $this->assertSame( AuthenticationResponse::UI, $ret->status );
880 $this->assertCount( 4, $ret->neededRequests );
881 $this->assertSame( $req3, $ret->neededRequests[0] );
882 $this->assertInstanceOf( UsernameAuthenticationRequest::class, $ret->neededRequests[1] );
883 $this->assertInstanceOf( UserDataAuthenticationRequest::class, $ret->neededRequests[2] );
884 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->neededRequests[3] );
885 $this->assertSame( $req2, $ret->neededRequests[3]->createRequest );
886 $this->assertEquals( [], $ret->neededRequests[3]->maybeLink );
887 $this->logger->setCollect( true );
888 $ret = $this->manager->continueAccountCreation( $ret->neededRequests );
889 $this->logger->setCollect( false );
890 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
891 $this->assertSame( 'fail', $ret->message->getKey() );
892 }
893
894 /**
895 * @dataProvider provideAuthentication
896 * @param StatusValue $preResponse
897 * @param array $primaryResponses
898 * @param array $secondaryResponses
899 * @param array $managerResponses
900 * @param bool $link Whether the primary authentication provider is a "link" provider
901 */
902 public function testAuthentication(
903 StatusValue $preResponse, array $primaryResponses, array $secondaryResponses,
904 array $managerResponses, $link = false
905 ) {
906 $this->initializeManager();
907 $user = \User::newFromName( 'UTSysop' );
908 $id = $user->getId();
909 $name = $user->getName();
910
911 // Set up lots of mocks...
912 $req = new RememberMeAuthenticationRequest;
913 $req->rememberMe = (bool)rand( 0, 1 );
914 $req->pre = $preResponse;
915 $req->primary = $primaryResponses;
916 $req->secondary = $secondaryResponses;
917 $mocks = [];
918 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
919 $class = ucfirst( $key ) . 'AuthenticationProvider';
920 $mocks[$key] = $this->getMockForAbstractClass(
921 "MediaWiki\\Auth\\$class", [], "Mock$class"
922 );
923 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
924 ->will( $this->returnValue( $key ) );
925 $mocks[$key . '2'] = $this->getMockForAbstractClass(
926 "MediaWiki\\Auth\\$class", [], "Mock$class"
927 );
928 $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
929 ->will( $this->returnValue( $key . '2' ) );
930 $mocks[$key . '3'] = $this->getMockForAbstractClass(
931 "MediaWiki\\Auth\\$class", [], "Mock$class"
932 );
933 $mocks[$key . '3']->expects( $this->any() )->method( 'getUniqueId' )
934 ->will( $this->returnValue( $key . '3' ) );
935 }
936 foreach ( $mocks as $mock ) {
937 $mock->expects( $this->any() )->method( 'getAuthenticationRequests' )
938 ->will( $this->returnValue( [] ) );
939 }
940
941 $mocks['pre']->expects( $this->once() )->method( 'testForAuthentication' )
942 ->will( $this->returnCallback( function ( $reqs ) use ( $req ) {
943 $this->assertContains( $req, $reqs );
944 return $req->pre;
945 } ) );
946
947 $ct = count( $req->primary );
948 $callback = $this->returnCallback( function ( $reqs ) use ( $req ) {
949 $this->assertContains( $req, $reqs );
950 return array_shift( $req->primary );
951 } );
952 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
953 ->method( 'beginPrimaryAuthentication' )
954 ->will( $callback );
955 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
956 ->method( 'continuePrimaryAuthentication' )
957 ->will( $callback );
958 if ( $link ) {
959 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
960 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
961 }
962
963 $ct = count( $req->secondary );
964 $callback = $this->returnCallback( function ( $user, $reqs ) use ( $id, $name, $req ) {
965 $this->assertSame( $id, $user->getId() );
966 $this->assertSame( $name, $user->getName() );
967 $this->assertContains( $req, $reqs );
968 return array_shift( $req->secondary );
969 } );
970 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
971 ->method( 'beginSecondaryAuthentication' )
972 ->will( $callback );
973 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
974 ->method( 'continueSecondaryAuthentication' )
975 ->will( $callback );
976
977 $abstain = AuthenticationResponse::newAbstain();
978 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAuthentication' )
979 ->will( $this->returnValue( StatusValue::newGood() ) );
980 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAuthentication' )
981 ->will( $this->returnValue( $abstain ) );
982 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAuthentication' );
983 $mocks['secondary2']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
984 ->will( $this->returnValue( $abstain ) );
985 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
986 $mocks['secondary3']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
987 ->will( $this->returnValue( $abstain ) );
988 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
989
990 $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
991 $this->primaryauthMocks = [ $mocks['primary'], $mocks['primary2'] ];
992 $this->secondaryauthMocks = [
993 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2'],
994 // So linking happens
995 new ConfirmLinkSecondaryAuthenticationProvider,
996 ];
997 $this->initializeManager( true );
998 $this->logger->setCollect( true );
999
1000 $constraint = \PHPUnit_Framework_Assert::logicalOr(
1001 $this->equalTo( AuthenticationResponse::PASS ),
1002 $this->equalTo( AuthenticationResponse::FAIL )
1003 );
1004 $providers = array_filter(
1005 array_merge(
1006 $this->preauthMocks, $this->primaryauthMocks, $this->secondaryauthMocks
1007 ),
1008 function ( $p ) {
1009 return is_callable( [ $p, 'expects' ] );
1010 }
1011 );
1012 foreach ( $providers as $p ) {
1013 $p->postCalled = false;
1014 $p->expects( $this->atMost( 1 ) )->method( 'postAuthentication' )
1015 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
1016 if ( $user !== null ) {
1017 $this->assertInstanceOf( 'User', $user );
1018 $this->assertSame( 'UTSysop', $user->getName() );
1019 }
1020 $this->assertInstanceOf( AuthenticationResponse::class, $response );
1021 $this->assertThat( $response->status, $constraint );
1022 $p->postCalled = $response->status;
1023 } );
1024 }
1025
1026 $session = $this->request->getSession();
1027 $session->setRememberUser( !$req->rememberMe );
1028
1029 foreach ( $managerResponses as $i => $response ) {
1030 $success = $response instanceof AuthenticationResponse &&
1031 $response->status === AuthenticationResponse::PASS;
1032 if ( $success ) {
1033 $this->hook( 'UserLoggedIn', $this->once() )
1034 ->with( $this->callback( function ( $user ) use ( $id, $name ) {
1035 return $user->getId() === $id && $user->getName() === $name;
1036 } ) );
1037 } else {
1038 $this->hook( 'UserLoggedIn', $this->never() );
1039 }
1040 if ( $success || (
1041 $response instanceof AuthenticationResponse &&
1042 $response->status === AuthenticationResponse::FAIL &&
1043 $response->message->getKey() !== 'authmanager-authn-not-in-progress' &&
1044 $response->message->getKey() !== 'authmanager-authn-no-primary'
1045 )
1046 ) {
1047 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
1048 } else {
1049 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->never() );
1050 }
1051
1052 $ex = null;
1053 try {
1054 if ( !$i ) {
1055 $ret = $this->manager->beginAuthentication( [ $req ], 'http://localhost/' );
1056 } else {
1057 $ret = $this->manager->continueAuthentication( [ $req ] );
1058 }
1059 if ( $response instanceof \Exception ) {
1060 $this->fail( 'Expected exception not thrown', "Response $i" );
1061 }
1062 } catch ( \Exception $ex ) {
1063 if ( !$response instanceof \Exception ) {
1064 throw $ex;
1065 }
1066 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
1067 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1068 "Response $i, exception, session state" );
1069 $this->unhook( 'UserLoggedIn' );
1070 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1071 return;
1072 }
1073
1074 $this->unhook( 'UserLoggedIn' );
1075 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1076
1077 $this->assertSame( 'http://localhost/', $req->returnToUrl );
1078
1079 $ret->message = $this->message( $ret->message );
1080 $this->assertEquals( $response, $ret, "Response $i, response" );
1081 if ( $success ) {
1082 $this->assertSame( $id, $session->getUser()->getId(),
1083 "Response $i, authn" );
1084 } else {
1085 $this->assertSame( 0, $session->getUser()->getId(),
1086 "Response $i, authn" );
1087 }
1088 if ( $success || $response->status === AuthenticationResponse::FAIL ) {
1089 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1090 "Response $i, session state" );
1091 foreach ( $providers as $p ) {
1092 $this->assertSame( $response->status, $p->postCalled,
1093 "Response $i, post-auth callback called" );
1094 }
1095 } else {
1096 $this->assertNotNull( $session->getSecret( 'AuthManager::authnState' ),
1097 "Response $i, session state" );
1098 $this->assertEquals(
1099 $ret->neededRequests,
1100 $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN_CONTINUE ),
1101 "Response $i, continuation check"
1102 );
1103 foreach ( $providers as $p ) {
1104 $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
1105 }
1106 }
1107
1108 $state = $session->getSecret( 'AuthManager::authnState' );
1109 $maybeLink = isset( $state['maybeLink'] ) ? $state['maybeLink'] : [];
1110 if ( $link && $response->status === AuthenticationResponse::RESTART ) {
1111 $this->assertEquals(
1112 $response->createRequest->maybeLink,
1113 $maybeLink,
1114 "Response $i, maybeLink"
1115 );
1116 } else {
1117 $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
1118 }
1119 }
1120
1121 if ( $success ) {
1122 $this->assertSame( $req->rememberMe, $session->shouldRememberUser(),
1123 'rememberMe checkbox had effect' );
1124 } else {
1125 $this->assertNotSame( $req->rememberMe, $session->shouldRememberUser(),
1126 'rememberMe checkbox wasn\'t applied' );
1127 }
1128 }
1129
1130 public function provideAuthentication() {
1131 $user = \User::newFromName( 'UTSysop' );
1132 $id = $user->getId();
1133 $name = $user->getName();
1134
1135 $rememberReq = new RememberMeAuthenticationRequest;
1136 $rememberReq->action = AuthManager::ACTION_LOGIN;
1137
1138 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1139 $req->foobar = 'baz';
1140 $restartResponse = AuthenticationResponse::newRestart(
1141 $this->message( 'authmanager-authn-no-local-user' )
1142 );
1143 $restartResponse->neededRequests = [ $rememberReq ];
1144
1145 $restartResponse2Pass = AuthenticationResponse::newPass( null );
1146 $restartResponse2Pass->linkRequest = $req;
1147 $restartResponse2 = AuthenticationResponse::newRestart(
1148 $this->message( 'authmanager-authn-no-local-user-link' )
1149 );
1150 $restartResponse2->createRequest = new CreateFromLoginAuthenticationRequest(
1151 null, [ $req->getUniqueId() => $req ]
1152 );
1153 $restartResponse2->neededRequests = [ $rememberReq, $restartResponse2->createRequest ];
1154
1155 return [
1156 'Failure in pre-auth' => [
1157 StatusValue::newFatal( 'fail-from-pre' ),
1158 [],
1159 [],
1160 [
1161 AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
1162 AuthenticationResponse::newFail(
1163 $this->message( 'authmanager-authn-not-in-progress' )
1164 ),
1165 ]
1166 ],
1167 'Failure in primary' => [
1168 StatusValue::newGood(),
1169 $tmp = [
1170 AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
1171 ],
1172 [],
1173 $tmp
1174 ],
1175 'All primary abstain' => [
1176 StatusValue::newGood(),
1177 [
1178 AuthenticationResponse::newAbstain(),
1179 ],
1180 [],
1181 [
1182 AuthenticationResponse::newFail( $this->message( 'authmanager-authn-no-primary' ) )
1183 ]
1184 ],
1185 'Primary UI, then redirect, then fail' => [
1186 StatusValue::newGood(),
1187 $tmp = [
1188 AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1189 AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
1190 AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
1191 ],
1192 [],
1193 $tmp
1194 ],
1195 'Primary redirect, then abstain' => [
1196 StatusValue::newGood(),
1197 [
1198 $tmp = AuthenticationResponse::newRedirect(
1199 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
1200 ),
1201 AuthenticationResponse::newAbstain(),
1202 ],
1203 [],
1204 [
1205 $tmp,
1206 new \DomainException(
1207 'MockPrimaryAuthenticationProvider::continuePrimaryAuthentication() returned ABSTAIN'
1208 )
1209 ]
1210 ],
1211 'Primary UI, then pass with no local user' => [
1212 StatusValue::newGood(),
1213 [
1214 $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1215 AuthenticationResponse::newPass( null ),
1216 ],
1217 [],
1218 [
1219 $tmp,
1220 $restartResponse,
1221 ]
1222 ],
1223 'Primary UI, then pass with no local user (link type)' => [
1224 StatusValue::newGood(),
1225 [
1226 $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1227 $restartResponse2Pass,
1228 ],
1229 [],
1230 [
1231 $tmp,
1232 $restartResponse2,
1233 ],
1234 true
1235 ],
1236 'Primary pass with invalid username' => [
1237 StatusValue::newGood(),
1238 [
1239 AuthenticationResponse::newPass( '<>' ),
1240 ],
1241 [],
1242 [
1243 new \DomainException( 'MockPrimaryAuthenticationProvider returned an invalid username: <>' ),
1244 ]
1245 ],
1246 'Secondary fail' => [
1247 StatusValue::newGood(),
1248 [
1249 AuthenticationResponse::newPass( $name ),
1250 ],
1251 $tmp = [
1252 AuthenticationResponse::newFail( $this->message( 'fail-in-secondary' ) ),
1253 ],
1254 $tmp
1255 ],
1256 'Secondary UI, then abstain' => [
1257 StatusValue::newGood(),
1258 [
1259 AuthenticationResponse::newPass( $name ),
1260 ],
1261 [
1262 $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1263 AuthenticationResponse::newAbstain()
1264 ],
1265 [
1266 $tmp,
1267 AuthenticationResponse::newPass( $name ),
1268 ]
1269 ],
1270 'Secondary pass' => [
1271 StatusValue::newGood(),
1272 [
1273 AuthenticationResponse::newPass( $name ),
1274 ],
1275 [
1276 AuthenticationResponse::newPass()
1277 ],
1278 [
1279 AuthenticationResponse::newPass( $name ),
1280 ]
1281 ],
1282 ];
1283 }
1284
1285 /**
1286 * @dataProvider provideUserExists
1287 * @param bool $primary1Exists
1288 * @param bool $primary2Exists
1289 * @param bool $expect
1290 */
1291 public function testUserExists( $primary1Exists, $primary2Exists, $expect ) {
1292 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1293 $mock1->expects( $this->any() )->method( 'getUniqueId' )
1294 ->will( $this->returnValue( 'primary1' ) );
1295 $mock1->expects( $this->any() )->method( 'testUserExists' )
1296 ->with( $this->equalTo( 'UTSysop' ) )
1297 ->will( $this->returnValue( $primary1Exists ) );
1298 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1299 $mock2->expects( $this->any() )->method( 'getUniqueId' )
1300 ->will( $this->returnValue( 'primary2' ) );
1301 $mock2->expects( $this->any() )->method( 'testUserExists' )
1302 ->with( $this->equalTo( 'UTSysop' ) )
1303 ->will( $this->returnValue( $primary2Exists ) );
1304 $this->primaryauthMocks = [ $mock1, $mock2 ];
1305
1306 $this->initializeManager( true );
1307 $this->assertSame( $expect, $this->manager->userExists( 'UTSysop' ) );
1308 }
1309
1310 public static function provideUserExists() {
1311 return [
1312 [ false, false, false ],
1313 [ true, false, true ],
1314 [ false, true, true ],
1315 [ true, true, true ],
1316 ];
1317 }
1318
1319 /**
1320 * @dataProvider provideAllowsAuthenticationDataChange
1321 * @param StatusValue $primaryReturn
1322 * @param StatusValue $secondaryReturn
1323 * @param Status $expect
1324 */
1325 public function testAllowsAuthenticationDataChange( $primaryReturn, $secondaryReturn, $expect ) {
1326 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1327
1328 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1329 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1330 $mock1->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1331 ->with( $this->equalTo( $req ) )
1332 ->will( $this->returnValue( $primaryReturn ) );
1333 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
1334 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1335 $mock2->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1336 ->with( $this->equalTo( $req ) )
1337 ->will( $this->returnValue( $secondaryReturn ) );
1338
1339 $this->primaryauthMocks = [ $mock1 ];
1340 $this->secondaryauthMocks = [ $mock2 ];
1341 $this->initializeManager( true );
1342 $this->assertEquals( $expect, $this->manager->allowsAuthenticationDataChange( $req ) );
1343 }
1344
1345 public static function provideAllowsAuthenticationDataChange() {
1346 $ignored = \Status::newGood( 'ignored' );
1347 $ignored->warning( 'authmanager-change-not-supported' );
1348
1349 $okFromPrimary = StatusValue::newGood();
1350 $okFromPrimary->warning( 'warning-from-primary' );
1351 $okFromSecondary = StatusValue::newGood();
1352 $okFromSecondary->warning( 'warning-from-secondary' );
1353
1354 return [
1355 [
1356 StatusValue::newGood(),
1357 StatusValue::newGood(),
1358 \Status::newGood(),
1359 ],
1360 [
1361 StatusValue::newGood(),
1362 StatusValue::newGood( 'ignore' ),
1363 \Status::newGood(),
1364 ],
1365 [
1366 StatusValue::newGood( 'ignored' ),
1367 StatusValue::newGood(),
1368 \Status::newGood(),
1369 ],
1370 [
1371 StatusValue::newGood( 'ignored' ),
1372 StatusValue::newGood( 'ignored' ),
1373 $ignored,
1374 ],
1375 [
1376 StatusValue::newFatal( 'fail from primary' ),
1377 StatusValue::newGood(),
1378 \Status::newFatal( 'fail from primary' ),
1379 ],
1380 [
1381 $okFromPrimary,
1382 StatusValue::newGood(),
1383 \Status::wrap( $okFromPrimary ),
1384 ],
1385 [
1386 StatusValue::newGood(),
1387 StatusValue::newFatal( 'fail from secondary' ),
1388 \Status::newFatal( 'fail from secondary' ),
1389 ],
1390 [
1391 StatusValue::newGood(),
1392 $okFromSecondary,
1393 \Status::wrap( $okFromSecondary ),
1394 ],
1395 ];
1396 }
1397
1398 public function testChangeAuthenticationData() {
1399 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1400 $req->username = 'UTSysop';
1401
1402 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1403 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1404 $mock1->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1405 ->with( $this->equalTo( $req ) );
1406 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1407 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1408 $mock2->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1409 ->with( $this->equalTo( $req ) );
1410
1411 $this->primaryauthMocks = [ $mock1, $mock2 ];
1412 $this->initializeManager( true );
1413 $this->logger->setCollect( true );
1414 $this->manager->changeAuthenticationData( $req );
1415 $this->assertSame( [
1416 [ LogLevel::INFO, 'Changing authentication data for {user} class {what}' ],
1417 ], $this->logger->getBuffer() );
1418 }
1419
1420 public function testCanCreateAccounts() {
1421 $types = [
1422 PrimaryAuthenticationProvider::TYPE_CREATE => true,
1423 PrimaryAuthenticationProvider::TYPE_LINK => true,
1424 PrimaryAuthenticationProvider::TYPE_NONE => false,
1425 ];
1426
1427 foreach ( $types as $type => $can ) {
1428 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1429 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
1430 $mock->expects( $this->any() )->method( 'accountCreationType' )
1431 ->will( $this->returnValue( $type ) );
1432 $this->primaryauthMocks = [ $mock ];
1433 $this->initializeManager( true );
1434 $this->assertSame( $can, $this->manager->canCreateAccounts(), $type );
1435 }
1436 }
1437
1438 public function testCheckAccountCreatePermissions() {
1439 global $wgGroupPermissions;
1440
1441 $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
1442
1443 $this->initializeManager( true );
1444
1445 $wgGroupPermissions['*']['createaccount'] = true;
1446 $this->assertEquals(
1447 \Status::newGood(),
1448 $this->manager->checkAccountCreatePermissions( new \User )
1449 );
1450
1451 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1452 $this->assertEquals(
1453 \Status::newFatal( 'readonlytext', 'Because' ),
1454 $this->manager->checkAccountCreatePermissions( new \User )
1455 );
1456 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1457
1458 $wgGroupPermissions['*']['createaccount'] = false;
1459 $status = $this->manager->checkAccountCreatePermissions( new \User );
1460 $this->assertFalse( $status->isOK() );
1461 $this->assertTrue( $status->hasMessage( 'badaccess-groups' ) );
1462 $wgGroupPermissions['*']['createaccount'] = true;
1463
1464 $user = \User::newFromName( 'UTBlockee' );
1465 if ( $user->getID() == 0 ) {
1466 $user->addToDatabase();
1467 \TestUser::setPasswordForUser( $user, 'UTBlockeePassword' );
1468 $user->saveSettings();
1469 }
1470 $oldBlock = \Block::newFromTarget( 'UTBlockee' );
1471 if ( $oldBlock ) {
1472 // An old block will prevent our new one from saving.
1473 $oldBlock->delete();
1474 }
1475 $blockOptions = [
1476 'address' => 'UTBlockee',
1477 'user' => $user->getID(),
1478 'reason' => __METHOD__,
1479 'expiry' => time() + 100500,
1480 'createAccount' => true,
1481 ];
1482 $block = new \Block( $blockOptions );
1483 $block->insert();
1484 $status = $this->manager->checkAccountCreatePermissions( $user );
1485 $this->assertFalse( $status->isOK() );
1486 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
1487
1488 $blockOptions = [
1489 'address' => '127.0.0.0/24',
1490 'reason' => __METHOD__,
1491 'expiry' => time() + 100500,
1492 'createAccount' => true,
1493 ];
1494 $block = new \Block( $blockOptions );
1495 $block->insert();
1496 $scopeVariable = new \ScopedCallback( [ $block, 'delete' ] );
1497 $status = $this->manager->checkAccountCreatePermissions( new \User );
1498 $this->assertFalse( $status->isOK() );
1499 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
1500 \ScopedCallback::consume( $scopeVariable );
1501
1502 $this->setMwGlobals( [
1503 'wgEnableDnsBlacklist' => true,
1504 'wgDnsBlacklistUrls' => [
1505 'local.wmftest.net', // This will resolve for every subdomain, which works to test "listed?"
1506 ],
1507 'wgProxyWhitelist' => [],
1508 ] );
1509 $status = $this->manager->checkAccountCreatePermissions( new \User );
1510 $this->assertFalse( $status->isOK() );
1511 $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
1512 $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
1513 $status = $this->manager->checkAccountCreatePermissions( new \User );
1514 $this->assertTrue( $status->isGood() );
1515 }
1516
1517 /**
1518 * @param string $uniq
1519 * @return string
1520 */
1521 private static function usernameForCreation( $uniq = '' ) {
1522 $i = 0;
1523 do {
1524 $username = "UTAuthManagerTestAccountCreation" . $uniq . ++$i;
1525 } while ( \User::newFromName( $username )->getId() !== 0 );
1526 return $username;
1527 }
1528
1529 public function testCanCreateAccount() {
1530 $username = self::usernameForCreation();
1531 $this->initializeManager();
1532
1533 $this->assertEquals(
1534 \Status::newFatal( 'authmanager-create-disabled' ),
1535 $this->manager->canCreateAccount( $username )
1536 );
1537
1538 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1539 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1540 $mock->expects( $this->any() )->method( 'accountCreationType' )
1541 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1542 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1543 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1544 ->will( $this->returnValue( StatusValue::newGood() ) );
1545 $this->primaryauthMocks = [ $mock ];
1546 $this->initializeManager( true );
1547
1548 $this->assertEquals(
1549 \Status::newFatal( 'userexists' ),
1550 $this->manager->canCreateAccount( $username )
1551 );
1552
1553 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1554 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1555 $mock->expects( $this->any() )->method( 'accountCreationType' )
1556 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1557 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1558 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1559 ->will( $this->returnValue( StatusValue::newGood() ) );
1560 $this->primaryauthMocks = [ $mock ];
1561 $this->initializeManager( true );
1562
1563 $this->assertEquals(
1564 \Status::newFatal( 'noname' ),
1565 $this->manager->canCreateAccount( $username . '<>' )
1566 );
1567
1568 $this->assertEquals(
1569 \Status::newFatal( 'userexists' ),
1570 $this->manager->canCreateAccount( 'UTSysop' )
1571 );
1572
1573 $this->assertEquals(
1574 \Status::newGood(),
1575 $this->manager->canCreateAccount( $username )
1576 );
1577
1578 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1579 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1580 $mock->expects( $this->any() )->method( 'accountCreationType' )
1581 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1582 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1583 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1584 ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
1585 $this->primaryauthMocks = [ $mock ];
1586 $this->initializeManager( true );
1587
1588 $this->assertEquals(
1589 \Status::newFatal( 'fail' ),
1590 $this->manager->canCreateAccount( $username )
1591 );
1592 }
1593
1594 public function testBeginAccountCreation() {
1595 $creator = \User::newFromName( 'UTSysop' );
1596 $userReq = new UsernameAuthenticationRequest;
1597 $this->logger = new \TestLogger( false, function ( $message, $level ) {
1598 return $level === LogLevel::DEBUG ? null : $message;
1599 } );
1600 $this->initializeManager();
1601
1602 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
1603 $this->hook( 'LocalUserCreated', $this->never() );
1604 try {
1605 $this->manager->beginAccountCreation(
1606 $creator, [], 'http://localhost/'
1607 );
1608 $this->fail( 'Expected exception not thrown' );
1609 } catch ( \LogicException $ex ) {
1610 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1611 }
1612 $this->unhook( 'LocalUserCreated' );
1613 $this->assertNull(
1614 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1615 );
1616
1617 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1618 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1619 $mock->expects( $this->any() )->method( 'accountCreationType' )
1620 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1621 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1622 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1623 ->will( $this->returnValue( StatusValue::newGood() ) );
1624 $this->primaryauthMocks = [ $mock ];
1625 $this->initializeManager( true );
1626
1627 $this->hook( 'LocalUserCreated', $this->never() );
1628 $ret = $this->manager->beginAccountCreation( $creator, [], 'http://localhost/' );
1629 $this->unhook( 'LocalUserCreated' );
1630 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1631 $this->assertSame( 'noname', $ret->message->getKey() );
1632
1633 $this->hook( 'LocalUserCreated', $this->never() );
1634 $userReq->username = self::usernameForCreation();
1635 $userReq2 = new UsernameAuthenticationRequest;
1636 $userReq2->username = $userReq->username . 'X';
1637 $ret = $this->manager->beginAccountCreation(
1638 $creator, [ $userReq, $userReq2 ], 'http://localhost/'
1639 );
1640 $this->unhook( 'LocalUserCreated' );
1641 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1642 $this->assertSame( 'noname', $ret->message->getKey() );
1643
1644 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1645 $this->hook( 'LocalUserCreated', $this->never() );
1646 $userReq->username = self::usernameForCreation();
1647 $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1648 $this->unhook( 'LocalUserCreated' );
1649 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1650 $this->assertSame( 'readonlytext', $ret->message->getKey() );
1651 $this->assertSame( [ 'Because' ], $ret->message->getParams() );
1652 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1653
1654 $this->hook( 'LocalUserCreated', $this->never() );
1655 $userReq->username = self::usernameForCreation();
1656 $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1657 $this->unhook( 'LocalUserCreated' );
1658 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1659 $this->assertSame( 'userexists', $ret->message->getKey() );
1660
1661 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1662 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1663 $mock->expects( $this->any() )->method( 'accountCreationType' )
1664 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1665 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1666 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1667 ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
1668 $this->primaryauthMocks = [ $mock ];
1669 $this->initializeManager( true );
1670
1671 $this->hook( 'LocalUserCreated', $this->never() );
1672 $userReq->username = self::usernameForCreation();
1673 $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1674 $this->unhook( 'LocalUserCreated' );
1675 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1676 $this->assertSame( 'fail', $ret->message->getKey() );
1677
1678 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1679 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1680 $mock->expects( $this->any() )->method( 'accountCreationType' )
1681 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1682 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1683 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1684 ->will( $this->returnValue( StatusValue::newGood() ) );
1685 $this->primaryauthMocks = [ $mock ];
1686 $this->initializeManager( true );
1687
1688 $this->hook( 'LocalUserCreated', $this->never() );
1689 $userReq->username = self::usernameForCreation() . '<>';
1690 $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1691 $this->unhook( 'LocalUserCreated' );
1692 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1693 $this->assertSame( 'noname', $ret->message->getKey() );
1694
1695 $this->hook( 'LocalUserCreated', $this->never() );
1696 $userReq->username = $creator->getName();
1697 $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1698 $this->unhook( 'LocalUserCreated' );
1699 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1700 $this->assertSame( 'userexists', $ret->message->getKey() );
1701
1702 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1703 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1704 $mock->expects( $this->any() )->method( 'accountCreationType' )
1705 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1706 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1707 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1708 ->will( $this->returnValue( StatusValue::newGood() ) );
1709 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
1710 ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
1711 $this->primaryauthMocks = [ $mock ];
1712 $this->initializeManager( true );
1713
1714 $req = $this->getMockBuilder( UserDataAuthenticationRequest::class )
1715 ->setMethods( [ 'populateUser' ] )
1716 ->getMock();
1717 $req->expects( $this->any() )->method( 'populateUser' )
1718 ->willReturn( \StatusValue::newFatal( 'populatefail' ) );
1719 $userReq->username = self::usernameForCreation();
1720 $ret = $this->manager->beginAccountCreation(
1721 $creator, [ $userReq, $req ], 'http://localhost/'
1722 );
1723 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1724 $this->assertSame( 'populatefail', $ret->message->getKey() );
1725
1726 $req = new UserDataAuthenticationRequest;
1727 $userReq->username = self::usernameForCreation();
1728
1729 $ret = $this->manager->beginAccountCreation(
1730 $creator, [ $userReq, $req ], 'http://localhost/'
1731 );
1732 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1733 $this->assertSame( 'fail', $ret->message->getKey() );
1734
1735 $this->manager->beginAccountCreation(
1736 \User::newFromName( $userReq->username ), [ $userReq, $req ], 'http://localhost/'
1737 );
1738 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1739 $this->assertSame( 'fail', $ret->message->getKey() );
1740 }
1741
1742 public function testContinueAccountCreation() {
1743 $creator = \User::newFromName( 'UTSysop' );
1744 $username = self::usernameForCreation();
1745 $this->logger = new \TestLogger( false, function ( $message, $level ) {
1746 return $level === LogLevel::DEBUG ? null : $message;
1747 } );
1748 $this->initializeManager();
1749
1750 $session = [
1751 'userid' => 0,
1752 'username' => $username,
1753 'creatorid' => 0,
1754 'creatorname' => $username,
1755 'reqs' => [],
1756 'primary' => null,
1757 'primaryResponse' => null,
1758 'secondary' => [],
1759 'ranPreTests' => true,
1760 ];
1761
1762 $this->hook( 'LocalUserCreated', $this->never() );
1763 try {
1764 $this->manager->continueAccountCreation( [] );
1765 $this->fail( 'Expected exception not thrown' );
1766 } catch ( \LogicException $ex ) {
1767 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1768 }
1769 $this->unhook( 'LocalUserCreated' );
1770
1771 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1772 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1773 $mock->expects( $this->any() )->method( 'accountCreationType' )
1774 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1775 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1776 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )->will(
1777 $this->returnValue( AuthenticationResponse::newFail( $this->message( 'fail' ) ) )
1778 );
1779 $this->primaryauthMocks = [ $mock ];
1780 $this->initializeManager( true );
1781
1782 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', null );
1783 $this->hook( 'LocalUserCreated', $this->never() );
1784 $ret = $this->manager->continueAccountCreation( [] );
1785 $this->unhook( 'LocalUserCreated' );
1786 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1787 $this->assertSame( 'authmanager-create-not-in-progress', $ret->message->getKey() );
1788
1789 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1790 [ 'username' => "$username<>" ] + $session );
1791 $this->hook( 'LocalUserCreated', $this->never() );
1792 $ret = $this->manager->continueAccountCreation( [] );
1793 $this->unhook( 'LocalUserCreated' );
1794 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1795 $this->assertSame( 'noname', $ret->message->getKey() );
1796 $this->assertNull(
1797 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1798 );
1799
1800 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', $session );
1801 $this->hook( 'LocalUserCreated', $this->never() );
1802 $cache = \ObjectCache::getLocalClusterInstance();
1803 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1804 $ret = $this->manager->continueAccountCreation( [] );
1805 unset( $lock );
1806 $this->unhook( 'LocalUserCreated' );
1807 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1808 $this->assertSame( 'usernameinprogress', $ret->message->getKey() );
1809 // This error shouldn't remove the existing session, because the
1810 // raced-with process "owns" it.
1811 $this->assertSame(
1812 $session, $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1813 );
1814
1815 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1816 [ 'username' => $creator->getName() ] + $session );
1817 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1818 $this->hook( 'LocalUserCreated', $this->never() );
1819 $ret = $this->manager->continueAccountCreation( [] );
1820 $this->unhook( 'LocalUserCreated' );
1821 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1822 $this->assertSame( 'readonlytext', $ret->message->getKey() );
1823 $this->assertSame( [ 'Because' ], $ret->message->getParams() );
1824 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1825
1826 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1827 [ 'username' => $creator->getName() ] + $session );
1828 $this->hook( 'LocalUserCreated', $this->never() );
1829 $ret = $this->manager->continueAccountCreation( [] );
1830 $this->unhook( 'LocalUserCreated' );
1831 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1832 $this->assertSame( 'userexists', $ret->message->getKey() );
1833 $this->assertNull(
1834 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1835 );
1836
1837 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1838 [ 'userid' => $creator->getId() ] + $session );
1839 $this->hook( 'LocalUserCreated', $this->never() );
1840 try {
1841 $ret = $this->manager->continueAccountCreation( [] );
1842 $this->fail( 'Expected exception not thrown' );
1843 } catch ( \UnexpectedValueException $ex ) {
1844 $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
1845 }
1846 $this->unhook( 'LocalUserCreated' );
1847 $this->assertNull(
1848 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1849 );
1850
1851 $id = $creator->getId();
1852 $name = $creator->getName();
1853 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1854 [ 'username' => $name, 'userid' => $id + 1 ] + $session );
1855 $this->hook( 'LocalUserCreated', $this->never() );
1856 try {
1857 $ret = $this->manager->continueAccountCreation( [] );
1858 $this->fail( 'Expected exception not thrown' );
1859 } catch ( \UnexpectedValueException $ex ) {
1860 $this->assertEquals(
1861 "User \"{$name}\" exists, but ID $id != " . ( $id + 1 ) . '!', $ex->getMessage()
1862 );
1863 }
1864 $this->unhook( 'LocalUserCreated' );
1865 $this->assertNull(
1866 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1867 );
1868
1869 $req = $this->getMockBuilder( UserDataAuthenticationRequest::class )
1870 ->setMethods( [ 'populateUser' ] )
1871 ->getMock();
1872 $req->expects( $this->any() )->method( 'populateUser' )
1873 ->willReturn( \StatusValue::newFatal( 'populatefail' ) );
1874 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1875 [ 'reqs' => [ $req ] ] + $session );
1876 $ret = $this->manager->continueAccountCreation( [] );
1877 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1878 $this->assertSame( 'populatefail', $ret->message->getKey() );
1879 $this->assertNull(
1880 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1881 );
1882 }
1883
1884 /**
1885 * @dataProvider provideAccountCreation
1886 * @param StatusValue $preTest
1887 * @param StatusValue $primaryTest
1888 * @param StatusValue $secondaryTest
1889 * @param array $primaryResponses
1890 * @param array $secondaryResponses
1891 * @param array $managerResponses
1892 */
1893 public function testAccountCreation(
1894 StatusValue $preTest, $primaryTest, $secondaryTest,
1895 array $primaryResponses, array $secondaryResponses, array $managerResponses
1896 ) {
1897 $creator = \User::newFromName( 'UTSysop' );
1898 $username = self::usernameForCreation();
1899
1900 $this->initializeManager();
1901
1902 // Set up lots of mocks...
1903 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1904 $req->preTest = $preTest;
1905 $req->primaryTest = $primaryTest;
1906 $req->secondaryTest = $secondaryTest;
1907 $req->primary = $primaryResponses;
1908 $req->secondary = $secondaryResponses;
1909 $mocks = [];
1910 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
1911 $class = ucfirst( $key ) . 'AuthenticationProvider';
1912 $mocks[$key] = $this->getMockForAbstractClass(
1913 "MediaWiki\\Auth\\$class", [], "Mock$class"
1914 );
1915 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
1916 ->will( $this->returnValue( $key ) );
1917 $mocks[$key]->expects( $this->any() )->method( 'testUserForCreation' )
1918 ->will( $this->returnValue( StatusValue::newGood() ) );
1919 $mocks[$key]->expects( $this->any() )->method( 'testForAccountCreation' )
1920 ->will( $this->returnCallback(
1921 function ( $user, $creatorIn, $reqs )
1922 use ( $username, $creator, $req, $key )
1923 {
1924 $this->assertSame( $username, $user->getName() );
1925 $this->assertSame( $creator->getId(), $creatorIn->getId() );
1926 $this->assertSame( $creator->getName(), $creatorIn->getName() );
1927 $foundReq = false;
1928 foreach ( $reqs as $r ) {
1929 $this->assertSame( $username, $r->username );
1930 $foundReq = $foundReq || get_class( $r ) === get_class( $req );
1931 }
1932 $this->assertTrue( $foundReq, '$reqs contains $req' );
1933 $k = $key . 'Test';
1934 return $req->$k;
1935 }
1936 ) );
1937
1938 for ( $i = 2; $i <= 3; $i++ ) {
1939 $mocks[$key . $i] = $this->getMockForAbstractClass(
1940 "MediaWiki\\Auth\\$class", [], "Mock$class"
1941 );
1942 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
1943 ->will( $this->returnValue( $key . $i ) );
1944 $mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' )
1945 ->will( $this->returnValue( StatusValue::newGood() ) );
1946 $mocks[$key . $i]->expects( $this->atMost( 1 ) )->method( 'testForAccountCreation' )
1947 ->will( $this->returnValue( StatusValue::newGood() ) );
1948 }
1949 }
1950
1951 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
1952 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1953 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
1954 ->will( $this->returnValue( false ) );
1955 $ct = count( $req->primary );
1956 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1957 $this->assertSame( $username, $user->getName() );
1958 $this->assertSame( 'UTSysop', $creator->getName() );
1959 $foundReq = false;
1960 foreach ( $reqs as $r ) {
1961 $this->assertSame( $username, $r->username );
1962 $foundReq = $foundReq || get_class( $r ) === get_class( $req );
1963 }
1964 $this->assertTrue( $foundReq, '$reqs contains $req' );
1965 return array_shift( $req->primary );
1966 } );
1967 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
1968 ->method( 'beginPrimaryAccountCreation' )
1969 ->will( $callback );
1970 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1971 ->method( 'continuePrimaryAccountCreation' )
1972 ->will( $callback );
1973
1974 $ct = count( $req->secondary );
1975 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1976 $this->assertSame( $username, $user->getName() );
1977 $this->assertSame( 'UTSysop', $creator->getName() );
1978 $foundReq = false;
1979 foreach ( $reqs as $r ) {
1980 $this->assertSame( $username, $r->username );
1981 $foundReq = $foundReq || get_class( $r ) === get_class( $req );
1982 }
1983 $this->assertTrue( $foundReq, '$reqs contains $req' );
1984 return array_shift( $req->secondary );
1985 } );
1986 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
1987 ->method( 'beginSecondaryAccountCreation' )
1988 ->will( $callback );
1989 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1990 ->method( 'continueSecondaryAccountCreation' )
1991 ->will( $callback );
1992
1993 $abstain = AuthenticationResponse::newAbstain();
1994 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
1995 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
1996 $mocks['primary2']->expects( $this->any() )->method( 'testUserExists' )
1997 ->will( $this->returnValue( false ) );
1998 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountCreation' )
1999 ->will( $this->returnValue( $abstain ) );
2000 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
2001 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
2002 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_NONE ) );
2003 $mocks['primary3']->expects( $this->any() )->method( 'testUserExists' )
2004 ->will( $this->returnValue( false ) );
2005 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountCreation' );
2006 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
2007 $mocks['secondary2']->expects( $this->atMost( 1 ) )
2008 ->method( 'beginSecondaryAccountCreation' )
2009 ->will( $this->returnValue( $abstain ) );
2010 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
2011 $mocks['secondary3']->expects( $this->atMost( 1 ) )
2012 ->method( 'beginSecondaryAccountCreation' )
2013 ->will( $this->returnValue( $abstain ) );
2014 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
2015
2016 $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
2017 $this->primaryauthMocks = [ $mocks['primary3'], $mocks['primary'], $mocks['primary2'] ];
2018 $this->secondaryauthMocks = [
2019 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2']
2020 ];
2021
2022 $this->logger = new \TestLogger( true, function ( $message, $level ) {
2023 return $level === LogLevel::DEBUG ? null : $message;
2024 } );
2025 $expectLog = [];
2026 $this->initializeManager( true );
2027
2028 $constraint = \PHPUnit_Framework_Assert::logicalOr(
2029 $this->equalTo( AuthenticationResponse::PASS ),
2030 $this->equalTo( AuthenticationResponse::FAIL )
2031 );
2032 $providers = array_merge(
2033 $this->preauthMocks, $this->primaryauthMocks, $this->secondaryauthMocks
2034 );
2035 foreach ( $providers as $p ) {
2036 $p->postCalled = false;
2037 $p->expects( $this->atMost( 1 ) )->method( 'postAccountCreation' )
2038 ->willReturnCallback( function ( $user, $creator, $response )
2039 use ( $constraint, $p, $username )
2040 {
2041 $this->assertInstanceOf( 'User', $user );
2042 $this->assertSame( $username, $user->getName() );
2043 $this->assertSame( 'UTSysop', $creator->getName() );
2044 $this->assertInstanceOf( AuthenticationResponse::class, $response );
2045 $this->assertThat( $response->status, $constraint );
2046 $p->postCalled = $response->status;
2047 } );
2048 }
2049
2050 // We're testing with $wgNewUserLog = false, so assert that it worked
2051 $dbw = wfGetDB( DB_MASTER );
2052 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2053
2054 $first = true;
2055 $created = false;
2056 foreach ( $managerResponses as $i => $response ) {
2057 $success = $response instanceof AuthenticationResponse &&
2058 $response->status === AuthenticationResponse::PASS;
2059 if ( $i === 'created' ) {
2060 $created = true;
2061 $this->hook( 'LocalUserCreated', $this->once() )
2062 ->with(
2063 $this->callback( function ( $user ) use ( $username ) {
2064 return $user->getName() === $username;
2065 } ),
2066 $this->equalTo( false )
2067 );
2068 $expectLog[] = [ LogLevel::INFO, "Creating user {user} during account creation" ];
2069 } else {
2070 $this->hook( 'LocalUserCreated', $this->never() );
2071 }
2072
2073 $ex = null;
2074 try {
2075 if ( $first ) {
2076 $userReq = new UsernameAuthenticationRequest;
2077 $userReq->username = $username;
2078 $ret = $this->manager->beginAccountCreation(
2079 $creator, [ $userReq, $req ], 'http://localhost/'
2080 );
2081 } else {
2082 $ret = $this->manager->continueAccountCreation( [ $req ] );
2083 }
2084 if ( $response instanceof \Exception ) {
2085 $this->fail( 'Expected exception not thrown', "Response $i" );
2086 }
2087 } catch ( \Exception $ex ) {
2088 if ( !$response instanceof \Exception ) {
2089 throw $ex;
2090 }
2091 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
2092 $this->assertNull(
2093 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2094 "Response $i, exception, session state"
2095 );
2096 $this->unhook( 'LocalUserCreated' );
2097 return;
2098 }
2099
2100 $this->unhook( 'LocalUserCreated' );
2101
2102 $this->assertSame( 'http://localhost/', $req->returnToUrl );
2103
2104 if ( $success ) {
2105 $this->assertNotNull( $ret->loginRequest, "Response $i, login marker" );
2106 $this->assertContains(
2107 $ret->loginRequest, $this->managerPriv->createdAccountAuthenticationRequests,
2108 "Response $i, login marker"
2109 );
2110
2111 $expectLog[] = [
2112 LogLevel::INFO,
2113 "MediaWiki\Auth\AuthManager::continueAccountCreation: Account creation succeeded for {user}"
2114 ];
2115
2116 // Set some fields in the expected $response that we couldn't
2117 // know in provideAccountCreation().
2118 $response->username = $username;
2119 $response->loginRequest = $ret->loginRequest;
2120 } else {
2121 $this->assertNull( $ret->loginRequest, "Response $i, login marker" );
2122 $this->assertSame( [], $this->managerPriv->createdAccountAuthenticationRequests,
2123 "Response $i, login marker" );
2124 }
2125 $ret->message = $this->message( $ret->message );
2126 $this->assertEquals( $response, $ret, "Response $i, response" );
2127 if ( $success || $response->status === AuthenticationResponse::FAIL ) {
2128 $this->assertNull(
2129 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2130 "Response $i, session state"
2131 );
2132 foreach ( $providers as $p ) {
2133 $this->assertSame( $response->status, $p->postCalled,
2134 "Response $i, post-auth callback called" );
2135 }
2136 } else {
2137 $this->assertNotNull(
2138 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2139 "Response $i, session state"
2140 );
2141 $this->assertEquals(
2142 $ret->neededRequests,
2143 $this->manager->getAuthenticationRequests( AuthManager::ACTION_CREATE_CONTINUE ),
2144 "Response $i, continuation check"
2145 );
2146 foreach ( $providers as $p ) {
2147 $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
2148 }
2149 }
2150
2151 if ( $created ) {
2152 $this->assertNotEquals( 0, \User::idFromName( $username ) );
2153 } else {
2154 $this->assertEquals( 0, \User::idFromName( $username ) );
2155 }
2156
2157 $first = false;
2158 }
2159
2160 $this->assertSame( $expectLog, $this->logger->getBuffer() );
2161
2162 $this->assertSame(
2163 $maxLogId,
2164 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2165 );
2166 }
2167
2168 public function provideAccountCreation() {
2169 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
2170 $good = StatusValue::newGood();
2171
2172 return [
2173 'Pre-creation test fail in pre' => [
2174 StatusValue::newFatal( 'fail-from-pre' ), $good, $good,
2175 [],
2176 [],
2177 [
2178 AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
2179 ]
2180 ],
2181 'Pre-creation test fail in primary' => [
2182 $good, StatusValue::newFatal( 'fail-from-primary' ), $good,
2183 [],
2184 [],
2185 [
2186 AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
2187 ]
2188 ],
2189 'Pre-creation test fail in secondary' => [
2190 $good, $good, StatusValue::newFatal( 'fail-from-secondary' ),
2191 [],
2192 [],
2193 [
2194 AuthenticationResponse::newFail( $this->message( 'fail-from-secondary' ) ),
2195 ]
2196 ],
2197 'Failure in primary' => [
2198 $good, $good, $good,
2199 $tmp = [
2200 AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
2201 ],
2202 [],
2203 $tmp
2204 ],
2205 'All primary abstain' => [
2206 $good, $good, $good,
2207 [
2208 AuthenticationResponse::newAbstain(),
2209 ],
2210 [],
2211 [
2212 AuthenticationResponse::newFail( $this->message( 'authmanager-create-no-primary' ) )
2213 ]
2214 ],
2215 'Primary UI, then redirect, then fail' => [
2216 $good, $good, $good,
2217 $tmp = [
2218 AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
2219 AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
2220 AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
2221 ],
2222 [],
2223 $tmp
2224 ],
2225 'Primary redirect, then abstain' => [
2226 $good, $good, $good,
2227 [
2228 $tmp = AuthenticationResponse::newRedirect(
2229 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
2230 ),
2231 AuthenticationResponse::newAbstain(),
2232 ],
2233 [],
2234 [
2235 $tmp,
2236 new \DomainException(
2237 'MockPrimaryAuthenticationProvider::continuePrimaryAccountCreation() returned ABSTAIN'
2238 )
2239 ]
2240 ],
2241 'Primary UI, then pass; secondary abstain' => [
2242 $good, $good, $good,
2243 [
2244 $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
2245 AuthenticationResponse::newPass(),
2246 ],
2247 [
2248 AuthenticationResponse::newAbstain(),
2249 ],
2250 [
2251 $tmp1,
2252 'created' => AuthenticationResponse::newPass( '' ),
2253 ]
2254 ],
2255 'Primary pass; secondary UI then pass' => [
2256 $good, $good, $good,
2257 [
2258 AuthenticationResponse::newPass( '' ),
2259 ],
2260 [
2261 $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
2262 AuthenticationResponse::newPass( '' ),
2263 ],
2264 [
2265 'created' => $tmp1,
2266 AuthenticationResponse::newPass( '' ),
2267 ]
2268 ],
2269 'Primary pass; secondary fail' => [
2270 $good, $good, $good,
2271 [
2272 AuthenticationResponse::newPass(),
2273 ],
2274 [
2275 AuthenticationResponse::newFail( $this->message( '...' ) ),
2276 ],
2277 [
2278 'created' => new \DomainException(
2279 'MockSecondaryAuthenticationProvider::beginSecondaryAccountCreation() returned FAIL. ' .
2280 'Secondary providers are not allowed to fail account creation, ' .
2281 'that should have been done via testForAccountCreation().'
2282 )
2283 ]
2284 ],
2285 ];
2286 }
2287
2288 /**
2289 * @dataProvider provideAccountCreationLogging
2290 * @param bool $isAnon
2291 * @param string|null $logSubtype
2292 */
2293 public function testAccountCreationLogging( $isAnon, $logSubtype ) {
2294 $creator = $isAnon ? new \User : \User::newFromName( 'UTSysop' );
2295 $username = self::usernameForCreation();
2296
2297 $this->initializeManager();
2298
2299 // Set up lots of mocks...
2300 $mock = $this->getMockForAbstractClass(
2301 "MediaWiki\\Auth\\PrimaryAuthenticationProvider", []
2302 );
2303 $mock->expects( $this->any() )->method( 'getUniqueId' )
2304 ->will( $this->returnValue( 'primary' ) );
2305 $mock->expects( $this->any() )->method( 'testUserForCreation' )
2306 ->will( $this->returnValue( StatusValue::newGood() ) );
2307 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
2308 ->will( $this->returnValue( StatusValue::newGood() ) );
2309 $mock->expects( $this->any() )->method( 'accountCreationType' )
2310 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
2311 $mock->expects( $this->any() )->method( 'testUserExists' )
2312 ->will( $this->returnValue( false ) );
2313 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
2314 ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
2315 $mock->expects( $this->any() )->method( 'finishAccountCreation' )
2316 ->will( $this->returnValue( $logSubtype ) );
2317
2318 $this->primaryauthMocks = [ $mock ];
2319 $this->initializeManager( true );
2320 $this->logger->setCollect( true );
2321
2322 $this->config->set( 'NewUserLog', true );
2323
2324 $dbw = wfGetDB( DB_MASTER );
2325 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2326
2327 $userReq = new UsernameAuthenticationRequest;
2328 $userReq->username = $username;
2329 $reasonReq = new CreationReasonAuthenticationRequest;
2330 $reasonReq->reason = $this->toString();
2331 $ret = $this->manager->beginAccountCreation(
2332 $creator, [ $userReq, $reasonReq ], 'http://localhost/'
2333 );
2334
2335 $this->assertSame( AuthenticationResponse::PASS, $ret->status );
2336
2337 $user = \User::newFromName( $username );
2338 $this->assertNotEquals( 0, $user->getId(), 'sanity check' );
2339 $this->assertNotEquals( $creator->getId(), $user->getId(), 'sanity check' );
2340
2341 $data = \DatabaseLogEntry::getSelectQueryData();
2342 $rows = iterator_to_array( $dbw->select(
2343 $data['tables'],
2344 $data['fields'],
2345 [
2346 'log_id > ' . (int)$maxLogId,
2347 'log_type' => 'newusers'
2348 ] + $data['conds'],
2349 __METHOD__,
2350 $data['options'],
2351 $data['join_conds']
2352 ) );
2353 $this->assertCount( 1, $rows );
2354 $entry = \DatabaseLogEntry::newFromRow( reset( $rows ) );
2355
2356 $this->assertSame( $logSubtype ?: ( $isAnon ? 'create' : 'create2' ), $entry->getSubtype() );
2357 $this->assertSame(
2358 $isAnon ? $user->getId() : $creator->getId(),
2359 $entry->getPerformer()->getId()
2360 );
2361 $this->assertSame(
2362 $isAnon ? $user->getName() : $creator->getName(),
2363 $entry->getPerformer()->getName()
2364 );
2365 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2366 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2367 $this->assertSame( $this->toString(), $entry->getComment() );
2368 }
2369
2370 public static function provideAccountCreationLogging() {
2371 return [
2372 [ true, null ],
2373 [ true, 'foobar' ],
2374 [ false, null ],
2375 [ false, 'byemail' ],
2376 ];
2377 }
2378
2379 public function testAutoAccountCreation() {
2380 global $wgGroupPermissions, $wgHooks;
2381
2382 // PHPUnit seems to have a bug where it will call the ->with()
2383 // callbacks for our hooks again after the test is run (WTF?), which
2384 // breaks here because $username no longer matches $user by the end of
2385 // the testing.
2386 $workaroundPHPUnitBug = false;
2387
2388 $username = self::usernameForCreation();
2389 $this->initializeManager();
2390
2391 $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
2392 $wgGroupPermissions['*']['createaccount'] = true;
2393 $wgGroupPermissions['*']['autocreateaccount'] = false;
2394
2395 \ObjectCache::$instances[__METHOD__] = new \HashBagOStuff();
2396 $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__ ] );
2397
2398 // Set up lots of mocks...
2399 $mocks = [];
2400 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2401 $class = ucfirst( $key ) . 'AuthenticationProvider';
2402 $mocks[$key] = $this->getMockForAbstractClass(
2403 "MediaWiki\\Auth\\$class", [], "Mock$class"
2404 );
2405 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2406 ->will( $this->returnValue( $key ) );
2407 }
2408
2409 $good = StatusValue::newGood();
2410 $callback = $this->callback( function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
2411 return $workaroundPHPUnitBug || $user->getName() === $username;
2412 } );
2413
2414 $mocks['pre']->expects( $this->exactly( 12 ) )->method( 'testUserForCreation' )
2415 ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
2416 ->will( $this->onConsecutiveCalls(
2417 StatusValue::newFatal( 'ok' ), StatusValue::newFatal( 'ok' ), // For testing permissions
2418 StatusValue::newFatal( 'fail-in-pre' ), $good, $good,
2419 $good, // backoff test
2420 $good, // addToDatabase fails test
2421 $good, // addToDatabase throws test
2422 $good, // addToDatabase exists test
2423 $good, $good, $good // success
2424 ) );
2425
2426 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
2427 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
2428 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
2429 ->will( $this->returnValue( true ) );
2430 $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
2431 ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
2432 ->will( $this->onConsecutiveCalls(
2433 StatusValue::newFatal( 'fail-in-primary' ), $good,
2434 $good, // backoff test
2435 $good, // addToDatabase fails test
2436 $good, // addToDatabase throws test
2437 $good, // addToDatabase exists test
2438 $good, $good, $good
2439 ) );
2440 $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2441 ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) );
2442
2443 $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
2444 ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
2445 ->will( $this->onConsecutiveCalls(
2446 StatusValue::newFatal( 'fail-in-secondary' ),
2447 $good, // backoff test
2448 $good, // addToDatabase fails test
2449 $good, // addToDatabase throws test
2450 $good, // addToDatabase exists test
2451 $good, $good, $good
2452 ) );
2453 $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2454 ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) );
2455
2456 $this->preauthMocks = [ $mocks['pre'] ];
2457 $this->primaryauthMocks = [ $mocks['primary'] ];
2458 $this->secondaryauthMocks = [ $mocks['secondary'] ];
2459 $this->initializeManager( true );
2460 $session = $this->request->getSession();
2461
2462 $logger = new \TestLogger( true, function ( $m ) {
2463 $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
2464 return $m;
2465 } );
2466 $this->manager->setLogger( $logger );
2467
2468 try {
2469 $user = \User::newFromName( 'UTSysop' );
2470 $this->manager->autoCreateUser( $user, 'InvalidSource', true );
2471 $this->fail( 'Expected exception not thrown' );
2472 } catch ( \InvalidArgumentException $ex ) {
2473 $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
2474 }
2475
2476 // First, check an existing user
2477 $session->clear();
2478 $user = \User::newFromName( 'UTSysop' );
2479 $this->hook( 'LocalUserCreated', $this->never() );
2480 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2481 $this->unhook( 'LocalUserCreated' );
2482 $expect = \Status::newGood();
2483 $expect->warning( 'userexists' );
2484 $this->assertEquals( $expect, $ret );
2485 $this->assertNotEquals( 0, $user->getId() );
2486 $this->assertSame( 'UTSysop', $user->getName() );
2487 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2488 $this->assertSame( [
2489 [ LogLevel::DEBUG, '{username} already exists locally' ],
2490 ], $logger->getBuffer() );
2491 $logger->clearBuffer();
2492
2493 $session->clear();
2494 $user = \User::newFromName( 'UTSysop' );
2495 $this->hook( 'LocalUserCreated', $this->never() );
2496 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
2497 $this->unhook( 'LocalUserCreated' );
2498 $expect = \Status::newGood();
2499 $expect->warning( 'userexists' );
2500 $this->assertEquals( $expect, $ret );
2501 $this->assertNotEquals( 0, $user->getId() );
2502 $this->assertSame( 'UTSysop', $user->getName() );
2503 $this->assertEquals( 0, $session->getUser()->getId() );
2504 $this->assertSame( [
2505 [ LogLevel::DEBUG, '{username} already exists locally' ],
2506 ], $logger->getBuffer() );
2507 $logger->clearBuffer();
2508
2509 // Wiki is read-only
2510 $session->clear();
2511 $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
2512 $user = \User::newFromName( $username );
2513 $this->hook( 'LocalUserCreated', $this->never() );
2514 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2515 $this->unhook( 'LocalUserCreated' );
2516 $this->assertEquals( \Status::newFatal( 'readonlytext', 'Because' ), $ret );
2517 $this->assertEquals( 0, $user->getId() );
2518 $this->assertNotEquals( $username, $user->getName() );
2519 $this->assertEquals( 0, $session->getUser()->getId() );
2520 $this->assertSame( [
2521 [ LogLevel::DEBUG, 'denied by wfReadOnly(): {reason}' ],
2522 ], $logger->getBuffer() );
2523 $logger->clearBuffer();
2524 $this->setMwGlobals( [ 'wgReadOnly' => false ] );
2525
2526 // Session blacklisted
2527 $session->clear();
2528 $session->set( 'AuthManager::AutoCreateBlacklist', 'test' );
2529 $user = \User::newFromName( $username );
2530 $this->hook( 'LocalUserCreated', $this->never() );
2531 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2532 $this->unhook( 'LocalUserCreated' );
2533 $this->assertEquals( \Status::newFatal( 'test' ), $ret );
2534 $this->assertEquals( 0, $user->getId() );
2535 $this->assertNotEquals( $username, $user->getName() );
2536 $this->assertEquals( 0, $session->getUser()->getId() );
2537 $this->assertSame( [
2538 [ LogLevel::DEBUG, 'blacklisted in session {sessionid}' ],
2539 ], $logger->getBuffer() );
2540 $logger->clearBuffer();
2541
2542 $session->clear();
2543 $session->set( 'AuthManager::AutoCreateBlacklist', StatusValue::newFatal( 'test2' ) );
2544 $user = \User::newFromName( $username );
2545 $this->hook( 'LocalUserCreated', $this->never() );
2546 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2547 $this->unhook( 'LocalUserCreated' );
2548 $this->assertEquals( \Status::newFatal( 'test2' ), $ret );
2549 $this->assertEquals( 0, $user->getId() );
2550 $this->assertNotEquals( $username, $user->getName() );
2551 $this->assertEquals( 0, $session->getUser()->getId() );
2552 $this->assertSame( [
2553 [ LogLevel::DEBUG, 'blacklisted in session {sessionid}' ],
2554 ], $logger->getBuffer() );
2555 $logger->clearBuffer();
2556
2557 // Uncreatable name
2558 $session->clear();
2559 $user = \User::newFromName( $username . '@' );
2560 $this->hook( 'LocalUserCreated', $this->never() );
2561 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2562 $this->unhook( 'LocalUserCreated' );
2563 $this->assertEquals( \Status::newFatal( 'noname' ), $ret );
2564 $this->assertEquals( 0, $user->getId() );
2565 $this->assertNotEquals( $username . '@', $user->getId() );
2566 $this->assertEquals( 0, $session->getUser()->getId() );
2567 $this->assertSame( [
2568 [ LogLevel::DEBUG, 'name "{username}" is not creatable' ],
2569 ], $logger->getBuffer() );
2570 $logger->clearBuffer();
2571 $this->assertSame( 'noname', $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2572
2573 // IP unable to create accounts
2574 $wgGroupPermissions['*']['createaccount'] = false;
2575 $wgGroupPermissions['*']['autocreateaccount'] = false;
2576 $session->clear();
2577 $user = \User::newFromName( $username );
2578 $this->hook( 'LocalUserCreated', $this->never() );
2579 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2580 $this->unhook( 'LocalUserCreated' );
2581 $this->assertEquals( \Status::newFatal( 'authmanager-autocreate-noperm' ), $ret );
2582 $this->assertEquals( 0, $user->getId() );
2583 $this->assertNotEquals( $username, $user->getName() );
2584 $this->assertEquals( 0, $session->getUser()->getId() );
2585 $this->assertSame( [
2586 [ LogLevel::DEBUG, 'IP lacks the ability to create or autocreate accounts' ],
2587 ], $logger->getBuffer() );
2588 $logger->clearBuffer();
2589 $this->assertSame(
2590 'authmanager-autocreate-noperm', $session->get( 'AuthManager::AutoCreateBlacklist' )
2591 );
2592
2593 // Test that both permutations of permissions are allowed
2594 // (this hits the two "ok" entries in $mocks['pre'])
2595 $wgGroupPermissions['*']['createaccount'] = false;
2596 $wgGroupPermissions['*']['autocreateaccount'] = true;
2597 $session->clear();
2598 $user = \User::newFromName( $username );
2599 $this->hook( 'LocalUserCreated', $this->never() );
2600 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2601 $this->unhook( 'LocalUserCreated' );
2602 $this->assertEquals( \Status::newFatal( 'ok' ), $ret );
2603
2604 $wgGroupPermissions['*']['createaccount'] = true;
2605 $wgGroupPermissions['*']['autocreateaccount'] = false;
2606 $session->clear();
2607 $user = \User::newFromName( $username );
2608 $this->hook( 'LocalUserCreated', $this->never() );
2609 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2610 $this->unhook( 'LocalUserCreated' );
2611 $this->assertEquals( \Status::newFatal( 'ok' ), $ret );
2612 $logger->clearBuffer();
2613
2614 // Test lock fail
2615 $session->clear();
2616 $user = \User::newFromName( $username );
2617 $this->hook( 'LocalUserCreated', $this->never() );
2618 $cache = \ObjectCache::getLocalClusterInstance();
2619 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2620 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2621 unset( $lock );
2622 $this->unhook( 'LocalUserCreated' );
2623 $this->assertEquals( \Status::newFatal( 'usernameinprogress' ), $ret );
2624 $this->assertEquals( 0, $user->getId() );
2625 $this->assertNotEquals( $username, $user->getName() );
2626 $this->assertEquals( 0, $session->getUser()->getId() );
2627 $this->assertSame( [
2628 [ LogLevel::DEBUG, 'Could not acquire account creation lock' ],
2629 ], $logger->getBuffer() );
2630 $logger->clearBuffer();
2631
2632 // Test pre-authentication provider fail
2633 $session->clear();
2634 $user = \User::newFromName( $username );
2635 $this->hook( 'LocalUserCreated', $this->never() );
2636 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2637 $this->unhook( 'LocalUserCreated' );
2638 $this->assertEquals( \Status::newFatal( 'fail-in-pre' ), $ret );
2639 $this->assertEquals( 0, $user->getId() );
2640 $this->assertNotEquals( $username, $user->getName() );
2641 $this->assertEquals( 0, $session->getUser()->getId() );
2642 $this->assertSame( [
2643 [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2644 ], $logger->getBuffer() );
2645 $logger->clearBuffer();
2646 $this->assertEquals(
2647 StatusValue::newFatal( 'fail-in-pre' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2648 );
2649
2650 $session->clear();
2651 $user = \User::newFromName( $username );
2652 $this->hook( 'LocalUserCreated', $this->never() );
2653 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2654 $this->unhook( 'LocalUserCreated' );
2655 $this->assertEquals( \Status::newFatal( 'fail-in-primary' ), $ret );
2656 $this->assertEquals( 0, $user->getId() );
2657 $this->assertNotEquals( $username, $user->getName() );
2658 $this->assertEquals( 0, $session->getUser()->getId() );
2659 $this->assertSame( [
2660 [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2661 ], $logger->getBuffer() );
2662 $logger->clearBuffer();
2663 $this->assertEquals(
2664 StatusValue::newFatal( 'fail-in-primary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2665 );
2666
2667 $session->clear();
2668 $user = \User::newFromName( $username );
2669 $this->hook( 'LocalUserCreated', $this->never() );
2670 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2671 $this->unhook( 'LocalUserCreated' );
2672 $this->assertEquals( \Status::newFatal( 'fail-in-secondary' ), $ret );
2673 $this->assertEquals( 0, $user->getId() );
2674 $this->assertNotEquals( $username, $user->getName() );
2675 $this->assertEquals( 0, $session->getUser()->getId() );
2676 $this->assertSame( [
2677 [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2678 ], $logger->getBuffer() );
2679 $logger->clearBuffer();
2680 $this->assertEquals(
2681 StatusValue::newFatal( 'fail-in-secondary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2682 );
2683
2684 // Test backoff
2685 $cache = \ObjectCache::getLocalClusterInstance();
2686 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2687 $cache->set( $backoffKey, true );
2688 $session->clear();
2689 $user = \User::newFromName( $username );
2690 $this->hook( 'LocalUserCreated', $this->never() );
2691 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2692 $this->unhook( 'LocalUserCreated' );
2693 $this->assertEquals( \Status::newFatal( 'authmanager-autocreate-exception' ), $ret );
2694 $this->assertEquals( 0, $user->getId() );
2695 $this->assertNotEquals( $username, $user->getName() );
2696 $this->assertEquals( 0, $session->getUser()->getId() );
2697 $this->assertSame( [
2698 [ LogLevel::DEBUG, '{username} denied by prior creation attempt failures' ],
2699 ], $logger->getBuffer() );
2700 $logger->clearBuffer();
2701 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2702 $cache->delete( $backoffKey );
2703
2704 // Test addToDatabase fails
2705 $session->clear();
2706 $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2707 $user->expects( $this->once() )->method( 'addToDatabase' )
2708 ->will( $this->returnValue( \Status::newFatal( 'because' ) ) );
2709 $user->setName( $username );
2710 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2711 $this->assertEquals( \Status::newFatal( 'because' ), $ret );
2712 $this->assertEquals( 0, $user->getId() );
2713 $this->assertNotEquals( $username, $user->getName() );
2714 $this->assertEquals( 0, $session->getUser()->getId() );
2715 $this->assertSame( [
2716 [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2717 [ LogLevel::ERROR, '{username} failed with message {message}' ],
2718 ], $logger->getBuffer() );
2719 $logger->clearBuffer();
2720 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2721
2722 // Test addToDatabase throws an exception
2723 $cache = \ObjectCache::getLocalClusterInstance();
2724 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2725 $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
2726 $session->clear();
2727 $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2728 $user->expects( $this->once() )->method( 'addToDatabase' )
2729 ->will( $this->throwException( new \Exception( 'Excepted' ) ) );
2730 $user->setName( $username );
2731 try {
2732 $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2733 $this->fail( 'Expected exception not thrown' );
2734 } catch ( \Exception $ex ) {
2735 $this->assertSame( 'Excepted', $ex->getMessage() );
2736 }
2737 $this->assertEquals( 0, $user->getId() );
2738 $this->assertEquals( 0, $session->getUser()->getId() );
2739 $this->assertSame( [
2740 [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2741 [ LogLevel::ERROR, '{username} failed with exception {exception}' ],
2742 ], $logger->getBuffer() );
2743 $logger->clearBuffer();
2744 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2745 $this->assertNotEquals( false, $cache->get( $backoffKey ) );
2746 $cache->delete( $backoffKey );
2747
2748 // Test addToDatabase fails because the user already exists.
2749 $session->clear();
2750 $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2751 $user->expects( $this->once() )->method( 'addToDatabase' )
2752 ->will( $this->returnCallback( function () use ( $username ) {
2753 $status = \User::newFromName( $username )->addToDatabase();
2754 $this->assertTrue( $status->isOK(), 'sanity check' );
2755 return \Status::newFatal( 'userexists' );
2756 } ) );
2757 $user->setName( $username );
2758 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2759 $expect = \Status::newGood();
2760 $expect->warning( 'userexists' );
2761 $this->assertEquals( $expect, $ret );
2762 $this->assertNotEquals( 0, $user->getId() );
2763 $this->assertEquals( $username, $user->getName() );
2764 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2765 $this->assertSame( [
2766 [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2767 [ LogLevel::INFO, '{username} already exists locally (race)' ],
2768 ], $logger->getBuffer() );
2769 $logger->clearBuffer();
2770 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2771
2772 // Success!
2773 $session->clear();
2774 $username = self::usernameForCreation();
2775 $user = \User::newFromName( $username );
2776 $this->hook( 'AuthPluginAutoCreate', $this->once() )
2777 ->with( $callback );
2778 $this->hideDeprecated( 'AuthPluginAutoCreate hook (used in ' .
2779 get_class( $wgHooks['AuthPluginAutoCreate'][0] ) . '::onAuthPluginAutoCreate)' );
2780 $this->hook( 'LocalUserCreated', $this->once() )
2781 ->with( $callback, $this->equalTo( true ) );
2782 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2783 $this->unhook( 'LocalUserCreated' );
2784 $this->unhook( 'AuthPluginAutoCreate' );
2785 $this->assertEquals( \Status::newGood(), $ret );
2786 $this->assertNotEquals( 0, $user->getId() );
2787 $this->assertEquals( $username, $user->getName() );
2788 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2789 $this->assertSame( [
2790 [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2791 ], $logger->getBuffer() );
2792 $logger->clearBuffer();
2793
2794 $dbw = wfGetDB( DB_MASTER );
2795 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2796 $session->clear();
2797 $username = self::usernameForCreation();
2798 $user = \User::newFromName( $username );
2799 $this->hook( 'LocalUserCreated', $this->once() )
2800 ->with( $callback, $this->equalTo( true ) );
2801 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
2802 $this->unhook( 'LocalUserCreated' );
2803 $this->assertEquals( \Status::newGood(), $ret );
2804 $this->assertNotEquals( 0, $user->getId() );
2805 $this->assertEquals( $username, $user->getName() );
2806 $this->assertEquals( 0, $session->getUser()->getId() );
2807 $this->assertSame( [
2808 [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2809 ], $logger->getBuffer() );
2810 $logger->clearBuffer();
2811 $this->assertSame(
2812 $maxLogId,
2813 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2814 );
2815
2816 $this->config->set( 'NewUserLog', true );
2817 $session->clear();
2818 $username = self::usernameForCreation();
2819 $user = \User::newFromName( $username );
2820 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
2821 $this->assertEquals( \Status::newGood(), $ret );
2822 $logger->clearBuffer();
2823
2824 $data = \DatabaseLogEntry::getSelectQueryData();
2825 $rows = iterator_to_array( $dbw->select(
2826 $data['tables'],
2827 $data['fields'],
2828 [
2829 'log_id > ' . (int)$maxLogId,
2830 'log_type' => 'newusers'
2831 ] + $data['conds'],
2832 __METHOD__,
2833 $data['options'],
2834 $data['join_conds']
2835 ) );
2836 $this->assertCount( 1, $rows );
2837 $entry = \DatabaseLogEntry::newFromRow( reset( $rows ) );
2838
2839 $this->assertSame( 'autocreate', $entry->getSubtype() );
2840 $this->assertSame( $user->getId(), $entry->getPerformer()->getId() );
2841 $this->assertSame( $user->getName(), $entry->getPerformer()->getName() );
2842 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2843 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2844
2845 $workaroundPHPUnitBug = true;
2846 }
2847
2848 /**
2849 * @dataProvider provideGetAuthenticationRequests
2850 * @param string $action
2851 * @param array $expect
2852 * @param array $state
2853 */
2854 public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
2855 $makeReq = function ( $key ) use ( $action ) {
2856 $req = $this->getMock( AuthenticationRequest::class );
2857 $req->expects( $this->any() )->method( 'getUniqueId' )
2858 ->will( $this->returnValue( $key ) );
2859 $req->action = $action === AuthManager::ACTION_UNLINK ? AuthManager::ACTION_REMOVE : $action;
2860 $req->key = $key;
2861 return $req;
2862 };
2863 $cmpReqs = function ( $a, $b ) {
2864 $ret = strcmp( get_class( $a ), get_class( $b ) );
2865 if ( !$ret ) {
2866 $ret = strcmp( $a->key, $b->key );
2867 }
2868 return $ret;
2869 };
2870
2871 $good = StatusValue::newGood();
2872
2873 $mocks = [];
2874 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2875 $class = ucfirst( $key ) . 'AuthenticationProvider';
2876 $mocks[$key] = $this->getMockForAbstractClass(
2877 "MediaWiki\\Auth\\$class", [], "Mock$class"
2878 );
2879 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2880 ->will( $this->returnValue( $key ) );
2881 $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2882 ->will( $this->returnCallback( function ( $action ) use ( $key, $makeReq ) {
2883 return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
2884 } ) );
2885 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
2886 ->will( $this->returnValue( $good ) );
2887 }
2888
2889 $primaries = [];
2890 foreach ( [
2891 PrimaryAuthenticationProvider::TYPE_NONE,
2892 PrimaryAuthenticationProvider::TYPE_CREATE,
2893 PrimaryAuthenticationProvider::TYPE_LINK
2894 ] as $type ) {
2895 $class = 'PrimaryAuthenticationProvider';
2896 $mocks["primary-$type"] = $this->getMockForAbstractClass(
2897 "MediaWiki\\Auth\\$class", [], "Mock$class"
2898 );
2899 $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
2900 ->will( $this->returnValue( "primary-$type" ) );
2901 $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
2902 ->will( $this->returnValue( $type ) );
2903 $mocks["primary-$type"]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2904 ->will( $this->returnCallback( function ( $action ) use ( $type, $makeReq ) {
2905 return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
2906 } ) );
2907 $mocks["primary-$type"]->expects( $this->any() )
2908 ->method( 'providerAllowsAuthenticationDataChange' )
2909 ->will( $this->returnValue( $good ) );
2910 $this->primaryauthMocks[] = $mocks["primary-$type"];
2911 }
2912
2913 $mocks['primary2'] = $this->getMockForAbstractClass(
2914 PrimaryAuthenticationProvider::class, [], "MockPrimaryAuthenticationProvider"
2915 );
2916 $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
2917 ->will( $this->returnValue( 'primary2' ) );
2918 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
2919 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
2920 $mocks['primary2']->expects( $this->any() )->method( 'getAuthenticationRequests' )
2921 ->will( $this->returnValue( [] ) );
2922 $mocks['primary2']->expects( $this->any() )
2923 ->method( 'providerAllowsAuthenticationDataChange' )
2924 ->will( $this->returnCallback( function ( $req ) use ( $good ) {
2925 return $req->key === 'generic' ? StatusValue::newFatal( 'no' ) : $good;
2926 } ) );
2927 $this->primaryauthMocks[] = $mocks['primary2'];
2928
2929 $this->preauthMocks = [ $mocks['pre'] ];
2930 $this->secondaryauthMocks = [ $mocks['secondary'] ];
2931 $this->initializeManager( true );
2932
2933 if ( $state ) {
2934 if ( isset( $state['continueRequests'] ) ) {
2935 $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
2936 }
2937 if ( $action === AuthManager::ACTION_LOGIN_CONTINUE ) {
2938 $this->request->getSession()->setSecret( 'AuthManager::authnState', $state );
2939 } elseif ( $action === AuthManager::ACTION_CREATE_CONTINUE ) {
2940 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', $state );
2941 } elseif ( $action === AuthManager::ACTION_LINK_CONTINUE ) {
2942 $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', $state );
2943 }
2944 }
2945
2946 $expectReqs = array_map( $makeReq, $expect );
2947 if ( $action === AuthManager::ACTION_LOGIN ) {
2948 $req = new RememberMeAuthenticationRequest;
2949 $req->action = $action;
2950 $req->required = AuthenticationRequest::REQUIRED;
2951 $expectReqs[] = $req;
2952 } elseif ( $action === AuthManager::ACTION_CREATE ) {
2953 $req = new UsernameAuthenticationRequest;
2954 $req->action = $action;
2955 $expectReqs[] = $req;
2956 $req = new UserDataAuthenticationRequest;
2957 $req->action = $action;
2958 $req->required = AuthenticationRequest::REQUIRED;
2959 $expectReqs[] = $req;
2960 }
2961 usort( $expectReqs, $cmpReqs );
2962
2963 $actual = $this->manager->getAuthenticationRequests( $action );
2964 foreach ( $actual as $req ) {
2965 // Don't test this here.
2966 $req->required = AuthenticationRequest::REQUIRED;
2967 }
2968 usort( $actual, $cmpReqs );
2969
2970 $this->assertEquals( $expectReqs, $actual );
2971
2972 // Test CreationReasonAuthenticationRequest gets returned
2973 if ( $action === AuthManager::ACTION_CREATE ) {
2974 $req = new CreationReasonAuthenticationRequest;
2975 $req->action = $action;
2976 $req->required = AuthenticationRequest::REQUIRED;
2977 $expectReqs[] = $req;
2978 usort( $expectReqs, $cmpReqs );
2979
2980 $actual = $this->manager->getAuthenticationRequests( $action, \User::newFromName( 'UTSysop' ) );
2981 foreach ( $actual as $req ) {
2982 // Don't test this here.
2983 $req->required = AuthenticationRequest::REQUIRED;
2984 }
2985 usort( $actual, $cmpReqs );
2986
2987 $this->assertEquals( $expectReqs, $actual );
2988 }
2989 }
2990
2991 public static function provideGetAuthenticationRequests() {
2992 return [
2993 [
2994 AuthManager::ACTION_LOGIN,
2995 [ 'pre-login', 'primary-none-login', 'primary-create-login',
2996 'primary-link-login', 'secondary-login', 'generic' ],
2997 ],
2998 [
2999 AuthManager::ACTION_CREATE,
3000 [ 'pre-create', 'primary-none-create', 'primary-create-create',
3001 'primary-link-create', 'secondary-create', 'generic' ],
3002 ],
3003 [
3004 AuthManager::ACTION_LINK,
3005 [ 'primary-link-link', 'generic' ],
3006 ],
3007 [
3008 AuthManager::ACTION_CHANGE,
3009 [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
3010 'secondary-change' ],
3011 ],
3012 [
3013 AuthManager::ACTION_REMOVE,
3014 [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
3015 'secondary-remove' ],
3016 ],
3017 [
3018 AuthManager::ACTION_UNLINK,
3019 [ 'primary-link-remove' ],
3020 ],
3021 [
3022 AuthManager::ACTION_LOGIN_CONTINUE,
3023 [],
3024 ],
3025 [
3026 AuthManager::ACTION_LOGIN_CONTINUE,
3027 $reqs = [ 'continue-login', 'foo', 'bar' ],
3028 [
3029 'continueRequests' => $reqs,
3030 ],
3031 ],
3032 [
3033 AuthManager::ACTION_CREATE_CONTINUE,
3034 [],
3035 ],
3036 [
3037 AuthManager::ACTION_CREATE_CONTINUE,
3038 $reqs = [ 'continue-create', 'foo', 'bar' ],
3039 [
3040 'continueRequests' => $reqs,
3041 ],
3042 ],
3043 [
3044 AuthManager::ACTION_LINK_CONTINUE,
3045 [],
3046 ],
3047 [
3048 AuthManager::ACTION_LINK_CONTINUE,
3049 $reqs = [ 'continue-link', 'foo', 'bar' ],
3050 [
3051 'continueRequests' => $reqs,
3052 ],
3053 ],
3054 ];
3055 }
3056
3057 public function testGetAuthenticationRequestsRequired() {
3058 $makeReq = function ( $key, $required ) {
3059 $req = $this->getMock( AuthenticationRequest::class );
3060 $req->expects( $this->any() )->method( 'getUniqueId' )
3061 ->will( $this->returnValue( $key ) );
3062 $req->action = AuthManager::ACTION_LOGIN;
3063 $req->key = $key;
3064 $req->required = $required;
3065 return $req;
3066 };
3067 $cmpReqs = function ( $a, $b ) {
3068 $ret = strcmp( get_class( $a ), get_class( $b ) );
3069 if ( !$ret ) {
3070 $ret = strcmp( $a->key, $b->key );
3071 }
3072 return $ret;
3073 };
3074
3075 $good = StatusValue::newGood();
3076
3077 $primary1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3078 $primary1->expects( $this->any() )->method( 'getUniqueId' )
3079 ->will( $this->returnValue( 'primary1' ) );
3080 $primary1->expects( $this->any() )->method( 'accountCreationType' )
3081 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3082 $primary1->expects( $this->any() )->method( 'getAuthenticationRequests' )
3083 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3084 return [
3085 $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3086 $makeReq( "required", AuthenticationRequest::REQUIRED ),
3087 $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3088 $makeReq( "foo", AuthenticationRequest::REQUIRED ),
3089 $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3090 $makeReq( "baz", AuthenticationRequest::OPTIONAL ),
3091 ];
3092 } ) );
3093
3094 $primary2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3095 $primary2->expects( $this->any() )->method( 'getUniqueId' )
3096 ->will( $this->returnValue( 'primary2' ) );
3097 $primary2->expects( $this->any() )->method( 'accountCreationType' )
3098 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3099 $primary2->expects( $this->any() )->method( 'getAuthenticationRequests' )
3100 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3101 return [
3102 $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3103 $makeReq( "required2", AuthenticationRequest::REQUIRED ),
3104 $makeReq( "optional2", AuthenticationRequest::OPTIONAL ),
3105 ];
3106 } ) );
3107
3108 $secondary = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
3109 $secondary->expects( $this->any() )->method( 'getUniqueId' )
3110 ->will( $this->returnValue( 'secondary' ) );
3111 $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
3112 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3113 return [
3114 $makeReq( "foo", AuthenticationRequest::OPTIONAL ),
3115 $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3116 $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3117 ];
3118 } ) );
3119
3120 $rememberReq = new RememberMeAuthenticationRequest;
3121 $rememberReq->action = AuthManager::ACTION_LOGIN;
3122
3123 $this->primaryauthMocks = [ $primary1, $primary2 ];
3124 $this->secondaryauthMocks = [ $secondary ];
3125 $this->initializeManager( true );
3126
3127 $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
3128 $expected = [
3129 $rememberReq,
3130 $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3131 $makeReq( "required", AuthenticationRequest::PRIMARY_REQUIRED ),
3132 $makeReq( "required2", AuthenticationRequest::PRIMARY_REQUIRED ),
3133 $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3134 $makeReq( "optional2", AuthenticationRequest::OPTIONAL ),
3135 $makeReq( "foo", AuthenticationRequest::PRIMARY_REQUIRED ),
3136 $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3137 $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3138 ];
3139 usort( $actual, $cmpReqs );
3140 usort( $expected, $cmpReqs );
3141 $this->assertEquals( $expected, $actual );
3142
3143 $this->primaryauthMocks = [ $primary1 ];
3144 $this->secondaryauthMocks = [ $secondary ];
3145 $this->initializeManager( true );
3146
3147 $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
3148 $expected = [
3149 $rememberReq,
3150 $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3151 $makeReq( "required", AuthenticationRequest::REQUIRED ),
3152 $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3153 $makeReq( "foo", AuthenticationRequest::REQUIRED ),
3154 $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3155 $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3156 ];
3157 usort( $actual, $cmpReqs );
3158 usort( $expected, $cmpReqs );
3159 $this->assertEquals( $expected, $actual );
3160 }
3161
3162 public function testAllowsPropertyChange() {
3163 $mocks = [];
3164 foreach ( [ 'primary', 'secondary' ] as $key ) {
3165 $class = ucfirst( $key ) . 'AuthenticationProvider';
3166 $mocks[$key] = $this->getMockForAbstractClass(
3167 "MediaWiki\\Auth\\$class", [], "Mock$class"
3168 );
3169 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3170 ->will( $this->returnValue( $key ) );
3171 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
3172 ->will( $this->returnCallback( function ( $prop ) use ( $key ) {
3173 return $prop !== $key;
3174 } ) );
3175 }
3176
3177 $this->primaryauthMocks = [ $mocks['primary'] ];
3178 $this->secondaryauthMocks = [ $mocks['secondary'] ];
3179 $this->initializeManager( true );
3180
3181 $this->assertTrue( $this->manager->allowsPropertyChange( 'foo' ) );
3182 $this->assertFalse( $this->manager->allowsPropertyChange( 'primary' ) );
3183 $this->assertFalse( $this->manager->allowsPropertyChange( 'secondary' ) );
3184 }
3185
3186 public function testAutoCreateOnLogin() {
3187 $username = self::usernameForCreation();
3188
3189 $req = $this->getMock( AuthenticationRequest::class );
3190
3191 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3192 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3193 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3194 ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
3195 $mock->expects( $this->any() )->method( 'accountCreationType' )
3196 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3197 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3198 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3199 ->will( $this->returnValue( StatusValue::newGood() ) );
3200
3201 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
3202 $mock2->expects( $this->any() )->method( 'getUniqueId' )
3203 ->will( $this->returnValue( 'secondary' ) );
3204 $mock2->expects( $this->any() )->method( 'beginSecondaryAuthentication' )->will(
3205 $this->returnValue(
3206 AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) )
3207 )
3208 );
3209 $mock2->expects( $this->any() )->method( 'continueSecondaryAuthentication' )
3210 ->will( $this->returnValue( AuthenticationResponse::newAbstain() ) );
3211 $mock2->expects( $this->any() )->method( 'testUserForCreation' )
3212 ->will( $this->returnValue( StatusValue::newGood() ) );
3213
3214 $this->primaryauthMocks = [ $mock ];
3215 $this->secondaryauthMocks = [ $mock2 ];
3216 $this->initializeManager( true );
3217 $this->manager->setLogger( new \Psr\Log\NullLogger() );
3218 $session = $this->request->getSession();
3219 $session->clear();
3220
3221 $this->assertSame( 0, \User::newFromName( $username )->getId(),
3222 'sanity check' );
3223
3224 $callback = $this->callback( function ( $user ) use ( $username ) {
3225 return $user->getName() === $username;
3226 } );
3227
3228 $this->hook( 'UserLoggedIn', $this->never() );
3229 $this->hook( 'LocalUserCreated', $this->once() )->with( $callback, $this->equalTo( true ) );
3230 $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
3231 $this->unhook( 'LocalUserCreated' );
3232 $this->unhook( 'UserLoggedIn' );
3233 $this->assertSame( AuthenticationResponse::UI, $ret->status );
3234
3235 $id = (int)\User::newFromName( $username )->getId();
3236 $this->assertNotSame( 0, \User::newFromName( $username )->getId() );
3237 $this->assertSame( 0, $session->getUser()->getId() );
3238
3239 $this->hook( 'UserLoggedIn', $this->once() )->with( $callback );
3240 $this->hook( 'LocalUserCreated', $this->never() );
3241 $ret = $this->manager->continueAuthentication( [] );
3242 $this->unhook( 'LocalUserCreated' );
3243 $this->unhook( 'UserLoggedIn' );
3244 $this->assertSame( AuthenticationResponse::PASS, $ret->status );
3245 $this->assertSame( $username, $ret->username );
3246 $this->assertSame( $id, $session->getUser()->getId() );
3247 }
3248
3249 public function testAutoCreateFailOnLogin() {
3250 $username = self::usernameForCreation();
3251
3252 $mock = $this->getMockForAbstractClass(
3253 PrimaryAuthenticationProvider::class, [], "MockPrimaryAuthenticationProvider" );
3254 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3255 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3256 ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
3257 $mock->expects( $this->any() )->method( 'accountCreationType' )
3258 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3259 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3260 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3261 ->will( $this->returnValue( StatusValue::newFatal( 'fail-from-primary' ) ) );
3262
3263 $this->primaryauthMocks = [ $mock ];
3264 $this->initializeManager( true );
3265 $this->manager->setLogger( new \Psr\Log\NullLogger() );
3266 $session = $this->request->getSession();
3267 $session->clear();
3268
3269 $this->assertSame( 0, $session->getUser()->getId(),
3270 'sanity check' );
3271 $this->assertSame( 0, \User::newFromName( $username )->getId(),
3272 'sanity check' );
3273
3274 $this->hook( 'UserLoggedIn', $this->never() );
3275 $this->hook( 'LocalUserCreated', $this->never() );
3276 $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
3277 $this->unhook( 'LocalUserCreated' );
3278 $this->unhook( 'UserLoggedIn' );
3279 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3280 $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message->getKey() );
3281
3282 $this->assertSame( 0, \User::newFromName( $username )->getId() );
3283 $this->assertSame( 0, $session->getUser()->getId() );
3284 }
3285
3286 public function testAuthenticationSessionData() {
3287 $this->initializeManager( true );
3288
3289 $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3290 $this->manager->setAuthenticationSessionData( 'foo', 'foo!' );
3291 $this->manager->setAuthenticationSessionData( 'bar', 'bar!' );
3292 $this->assertSame( 'foo!', $this->manager->getAuthenticationSessionData( 'foo' ) );
3293 $this->assertSame( 'bar!', $this->manager->getAuthenticationSessionData( 'bar' ) );
3294 $this->manager->removeAuthenticationSessionData( 'foo' );
3295 $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3296 $this->assertSame( 'bar!', $this->manager->getAuthenticationSessionData( 'bar' ) );
3297 $this->manager->removeAuthenticationSessionData( 'bar' );
3298 $this->assertNull( $this->manager->getAuthenticationSessionData( 'bar' ) );
3299
3300 $this->manager->setAuthenticationSessionData( 'foo', 'foo!' );
3301 $this->manager->setAuthenticationSessionData( 'bar', 'bar!' );
3302 $this->manager->removeAuthenticationSessionData( null );
3303 $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3304 $this->assertNull( $this->manager->getAuthenticationSessionData( 'bar' ) );
3305
3306 }
3307
3308 public function testCanLinkAccounts() {
3309 $types = [
3310 PrimaryAuthenticationProvider::TYPE_CREATE => true,
3311 PrimaryAuthenticationProvider::TYPE_LINK => true,
3312 PrimaryAuthenticationProvider::TYPE_NONE => false,
3313 ];
3314
3315 foreach ( $types as $type => $can ) {
3316 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3317 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
3318 $mock->expects( $this->any() )->method( 'accountCreationType' )
3319 ->will( $this->returnValue( $type ) );
3320 $this->primaryauthMocks = [ $mock ];
3321 $this->initializeManager( true );
3322 $this->assertSame( $can, $this->manager->canCreateAccounts(), $type );
3323 }
3324 }
3325
3326 public function testBeginAccountLink() {
3327 $user = \User::newFromName( 'UTSysop' );
3328 $this->initializeManager();
3329
3330 $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', 'test' );
3331 try {
3332 $this->manager->beginAccountLink( $user, [], 'http://localhost/' );
3333 $this->fail( 'Expected exception not thrown' );
3334 } catch ( \LogicException $ex ) {
3335 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3336 }
3337 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3338
3339 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3340 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3341 $mock->expects( $this->any() )->method( 'accountCreationType' )
3342 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3343 $this->primaryauthMocks = [ $mock ];
3344 $this->initializeManager( true );
3345
3346 $ret = $this->manager->beginAccountLink( new \User, [], 'http://localhost/' );
3347 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3348 $this->assertSame( 'noname', $ret->message->getKey() );
3349
3350 $ret = $this->manager->beginAccountLink(
3351 \User::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
3352 );
3353 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3354 $this->assertSame( 'authmanager-userdoesnotexist', $ret->message->getKey() );
3355 }
3356
3357 public function testContinueAccountLink() {
3358 $user = \User::newFromName( 'UTSysop' );
3359 $this->initializeManager();
3360
3361 $session = [
3362 'userid' => $user->getId(),
3363 'username' => $user->getName(),
3364 'primary' => 'X',
3365 ];
3366
3367 try {
3368 $this->manager->continueAccountLink( [] );
3369 $this->fail( 'Expected exception not thrown' );
3370 } catch ( \LogicException $ex ) {
3371 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3372 }
3373
3374 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3375 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3376 $mock->expects( $this->any() )->method( 'accountCreationType' )
3377 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3378 $mock->expects( $this->any() )->method( 'beginPrimaryAccountLink' )->will(
3379 $this->returnValue( AuthenticationResponse::newFail( $this->message( 'fail' ) ) )
3380 );
3381 $this->primaryauthMocks = [ $mock ];
3382 $this->initializeManager( true );
3383
3384 $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', null );
3385 $ret = $this->manager->continueAccountLink( [] );
3386 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3387 $this->assertSame( 'authmanager-link-not-in-progress', $ret->message->getKey() );
3388
3389 $this->request->getSession()->setSecret( 'AuthManager::accountLinkState',
3390 [ 'username' => $user->getName() . '<>' ] + $session );
3391 $ret = $this->manager->continueAccountLink( [] );
3392 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3393 $this->assertSame( 'noname', $ret->message->getKey() );
3394 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3395
3396 $id = $user->getId();
3397 $this->request->getSession()->setSecret( 'AuthManager::accountLinkState',
3398 [ 'userid' => $id + 1 ] + $session );
3399 try {
3400 $ret = $this->manager->continueAccountLink( [] );
3401 $this->fail( 'Expected exception not thrown' );
3402 } catch ( \UnexpectedValueException $ex ) {
3403 $this->assertEquals(
3404 "User \"{$user->getName()}\" is valid, but ID $id != " . ( $id + 1 ) . '!',
3405 $ex->getMessage()
3406 );
3407 }
3408 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3409 }
3410
3411 /**
3412 * @dataProvider provideAccountLink
3413 * @param StatusValue $preTest
3414 * @param array $primaryResponses
3415 * @param array $managerResponses
3416 */
3417 public function testAccountLink(
3418 StatusValue $preTest, array $primaryResponses, array $managerResponses
3419 ) {
3420 $user = \User::newFromName( 'UTSysop' );
3421
3422 $this->initializeManager();
3423
3424 // Set up lots of mocks...
3425 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
3426 $req->primary = $primaryResponses;
3427 $mocks = [];
3428
3429 foreach ( [ 'pre', 'primary' ] as $key ) {
3430 $class = ucfirst( $key ) . 'AuthenticationProvider';
3431 $mocks[$key] = $this->getMockForAbstractClass(
3432 "MediaWiki\\Auth\\$class", [], "Mock$class"
3433 );
3434 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3435 ->will( $this->returnValue( $key ) );
3436
3437 for ( $i = 2; $i <= 3; $i++ ) {
3438 $mocks[$key . $i] = $this->getMockForAbstractClass(
3439 "MediaWiki\\Auth\\$class", [], "Mock$class"
3440 );
3441 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
3442 ->will( $this->returnValue( $key . $i ) );
3443 }
3444 }
3445
3446 $mocks['pre']->expects( $this->any() )->method( 'testForAccountLink' )
3447 ->will( $this->returnCallback(
3448 function ( $u )
3449 use ( $user, $preTest )
3450 {
3451 $this->assertSame( $user->getId(), $u->getId() );
3452 $this->assertSame( $user->getName(), $u->getName() );
3453 return $preTest;
3454 }
3455 ) );
3456
3457 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
3458 ->will( $this->returnValue( StatusValue::newGood() ) );
3459
3460 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
3461 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3462 $ct = count( $req->primary );
3463 $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req ) {
3464 $this->assertSame( $user->getId(), $u->getId() );
3465 $this->assertSame( $user->getName(), $u->getName() );
3466 $foundReq = false;
3467 foreach ( $reqs as $r ) {
3468 $this->assertSame( $user->getName(), $r->username );
3469 $foundReq = $foundReq || get_class( $r ) === get_class( $req );
3470 }
3471 $this->assertTrue( $foundReq, '$reqs contains $req' );
3472 return array_shift( $req->primary );
3473 } );
3474 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
3475 ->method( 'beginPrimaryAccountLink' )
3476 ->will( $callback );
3477 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
3478 ->method( 'continuePrimaryAccountLink' )
3479 ->will( $callback );
3480
3481 $abstain = AuthenticationResponse::newAbstain();
3482 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
3483 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3484 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
3485 ->will( $this->returnValue( $abstain ) );
3486 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3487 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
3488 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3489 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
3490 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3491
3492 $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
3493 $this->primaryauthMocks = [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
3494 $this->logger = new \TestLogger( true, function ( $message, $level ) {
3495 return $level === LogLevel::DEBUG ? null : $message;
3496 } );
3497 $this->initializeManager( true );
3498
3499 $constraint = \PHPUnit_Framework_Assert::logicalOr(
3500 $this->equalTo( AuthenticationResponse::PASS ),
3501 $this->equalTo( AuthenticationResponse::FAIL )
3502 );
3503 $providers = array_merge( $this->preauthMocks, $this->primaryauthMocks );
3504 foreach ( $providers as $p ) {
3505 $p->postCalled = false;
3506 $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
3507 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
3508 $this->assertInstanceOf( 'User', $user );
3509 $this->assertSame( 'UTSysop', $user->getName() );
3510 $this->assertInstanceOf( AuthenticationResponse::class, $response );
3511 $this->assertThat( $response->status, $constraint );
3512 $p->postCalled = $response->status;
3513 } );
3514 }
3515
3516 $first = true;
3517 $created = false;
3518 $expectLog = [];
3519 foreach ( $managerResponses as $i => $response ) {
3520 if ( $response instanceof AuthenticationResponse &&
3521 $response->status === AuthenticationResponse::PASS
3522 ) {
3523 $expectLog[] = [ LogLevel::INFO, 'Account linked to {user} by primary' ];
3524 }
3525
3526 $ex = null;
3527 try {
3528 if ( $first ) {
3529 $ret = $this->manager->beginAccountLink( $user, [ $req ], 'http://localhost/' );
3530 } else {
3531 $ret = $this->manager->continueAccountLink( [ $req ] );
3532 }
3533 if ( $response instanceof \Exception ) {
3534 $this->fail( 'Expected exception not thrown', "Response $i" );
3535 }
3536 } catch ( \Exception $ex ) {
3537 if ( !$response instanceof \Exception ) {
3538 throw $ex;
3539 }
3540 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
3541 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3542 "Response $i, exception, session state" );
3543 return;
3544 }
3545
3546 $this->assertSame( 'http://localhost/', $req->returnToUrl );
3547
3548 $ret->message = $this->message( $ret->message );
3549 $this->assertEquals( $response, $ret, "Response $i, response" );
3550 if ( $response->status === AuthenticationResponse::PASS ||
3551 $response->status === AuthenticationResponse::FAIL
3552 ) {
3553 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3554 "Response $i, session state" );
3555 foreach ( $providers as $p ) {
3556 $this->assertSame( $response->status, $p->postCalled,
3557 "Response $i, post-auth callback called" );
3558 }
3559 } else {
3560 $this->assertNotNull(
3561 $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3562 "Response $i, session state"
3563 );
3564 $this->assertEquals(
3565 $ret->neededRequests,
3566 $this->manager->getAuthenticationRequests( AuthManager::ACTION_LINK_CONTINUE ),
3567 "Response $i, continuation check"
3568 );
3569 foreach ( $providers as $p ) {
3570 $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
3571 }
3572 }
3573
3574 $first = false;
3575 }
3576
3577 $this->assertSame( $expectLog, $this->logger->getBuffer() );
3578 }
3579
3580 public function provideAccountLink() {
3581 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
3582 $good = StatusValue::newGood();
3583
3584 return [
3585 'Pre-link test fail in pre' => [
3586 StatusValue::newFatal( 'fail-from-pre' ),
3587 [],
3588 [
3589 AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
3590 ]
3591 ],
3592 'Failure in primary' => [
3593 $good,
3594 $tmp = [
3595 AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
3596 ],
3597 $tmp
3598 ],
3599 'All primary abstain' => [
3600 $good,
3601 [
3602 AuthenticationResponse::newAbstain(),
3603 ],
3604 [
3605 AuthenticationResponse::newFail( $this->message( 'authmanager-link-no-primary' ) )
3606 ]
3607 ],
3608 'Primary UI, then redirect, then fail' => [
3609 $good,
3610 $tmp = [
3611 AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
3612 AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
3613 AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
3614 ],
3615 $tmp
3616 ],
3617 'Primary redirect, then abstain' => [
3618 $good,
3619 [
3620 $tmp = AuthenticationResponse::newRedirect(
3621 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
3622 ),
3623 AuthenticationResponse::newAbstain(),
3624 ],
3625 [
3626 $tmp,
3627 new \DomainException(
3628 'MockPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
3629 )
3630 ]
3631 ],
3632 'Primary UI, then pass' => [
3633 $good,
3634 [
3635 $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
3636 AuthenticationResponse::newPass(),
3637 ],
3638 [
3639 $tmp1,
3640 AuthenticationResponse::newPass( '' ),
3641 ]
3642 ],
3643 'Primary pass' => [
3644 $good,
3645 [
3646 AuthenticationResponse::newPass( '' ),
3647 ],
3648 [
3649 AuthenticationResponse::newPass( '' ),
3650 ]
3651 ],
3652 ];
3653 }
3654 }