Login to bookmark this video
Buy Access to Course
25.

Prueba de Usuario + Contraseña Simple

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Tenemos un bonito DragonTreasureResourceTest, así que vamos a hacer un bootstrap para Usuario.

Crear la prueba de usuario

Crea una nueva clase PHP llamada, qué tal, UserResourceTest. Haz que extienda nuestra clase personalizada ApiTestCase, entonces sólo necesitamos use ResetDatabase:

16 lines | tests/Functional/UserResourceTest.php
// ... lines 1 - 2
namespace App\Tests\Functional;
use Zenstruck\Foundry\Test\ResetDatabase;
class UserResourceTest extends ApiTestCase
{
use ResetDatabase;
// ... lines 10 - 14
}

No necesitamos HasBrowser porque eso ya está hecho en la clase base.

Empieza con public function testPostToCreateUser():

16 lines | tests/Functional/UserResourceTest.php
// ... lines 1 - 6
class UserResourceTest extends ApiTestCase
{
// ... lines 9 - 10
public function testPostToCreateUser(): void
{
}
}

Haz una petición ->post() a /api/users, añade algo de json con email ypassword, y assertStatus(201).

Y ahora que hemos creado el nuevo usuario, ¡vamos a probar si podemos iniciar sesión con sus credenciales! Haz otra petición ->post() a/login, pasa también algo de json - copia los email y password de arriba - y luego assertSuccessful():

32 lines | tests/Functional/UserResourceTest.php
// ... lines 1 - 6
class UserResourceTest extends ApiTestCase
{
// ... lines 9 - 10
public function testPostToCreateUser(): void
{
$this->browser()
->post('/api/users', [
'json' => [
'email' => 'draggin_in_the_morning@coffee.com',
'username' => 'draggin_in_the_morning',
'password' => 'password',
]
])
->assertStatus(201)
->post('/login', [
'json' => [
'email' => 'draggin_in_the_morning@coffee.com',
'password' => 'password',
]
])
->assertSuccessful()
;
}
}

Vamos a probar: symfony php bin/phpunit y ejecuta todo el archivotests/Functional/UserResourceTest.php:

symfony php bin/phpunit tests/Functional/UserResourceTest.php

Y... ¡ok! Un código de estado 422, pero 201 esperado. Veamos: esto significa que algo ha ido mal al crear el usuario. Abramos la última respuesta. ¡Ah! Culpa mía: olvidé pasar el campo obligatorio username: ¡estamos fallando en la validación!

Pasa username... puesto a cualquier cosa:

32 lines | tests/Functional/UserResourceTest.php
// ... lines 1 - 6
class UserResourceTest extends ApiTestCase
{
// ... lines 9 - 10
public function testPostToCreateUser(): void
{
$this->browser()
->post('/api/users', [
'json' => [
// ... line 16
'username' => 'draggin_in_the_morning',
// ... line 18
]
])
// ... lines 21 - 28
;
}
}

Inténtalo de nuevo:

symfony php bin/phpunit tests/Functional/UserResourceTest.php

Esto es lo que quería:

Esperaba un código de estado correcto, pero obtuve 401.

Así que el fallo está aquí abajo. Pudimos crear el usuario... pero cuando intentamos iniciar sesión, falló. Si estuviste con nosotros en el episodio uno, ¡tal vez recuerdes por qué! Nunca configuramos nuestra API para hacer hash de la contraseña.

Compruébalo: dentro de User, sí hicimos que password formara parte de nuestra API. El usuario envía la contraseña en texto plano que desea... y nosotros la guardamos directamente en la base de datos. Eso es un gran problema de seguridad... y hace imposible iniciar sesión como este usuario, porque Symfony espera que la propiedad password contenga una contraseña con hash.

Configurar el campo plainPassword

Así que nuestro objetivo está claro: permitir al usuario enviar una contraseña sin formato, pero luego hashearla antes de almacenarla en la base de datos. Para ello, en lugar de almacenar temporalmente la contraseña en texto plano en la propiedad password, vamos a crear una propiedad totalmente nueva: private ?string $plainPassword = null:

292 lines | src/Entity/User.php
// ... lines 1 - 66
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
// ... lines 69 - 92
private ?string $plainPassword = null;
// ... lines 94 - 290
}

Ésta no se almacenará en la base de datos: es sólo un lugar temporal para guardar la contraseña en texto plano antes de que le apliquemos el hash y la establezcamos en la propiedad real password.

Abajo del todo, iré a "Código"->"Generar", o Command+N en un Mac, y generaré un "Getter y setter" para esto. Limpiemos esto un poco: acepta sólo una cadena, y el PHPDoc es redundante:

292 lines | src/Entity/User.php
// ... lines 1 - 66
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
// ... lines 69 - 279
public function setPlainPassword(string $plainPassword): User
{
$this->plainPassword = $plainPassword;
return $this;
}
public function getPlainPassword(): ?string
{
return $this->plainPassword;
}
}

A continuación, desplázate hasta la parte superior y encuentra password. Elimínalo por completo de nuestra API:

294 lines | src/Entity/User.php
// ... lines 1 - 67
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
// ... lines 70 - 86
/**
* @var string The hashed password
*/
#[ORM\Column]
private ?string $password = null;
// ... lines 92 - 292
}

En su lugar, expone plainPassword... pero utiliza SerializedName para que se llamepassword:

294 lines | src/Entity/User.php
// ... lines 1 - 67
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
// ... lines 70 - 92
#[Groups(['user:write'])]
#[SerializedName('password')]
private ?string $plainPassword = null;
// ... lines 96 - 292
}

Obviamente, aún no hemos terminado... y si ejecutas las pruebas:

symfony php bin/phpunit tests/Functional/UserResourceTest.php

¡Las cosas van peor! Un error 500 debido a una violación de no nulo. Estamos enviando password, que se almacena en plainPassword... y luego no hacemos absolutamente nada con él. Así que la propiedad real password permanece nula y explota cuando llega a la base de datos.

Así que aquí está la pregunta del millón: ¿cómo podemos hacer hash de la propiedad plainPassword? O, en términos más sencillos, ¿cómo podemos ejecutar código en API Platform después de que los datos se deserialicen pero antes de que se guarden en la base de datos? La respuesta es: procesadores de estado. Vamos a sumergirnos en este potente concepto a continuación.