Services: Convert PasswordReset's static to a const now HHVM is gone
[lhc/web/wiklou.git] / tests / phpunit / includes / user / PasswordResetTest.php
1 <?php
2
3 use MediaWiki\Auth\AuthManager;
4 use MediaWiki\Auth\TemporaryPasswordAuthenticationRequest;
5 use MediaWiki\Block\DatabaseBlock;
6 use MediaWiki\Block\CompositeBlock;
7 use MediaWiki\Block\SystemBlock;
8 use MediaWiki\Config\ServiceOptions;
9 use MediaWiki\Permissions\PermissionManager;
10 use Psr\Log\NullLogger;
11 use Wikimedia\Rdbms\ILoadBalancer;
12
13 /**
14 * @covers PasswordReset
15 * @group Database
16 */
17 class PasswordResetTest extends MediaWikiTestCase {
18 const VALID_IP = '1.2.3.4';
19 const VALID_EMAIL = 'foo@bar.baz';
20
21 /**
22 * @dataProvider provideIsAllowed
23 */
24 public function testIsAllowed( $passwordResetRoutes, $enableEmail,
25 $allowsAuthenticationDataChange, $canEditPrivate, $block, $globalBlock, $isAllowed
26 ) {
27 $config = $this->makeConfig( $enableEmail, $passwordResetRoutes, false );
28
29 $authManager = $this->getMockBuilder( AuthManager::class )->disableOriginalConstructor()
30 ->getMock();
31 $authManager->expects( $this->any() )->method( 'allowsAuthenticationDataChange' )
32 ->willReturn( $allowsAuthenticationDataChange ? Status::newGood() : Status::newFatal( 'foo' ) );
33
34 $user = $this->getMockBuilder( User::class )->getMock();
35 $user->expects( $this->any() )->method( 'getName' )->willReturn( 'Foo' );
36 $user->expects( $this->any() )->method( 'getBlock' )->willReturn( $block );
37 $user->expects( $this->any() )->method( 'getGlobalBlock' )->willReturn( $globalBlock );
38
39 $permissionManager = $this->getMockBuilder( PermissionManager::class )
40 ->disableOriginalConstructor()
41 ->getMock();
42 $permissionManager->method( 'userHasRight' )
43 ->with( $user, 'editmyprivateinfo' )
44 ->willReturn( $canEditPrivate );
45
46 $loadBalancer = $this->getMockBuilder( ILoadBalancer::class )->getMock();
47
48 $passwordReset = new PasswordReset(
49 $config,
50 $authManager,
51 $permissionManager,
52 $loadBalancer,
53 new NullLogger()
54 );
55
56 $this->assertSame( $isAllowed, $passwordReset->isAllowed( $user )->isGood() );
57 }
58
59 public function provideIsAllowed() {
60 return [
61 'no routes' => [
62 'passwordResetRoutes' => [],
63 'enableEmail' => true,
64 'allowsAuthenticationDataChange' => true,
65 'canEditPrivate' => true,
66 'block' => null,
67 'globalBlock' => null,
68 'isAllowed' => false,
69 ],
70 'email disabled' => [
71 'passwordResetRoutes' => [ 'username' => true ],
72 'enableEmail' => false,
73 'allowsAuthenticationDataChange' => true,
74 'canEditPrivate' => true,
75 'block' => null,
76 'globalBlock' => null,
77 'isAllowed' => false,
78 ],
79 'auth data change disabled' => [
80 'passwordResetRoutes' => [ 'username' => true ],
81 'enableEmail' => true,
82 'allowsAuthenticationDataChange' => false,
83 'canEditPrivate' => true,
84 'block' => null,
85 'globalBlock' => null,
86 'isAllowed' => false,
87 ],
88 'cannot edit private data' => [
89 'passwordResetRoutes' => [ 'username' => true ],
90 'enableEmail' => true,
91 'allowsAuthenticationDataChange' => true,
92 'canEditPrivate' => false,
93 'block' => null,
94 'globalBlock' => null,
95 'isAllowed' => false,
96 ],
97 'blocked with account creation disabled' => [
98 'passwordResetRoutes' => [ 'username' => true ],
99 'enableEmail' => true,
100 'allowsAuthenticationDataChange' => true,
101 'canEditPrivate' => true,
102 'block' => new DatabaseBlock( [ 'createAccount' => true ] ),
103 'globalBlock' => null,
104 'isAllowed' => false,
105 ],
106 'blocked w/o account creation disabled' => [
107 'passwordResetRoutes' => [ 'username' => true ],
108 'enableEmail' => true,
109 'allowsAuthenticationDataChange' => true,
110 'canEditPrivate' => true,
111 'block' => new DatabaseBlock( [] ),
112 'globalBlock' => null,
113 'isAllowed' => true,
114 ],
115 'using blocked proxy' => [
116 'passwordResetRoutes' => [ 'username' => true ],
117 'enableEmail' => true,
118 'allowsAuthenticationDataChange' => true,
119 'canEditPrivate' => true,
120 'block' => new SystemBlock(
121 [ 'systemBlock' => 'proxy' ]
122 ),
123 'globalBlock' => null,
124 'isAllowed' => false,
125 ],
126 'globally blocked with account creation not disabled' => [
127 'passwordResetRoutes' => [ 'username' => true ],
128 'enableEmail' => true,
129 'allowsAuthenticationDataChange' => true,
130 'canEditPrivate' => true,
131 'block' => null,
132 'globalBlock' => new SystemBlock(
133 [ 'systemBlock' => 'global-block' ]
134 ),
135 'isAllowed' => true,
136 ],
137 'blocked via wgSoftBlockRanges' => [
138 'passwordResetRoutes' => [ 'username' => true ],
139 'enableEmail' => true,
140 'allowsAuthenticationDataChange' => true,
141 'canEditPrivate' => true,
142 'block' => new SystemBlock(
143 [ 'systemBlock' => 'wgSoftBlockRanges', 'anonOnly' => true ]
144 ),
145 'globalBlock' => null,
146 'isAllowed' => true,
147 ],
148 'blocked with an unknown system block type' => [
149 'passwordResetRoutes' => [ 'username' => true ],
150 'enableEmail' => true,
151 'allowsAuthenticationDataChange' => true,
152 'canEditPrivate' => true,
153 'block' => new SystemBlock( [ 'systemBlock' => 'unknown' ] ),
154 'globalBlock' => null,
155 'isAllowed' => false,
156 ],
157 'blocked with multiple blocks, all allowing password reset' => [
158 'passwordResetRoutes' => [ 'username' => true ],
159 'enableEmail' => true,
160 'allowsAuthenticationDataChange' => true,
161 'canEditPrivate' => true,
162 'block' => new CompositeBlock( [
163 'originalBlocks' => [
164 new SystemBlock( [ 'systemBlock' => 'wgSoftBlockRanges', 'anonOnly' => true ] ),
165 new Block( [] ),
166 ]
167 ] ),
168 'globalBlock' => null,
169 'isAllowed' => true,
170 ],
171 'blocked with multiple blocks, not all allowing password reset' => [
172 'passwordResetRoutes' => [ 'username' => true ],
173 'enableEmail' => true,
174 'allowsAuthenticationDataChange' => true,
175 'canEditPrivate' => true,
176 'block' => new CompositeBlock( [
177 'originalBlocks' => [
178 new SystemBlock( [ 'systemBlock' => 'wgSoftBlockRanges', 'anonOnly' => true ] ),
179 new SystemBlock( [ 'systemBlock' => 'proxy' ] ),
180 ]
181 ] ),
182 'globalBlock' => null,
183 'isAllowed' => false,
184 ],
185 'all OK' => [
186 'passwordResetRoutes' => [ 'username' => true ],
187 'enableEmail' => true,
188 'allowsAuthenticationDataChange' => true,
189 'canEditPrivate' => true,
190 'block' => null,
191 'globalBlock' => null,
192 'isAllowed' => true,
193 ],
194 ];
195 }
196
197 /**
198 * @expectedException \LogicException
199 */
200 public function testExecute_notAllowed() {
201 $user = $this->getMock( User::class );
202 /** @var User $user */
203
204 $passwordReset = $this->getMockBuilder( PasswordReset::class )
205 ->disableOriginalConstructor()
206 ->setMethods( [ 'isAllowed' ] )
207 ->getMock();
208 $passwordReset->expects( $this->any() )
209 ->method( 'isAllowed' )
210 ->with( $user )
211 ->willReturn( Status::newFatal( 'somestatuscode' ) );
212 /** @var PasswordReset $passwordReset */
213
214 $passwordReset->execute( $user );
215 }
216
217 /**
218 * @dataProvider provideExecute
219 * @param string|bool $expectedError
220 * @param ServiceOptions $config
221 * @param User $performingUser
222 * @param PermissionManager $permissionManager
223 * @param AuthManager $authManager
224 * @param string|null $username
225 * @param string|null $email
226 * @param User[] $usersWithEmail
227 */
228 public function testExecute(
229 $expectedError,
230 ServiceOptions $config,
231 User $performingUser,
232 PermissionManager $permissionManager,
233 AuthManager $authManager,
234 $username = '',
235 $email = '',
236 array $usersWithEmail = []
237 ) {
238 // Unregister the hooks for proper unit testing
239 $this->mergeMwGlobalArrayValue( 'wgHooks', [
240 'User::mailPasswordInternal' => [],
241 'SpecialPasswordResetOnSubmit' => [],
242 ] );
243
244 $loadBalancer = $this->getMockBuilder( ILoadBalancer::class )
245 ->getMock();
246
247 $users = $this->makeUsers();
248
249 $lookupUser = function ( $username ) use ( $users ) {
250 return $users[ $username ] ?? false;
251 };
252
253 $passwordReset = $this->getMockBuilder( PasswordReset::class )
254 ->setMethods( [ 'getUsersByEmail', 'isAllowed', 'lookupUser' ] )
255 ->setConstructorArgs( [
256 $config,
257 $authManager,
258 $permissionManager,
259 $loadBalancer,
260 new NullLogger()
261 ] )
262 ->getMock();
263 $passwordReset->method( 'getUsersByEmail' )->with( $email )
264 ->willReturn( array_map( $lookupUser, $usersWithEmail ) );
265 $passwordReset->method( 'isAllowed' )
266 ->willReturn( Status::newGood() );
267 $passwordReset->method( 'lookupUser' )
268 ->willReturnCallback( $lookupUser );
269
270 /** @var PasswordReset $passwordReset */
271 $status = $passwordReset->execute( $performingUser, $username, $email );
272 $this->assertStatus( $status, $expectedError );
273 }
274
275 public function provideExecute() {
276 $defaultConfig = $this->makeConfig( true, [ 'username' => true, 'email' => true ], false );
277 $emailRequiredConfig = $this->makeConfig( true, [ 'username' => true, 'email' => true ], true );
278 $performingUser = $this->makePerformingUser( self::VALID_IP, false );
279 $throttledUser = $this->makePerformingUser( self::VALID_IP, true );
280 $permissionManager = $this->makePermissionManager( $performingUser, true );
281
282 return [
283 'Invalid email' => [
284 'expectedError' => 'passwordreset-invalidemail',
285 'config' => $defaultConfig,
286 'performingUser' => $throttledUser,
287 'permissionManager' => $permissionManager,
288 'authManager' => $this->makeAuthManager(),
289 'username' => '',
290 'email' => '[invalid email]',
291 'usersWithEmail' => [],
292 ],
293 'No username, no email' => [
294 'expectedError' => 'passwordreset-nodata',
295 'config' => $defaultConfig,
296 'performingUser' => $throttledUser,
297 'permissionManager' => $permissionManager,
298 'authManager' => $this->makeAuthManager(),
299 'username' => '',
300 'email' => '',
301 'usersWithEmail' => [],
302 ],
303 'Email route not enabled' => [
304 'expectedError' => 'passwordreset-nodata',
305 'config' => $this->makeConfig( true, [ 'username' => true ], false ),
306 'performingUser' => $throttledUser,
307 'permissionManager' => $permissionManager,
308 'authManager' => $this->makeAuthManager(),
309 'username' => '',
310 'email' => self::VALID_EMAIL,
311 'usersWithEmail' => [],
312 ],
313 'Username route not enabled' => [
314 'expectedError' => 'passwordreset-nodata',
315 'config' => $this->makeConfig( true, [ 'email' => true ], false ),
316 'performingUser' => $throttledUser,
317 'permissionManager' => $permissionManager,
318 'authManager' => $this->makeAuthManager(),
319 'username' => 'User1',
320 'email' => '',
321 'usersWithEmail' => [],
322 ],
323 'No routes enabled' => [
324 'expectedError' => 'passwordreset-nodata',
325 'config' => $this->makeConfig( true, [], false ),
326 'performingUser' => $throttledUser,
327 'permissionManager' => $permissionManager,
328 'authManager' => $this->makeAuthManager(),
329 'username' => 'User1',
330 'email' => self::VALID_EMAIL,
331 'usersWithEmail' => [],
332 ],
333 'Email reqiured for resets, but is empty' => [
334 'expectedError' => 'passwordreset-username-email-required',
335 'config' => $emailRequiredConfig,
336 'performingUser' => $throttledUser,
337 'permissionManager' => $permissionManager,
338 'authManager' => $this->makeAuthManager(),
339 'username' => 'User1',
340 'email' => '',
341 'usersWithEmail' => [],
342 ],
343 'Email reqiured for resets, is invalid' => [
344 'expectedError' => 'passwordreset-invalidemail',
345 'config' => $emailRequiredConfig,
346 'performingUser' => $throttledUser,
347 'permissionManager' => $permissionManager,
348 'authManager' => $this->makeAuthManager(),
349 'username' => 'User1',
350 'email' => '[invalid email]',
351 'usersWithEmail' => [],
352 ],
353 'Throttled' => [
354 'expectedError' => 'actionthrottledtext',
355 'config' => $defaultConfig,
356 'performingUser' => $throttledUser,
357 'permissionManager' => $permissionManager,
358 'authManager' => $this->makeAuthManager(),
359 'username' => 'User1',
360 'email' => '',
361 'usersWithEmail' => [],
362 ],
363 'No user by this username' => [
364 'expectedError' => 'nosuchuser',
365 'config' => $defaultConfig,
366 'performingUser' => $performingUser,
367 'permissionManager' => $permissionManager,
368 'authManager' => $this->makeAuthManager(),
369 'username' => 'Nonexistent user',
370 'email' => '',
371 'usersWithEmail' => [],
372 ],
373 'If no users with this email found, pretend everything is OK' => [
374 'expectedError' => false,
375 'config' => $defaultConfig,
376 'performingUser' => $performingUser,
377 'permissionManager' => $permissionManager,
378 'authManager' => $this->makeAuthManager(),
379 'username' => '',
380 'email' => 'some@not.found.email',
381 'usersWithEmail' => [],
382 ],
383 'No email for the user' => [
384 'expectedError' => 'noemail',
385 'config' => $defaultConfig,
386 'performingUser' => $performingUser,
387 'permissionManager' => $permissionManager,
388 'authManager' => $this->makeAuthManager(),
389 'username' => 'BadUser',
390 'email' => '',
391 'usersWithEmail' => [],
392 ],
393 'Email reqiured for resets, no match' => [
394 'expectedError' => false,
395 'config' => $emailRequiredConfig,
396 'performingUser' => $performingUser,
397 'permissionManager' => $permissionManager,
398 'authManager' => $this->makeAuthManager(),
399 'username' => 'User1',
400 'email' => 'some@other.email',
401 'usersWithEmail' => [],
402 ],
403 "Couldn't determine the performing user's IP" => [
404 'expectedError' => 'badipaddress',
405 'config' => $defaultConfig,
406 'performingUser' => $this->makePerformingUser( null, false ),
407 'permissionManager' => $permissionManager,
408 'authManager' => $this->makeAuthManager(),
409 'username' => 'User1',
410 'email' => '',
411 'usersWithEmail' => [],
412 ],
413 'User is allowed, but ignored' => [
414 'expectedError' => 'passwordreset-ignored',
415 'config' => $defaultConfig,
416 'performingUser' => $performingUser,
417 'permissionManager' => $permissionManager,
418 'authManager' => $this->makeAuthManager( [ 'User1' ], 0, [ 'User1' ] ),
419 'username' => 'User1',
420 'email' => '',
421 'usersWithEmail' => [],
422 ],
423 'One of users is ignored' => [
424 'expectedError' => 'passwordreset-ignored',
425 'config' => $defaultConfig,
426 'performingUser' => $performingUser,
427 'permissionManager' => $permissionManager,
428 'authManager' => $this->makeAuthManager( [ 'User1', 'User2' ], 0, [ 'User2' ] ),
429 'username' => '',
430 'email' => self::VALID_EMAIL,
431 'usersWithEmail' => [ 'User1', 'User2' ],
432 ],
433 'User is rejected' => [
434 'expectedError' => 'rejected by test mock',
435 'config' => $defaultConfig,
436 'performingUser' => $performingUser,
437 'permissionManager' => $permissionManager,
438 'authManager' => $this->makeAuthManager(),
439 'username' => 'User1',
440 'email' => '',
441 'usersWithEmail' => [],
442 ],
443 'One of users is rejected' => [
444 'expectedError' => 'rejected by test mock',
445 'config' => $defaultConfig,
446 'performingUser' => $performingUser,
447 'permissionManager' => $permissionManager,
448 'authManager' => $this->makeAuthManager( [ 'User1' ] ),
449 'username' => '',
450 'email' => self::VALID_EMAIL,
451 'usersWithEmail' => [ 'User1', 'User2' ],
452 ],
453 'Reset one user via password' => [
454 'expectedError' => false,
455 'config' => $defaultConfig,
456 'performingUser' => $performingUser,
457 'permissionManager' => $permissionManager,
458 'authManager' => $this->makeAuthManager( [ 'User1' ], 1 ),
459 'username' => 'User1',
460 'email' => self::VALID_EMAIL,
461 // Make sure that only the user specified by username is reset
462 'usersWithEmail' => [ 'User1', 'User2' ],
463 ],
464 'Reset one user via email' => [
465 'expectedError' => false,
466 'config' => $defaultConfig,
467 'performingUser' => $performingUser,
468 'permissionManager' => $permissionManager,
469 'authManager' => $this->makeAuthManager( [ 'User1' ], 1 ),
470 'username' => '',
471 'email' => self::VALID_EMAIL,
472 'usersWithEmail' => [ 'User1' ],
473 ],
474 'Reset multiple users via email' => [
475 'expectedError' => false,
476 'config' => $defaultConfig,
477 'performingUser' => $performingUser,
478 'permissionManager' => $permissionManager,
479 'authManager' => $this->makeAuthManager( [ 'User1', 'User2' ], 2 ),
480 'username' => '',
481 'email' => self::VALID_EMAIL,
482 'usersWithEmail' => [ 'User1', 'User2' ],
483 ],
484 "Email is required for resets, user didn't opt in" => [
485 'expectedError' => false,
486 'config' => $emailRequiredConfig,
487 'performingUser' => $performingUser,
488 'permissionManager' => $permissionManager,
489 'authManager' => $this->makeAuthManager( [ 'User2' ], 1 ),
490 'username' => 'User2',
491 'email' => self::VALID_EMAIL,
492 'usersWithEmail' => [ 'User2' ],
493 ],
494 ];
495 }
496
497 private function assertStatus( StatusValue $status, $error = false ) {
498 if ( $error === false ) {
499 $this->assertTrue( $status->isGood(), 'Expected status to be good' );
500 } else {
501 $this->assertFalse( $status->isGood(), 'Expected status to not be good' );
502 if ( is_string( $error ) ) {
503 $this->assertNotEmpty( $status->getErrors() );
504 $message = $status->getErrors()[0]['message'];
505 if ( $message instanceof MessageSpecifier ) {
506 $message = $message->getKey();
507 }
508 $this->assertSame( $error, $message );
509 }
510 }
511 }
512
513 private function makeConfig( $enableEmail, array $passwordResetRoutes, $emailForResets ) {
514 $hash = [
515 'AllowRequiringEmailForResets' => $emailForResets,
516 'EnableEmail' => $enableEmail,
517 'PasswordResetRoutes' => $passwordResetRoutes,
518 ];
519
520 return new ServiceOptions( PasswordReset::CONSTRUCTOR_OPTIONS, $hash );
521 }
522
523 /**
524 * @param string|null $ip
525 * @param bool $pingLimited
526 * @return User
527 */
528 private function makePerformingUser( $ip, $pingLimited ) : User {
529 $request = $this->getMockBuilder( WebRequest::class )
530 ->getMock();
531 $request->method( 'getIP' )
532 ->willReturn( $ip );
533 /** @var WebRequest $request */
534
535 $user = $this->getMockBuilder( User::class )
536 ->setMethods( [ 'getName', 'pingLimiter', 'getRequest' ] )
537 ->getMock();
538
539 $user->method( 'getName' )
540 ->willReturn( 'SomeUser' );
541 $user->method( 'pingLimiter' )
542 ->with( 'mailpassword' )
543 ->willReturn( $pingLimited );
544 $user->method( 'getRequest' )
545 ->willReturn( $request );
546
547 /** @var User $user */
548 return $user;
549 }
550
551 private function makePermissionManager( User $performingUser, $isAllowed ) : PermissionManager {
552 $permissionManager = $this->getMockBuilder( PermissionManager::class )
553 ->disableOriginalConstructor()
554 ->getMock();
555 $permissionManager->method( 'userHasRight' )
556 ->with( $performingUser, 'editmyprivateinfo' )
557 ->willReturn( $isAllowed );
558
559 /** @var PermissionManager $permissionManager */
560 return $permissionManager;
561 }
562
563 /**
564 * @param string[] $allowed
565 * @param int $numUsersToAuth
566 * @param string[] $ignored
567 * @return AuthManager
568 */
569 private function makeAuthManager(
570 array $allowed = [],
571 $numUsersToAuth = 0,
572 array $ignored = []
573 ) : AuthManager {
574 $authManager = $this->getMockBuilder( AuthManager::class )
575 ->disableOriginalConstructor()
576 ->getMock();
577 $authManager->method( 'allowsAuthenticationDataChange' )
578 ->willReturnCallback(
579 function ( TemporaryPasswordAuthenticationRequest $req ) use ( $allowed, $ignored ) {
580 $value = in_array( $req->username, $ignored, true )
581 ? 'ignored'
582 : 'okie dokie';
583 return in_array( $req->username, $allowed, true )
584 ? Status::newGood( $value )
585 : Status::newFatal( 'rejected by test mock' );
586 } );
587 $authManager->expects( $this->exactly( $numUsersToAuth ) )
588 ->method( 'changeAuthenticationData' );
589
590 /** @var AuthManager $authManager */
591 return $authManager;
592 }
593
594 /**
595 * @return User[]
596 */
597 private function makeUsers() {
598 $user1 = $this->getMockBuilder( User::class )->getMock();
599 $user2 = $this->getMockBuilder( User::class )->getMock();
600 $user1->method( 'getName' )->willReturn( 'User1' );
601 $user2->method( 'getName' )->willReturn( 'User2' );
602 $user1->method( 'getId' )->willReturn( 1 );
603 $user2->method( 'getId' )->willReturn( 2 );
604 $user1->method( 'getEmail' )->willReturn( self::VALID_EMAIL );
605 $user2->method( 'getEmail' )->willReturn( self::VALID_EMAIL );
606
607 $user1->method( 'getBoolOption' )
608 ->with( 'requireemail' )
609 ->willReturn( true );
610
611 $badUser = $this->getMockBuilder( User::class )->getMock();
612 $badUser->method( 'getName' )->willReturn( 'BadUser' );
613 $badUser->method( 'getId' )->willReturn( 3 );
614 $badUser->method( 'getEmail' )->willReturn( null );
615
616 return [
617 'User1' => $user1,
618 'User2' => $user2,
619 'BadUser' => $badUser,
620 ];
621 }
622 }