Paulund

Laravel Make Auth Tests

In this tutorial we're going to expand on a built in Laravel command I use in almost every project the php artisan make:auth command.

Make Auth

The make auth command will generate the routes, controllers and views for authentication in your Laravel project.

When you run this command on a new project it will make the login controller, register controller, forgotten password controller and reset password controller. This command makes it very easy to get started with any project.

The thing that's missing from this command is the tests that will make sure that you've implemented it correctly. Therefore in this tutorial we're going to create the missing auth tests for this command.

Forgotten Password Tests

The forgotten password page is used for users to enter their email into a form to be emailed a link that will allow them to reset their password to login into your application.

The tests that we need to perform are going to be for:

  • Show the forgotten password form
  • When the user enters their email address an forgotten password is sent to the user.
  • Test the application doesn't send the forgotten password email if the user doesn't exist.
  • Test the forgotten password form requires an email to be entered.
<?php

namespace Tests\Feature\Auth;

use App\Models\User;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;

/**
 * Class ForgotPasswordTest
 * @package Tests\Feature\Auth
 *
 * @group auth
 */
class ForgotPasswordTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function it_shows_password_form()
    {
        // Given

        // When
        $response = $this->get(
            route('password.request')
        );

        // Then
        $response->assertSuccessful();
        $response->assertViewIs('auth.passwords.email');
    }

    /** @test */
    public function it_will_send_an_email_to_user_with_reset_password_link()
    {
        // Given
        Notification::fake();
        $user = factory(User::class)->create();

        // When
        $response = $this->post(
            route('password.email'),
            [
                'email' => $user->email
            ]
        );

        // Then
        $this->assertNotNull($token = DB::table('password_resets')->first());
        Notification::assertSentTo($user, ResetPassword::class, function ($notification, $channels) use ($token) {
            return Hash::check($notification->token, $token->token) === true;
        });
    }

    /** @test */
    public function it_does_not_send_email_if_not_registered()
    {
        // Given
        Notification::fake();
        $user = factory(User::class)->make();

        // When
        $response = $this->from(route('password.email'))
            ->post(
                route('password.email'),
                [
                    'email' => $user->email
                ]
            );

        // Then
        $response->assertRedirect(route('password.email'));
        $response->assertSessionHasErrors('email');
        Notification::assertNotSentTo($user, ResetPassword::class);
    }

    /** @test */
    public function it_requires_email_on_post_form()
    {
        // Given

        // When
        $response = $this->from(route('password.email'))
            ->post(
                route('password.email'),
                []
            );

        // Then
        $response->assertRedirect(route('password.email'));
        $response->assertSessionHasErrors('email');
    }
}

Login Test

The login page is used by your users to access a locked down area of your site. It's important to test that only logged in users can access these pages and only correct credentials can log into your account.

The tests will need to perform:

  • It shows the login form
  • It will log in the user with correct credentials
  • It will not log in the user if they have the wrong password
  • It will not login the user if the email doesn't exist
  • It will allow a user to logout
<?php

namespace Tests\Feature\Auth;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

/**
 * Class LoginTest
 * @package Tests\Feature\Auth
 *
 * @group auth
 */
class LoginTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function it_display_login_form()
    {
        // Given

        // When
        $response = $this->get(route('login'));

        // Then
        $response->assertSuccessful();
    }

    /** @test */
    public function it_logs_user_in_with_correct_credentials()
    {
        // Given
        $user = factory(User::class)->create([
            'password' => bcrypt($password = 'random-password'),
        ]);

        // When
        $response = $this->post(route('login'), [
            'email' => $user->email,
            'password' => $password,
        ]);

        // Then
        $this->assertAuthenticatedAs($user);
    }

    /** @test */
    public function it_will_not_login_user_with_wrong_password()
    {
        // Given
        $user = factory(User::class)->create([
            'password' => bcrypt($password = 'random-password'),
        ]);

        // When
        $response = $this->from(route('login'))
            ->post(route('login'), [
                'email' => $user->email,
                'password' => 'wrong-password',
            ]);

        // Then
        $response->assertRedirect(route('login'));
        $response->assertSessionHasErrors('email');
        $this->assertGuest();
    }

    /** @test */
    public function it_can_not_login_if_user_doesnt_exist()
    {
        // Given

        // When
        $response = $this->from(route('login'))
            ->post(route('login'), [
                'email' => 'doesnt-exist-email',
                'password' => 'wrong-password',
            ]);

        // Then
        $response->assertRedirect(route('login'));
        $response->assertSessionHasErrors('email');
        $this->assertGuest();
    }

    /** @test */
    public function it_allows_user_to_logout()
    {
        // Given
        $user = factory(User::class)->create();
        $this->be($user);

        // When
        $this->post(route('logout'));

        // Then
        $this->assertGuest();
    }
}

Register Test

This page is used by new visitors to register to become a user of your application. Therefore we need to make sure that when the user signs up a new user is created and they can login.

The tests we're going to perform are:

  • Test it can register a new user
  • Test is validates the user for required information.
  • Test is validates the password to make sure the user has confirmed their password.
  • Test it makes sure the email address doesn't already exist.
<?php

namespace Tests\Feature\Auth;

use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Hash;
use Tests\TestCase;

/**
 * Class RegisterTest
 * @package Tests\Feature\Auth
 *
 * @group auth
 */
class RegisterTest extends TestCase
{
    use RefreshDatabase;
    
    /** @test */
    public function it_can_register_a_user()
    {
        // Given
        Event::fake();
            
        // When
        $response = $this->post(route('register'), [
            'name' => 'John Smith',
            'email' => '[email protected]',
            'password' => 'password',
            'password_confirmation' => 'password'
        ]);
            
        // Then
        $users = User::all();
        $user = $users->first();
        $this->assertCount(1, $users);
        $this->assertAuthenticatedAs($user);
        $this->assertEquals('John Smith', $user->name);
        $this->assertEquals('[email protected]', $user->email);
        $this->assertTrue(Hash::check('password', $user->password));
        Event::assertDispatched(Registered::class, function ($e) use ($user) {
            return $e->user->id === $user->id;
        });
    }
    
    /** @test */
    public function it_validates_a_user_without_name()
    {
        // Given
        Event::fake();
            
        // When
        $response = $this->post(route('register'), [
            'name' => '',
            'email' => '[email protected]',
            'password' => 'password',
            'password_confirmation' => 'password'
        ]);
            
        // Then
        $users = User::all();
        $this->assertCount(0, $users);
        $this->assertGuest();
        $response->assertSessionHasErrors('name');
        Event::assertNotDispatched(Registered::class);
    }
    
    /** @test */
    public function it_validates_a_user_without_email()
    {
        // Given
        Event::fake();

        // When
        $response = $this->post(route('register'), [
            'name' => 'John Smith',
            'email' => '',
            'password' => 'password',
            'password_confirmation' => 'password'
        ]);

        // Then
        $users = User::all();
        $this->assertCount(0, $users);
        $this->assertGuest();
        $response->assertSessionHasErrors('email');
        Event::assertNotDispatched(Registered::class);
            
    }

    /** @test */
    public function it_validates_a_user_without_password()
    {
        // Given
        Event::fake();

        // When
        $response = $this->post(route('register'), [
            'name' => 'John Smith',
            'email' => '[email protected]',
            'password' => '',
            'password_confirmation' => 'password'
        ]);

        // Then
        $users = User::all();
        $this->assertCount(0, $users);
        $this->assertGuest();
        $response->assertSessionHasErrors('password');
        Event::assertNotDispatched(Registered::class);

    }

    /** @test */
    public function it_validates_a_user_without_password_confirmation()
    {
        // Given
        Event::fake();

        // When
        $response = $this->post(route('register'), [
            'name' => 'John Smith',
            'email' => '[email protected]',
            'password' => 'password',
            'password_confirmation' => ''
        ]);

        // Then
        $users = User::all();
        $this->assertCount(0, $users);
        $this->assertGuest();
        $response->assertSessionHasErrors('password');
        Event::assertNotDispatched(Registered::class);

    }
    
    /** @test */
    public function it_validates_a_user_without_matching_password()
    {
        // Given
        Event::fake();

        // When
        $response = $this->post(route('register'), [
            'name' => 'John Smith',
            'email' => '[email protected]',
            'password' => 'password',
            'password_confirmation' => 'doesntmatch'
        ]);

        // Then
        $users = User::all();
        $this->assertCount(0, $users);
        $this->assertGuest();
        $response->assertSessionHasErrors('password');
        Event::assertNotDispatched(Registered::class);
            
    }
    
    /** @test */
    public function it_validates_email_if_already_exists()
    {
        // Given
        Event::fake();
        $user = factory(User::class)->create([
            'name' => 'John Smith',
            'email' => '[email protected]',
            'password' => bcrypt('password')
        ]);
            
        // When
        $response = $this->post(route('register'), [
            'name' => 'John Smith',
            'email' => '[email protected]',
            'password' => 'password',
            'password_confirmation' => 'password'
        ]);
            
        // Then
        $users = User::all();
        $this->assertCount(1, $users);
        $this->assertGuest();
        Event::assertNotDispatched(Registered::class);
    }
}

Reset Password Test

When the user comes back from the forgotten password email they will see the reset password form, this requires a unique valid token to be generated and link to the user email address so that only they can reset the password on their account.

Then tests we're going to perform are:

  • It shows the reset password page
  • You can reset the password with a valid token
  • It doesn't reset the password with an invalid token
  • It doesn't update the password if it's empty
  • It doesn't update linked user to token with an empty email
<?php

namespace Tests\Feature\Auth;

use App\Models\User;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Tests\TestCase;

/**
 * Class ResetPasswordTest
 * @package Tests\Feature\Auth
 *
 * @group auth
 */
class ResetPasswordTest extends TestCase
{
    use RefreshDatabase;

    /**
     * @param $user
     * @return mixed
     */
    private function getValidToken($user)
    {
        return Password::broker()->createToken($user);
    }
    
    /** @test */
    public function it_shows_password_reset_page()
    {
        // Given
        $user = factory(User::class)->create();
        $token = $this->getValidToken($user);
            
        // When
        $response = $this->get(route('password.reset', $token));
            
        // Then
        $response->assertSuccessful();
        $response->assertViewHas('token', $token);
    }
    
    /** @test */
    public function it_reset_password_with_valid_token()
    {
        // Given
        Event::fake();
        $user = factory(User::class)->create();
            
        // When
        $response = $this->post('/password/reset', [
            'token' => $this->getValidToken($user),
            'email' => $user->email,
            'password' => 'new-password',
            'password_confirmation' => 'new-password',
        ]);
            
        // Then
        $this->assertEquals($user->email, $user->fresh()->email);
        $this->assertTrue(Hash::check('new-password', $user->fresh()->password));
        $this->assertAuthenticatedAs($user);
        Event::assertDispatched(PasswordReset::class, function ($e) use ($user) {
            return $e->user->id === $user->id;
        });
    }
    
    /** @test */
    public function it_doesnt_reset_password_with_invalid_token()
    {
        // Given
        Event::fake();
        $user = factory(User::class)->create([
            'password' => bcrypt('password')
        ]);
        $token = $this->getValidToken($user);
            
        // When
        $response = $this->from(route('password.reset', $token))->post('/password/reset', [
            'token' => str_random(24),
            'email' => $user->email,
            'password' => 'new-password',
            'password_confirmation' => 'new-password',
        ]);
            
        // Then
        $this->assertEquals($user->email, $user->fresh()->email);
        $this->assertTrue(Hash::check('password', $user->fresh()->password));
        $this->assertGuest();
    }
    
    /** @test */
    public function it_doesnt_update_with_empty_password()
    {
        // Given
        Event::fake();
        $user = factory(User::class)->create([
            'password' => bcrypt('password')
        ]);
        $token = $this->getValidToken($user);

        // When
        $response = $this->from(route('password.reset', $token))->post('/password/reset', [
            'token' => str_random(24),
            'email' => $user->email,
            'password' => '',
            'password_confirmation' => '',
        ]);

        // Then
        $response->assertSessionHasErrors('password');
        $this->assertTrue(Hash::check('password', $user->fresh()->password));
        $this->assertGuest();
    }
    
    /** @test */
    public function it_doesnt_update_password_with_blank_email()
    {
        // Given
        Event::fake();
        $user = factory(User::class)->create([
            'password' => bcrypt('password')
        ]);
        $token = $this->getValidToken($user);

        // When
        $response = $this->from(route('password.reset', $token))->post('/password/reset', [
            'token' => str_random(24),
            'email' => '',
            'password' => 'new-password',
            'password_confirmation' => 'new-password',
        ]);

        // Then
        $response->assertSessionHasErrors('email');
        $this->assertTrue(Hash::check('password', $user->fresh()->password));
        $this->assertGuest();
            
    }
}