Pruebas Unitarias en Autenticación con Laravel y Passport
Tags: spanish, laravel, passport, testing
Published: 2021-09-13 17:57:55

Mis niños, la verdad, así como los he dejado solos un buen rato, ahora ando inspirado para publicar algo que me trajo muchos dolores de cabeza: las pruebas unitarias con Passport, no sin antes, quiero felicitarlos hoy por el día del programador a todos y cada uno de mis colegas y de la comunidad DEV en español.

Para los que no conocen Passport, es una librería dentro de Laravel que te permite realizar autenticación con OAuth 2 mediante Personal Access Tokens o por medio de las clásicas JWT junto con una Refresh Token para mantener el login el mayor tiempo posible.

## El rollo de implementar UT

![ANGRY GERMAN KID](https://i.makeagif.com/media/12-17-2014/iP6A8a.gif)

Cuando uno está acostumbrado a desarrollar sin Unit Testing, por lo general se vuelve un dolor de cabeza el implementarlas, sin embargo conforme va uno agarrando el hilo de las mismas, además de que es una economía al momento de poner en producción un desarrollo te da la certeza de que desarrollaste un módulo correctamente.

Pero antes que nada dirán los que empiezan ¿qué es una prueba unitaria? Ah pues como lo dice la gran amiga Wikipedia _una prueba unitaria es una forma de comprobar el correcto funcionamiento de una unidad de código_. 

Entonces, cuando nosotros hacemos algún comportamiento dentro de un sistema, tenemos que pasarlo por pruebas diversas, sin embargo, las que nos alertan desde un principio si están bien son las unitarias. En PHP (lenguaje de Laravel) usamos lo que es PHPUnit para realizar las mismas y este por lo general ya viene incluido cuando instalas Laravel.

El tema viene cuando quieres hacer unit testing con Passport. Puesto que en uno de los pasos, tienes que llamar al servidor para generar tus respectivas tokens. Sin embargo sucede que tu UT va a tronar si careces de un servidor web implementado en la misma. Veamos qué sucede.

Ahora bien, si quieres saber más a fondo sobre cómo implementar Passport en Laravel, te dejo este [link](https://dev.to/azibom/create-api-rest-with-laravel-7-x-passport-authentication-and-implement-refresh-token-part-1-43ja), mismo que es en el que me basé para armar este post.

 
## El código a testear

Primeramente, tomamos en cuenta el siguiente método:

```php
public function getAuthAndRefreshToken($email, $password) { 
        $oClient = OClient::where('password_client', 1)->first();
        $http = new Client;
        $oauthUrl = (env('APP_ENV') != 'local') ? route('passport.token') : env('APP_URL') . 'oauth/token';
        $response = $http->request('POST', $oauthUrl, [
            'form_params' => [
                'grant_type' => 'password',
                'client_id' => $oClient->id,
                'client_secret' => $oClient->secret,
                'username' => $email,
                'password' => $password,
                'scope' => '*',
            ],
        ]);

        $result = json_decode((string) $response->getBody(), true);
        return response()->json($result, $this->successStatus);
    }
```
La implementación en el método a testear:

```php
public function login(Request $request){
        $validator = Validator::make($request->all(), [
            'email' => 'email|required',
            'password' => 'required'
        ]);

        if($validator->fails()) {
            return response()->json(['success' => false, 'details' => $validator->errors()], 401);
        }
        
        $result = $this->getAuthAndRefreshToken(request('email'), request('password'));
        
        if($result['success'] == true) {
            return response()->json($result, 200);
        } else { 
            return response()->json(['success' => false, 'error'=>'Unauthorized'], 401); 
        } 
    }
```

Y su respectiva UT:

```php
   public function test_login_success() {
        $user = User::factory()->make();

        $response = $this->postJson('/api/login', [
            'email' => $user->email,
            'password' => 'password'
        ]);

        $response->assertStatus(200)->assertJson([
            'success' => true
        ]);
    }
```
Al momento de nosotros realizar nuestras pruebas unitarias, van a fallar irremediablemente y la razón es de que no hay como tal una implementación de un servidor web existente dentro de PHPUnit. Nosotros para poder realizar nuestras pruebas, tenemos que **simular** el request dentro del primer método y precisamente el bloque siguiente:

```php
$response = $http->request('POST', $oauthUrl, [
            'form_params' => [
                'grant_type' => 'password',
                'client_id' => $oClient->id,
                'client_secret' => $oClient->secret,
                'username' => $email,
                'password' => $password,
                'scope' => '*',
            ],
        ]);
```

es el que nos está dando lata :(. Si lo pasas por un Github Actions, va a tronar riquísimo marcándote que no existe la URL que tienes en la siguiente línea:

```php
$oauthUrl = (env('APP_ENV') != 'local') ? route('passport.token') : env('APP_URL') . 'oauth/token';
```

## La solución a todos nuestros problemas

Primeramente, el método que tenemos que refactorizar es el de getAuthAndRefreshToken. Tenemos que cambiar el modo como llamamos el request y que sea funcional tanto en la implementación como en las pruebas. 

La forma de realizarlo es:

1. Asignando al request principal los parámetros que el endpoint OAuth requiere para armar un nuevo request.
2. Despachar una ruta en POST: el endpoint _oauth/token_ por medio de un nuevo Request interno.

Lo anterior sin utilizar cliente alguno como Guzzle o similares, mismas que requieren tener un servidor Apache o nginx implementado, features que no se utilizan en pruebas unitarias.

Para ello cambiamos a lo siguiente:

```php
private function getAuthAndRefreshToken(Request $request, $email, $password) {
        $oClient = OClient::where('password_client', env('DEFAULT_PASSWORD_CLIENT_ID', 1))->first();

        $request->request->add([
            'grant_type' => 'password',
            'client_id' => $oClient->id,
            'client_secret' => $oClient->secret,
            'username' => $email,
            'password' => $password,
            'scope' => '*',
        ]);

        $response = Route::dispatch(Request::create('oauth/token', 'POST'));

        $result = json_decode((string) $response->getContent(), true);
        $result['success'] = (isset($result['error'])) ? false : true;

        return $result;
    }
```
En este refactor, hemos cambiado varias cosas:

1. Ya no necesitarás una URL ni un cliente para armar el request. Es más sano, pues ya así puedes asignar la implementación sin importar la URL o factores externos al método y puedes testear sin problemas desde consola.

2. Obtienes mayores detalles de la respuesta que te da Passport al momento de llamar el endpoint de OAuth para poder dar más transparencia al proceso, igualmente ya podrás hacer manejo de errores del método sin problemas.

Y ya con este refactor, tendrías que cambiar la implementación en tu método login, concretamente el inicio del método ya sería así:

```php
$result = $this->getAuthAndRefreshToken(request(), request('email'), request('password'));
```

Al terminar todo esto, puedes pasar tus unit testings de forma manual en tu código o por Github Actions para poder confirmar las mismas y ya proceder a realizar implementaciones adicionales.

Y pues bien, esto era lo que me traía de los pelos en cuestiones de pruebas unitarias y quiero compartirlo con ustedes para que puedan sacar más rápido este tema con Passport. Ahora, no olvides de reaccionar y compartirlo con los colegas para que haya más y mejores posts en español dentro de DEV.

Happy coding!