Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Ramon's avatar
Level 7

Testing custom AuthServiceProvider

Hey there, I have a custom AuthServiceProvider due to some technical requirements from my backend (basically, the authentication is done via Stored Procedures instead of a regular query to the database.)

So right now I have a Model that looks like this

class CustomUser extends Model implements Authenticatable
{
    use AuthenticatableTrait;

    public static string $SESSION_KEY = 'custom_user';

    protected $primaryKey = 'CdUser';

    protected $fillable = [
        'CdUser',
		...
    ];

    public function spAuth($username, $password): array
    {
        return DB::select(
            sprintf("exec spAuth '%s', '%s'", $username, $password)
        );
    }

    public function createUserEntity($user): CustomUser
    {
        return $this->newFromBuilder((array) $user);
    }
}

And my Service Provider looks like this:

class CustomUserProvider implements UserProvider
{
    public CustomUser $model;

    public function __construct(CustomUser $model)
    {
        $this->model = $model;
    }

    /**
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    public function retrieveById($identifier)
    {
        $session = session()->get(CustomUser::$SESSION_KEY);

        if ($session !== null && $identifier == $session->getAuthIdentifier()) {
            return $session;
        }

        return null;
    }

    public function retrieveByCredentials(array $credentials): Authenticatable|null
    {
        if (empty($credentials)) {
            return null;
        }

        [$auth] = $this->model->spAuth(
            $credentials['username'],
            $credentials['password']
        );

        if ($auth->FlAuth == 0) {
            return null;
        }

        $user = $this->model->createUserEntity($auth);
        $this->setUserSession($user);

        return $user;
    }

    protected function setUserSession($user)
    {
        session([CustomUser::$SESSION_KEY => $user]);
        session()->regenerate();
    }

    // We can ignore these methods since we don't use them

    // @codeCoverageIgnoreStart
    public function validateCredentials(Authenticatable $user, array $credentials): bool
    {
        // Not needed since validation is done in retrieveByCredentials
        return true;
    }

    public function retrieveByToken($identifier, $token)
    {
        return null;
    }

    public function updateRememberToken(Authenticatable $user, $token)
    {
        return null;
    }
    // @codeCoverageIgnoreEnd
}

The logic seems correct and working, as I can validate it via manual navigation and thru unit tests, but I have been struggling to create an e2e test.

Here are my Unit tests

test('it returns null when user is not authenticated', function () {
    $provider = new CustomUserProvider(new CustomUser());
    $provider->model = m::mock(CustomUser::class);
    $provider->model
        ->shouldReceive('spAuth')
        ->once()
        ->andReturn([
            (object) [
                'FlAuth' => 0,
            ],
        ]);

    expect(
        $provider->retrieveByCredentials([
            'username' => 'foo',
            'password' => 'bar',
        ])
    )->toBeNull();
});

test('it returns user when user is authenticated', function () {
    $provider = new CustomUserProvider(new CustomUser());
    $provider->model = m::mock(CustomUser::class);
    $provider->model
        ->shouldReceive('spAuth')
        ->once()
        ->andReturn([
            (object) [
                'FlAuth' => 1,
                'CdUser' => 1,
            ],
        ]);

    $provider->model
        ->shouldReceive('createUserEntity')
        ->once()
        ->andReturn(
            new CustomUser([
                'CdUser' => 1,
            ])
        );

    $user = $provider->retrieveByCredentials([
        'username' => 'foo',
        'password' => 'bar',
    ]);

    $this->assertInstanceOf(CustomUser::class, $user);
    $this->assertEquals(1, $user->getAttribute('CdUser'));
});

This is the "draft" code of my e2e:

test('it authenticates user when username and password are correct', function () {
    $userProvider = m::mock(CustomUserProvider::class)->makePartial();
    $userProvider->model = m::mock(CustomUser::class);
    $userProvider->model
        ->shouldReceive('spAuth')
        ->once()
        ->andReturn([
            (object) [
                'FlAuth' => 1,
            ],
        ]);
    
    $this->app->instance(CustomUserProvider::class, $userProvider);
    $this->app->register(\App\Providers\AuthServiceProvider::class);
    $this->app->boot();
    
    $response = $this->post('/login', ['username' => 'foo', 'password' => 'bar']);

    // success, redirect
    expect($response->getStatusCode())
        ->toBe(302)
        ->and($response->headers->get('Location'))
        ->toBe('http://localhost/dashboard');

    $this->assertAuthenticated();
});

The issue is that the logic is still hitting my database, and ofc the credentials are invalid. Does anyone have any clue on how I can get this working? Do I need to mock things downstream (SessionGuard, Auth...)?

0 likes
1 reply
LaryAI's avatar
Level 58

To test the CustomUserProvider without hitting the database, you can mock the DB facade and the Session facade. Here's an example of how you can modify the e2e test to achieve this:

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Session;

test('it authenticates user when username and password are correct', function () {
    // Mock the DB facade to return a fake result
    DB::shouldReceive('select')->once()->andReturn([
        (object) [
            'FlAuth' => 1,
        ],
    ]);

    // Mock the Session facade to set the user session
    Session::shouldReceive('put')->once()->with('custom_user', m::type(CustomUser::class));
    Session::shouldReceive('regenerate')->once();

    // Make a request to the login endpoint
    $response = $this->post('/login', ['username' => 'foo', 'password' => 'bar']);

    // Assert that the response is a redirect to the dashboard
    $response->assertRedirect('/dashboard');

    // Assert that the user is authenticated
    $this->assertAuthenticatedAs(CustomUser::class);
});

In this example, we're using the shouldReceive method from the Mockery library to mock the DB and Session facades. We're also using the assertRedirect and assertAuthenticatedAs methods provided by Laravel's testing framework to assert that the response is a redirect to the dashboard and that the user is authenticated, respectively.

Please or to participate in this conversation.