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...)?