Probar la autenticación
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeVamos a crear una prueba para publicar y crear un nuevo tesoro. Digamospublic function testPostToCreateTreasure() que devuelve void. Y empezamos igual que antes: $this->browser()->post('/api/treasures'):
| // ... lines 1 - 10 | |
| class DragonTreasureResourceTest extends KernelTestCase | |
| { | |
| // ... lines 13 - 40 | |
| public function testPostToCreateTreasure(): void | |
| { | |
| $this->browser() | |
| ->post('/api/treasures', [ | |
| // ... line 45 | |
| ]) | |
| // ... lines 47 - 48 | |
| ; | |
| } | |
| } |
En este caso necesitamos enviar datos. El segundo argumento de cualquiera de estos métodospost() o get() es una matriz de opciones, que puede incluir parámetros headers,query u otras cosas. Una clave es json, que puedes establecer en una matriz, que se codificará en JSON para ti. Empieza enviando JSON vacío... y luego->assertStatus(422). Para ver cómo es la respuesta, añade ->dump():
| // ... lines 1 - 10 | |
| class DragonTreasureResourceTest extends KernelTestCase | |
| { | |
| // ... lines 13 - 40 | |
| public function testPostToCreateTreasure(): void | |
| { | |
| $this->browser() | |
| ->post('/api/treasures', [ | |
| 'json' => [], | |
| ]) | |
| ->assertStatus(422) | |
| ->dump() | |
| ; | |
| } | |
| } |
¡Impresionante! Copia el nombre del método de prueba. Quiero centrarme sólo en esta prueba. Para ello, ejecuta:
symfony php bin/phpunit --filter=testPostToCreateTreasure
Y... ¡oh! El código de estado de respuesta actual es 401, pero se esperaba 422.
Volcado de respuestas fallidas en el navegador
Cuando una prueba falla con el navegador, guarda automáticamente la última respuesta en un archivo... lo cual es genial. De hecho, está en el directorio var/. En mi terminal, puedo mantener pulsado Command y hacer clic para abrirlo en el navegador. Eso está muy bien. Me verás hacer esto un montón de veces.
Vale, esto devuelve un código de estado 401. Por supuesto: ¡la ruta requiere autenticación! Nuestra aplicación tiene dos formas de autenticarse: mediante el formulario de acceso y la sesión o mediante un token de API. Vamos a probar ambas, empezando por el formulario de inicio de sesión.
Iniciar sesión durante la prueba
Para iniciar sesión como usuario... ese usuario primero tiene que existir en la base de datos. Recuerda: al inicio de cada prueba, nuestra base de datos está vacía. Nuestro trabajo consiste en llenarla con lo que necesitemos.
Crea un usuario con UserFactory::createOne(['password' => 'pass']) para que sepamos cuál será la contraseña. A continuación, antes de hacer la petición POST para crear un tesoro, ->post() a /login y envía json con email ajustado a$user->getEmail() -para utilizar cualquier dirección de correo electrónico aleatoria que Faker haya elegido- y luego password ajustado a pass. Para asegurarnos de que ha funcionado, ->assertStatus(204):
| // ... lines 1 - 5 | |
| use App\Factory\UserFactory; | |
| // ... lines 7 - 11 | |
| class DragonTreasureResourceTest extends KernelTestCase | |
| { | |
| // ... lines 14 - 41 | |
| public function testPostToCreateTreasure(): void | |
| { | |
| $user = UserFactory::createOne(['password' => 'pass']); | |
| $this->browser() | |
| ->post('/login', [ | |
| 'json' => [ | |
| 'email' => $user->getEmail(), | |
| 'password' => 'pass', | |
| ], | |
| ]) | |
| ->assertStatus(204) | |
| // ... lines 54 - 58 | |
| ; | |
| } | |
| } |
Ese es el código de estado que devolvemos tras una autenticación correcta.
¡Vamos a probarlo! Muévete y ejecuta la prueba:
symfony php bin/phpunit --filter=testPostToCreateTreasure
¡Pasa! ¡Obtenemos el código de estado 422 y vemos los mensajes de validación!
Atajo para iniciar sesión: actingAs()
Así que... iniciar sesión es... ¡así de fácil! Y te recomiendo que hagas una prueba que envíe un POST específico a tu ruta de inicio de sesión, como acabamos de hacer, para asegurarte de que funciona correctamente.
Sin embargo, en el resto de mis pruebas... cuando simplemente necesito autenticarme para hacer el trabajo real, hay una forma más rápida de iniciar sesión. En lugar de hacer la petición POST, digamos ->actingAs($user):
| // ... lines 1 - 11 | |
| class DragonTreasureResourceTest extends KernelTestCase | |
| { | |
| // ... lines 14 - 41 | |
| public function testPostToCreateTreasure(): void | |
| { | |
| // ... lines 44 - 45 | |
| $this->browser() | |
| ->actingAs($user) | |
| // ... lines 48 - 52 | |
| ; | |
| } | |
| } |
Esta es una forma astuta de tomar el objeto User e introducirlo directamente en el sistema de seguridad de Symfony sin hacer ninguna petición. Es más fácil y más rápido. Y ahora, no nos importa en absoluto cuál es la contraseña, así que podemos simplificarlo.
Vamos a comprobarlo:
symfony php bin/phpunit --filter=testPostToCreateTreasure
¡Sigue bien!
Comprobando el éxito de la creación del tesoro
Hagamos otro POST aquí abajo. Sigue encadenando y añade ->post(). En realidad... me da pereza. Copia el ->post() existente... y úsalo. Pero esta vez, envía datos reales: Voy a teclear rápidamente algunos... estos pueden ser cualquier cosa. La última clave que necesitamos es owner. Ahora mismo, estamos obligados a enviar el owner cuando creamos un tesoro. Pronto lo haremos opcional: si no lo enviamos, lo hará por defecto quien esté autentificado. Pero por ahora, ponlo en /api/users/y luego en $user->getId(). Termina con assertStatus(201):
| // ... lines 1 - 11 | |
| class DragonTreasureResourceTest extends KernelTestCase | |
| { | |
| // ... lines 14 - 41 | |
| public function testPostToCreateTreasure(): void | |
| { | |
| $user = UserFactory::createOne(); | |
| $this->browser() | |
| ->actingAs($user) | |
| ->post('/api/treasures', [ | |
| 'json' => [], | |
| ]) | |
| ->assertStatus(422) | |
| ->post('/api/treasures', [ | |
| 'json' => [ | |
| 'name' => 'A shiny thing', | |
| 'description' => 'It sparkles when I wave it in the air.', | |
| 'value' => 1000, | |
| 'coolFactor' => 5, | |
| 'owner' => '/api/users/'.$user->getId(), | |
| ], | |
| ]) | |
| ->assertStatus(201) | |
| ; | |
| } | |
| } |
Porque 201 es lo que devuelve la API cuando se crea un objeto.
Muy bien, a probar:
symfony php bin/phpunit --filter=testPostToCreateTreasure
¡Sigue pasando! ¡Estamos en racha! Añade un ->dump() para ayudarnos a depurar y luego una comprobación de cordura: ->assertJsonMatches() que name es A shiny thing:
| // ... lines 1 - 11 | |
| class DragonTreasureResourceTest extends KernelTestCase | |
| { | |
| // ... lines 14 - 41 | |
| public function testPostToCreateTreasure(): void | |
| { | |
| // ... lines 44 - 45 | |
| $this->browser() | |
| // ... lines 47 - 60 | |
| ->assertStatus(201) | |
| ->dump() | |
| ->assertJsonMatches('name', 'A shiny thing') | |
| ; | |
| } | |
| } |
Cuando lo probemos
symfony php bin/phpunit --filter=testPostToCreateTreasure
Enviando la cabecera Accept: application/ld+json
Ninguna sorpresa: todo verde. Pero mira la respuesta volcada: ¡no es JSON-LD! Nos devuelve JSON estándar. Puedes verlo en la cabecera Content-Type: 'application/json', no application/ld+json, que es lo que esperaba.
Averigüemos qué está pasando y solucionémoslo globalmente personalizando el funcionamiento del Navegador en todo nuestro conjunto de pruebas.
15 Comments
On postToCreateTreasure() test:
login succeeds (status: 204).
post('/api/treasures') returns 401 instead of 422.
While the same testing with /api (swagger) post to /api/treasures returns 422 (the expected).
I do not prefix with 'test' my test methods, I am using the @test keyword in docblock:
`...
/**
There was 1 failure:
1) App\Tests\Functional\DragonTreasureResourceTest::postToCreateTreasure
Current response status code is 401, but 422 expected.
Hi @Alkiviadis-D
Hm looks like session is not working correctly between requests, or maybe login request was not fully successful... have you tried to use
->actingAs($user)shortcut to see if it works correctly?Cheers!
Okkk.. had a typo inside getRoles(). Sorry for this. Thanks for your help.
What could be the issue ?
{
I get this error,
when I use ->actingAs($user) method , as I'm experimenting on new Symfony 7project, but seems working when using ->login()
Hey @sujal_k ,
Does your User entity have $id field? Does that field has the same mapping as in this course code? i.e. it should be :
Also, make sure you don't have an invalid Doctrine mapping by calling this command:
Ad make sure both mapping and DB are OK.
I hope this helps!
Cheers!
I'm getting the exact same error.
Started my project from latest ApiPlatform Distribution (AP 3.4, Symfony 6.4). When I log in making POST to json_login path I'm able to fetch resources which require full authetication, but when I swap login process with
->actingAs($user)I get 500 error described above while trying to fetch any resource.I checked my schema as recommended above (everyting is ok) and my User entity is rather standard (created by make:user). It's id is on place.
Do you have any other ideas what can be wrong here? :(
Hey Kriss,
Hm, difficult to say without further debugging, probably try to find the place where that error is thrown and before that exception try to dump the actual user to make sure it really has an ID.
Also, if you’re customizing serialization groups, ensure that the identifier field is included in the serialization group used when the API Platform serializes the user.
Cheers!
Hey Victor,
I performed some further debuging and I discovered that when I use $entityManager to create my $user object like so:
then I'm am able to use
$browser->actingAs($user)normaly. Also when I get type of created entity usinggetMetatadaFormethod I getApp\Entity\User.When I create
$userusingUserFactoryand try to get object's type using the same method I get an error:Looks like there is something wrong with doctrine mapping? Or maybe It's something wrong with UserFactory configuration?
I used ApiPlatform distribution as a starter, created Facotry with
make:facotryand didn't mess with doctrine mappings at all, so why would they be wrong... Do you have any idea? :)Hey Kriss,
Ah, good catch! Yeah, that's actually on purpose, entity factories from the Foundry lib need that to add more magic in tests - those proxy objects helps you to do not worry about the problem with refreshing the entity in tests. But that AppEntityUserProxy actually extends your User class, so
instanceof Userstill should work.In short, nothing is wrong with this behavior, that's how it's suppose to work in Foundry, you just need to keep this in mind. If it bothers you or causing some issues - you can create objects manually bypassing creating it via Foundry factories.
P.S. Sorry for the long reply, I somehow missed this ticket 🙃
Cheers!
Hey Victor,
thanks for your explanation, however it seems that this does not resolve the issue this thread is about.
Namely: why in the tutorial Ryan was able to use
browser->actingAs($user)with$usercreated with Foundry factory and when I try to do this I get the 500 error mentioned by @sujal_k at the beginning of this thread? And why am I able to use this method when creating user using entity manager?Do you think it can have something to do with the fact I stared my project from AP Distribution, not from cli installation?
Hey @Kriss_G!
Can you tell me what version of
zenstruck/browser&zenstruck/foundryyou are using?(I'm thinking you may be on foundry 2+ but your browser version does not support it yet - I'm hoping upgrading
zenstruck/browserwill fix)--Kevin
Hey Kevin @kbond!,
Indeed, I used foundry v2.1.0 and browser v1.9.0.
I ran
composer update zenstruck/browser, which brought my browser version to 1.9.1 and it seems to solve the issue.I can now use
browser->actingAs($user)with$usergenerated withUserFactoryclass!Thank you for your help! :)
Kriss
Awesome, great to hear!
The authentication fails while testing at: 2:33 timestamp, in the video it passes, but in my dummy Symfony 7 project, same code failed, always says 401 as response code,
This is the error I get in response when I dump the
/loginrequest in test,below is the code:
Hey @sujal_k!
These invalid credentials can be tricky as you can't really see what's going on since the passwords are all hashed. And I don't see any obvious problems. Triple check that the password IS being hashed before saving to the DB. You could also temporarily change the hashing algo to plaintext. It may help you see the underlying problem.
Cheers!
"Houston: no signs of life"
Start the conversation!