Testing Authentication
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 SubscribeLet's create a test to post and create a new treasure. Say public function testPostToCreateTreasure() that returns void. And start the same way as before: $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 | |
| ; | |
| } | |
| } |
In this case we need to send data. The second argument to any of these post() or get() methods is an array of options, which can include headers, query parameters or other stuff. One key is json, which you can set to an array, which will be JSON-encoded for you. Start by sending empty JSON... then ->assertStatus(422). To see what the response looks like, add ->dump():
| // ... lines 1 - 10 | |
| class DragonTreasureResourceTest extends KernelTestCase | |
| { | |
| // ... lines 13 - 40 | |
| public function testPostToCreateTreasure(): void | |
| { | |
| $this->browser() | |
| ->post('/api/treasures', [ | |
| 'json' => [], | |
| ]) | |
| ->assertStatus(422) | |
| ->dump() | |
| ; | |
| } | |
| } |
Awesome! Copy the test method name. I want to focus just on this one test. To do that, run:
symfony php bin/phpunit --filter=testPostToCreateTreasure
And... oh! Current response status code is 401, but 422 expected.
Dumped Failed Responses in Browser
When a test fails with browser, it automatically saves the last response to a file... which is awesome. It's actually in the var/ directory. In my terminal, I can hold Command and click to open that in my browser. That is nice. You'll see me do this a bunch of times.
Ok, so this returned a 401 status code. Of course: the endpoint requires authentication! Our app has two ways to authenticate: via the login form and session or via an API token. We're going to test both, starting with the login form.
Logging in during the Test
To log in as a user... that user first needs to exist in the database. Remember: at the start of each test, our database is empty. It's then our job to populate it with whatever we need.
Create a user with UserFactory::createOne(['password' => 'pass']) so that we know what the password will be. Then, before we make the POST request to create a treasure, ->post() to /login and send json with email set to $user->getEmail() - to use whatever random email address Faker chose - then password set to pass. To make sure that worked, ->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 | |
| ; | |
| } | |
| } |
That's the status code we're returning after successful authentication.
Let's give this a try! Move over and run the test:
symfony php bin/phpunit --filter=testPostToCreateTreasure
It passes! We're getting the 422 status code and see the validation messages!
Shortcut to Logging in: actingAs()
So... logging in is... just that easy! And I would recommend having a test that specifically POSTs to your login endpoint like we just did, to make sure its working correctly.
However, in all of my other tests... when I simply need to be authenticated to do the real work, there's a faster way to log in. Instead of making the POST request, say ->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 | |
| ; | |
| } | |
| } |
This is a sneaky way of taking the User object and pushing it directly into Symfony's security system without making any requests. It's easier, and faster. And now, we don't care what the password is at all, so we can simplify that.
Let's check it:
symfony php bin/phpunit --filter=testPostToCreateTreasure
Still good!
Testing Successful Treasure Creation
Let's do another POST down here. Keep chaining and add ->post(). Actually... I'm lazy. Copy the existing ->post()... and use that. But this time, send real data: I'll quickly type in some... these can be anything. The last key we need is owner. Right now, we are required to send the owner when we create a treasure. Soon, we'll make that optional: if we don't send it, it will default to whoever is authenticated. But for now, set it to /api/users/ then $user->getId(). Finish with 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) | |
| ; | |
| } | |
| } |
Because 201 is what the API returns when an object is created.
Alright, go test, go:
symfony php bin/phpunit --filter=testPostToCreateTreasure
Still passing! We're on a roll! Add a ->dump() to help us debug then a sanity check: ->assertJsonMatches() that name is 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') | |
| ; | |
| } | |
| } |
When we try that:
symfony php bin/phpunit --filter=testPostToCreateTreasure
Sending the Accept: application/ld+json Header
No surprise: all green. But look at the dumped response: it's not JSON-LD! We're getting back standard JSON. You can see it in the Content-Type header: 'application/json', not application/ld+json, which is what I was expecting.
Let's find out what's going on next and fix it globally by customizing how Browser works across our entire test suite.
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!