Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

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

Login Subscribe

Let'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.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.1.2
        "doctrine/annotations": "^2.0", // 2.0.1
        "doctrine/doctrine-bundle": "^2.8", // 2.8.3
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.1
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.66.0
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.16.1
        "symfony/asset": "6.2.*", // v6.2.5
        "symfony/console": "6.2.*", // v6.2.5
        "symfony/dotenv": "6.2.*", // v6.2.5
        "symfony/expression-language": "6.2.*", // v6.2.5
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.5
        "symfony/property-access": "6.2.*", // v6.2.5
        "symfony/property-info": "6.2.*", // v6.2.5
        "symfony/runtime": "6.2.*", // v6.2.5
        "symfony/security-bundle": "6.2.*", // v6.2.6
        "symfony/serializer": "6.2.*", // v6.2.5
        "symfony/twig-bundle": "6.2.*", // v6.2.5
        "symfony/ux-react": "^2.6", // v2.7.1
        "symfony/ux-vue": "^2.7", // v2.7.1
        "symfony/validator": "6.2.*", // v6.2.5
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.1
        "symfony/yaml": "6.2.*" // v6.2.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "mtdowling/jmespath.php": "^2.6", // 2.6.1
        "phpunit/phpunit": "^9.5", // 9.6.3
        "symfony/browser-kit": "6.2.*", // v6.2.5
        "symfony/css-selector": "6.2.*", // v6.2.5
        "symfony/debug-bundle": "6.2.*", // v6.2.5
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/phpunit-bridge": "^6.2", // v6.2.5
        "symfony/stopwatch": "6.2.*", // v6.2.5
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.5
        "zenstruck/browser": "^1.2", // v1.2.0
        "zenstruck/foundry": "^1.26" // v1.28.0
    }
}
userVoice