Testing Laravel Password Reset - php

I'm implementing the password reset functionality described in the Laravel docs at https://laravel.com/docs/8.x/passwords. My method for resetting passwords is as follows:
public function doPasswordReset(Request $request)
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => 'required|min:8|confirmed',
]);
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user, $password) {
$user->forceFill([
'password' => Hash::make($password)
]);
//remember token not needed
//->setRememberToken(Str::random(60));
$user->save();
event(new PasswordReset($user));
}
);
return $status === Password::PASSWORD_RESET
? redirect()->route('login')->with('status', __($status))
: back()->withErrors(['email' => [__($status)]]);
}
My test for this method is:
/** #test */
public function the_user_can_update_their_password()
{
ParentUser::factory()->create([
'email' => 'user#domain.com',
'password' => Hash::make('oldpassword')
]);
$token = Password::createToken(ParentUser::first());
Password::shouldReceive('reset')
->once()
->withSomeofArgs([
'email' => 'user#domain.com',
'password' => 'newpassword',
'password_confirmation' => 'newpassword',
'token' => $token
])
->andReturn(Password::PASSWORD_RESET);
$response = $this->post(route('password.update'), [
'email' => 'user#domain.com',
'password' => 'newpassword',
'password_confirmation' => 'newpassword',
'token' => $token
]);
$response->assertRedirect(route('login'));
//failures from here
$this->assertEquals(Hash::make('newpassword'), Hash::make(ParentUser::first()->password));
$this->assertNotEquals(Hash::make('oldpassword'), Hash::make(ParentUser::first()->password));
Event::fake();
Event::assertDispatched(PasswordReset::class, ParentUser::first());
}
My issue is that the last three assertations fail. I realize that this is happening because my mock is not calling the closure to effect the password change and raise the event. So my question is whether it is possible to mock some arguments of a function call.
One solution I am thinking is to factor out the closure into its own method and test that separately. In the absence of anything else, I think this may be the only way.

Props to #tim-lewis and #apokryfos who both provided answers to help. The following is the amended test which now passes.
#apokryfos stated that mocking the call shouldn't happen as we need to actuallly effect the password change.
#tim-lewis in his answer showed that a hash needs to be checked with the Hash::check() method
/** #test */
public function the_user_can_update_their_password()
{
ParentUser::factory()->create([
'email' => 'user#domain.com',
'password' => Hash::make('oldpassword')
]);
$token = Password::createToken(ParentUser::first());
Event::fake();
$response = $this->post(route('password.update'), [
'email' => 'user#domain.com',
'password' => 'newpassword',
'password_confirmation' => 'newpassword',
'token' => $token
]);
dump(ParentUser::first()->password);
$response->assertRedirect(route('login'));
$this->assertTrue(Hash::check('newpassword', ParentUser::first()->password));
Event::assertDispatched(PasswordReset::class);
}

Hash::make() doesn't generate the same Hash for any given value. Take a look:
Hash::make('newpassword') == Hash::make('newpassword')
// false
Since this is false, I would expect assetEquals() to fail here, since they values do not equal each other. Instead, take a look at the Hash::check() method:
Hash::check('newpassword', Hash::make('newpassword'))
// true
You can use assertTrue() against it instead, something like:
$this->assertTrue(Hash::check('newpassword', ParentUser::first()->password));
$this->assertFalse(Hash::check('oldpassword', ParentUser::first()->password));
Sidenote, ->password should already be Hashed, so there's no need to wrap that in another Hash::make(), as you'd then have a "Hash-of-a-hash", which would fail the Hash::check().

Related

assertRedirect causing output of email already exists - PHP Testing

So my test case in laravel is the following:
public function test_user_can_sign_up_using_the_sign_up_form()
{
$user = User::factory()->create();
$user = [
'username' => $user->username,
'email' => $user->email,
'password' => $user->password,
'password_confirmation' => $user->password,
'dob' => $user->dob
];
$response = $this->post('/register', $user);
// Removes password confirmation from array
array_splice($user, 3);
$this->assertDatabaseHas('users', $user);
$response->assertRedirect('/home');
}
This line:
$response->assertRedirect('/home');
is causing the test to fail and get an output of 'The email has already been taken' Why is this the case? I want to check upon sign up, the user is directed to the home page which it does but my test fails.
The user is being created in the database so that part works fine.
UserFactory:
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* #extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
*/
class UserFactory extends Factory
{
/**
* Define the model's default state.
*
* #return array<string, mixed>
*/
public function definition()
{
return [
'username' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'dob' => fake()->date(),
];
}
/**
* Indicate that the model's email address should be unverified.
*
* #return static
*/
public function unverified()
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}
Okay I will explain your test case to you.
You are testing a register feature of your app.
$user = User::factory()->create();
$user = [
'username' => $user->username,
'email' => $user->email,
'password' => $user->password,
'password_confirmation' => $user->password,
'dob' => $user->dob
];
$response = $this->post('/register', $user);
this block of code is wrong because you are using the data of a User already inside the database to create an account. So it will naturally fails since the data is already inside the database the moment you call the User::factory()->create().
So instead of that you should pass a data that looks like you are the one registering.Remove the User::factory()->create() then replace the array $user with hard coded data or use the fake() helper.
$user = [
'username' => fake()->userName(),
'email' => fake()->unique()->safeEmail(),
'password' => 'password',
'password_confirmation' => 'password',
'dob' => 'asldkjasd'
];
$response = $this->post('/register', $user);
$response->assertValid();
$this->assertDatabaseHas('users', [
'email' => $user['email'],
]);
$response->assertRedirect('/home');

Concaternate Array value with Ternary Operator result

I'm using PHP 7 and Laravel 6. I got errors when I made a user request rule and used it in user controller. The request rule I made is to be reusable in create and update function, so if i pass the id of user, it will validate the unique of user except that id. But if not, it will search all the ids and validate if it's unique. I follow BaM solution, here: https://stackoverflow.com/a/24205849
This my UserRequest.php:
public static function rules ($id=0, $merge=[]) {
return array_merge(
[
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users' . ($id ? ",$id" : ''),
'phone_number' => 'required|string|min:9|max:10|unique:users' . ($id ? ",$id" : ''),
'user_img' => 'required|mimes:jpeg,jpg,png,gif|max:10000',
],
$merge);
}
This is my UserController:
public function store(Request $request)
{
$extend_rules = [
'pass' => 'required|string|min:8',
];
$validator = Validator::make($request->all(), UserRequest::rules($extend_rules));
if ($validator->fails())
{
return redirect()->back();
}
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->pass),
'phone_number' => $request->phone_number,
'user_img' => $request->user_image->store('user_img'),
]);
$user->save();
Session::flash('message', 'Your account is successfully created !');
Session::flash('alert-class', 'alert alert-success');
return redirect()->route('users.index');
}
And I got this errors:
ErrorException: Array to string conversion
I tried to search for solutions but couldn't seem to find anything much my case.
If anyone know, please help me!
That's because you're just passing an array while the method accept two different type of parameter
$validator = Validator::make($request->all(), UserRequest::rules(0, $extend_rules)); // <-- you need to either pass 1 or 0 for the id and then $extended rules
// here is your method signature
public static function rules ($id=0, $merge=[]) {

Laravel update password only if it is set

I am working on a laravel project with user login. The admin can create new users and edit existing users. I have got a password and a passwordConfirm field in the update-user-form. If the admin puts a new password in the form, it should check for validation and update the record in the db. If not, it shouldn't change the password (keep old one), but update the other user data (like the firstname).
If I try to send the form with an empty password and passwordConfirm field, it doesn't validate. I got a validation error, that the password must be a string and at least 6 characters long, but I don't know why. It seems like the first line of my update function will be ignored.
UserController.php
public function update(User $user, UserRequest $request) {
$data = $request->has('password') ? $request->all() : $request->except(['password', 'passwordConfirm']);
$user->update($data);
return redirect('/users');
}
UserRequest.php
public function rules() {
return [
'firstname' => 'required|string|max:255',
'lastname' => 'required|string|max:255',
'email' => 'required|string|email|max:255',
'password' => 'string|min:6',
'passwordConfirm' => 'required_with:password|same:password',
];
}
If you want to validate a field only when it is present then use sometimes validation rule in such cases.
Add sometimes validation to both password & passwordConfirm. Remove the $data line from update();
// UserController.php
public function update(User $user, UserRequest $request) {
$user->update($request->all());
return redirect('/users');
}
// UserRequest.php
public function rules() {
return [
'firstname' => 'required|string|max:255',
'lastname' => 'required|string|max:255',
'email' => 'required|string|email|max:255',
'password' => 'sometimes|required|string|min:6',
'passwordConfirm' => 'sometimes|required_with:password|same:password',
];
}
Reference - https://laravel.com/docs/5.4/validation#conditionally-adding-rules
I always do this in my projects:
//Your UserController file
public function update(User $user, UserRequest $request) {
$user->update($request->all());
return redirect('/users');
}
//Your UserRequest file
public function rules() {
$rules= [
'firstname' => 'required|string|max:255',
'lastname' => 'required|string|max:255',
'email' => 'required|string|email|max:255'
];
if($this->method()=="POST"){
$rules['password']='sometimes|required|string|min:6';
$rules['passwordConfirm']='sometimes|required_with:password|same:password';
}
return $rules;
}
So, as you can see if your method is POST it means that you want to add a new user so its going to ask for password and passwordConfirm but if your method is PATCH or PUT it means you don't need to validate password and passwordConfirm.
Hope it helps
Maybe you should try the following:
// ... more code
// Removes password field if it's null
if (!$request->password) {
unset($request['password']);
}
$request->validate([
// ... other fields,
'password' => 'sometimes|min:6'
// ... other fields,
]);
// ... more code
you should replace "has" with "filled" in your code
$data = $request->filled('password') ? $request->all() : $request->except(['password', 'passwordConfirm']);
and actually it's better if you use the expression like this
$request->request->remove('password_confirmation');
( ! $request->filled('password') ) ? $request->request->remove('password'):"";
( $request->has('password') ) ? $request->merge([ 'password' => Hash::make($request->post()['password']) ]):"";
//then you can use
$user->update($request->all());
Even better, however, you have to use separate request classes for create and update "php artisan make:request" for ex:
UserUpdateRequest.php and UserCreateRequest.php
for UserCreateRequest your rule is
'password' => 'required|confirmed|min:6',
for UserUpdateRequest your rule is
'password' => 'sometimes|nullable|confirmed|min:6',
and your controller head add this line
use App\Http\Requests\UserCreateRequest;
use App\Http\Requests\UserUpdateRequest;
and your update method must change
public function update(UserUpdateRequest $request, $id)
{
//
}
Standard way of doing this
UserRequest.php
first import Rule
use Illuminate\Validation\Rule;
in your rules array:
'password' => [Rule::requiredIf(fn () => $this->route()->method == "POST")]
Example:
public function rules()
{
return [
'name' => 'required',
'email' => ['required', 'email'],
'password' => [Rule::requiredIf(fn () => $this->route()->method == "POST"), 'confirmed'],
];
}
below php 7.4 use this way
'password' => [Rule::requiredIf(function(){
return $this->route()->method == "POST";
})]

Laravel Passport password grant returns Invalid Credentials Exception

I am trying to setup a SPA that consumes a Laravel API protected with Passport.
I started by creating a new Laravel app specifically for this and I then followed the instructions for setting up passport and set up a password grant client.
I can successfully create a new user, save the user to the database, and log the user in. After that, I try to use the newly created user's information along with the password grant clients id and secret to create an access token. At this point I receive the exception.
I read through the log and I saw where the exception was being thrown. Inside League\OAuth2\Server\Grant\PasswordGrant the validateUser method has the following:
if ($user instanceof UserEntityInterface === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidCredentials();
}
Seeing this I implemented the UserEntityInterface on my user model and implemented the getIdentifier method but I still receive the Exception. I'm really not too sure where to go from here, any help would be greatly appreciated. Below is some of my code.
Here is my Registration controller:
class RegisterController extends Controller
{
private $tokenService;
public function __construct(AccessTokenService $tokenService)
{
//$this->middleware('guest');
$this->tokenService = $tokenService;
}
public function register(Request $request)
{
$this->validateWith($this->validator($request->all()));
Log::debug('Validated');
$user = $this->create($request->all());
$this->guard()->login($user);
$this->tokenService->boot(Auth::user());
return response()->json($this->tokenService->getNewAccessToken(), 200);
}
protected function guard()
{
return Auth::guard();
}
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|min:6|confirmed',
'password_confirmation' => 'required'
]);
}
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
}
}
And these are the relevant portions of AccessTokenService:
public function getNewAccessToken() {
$http = new Client();
$client = \Laravel\Passport\Client::where('id', 6)->first();
Log::debug($client->getAttribute('secret'));
Log::debug($this->user->getAttribute('email'));
Log::debug($this->user->getAuthPassword());
$response = $http->post('homestead.app/oauth/token', [
'form_params' => [
'grant_type' => 'password',
'client_id' => 6,
'client_secret' => $client->getAttribute('secret'),
'username' => $this->user->getAttribute('email'),
'password' => $this->user->getAuthPassword(),
'scope' => '*'
]]);
unset($client);
$status = $response->getStatusCode();
$body = $response->getBody();
Log::debug($body->getContents());
Log::debug($status);
switch($status)
{
case 200:case 201:
case 202:
$tokens = array(
"user_id" => $this->user->getAttribute('id'),
"access_token" => $body['access_token'],
"refresh_token" => $body['refresh_token']
);
$output = ["access_token" => $this->storeTokens($tokens), 'status_code' => $status];
break;
default:
$output = ["access_token" => '', 'status_code' => $status];
break;
}
return $output;
}
private function storeTokens(array $tokens) {
UserToken::create([
"user_id" => $tokens['user_id'],
"access_token" => bcrypt($tokens['access_token']),
"refresh_token" => bcrypt($tokens['refresh_token'])
]);
return $tokens['access_token'];
}
So I figured out the issue. When I was requesting the access token I was passing in the user's email and password but I was passing the hashed password when I needed to pass in the unhashed password.
My request for an access token looked like this:
$response = $http->post('homestead.app/oauth/token', [
'form_params' => [
'grant_type' => 'password',
'client_id' => 6,
'client_secret' => $client->getAttribute('secret'),
'username' => $this->user->getAttribute('email'),
'password' => $this->user->getAuthPassword(), //Here is the problem
'scope' => '*'
]]);
By passing the Request to the function using the unhashed password like this solved the problem:
$response = $http->post('homestead.app/oauth/token', [
'form_params' => [
'grant_type' => 'password',
'client_id' => 6,
'client_secret' => $client->getAttribute('secret'),
'username' => $request['email'],
'password' => $request['password'],
'scope' => '*'
]]);
In my case it was magic
when i changed username from email format to simple (alphanumeric only) format it works.
please tell the reason if anyone have for my case.
Thanks

Authenticating registration against two tables: Laravel 5.1

I'm new to Laravel and am trying to figure out how to authenticate against two tables during new user registration.
I've modified the default methods in AuthController - I'm checking to see if a store number is valid, and if it is, register the user. This works fine - if the store number provided checks out, the user is inserted into both tables (user and user_store) and redirected to the dashboard page.
However, if the validation against $store is false, then I receive the following error
Argument 1 passed to Illuminate\Auth\Guard::login() must be an instance of Illuminate\Contracts\Auth\Authenticatable, instance of Illuminate\Http\RedirectResponse given
As you can see in the code I'm just trying to redirect to the auth/register view and provide an error message that the store number was invalid. Where am I going wrong?
SEE UPDATED CODE BELOW THIS BLOCK...
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:user',
'password' => 'required|min:6',
'store_number' => 'required',
]);
}
protected function create(array $data)
{
if(!$store = Store::where('number', $data['store_number'])->first()) {
// HERE'S WHERE I'M HAVING THE PROBLEM
return redirect('auth/register')->withErrors('store_number','Could not find a match for the Store Number');
} else {
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
$data['sid'] = $store->id;
$data['uid'] = $user->id;
$store = UserStore::create($data);
return $user;
}
}
UPDATE
I've since moved this into the validator() method, because, well, it makes more sense to do the validation in the validator() method... right?
Here's my new code.
protected function validator(array $data)
{
if(!Store::where('number', $data['store_number'])->first()) {
// still not working!
return redirect('auth/register')->withErrors('store_number','Could not find a match for the Store Number');
}
return Validator::make($data, [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:user',
'password' => 'required|min:6',
'store_number' => 'required',
]);
}
protected function create(array $data)
{
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
$s['sid'] = $data['store_number'];
$s['uid'] = $user->id;
$store = UserStore::create($s);
return $user;
}
And here's my new error message
BadMethodCallException in RedirectResponse.php line 198:
Method [fails] does not exist on Redirect.
Figured it out. I needed to use the After validation hook inside the validator() method. :)
protected function validator(array $data)
{
$validator = Validator::make($data, [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:user',
'password' => 'required|min:6',
'store_number' => 'required',
]);
$validator->after(function($validator) {
if(!Store::where('number', $_POST['store_number'])->first()) {
$validator->errors()->add('store_number', 'Could not find a match for the Store number');
}
});
return $validator;
}

Categories