1<?php
2
3namespace MediaWiki\Auth;
4
5use MediaWiki\MediaWikiServices;
6use MediaWiki\Tests\Unit\Auth\AuthenticationProviderTestTrait;
7use MediaWiki\User\UserNameUtils;
8use Psr\Container\ContainerInterface;
9use Wikimedia\ScopedCallback;
10use Wikimedia\TestingAccessWrapper;
11
12/**
13 * TODO clean up and reduce duplication
14 *
15 * @group AuthManager
16 * @group Database
17 * @covers \MediaWiki\Auth\TemporaryPasswordPrimaryAuthenticationProvider
18 */
19class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiIntegrationTestCase {
20	use AuthenticationProviderTestTrait;
21
22	private $manager = null;
23	private $config = null;
24	private $validity = null;
25
26	/**
27	 * Get an instance of the provider
28	 *
29	 * $provider->checkPasswordValidity is mocked to return $this->validity,
30	 * because we don't need to test that here.
31	 *
32	 * @param array $params
33	 * @return TemporaryPasswordPrimaryAuthenticationProvider
34	 */
35	protected function getProvider( $params = [] ) {
36		$mwServices = MediaWikiServices::getInstance();
37		if ( !$this->config ) {
38			$this->config = new \HashConfig( [
39				'EmailEnabled' => true,
40			] );
41		}
42		$config = new \MultiConfig( [
43			$this->config,
44			$mwServices->getMainConfig()
45		] );
46		$hookContainer = $this->createHookContainer();
47
48		if ( !$this->manager ) {
49			$services = $this->createNoOpAbstractMock( ContainerInterface::class );
50			$objectFactory = new \Wikimedia\ObjectFactory( $services );
51			$userNameUtils = $this->createNoOpMock( UserNameUtils::class );
52
53			$this->manager = new AuthManager(
54				new \FauxRequest(),
55				$config,
56				$objectFactory,
57				$hookContainer,
58				$mwServices->getReadOnlyMode(),
59				$userNameUtils,
60				$mwServices->getBlockManager(),
61				$mwServices->getWatchlistManager(),
62				$mwServices->getDBLoadBalancer(),
63				$mwServices->getContentLanguage(),
64				$mwServices->getLanguageConverterFactory(),
65				$mwServices->getBotPasswordStore(),
66				$mwServices->getUserFactory(),
67				$mwServices->getUserIdentityLookup(),
68				$mwServices->getUserOptionsManager()
69			);
70		}
71		$this->validity = \Status::newGood();
72
73		$mockedMethods[] = 'checkPasswordValidity';
74		$provider = $this->getMockBuilder( TemporaryPasswordPrimaryAuthenticationProvider::class )
75			->onlyMethods( $mockedMethods )
76			->setConstructorArgs( [ $mwServices->getDBLoadBalancer(), $params ] )
77			->getMock();
78		$provider->method( 'checkPasswordValidity' )
79			->will( $this->returnCallback( function () {
80				return $this->validity;
81			} ) );
82		$this->initProvider(
83			$provider, $config, null, $this->manager, null, $this->getServiceContainer()->getUserNameUtils()
84		);
85
86		return $provider;
87	}
88
89	protected function hookMailer( $func = null ) {
90		$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
91		if ( $func ) {
92			$reset = $hookContainer->scopedRegister( 'AlternateUserMailer', $func, true );
93		} else {
94			$reset = $hookContainer->scopedRegister( 'AlternateUserMailer', function () {
95				$this->fail( 'AlternateUserMailer hook called unexpectedly' );
96				return false;
97			}, true );
98		}
99		return $reset;
100	}
101
102	public function testBasics() {
103		$provider = $this->getProvider();
104
105		$this->assertSame(
106			PrimaryAuthenticationProvider::TYPE_CREATE,
107			$provider->accountCreationType()
108		);
109
110		$this->assertTrue( $provider->testUserExists( 'UTSysop' ) );
111		$this->assertTrue( $provider->testUserExists( 'uTSysop' ) );
112		$this->assertFalse( $provider->testUserExists( 'DoesNotExist' ) );
113		$this->assertFalse( $provider->testUserExists( '<invalid>' ) );
114
115		$req = new PasswordAuthenticationRequest;
116		$req->action = AuthManager::ACTION_CHANGE;
117		$req->username = '<invalid>';
118		$provider->providerChangeAuthenticationData( $req );
119	}
120
121	public function testConfig() {
122		$config = new \HashConfig( [
123			'EnableEmail' => false,
124			'NewPasswordExpiry' => 100,
125			'PasswordReminderResendTime' => 101,
126			'AllowRequiringEmailForResets' => false,
127		] );
128
129		$provider = new TemporaryPasswordPrimaryAuthenticationProvider(
130			$this->getServiceContainer()->getDBLoadBalancer()
131		);
132		$providerPriv = TestingAccessWrapper::newFromObject( $provider );
133		$this->initProvider( $provider, $config );
134		$this->assertSame( false, $providerPriv->emailEnabled );
135		$this->assertSame( 100, $providerPriv->newPasswordExpiry );
136		$this->assertSame( 101, $providerPriv->passwordReminderResendTime );
137
138		$provider = new TemporaryPasswordPrimaryAuthenticationProvider(
139			$this->getServiceContainer()->getDBLoadBalancer(),
140			[
141				'emailEnabled' => true,
142				'newPasswordExpiry' => 42,
143				'passwordReminderResendTime' => 43,
144				'allowRequiringEmailForResets' => true,
145			]
146		);
147		$providerPriv = TestingAccessWrapper::newFromObject( $provider );
148		$this->initProvider( $provider, $config );
149		$this->assertSame( true, $providerPriv->emailEnabled );
150		$this->assertSame( 42, $providerPriv->newPasswordExpiry );
151		$this->assertSame( 43, $providerPriv->passwordReminderResendTime );
152		$this->assertSame( true, $providerPriv->allowRequiringEmail );
153	}
154
155	public function testTestUserCanAuthenticate() {
156		$user = self::getMutableTestUser()->getUser();
157
158		$dbw = wfGetDB( DB_PRIMARY );
159		$config = MediaWikiServices::getInstance()->getMainConfig();
160		// A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
161		$passwordFactory = new \PasswordFactory( $config->get( 'PasswordConfig' ), 'A' );
162
163		$pwhash = $passwordFactory->newFromPlaintext( 'password' )->toString();
164
165		$provider = $this->getProvider();
166		$providerPriv = TestingAccessWrapper::newFromObject( $provider );
167
168		$this->assertFalse( $provider->testUserCanAuthenticate( '<invalid>' ) );
169		$this->assertFalse( $provider->testUserCanAuthenticate( 'DoesNotExist' ) );
170
171		$dbw->update(
172			'user',
173			[
174				'user_newpassword' => \PasswordFactory::newInvalidPassword()->toString(),
175				'user_newpass_time' => null,
176			],
177			[ 'user_id' => $user->getId() ]
178		);
179		$this->assertFalse( $provider->testUserCanAuthenticate( $user->getName() ) );
180
181		$dbw->update(
182			'user',
183			[
184				'user_newpassword' => $pwhash,
185				'user_newpass_time' => null,
186			],
187			[ 'user_id' => $user->getId() ]
188		);
189		$this->assertTrue( $provider->testUserCanAuthenticate( $user->getName() ) );
190		$this->assertTrue( $provider->testUserCanAuthenticate( lcfirst( $user->getName() ) ) );
191
192		$dbw->update(
193			'user',
194			[
195				'user_newpassword' => $pwhash,
196				'user_newpass_time' => $dbw->timestamp( time() - 10 ),
197			],
198			[ 'user_id' => $user->getId() ]
199		);
200		$providerPriv->newPasswordExpiry = 100;
201		$this->assertTrue( $provider->testUserCanAuthenticate( $user->getName() ) );
202		$providerPriv->newPasswordExpiry = 1;
203		$this->assertFalse( $provider->testUserCanAuthenticate( $user->getName() ) );
204
205		$dbw->update(
206			'user',
207			[
208				'user_newpassword' => \PasswordFactory::newInvalidPassword()->toString(),
209				'user_newpass_time' => null,
210			],
211			[ 'user_id' => $user->getId() ]
212		);
213	}
214
215	/**
216	 * @dataProvider provideGetAuthenticationRequests
217	 * @param string $action
218	 * @param array $options
219	 * @param array $expected
220	 */
221	public function testGetAuthenticationRequests( $action, $options, $expected ) {
222		$actual = $this->getProvider( [ 'emailEnabled' => true ] )
223			->getAuthenticationRequests( $action, $options );
224		foreach ( $actual as $req ) {
225			if ( $req instanceof TemporaryPasswordAuthenticationRequest && $req->password !== null ) {
226				$req->password = 'random';
227			}
228		}
229		$this->assertEquals( $expected, $actual );
230	}
231
232	public static function provideGetAuthenticationRequests() {
233		$anon = [ 'username' => null ];
234		$registered = [ 'username' => 'UTSysop' ];
235
236		return [
237			[ AuthManager::ACTION_LOGIN, $anon, [
238				new PasswordAuthenticationRequest
239			] ],
240			[ AuthManager::ACTION_LOGIN, $registered, [
241				new PasswordAuthenticationRequest
242			] ],
243			[ AuthManager::ACTION_CREATE, $anon, [] ],
244			[ AuthManager::ACTION_CREATE, $registered, [
245				new TemporaryPasswordAuthenticationRequest( 'random' )
246			] ],
247			[ AuthManager::ACTION_LINK, $anon, [] ],
248			[ AuthManager::ACTION_LINK, $registered, [] ],
249			[ AuthManager::ACTION_CHANGE, $anon, [
250				new TemporaryPasswordAuthenticationRequest( 'random' )
251			] ],
252			[ AuthManager::ACTION_CHANGE, $registered, [
253				new TemporaryPasswordAuthenticationRequest( 'random' )
254			] ],
255			[ AuthManager::ACTION_REMOVE, $anon, [
256				new TemporaryPasswordAuthenticationRequest
257			] ],
258			[ AuthManager::ACTION_REMOVE, $registered, [
259				new TemporaryPasswordAuthenticationRequest
260			] ],
261		];
262	}
263
264	public function testAuthentication() {
265		$user = self::getMutableTestUser()->getUser();
266
267		$password = 'TemporaryPassword';
268		$hash = ':A:' . md5( $password );
269		$dbw = wfGetDB( DB_PRIMARY );
270		$dbw->update(
271			'user',
272			[ 'user_newpassword' => $hash, 'user_newpass_time' => $dbw->timestamp( time() - 10 ) ],
273			[ 'user_id' => $user->getId() ]
274		);
275
276		$req = new PasswordAuthenticationRequest();
277		$req->action = AuthManager::ACTION_LOGIN;
278		$reqs = [ PasswordAuthenticationRequest::class => $req ];
279
280		$provider = $this->getProvider();
281		$providerPriv = TestingAccessWrapper::newFromObject( $provider );
282
283		$providerPriv->newPasswordExpiry = 100;
284
285		// General failures
286		$this->assertEquals(
287			AuthenticationResponse::newAbstain(),
288			$provider->beginPrimaryAuthentication( [] )
289		);
290
291		$req->username = 'foo';
292		$req->password = null;
293		$this->assertEquals(
294			AuthenticationResponse::newAbstain(),
295			$provider->beginPrimaryAuthentication( $reqs )
296		);
297
298		$req->username = null;
299		$req->password = 'bar';
300		$this->assertEquals(
301			AuthenticationResponse::newAbstain(),
302			$provider->beginPrimaryAuthentication( $reqs )
303		);
304
305		$req->username = '<invalid>';
306		$req->password = 'WhoCares';
307		$ret = $provider->beginPrimaryAuthentication( $reqs );
308		$this->assertEquals(
309			AuthenticationResponse::newAbstain(),
310			$provider->beginPrimaryAuthentication( $reqs )
311		);
312
313		$req->username = 'DoesNotExist';
314		$req->password = 'DoesNotExist';
315		$ret = $provider->beginPrimaryAuthentication( $reqs );
316		$this->assertEquals(
317			AuthenticationResponse::newAbstain(),
318			$provider->beginPrimaryAuthentication( $reqs )
319		);
320
321		// Validation failure
322		$req->username = $user->getName();
323		$req->password = $password;
324		$this->validity = \Status::newFatal( 'arbitrary-failure' );
325		$ret = $provider->beginPrimaryAuthentication( $reqs );
326		$this->assertEquals(
327			AuthenticationResponse::FAIL,
328			$ret->status
329		);
330		$this->assertEquals(
331			'arbitrary-failure',
332			$ret->message->getKey()
333		);
334
335		// Successful auth
336		$this->manager->removeAuthenticationSessionData( null );
337		$this->validity = \Status::newGood();
338		$this->assertEquals(
339			AuthenticationResponse::newPass( $user->getName() ),
340			$provider->beginPrimaryAuthentication( $reqs )
341		);
342		$this->assertNotNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
343
344		$this->manager->removeAuthenticationSessionData( null );
345		$this->validity = \Status::newGood();
346		$req->username = lcfirst( $user->getName() );
347		$this->assertEquals(
348			AuthenticationResponse::newPass( $user->getName() ),
349			$provider->beginPrimaryAuthentication( $reqs )
350		);
351		$this->assertNotNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
352		$req->username = $user->getName();
353
354		// Expired password
355		$providerPriv->newPasswordExpiry = 1;
356		$ret = $provider->beginPrimaryAuthentication( $reqs );
357		$this->assertEquals(
358			AuthenticationResponse::FAIL,
359			$ret->status
360		);
361		$this->assertEquals(
362			'wrongpassword',
363			$ret->message->getKey()
364		);
365
366		// Bad password
367		$providerPriv->newPasswordExpiry = 100;
368		$this->validity = \Status::newGood();
369		$req->password = 'Wrong';
370		$ret = $provider->beginPrimaryAuthentication( $reqs );
371		$this->assertEquals(
372			AuthenticationResponse::FAIL,
373			$ret->status
374		);
375		$this->assertEquals(
376			'wrongpassword',
377			$ret->message->getKey()
378		);
379	}
380
381	/**
382	 * @dataProvider provideProviderAllowsAuthenticationDataChange
383	 * @param string $type
384	 * @param string $user
385	 * @param \Status $validity Result of the password validity check
386	 * @param \StatusValue $expect1 Expected result with $checkData = false
387	 * @param \StatusValue $expect2 Expected result with $checkData = true
388	 */
389	public function testProviderAllowsAuthenticationDataChange( $type, $user, \Status $validity,
390		\StatusValue $expect1, \StatusValue $expect2
391	) {
392		if ( $type === PasswordAuthenticationRequest::class ||
393			$type === TemporaryPasswordAuthenticationRequest::class
394		) {
395			$req = new $type();
396		} else {
397			$req = $this->createMock( $type );
398		}
399		$req->action = AuthManager::ACTION_CHANGE;
400		$req->username = $user;
401		$req->password = 'NewPassword';
402
403		$provider = $this->getProvider();
404		$this->validity = $validity;
405		$this->assertEquals( $expect1, $provider->providerAllowsAuthenticationDataChange( $req, false ) );
406		$this->assertEquals( $expect2, $provider->providerAllowsAuthenticationDataChange( $req, true ) );
407	}
408
409	public static function provideProviderAllowsAuthenticationDataChange() {
410		$err = \StatusValue::newGood();
411		$err->error( 'arbitrary-warning' );
412
413		return [
414			[ AuthenticationRequest::class, 'UTSysop', \Status::newGood(),
415				\StatusValue::newGood( 'ignored' ), \StatusValue::newGood( 'ignored' ) ],
416			[ PasswordAuthenticationRequest::class, 'UTSysop', \Status::newGood(),
417				\StatusValue::newGood( 'ignored' ), \StatusValue::newGood( 'ignored' ) ],
418			[ TemporaryPasswordAuthenticationRequest::class, 'UTSysop', \Status::newGood(),
419				\StatusValue::newGood(), \StatusValue::newGood() ],
420			[ TemporaryPasswordAuthenticationRequest::class, 'uTSysop', \Status::newGood(),
421				\StatusValue::newGood(), \StatusValue::newGood() ],
422			[ TemporaryPasswordAuthenticationRequest::class, 'UTSysop', \Status::wrap( $err ),
423				\StatusValue::newGood(), $err ],
424			[ TemporaryPasswordAuthenticationRequest::class, 'UTSysop',
425				\Status::newFatal( 'arbitrary-error' ), \StatusValue::newGood(),
426				\StatusValue::newFatal( 'arbitrary-error' ) ],
427			[ TemporaryPasswordAuthenticationRequest::class, 'DoesNotExist', \Status::newGood(),
428				\StatusValue::newGood(), \StatusValue::newGood( 'ignored' ) ],
429			[ TemporaryPasswordAuthenticationRequest::class, '<invalid>', \Status::newGood(),
430				\StatusValue::newGood(), \StatusValue::newGood( 'ignored' ) ],
431		];
432	}
433
434	/**
435	 * @dataProvider provideProviderChangeAuthenticationData
436	 * @param string $user
437	 * @param string $type
438	 * @param bool $changed
439	 */
440	public function testProviderChangeAuthenticationData( $user, $type, $changed ) {
441		$cuser = ucfirst( $user );
442		$oldpass = 'OldTempPassword';
443		$newpass = 'NewTempPassword';
444
445		$dbw = wfGetDB( DB_PRIMARY );
446		$oldHash = $dbw->selectField( 'user', 'user_newpassword', [ 'user_name' => $cuser ] );
447		$cb = new ScopedCallback( static function () use ( $dbw, $cuser, $oldHash ) {
448			$dbw->update( 'user', [ 'user_newpassword' => $oldHash ], [ 'user_name' => $cuser ] );
449		} );
450
451		$hash = ':A:' . md5( $oldpass );
452		$dbw->update(
453			'user',
454			[ 'user_newpassword' => $hash, 'user_newpass_time' => $dbw->timestamp( time() + 10 ) ],
455			[ 'user_name' => $cuser ]
456		);
457
458		$provider = $this->getProvider();
459
460		// Sanity check
461		$loginReq = new PasswordAuthenticationRequest();
462		$loginReq->action = AuthManager::ACTION_CHANGE;
463		$loginReq->username = $user;
464		$loginReq->password = $oldpass;
465		$loginReqs = [ PasswordAuthenticationRequest::class => $loginReq ];
466		$this->assertEquals(
467			AuthenticationResponse::newPass( $cuser ),
468			$provider->beginPrimaryAuthentication( $loginReqs ),
469			'Sanity check'
470		);
471
472		if ( $type === PasswordAuthenticationRequest::class ||
473			$type === TemporaryPasswordAuthenticationRequest::class
474		) {
475			$changeReq = new $type();
476		} else {
477			$changeReq = $this->createMock( $type );
478		}
479		$changeReq->action = AuthManager::ACTION_CHANGE;
480		$changeReq->username = $user;
481		$changeReq->password = $newpass;
482		$resetMailer = $this->hookMailer();
483		$provider->providerChangeAuthenticationData( $changeReq );
484		ScopedCallback::consume( $resetMailer );
485
486		$loginReq->password = $oldpass;
487		$ret = $provider->beginPrimaryAuthentication( $loginReqs );
488		$this->assertEquals(
489			AuthenticationResponse::FAIL,
490			$ret->status,
491			'old password should fail'
492		);
493		$this->assertEquals(
494			'wrongpassword',
495			$ret->message->getKey(),
496			'old password should fail'
497		);
498
499		$loginReq->password = $newpass;
500		$ret = $provider->beginPrimaryAuthentication( $loginReqs );
501		if ( $changed ) {
502			$this->assertEquals(
503				AuthenticationResponse::newPass( $cuser ),
504				$ret,
505				'new password should pass'
506			);
507			$this->assertNotNull(
508				$dbw->selectField( 'user', 'user_newpass_time', [ 'user_name' => $cuser ] )
509			);
510		} else {
511			$this->assertEquals(
512				AuthenticationResponse::FAIL,
513				$ret->status,
514				'new password should fail'
515			);
516			$this->assertEquals(
517				'wrongpassword',
518				$ret->message->getKey(),
519				'new password should fail'
520			);
521			$this->assertNull(
522				$dbw->selectField( 'user', 'user_newpass_time', [ 'user_name' => $cuser ] )
523			);
524		}
525	}
526
527	public static function provideProviderChangeAuthenticationData() {
528		return [
529			[ 'UTSysop', AuthenticationRequest::class, false ],
530			[ 'UTSysop', PasswordAuthenticationRequest::class, false ],
531			[ 'UTSysop', TemporaryPasswordAuthenticationRequest::class, true ],
532		];
533	}
534
535	public function testProviderChangeAuthenticationDataEmail() {
536		$user = self::getMutableTestUser()->getUser();
537
538		$dbw = wfGetDB( DB_PRIMARY );
539		$dbw->update(
540			'user',
541			[ 'user_newpass_time' => $dbw->timestamp( time() - 5 * 3600 ) ],
542			[ 'user_id' => $user->getId() ]
543		);
544
545		$req = TemporaryPasswordAuthenticationRequest::newRandom();
546		$req->username = $user->getName();
547		$req->mailpassword = true;
548
549		$provider = $this->getProvider( [ 'emailEnabled' => false ] );
550		$status = $provider->providerAllowsAuthenticationDataChange( $req, true );
551		$this->assertEquals( \StatusValue::newFatal( 'passwordreset-emaildisabled' ), $status );
552
553		$provider = $this->getProvider( [
554			'emailEnabled' => true, 'passwordReminderResendTime' => 10
555		] );
556		$status = $provider->providerAllowsAuthenticationDataChange( $req, true );
557		$this->assertEquals( \StatusValue::newFatal( 'throttled-mailpassword', 10 ), $status );
558
559		$provider = $this->getProvider( [
560			'emailEnabled' => true, 'passwordReminderResendTime' => 3
561		] );
562		$status = $provider->providerAllowsAuthenticationDataChange( $req, true );
563		$this->assertFalse( $status->hasMessage( 'throttled-mailpassword' ) );
564
565		$dbw->update(
566			'user',
567			[ 'user_newpass_time' => $dbw->timestamp( time() + 5 * 3600 ) ],
568			[ 'user_id' => $user->getId() ]
569		);
570		$provider = $this->getProvider( [
571			'emailEnabled' => true, 'passwordReminderResendTime' => 0
572		] );
573		$status = $provider->providerAllowsAuthenticationDataChange( $req, true );
574		$this->assertFalse( $status->hasMessage( 'throttled-mailpassword' ) );
575
576		$req->caller = null;
577		$status = $provider->providerAllowsAuthenticationDataChange( $req, true );
578		$this->assertEquals( \StatusValue::newFatal( 'passwordreset-nocaller' ), $status );
579
580		$req->caller = '127.0.0.256';
581		$status = $provider->providerAllowsAuthenticationDataChange( $req, true );
582		$this->assertEquals( \StatusValue::newFatal( 'passwordreset-nosuchcaller', '127.0.0.256' ),
583			$status );
584
585		$req->caller = '<Invalid>';
586		$status = $provider->providerAllowsAuthenticationDataChange( $req, true );
587		$this->assertEquals( \StatusValue::newFatal( 'passwordreset-nosuchcaller', '<Invalid>' ),
588			$status );
589
590		$req->caller = '127.0.0.1';
591		$status = $provider->providerAllowsAuthenticationDataChange( $req, true );
592		$this->assertEquals( \StatusValue::newGood(), $status );
593
594		$req->caller = $user->getName();
595		$status = $provider->providerAllowsAuthenticationDataChange( $req, true );
596		$this->assertEquals( \StatusValue::newGood(), $status );
597
598		$mailed = false;
599		$resetMailer = $this->hookMailer( function ( $headers, $to, $from, $subject, $body )
600			use ( &$mailed, $req, $user )
601		{
602			$mailed = true;
603			$this->assertSame( $user->getEmail(), $to[0]->address );
604			$this->assertStringContainsString( $req->password, $body );
605			return false;
606		} );
607		$provider->providerChangeAuthenticationData( $req );
608		ScopedCallback::consume( $resetMailer );
609		$this->assertTrue( $mailed );
610
611		$priv = TestingAccessWrapper::newFromObject( $provider );
612		$req->username = '<invalid>';
613		$status = $priv->sendPasswordResetEmail( $req );
614		$this->assertEquals( \Status::newFatal( 'noname' ), $status );
615	}
616
617	public function testTestForAccountCreation() {
618		$user = \User::newFromName( 'foo' );
619		$req = new TemporaryPasswordAuthenticationRequest();
620		$req->username = 'Foo';
621		$req->password = 'Bar';
622		$reqs = [ TemporaryPasswordAuthenticationRequest::class => $req ];
623
624		$provider = $this->getProvider();
625		$this->assertEquals(
626			\StatusValue::newGood(),
627			$provider->testForAccountCreation( $user, $user, [] ),
628			'No password request'
629		);
630
631		$this->assertEquals(
632			\StatusValue::newGood(),
633			$provider->testForAccountCreation( $user, $user, $reqs ),
634			'Password request, validated'
635		);
636
637		$this->validity->error( 'arbitrary warning' );
638		$expect = \StatusValue::newGood();
639		$expect->error( 'arbitrary warning' );
640		$this->assertEquals(
641			$expect,
642			$provider->testForAccountCreation( $user, $user, $reqs ),
643			'Password request, not validated'
644		);
645	}
646
647	public function testAccountCreation() {
648		$resetMailer = $this->hookMailer();
649
650		$user = \User::newFromName( 'Foo' );
651
652		$req = new TemporaryPasswordAuthenticationRequest();
653		$reqs = [ TemporaryPasswordAuthenticationRequest::class => $req ];
654
655		$authreq = new PasswordAuthenticationRequest();
656		$authreq->action = AuthManager::ACTION_CREATE;
657		$authreqs = [ PasswordAuthenticationRequest::class => $authreq ];
658
659		$provider = $this->getProvider();
660
661		$this->assertEquals(
662			AuthenticationResponse::newAbstain(),
663			$provider->beginPrimaryAccountCreation( $user, $user, [] )
664		);
665
666		$req->username = 'foo';
667		$req->password = null;
668		$this->assertEquals(
669			AuthenticationResponse::newAbstain(),
670			$provider->beginPrimaryAccountCreation( $user, $user, $reqs )
671		);
672
673		$req->username = null;
674		$req->password = 'bar';
675		$this->assertEquals(
676			AuthenticationResponse::newAbstain(),
677			$provider->beginPrimaryAccountCreation( $user, $user, $reqs )
678		);
679
680		$req->username = 'foo';
681		$req->password = 'bar';
682
683		$expect = AuthenticationResponse::newPass( 'Foo' );
684		$expect->createRequest = clone $req;
685		$expect->createRequest->username = 'Foo';
686		$this->assertEquals( $expect, $provider->beginPrimaryAccountCreation( $user, $user, $reqs ) );
687		$this->assertNull( $this->manager->getAuthenticationSessionData( 'no-email' ) );
688
689		$user = self::getMutableTestUser()->getUser();
690		$req->username = $authreq->username = $user->getName();
691		$req->password = $authreq->password = 'NewPassword';
692		$expect = AuthenticationResponse::newPass( $user->getName() );
693		$expect->createRequest = $req;
694
695		$res2 = $provider->beginPrimaryAccountCreation( $user, $user, $reqs );
696		$this->assertEquals( $expect, $res2, 'Sanity check' );
697
698		$ret = $provider->beginPrimaryAuthentication( $authreqs );
699		$this->assertEquals( AuthenticationResponse::FAIL, $ret->status, 'sanity check' );
700
701		$this->assertSame( null, $provider->finishAccountCreation( $user, $user, $res2 ) );
702
703		$ret = $provider->beginPrimaryAuthentication( $authreqs );
704		$this->assertEquals( AuthenticationResponse::PASS, $ret->status, 'new password is set' );
705	}
706
707	public function testAccountCreationEmail() {
708		$creator = \User::newFromName( 'Foo' );
709
710		$user = self::getMutableTestUser()->getUser();
711		$user->setEmail( '' );
712
713		$req = TemporaryPasswordAuthenticationRequest::newRandom();
714		$req->username = $user->getName();
715		$req->mailpassword = true;
716
717		$provider = $this->getProvider( [ 'emailEnabled' => false ] );
718		$status = $provider->testForAccountCreation( $user, $creator, [ $req ] );
719		$this->assertEquals( \StatusValue::newFatal( 'emaildisabled' ), $status );
720
721		$provider = $this->getProvider( [ 'emailEnabled' => true ] );
722		$status = $provider->testForAccountCreation( $user, $creator, [ $req ] );
723		$this->assertEquals( \StatusValue::newFatal( 'noemailcreate' ), $status );
724
725		$user->setEmail( 'test@localhost.localdomain' );
726		$status = $provider->testForAccountCreation( $user, $creator, [ $req ] );
727		$this->assertEquals( \StatusValue::newGood(), $status );
728
729		$mailed = false;
730		$resetMailer = $this->hookMailer( function ( $headers, $to, $from, $subject, $body )
731			use ( &$mailed, $req )
732		{
733			$mailed = true;
734			$this->assertSame( 'test@localhost.localdomain', $to[0]->address );
735			$this->assertStringContainsString( $req->password, $body );
736			return false;
737		} );
738
739		$expect = AuthenticationResponse::newPass( $user->getName() );
740		$expect->createRequest = clone $req;
741		$expect->createRequest->username = $user->getName();
742		$res = $provider->beginPrimaryAccountCreation( $user, $creator, [ $req ] );
743		$this->assertEquals( $expect, $res );
744		$this->assertTrue( $this->manager->getAuthenticationSessionData( 'no-email' ) );
745		$this->assertFalse( $mailed );
746
747		$this->assertSame( 'byemail', $provider->finishAccountCreation( $user, $creator, $res ) );
748		$this->assertTrue( $mailed );
749
750		ScopedCallback::consume( $resetMailer );
751		$this->assertTrue( $mailed );
752	}
753
754}
755