Testing Passport Authenticated Controllers and Routes in Laravel

Laravel is lacking when it comes to testing API authentication via Passport - here's how you can make testing those routes simple.

Simon Archer
by Simon Archer

Laravel attempts to make PHPUnit testing as simple as possible. Yet it is still lacking when it comes to testing API authentication via Passport.

In this article, we'll discuss how you can make testing those routes simple.

Laravel's WithoutMiddleware trait gives us the ability to circumvent middleware altogether when testing. This works great if you're testing controller functionality and their routes. But what if we want to test that only certain users, with specific permissions, can access certain routes?

In order to test Passport authenticated controllers and routes, we can extend the functionality of the default Laravel default test case...

Extending the TestCase class with a new PassportTestCase looks like this:

<?php
 
use App\User;
use Laravel\Passport\ClientRepository;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
 
class PassportTestCase extends TestCase
{
 use DatabaseTransactions;
 
 protected $headers = [];
 protected $scopes = [];
 protected $user;
 
 public function setUp()
 {
 parent::setUp();
 // Important code goes here.
 }
}

We implement one of the DatabaseTransactions and DatabaseMigrations traits to prevent data from persisting to the database as we'll be creating test users and OAuth clients.

You can place this in the tests/ directory alongside the TestCase.php file.

Then you need to add tests/PassportTestCase.php to the composer.json autoload-dev section.

{
 "autoload-dev": {
 "classmap": [
 "tests/TestCase.php",
 "tests/PassportTestCase.php"
 ]
 }
}

Next, run the following command in your console and in your project folder to update the autoload definitions:

composer dumpautoload

This allows you to extend the PassportTestCase class from any class in your tests directory.

Now for the actual meat of the class; which goes in the setUp() function of PassportTestCase.php:

$clientRepository = new ClientRepository();
$client = $clientRepository->createPersonalAccessClient(
 null, 'Test Personal Access Client', $this->baseUrl
);
 
DB::table('oauth_personal_access_clients')->insert([
 'client_id' => $client->id,
 'created_at' => new DateTime,
 'updated_at' => new DateTime,
]);
 
$this->user = factory(User::class)->create();
$token = $this->user->createToken('TestToken', $this->scopes)->accessToken;
$this->headers['Accept'] = 'application/json';
$this->headers['Authorization'] = 'Bearer '.$token;

There's a fair bit to digest there so let's break it down...

We create a new OAuth client and insert a row for the client in the oauth_personal_access_clients table. This second step allows us to create personal access tokens specifically for that client.

$clientRepository = new ClientRepository();
$client = $clientRepository->createPersonalAccessClient(
 null, 'Test Personal Access Client', $this->baseUrl
);
 
DB::table('oauth_personal_access_clients')->insert([
 'client_id' => $client->id,
 'created_at' => new DateTime,
 'updated_at' => new DateTime,
]);

Personal access tokens are long-life tokens that allow apps to access your API. You create them once and don't need to reauthorise them at a later date. Read more about personal access tokens in the Laravel documentation.

Next, we create a new test user and a personal access token for that user. We're also setting the scopes that the token should have access to. These are the permissions granted to the access token; if you don't have any defined you can ignore this. There is more information on token scopes in the Laravel documentation.

$this->user = factory(User::class)->create();
$token = $this->user->createToken('TestToken', $this->scopes)->accessToken;

Lastly, we define a couple of headers to use when sending all requests. The first accepts JSON responses; the second will send the token so the application can authenticate the request.

$this->headers['Accept'] = 'application/json';
$this->headers['Authorization'] = 'Bearer '.$token;

And that's everything for setting up our requests.

Our final task is to add the headers to requests by overloading the request functions of the TestCase. One example might be as follows:

public function get($uri, array $headers = [])
{
 return parent::get($uri, array_merge($this->headers, $headers));
}

This will intercept all calls to the get() function and attach the authentication headers to the request. Note: The get() function is actually defined in the MakesHttpRequests trait. You can continue to do this for the other request functions such as get() , put() and patch() . Examples of which are in the example code, linked at the bottom of the page.

At this stage, we can write a test case to make use of this functionality. Here's a basic example:

<?php
 
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
 
class ExamplePassportTest extends PassportTestCase
{ 
 protected $scopes = ['restricted-scope'];
 
 public function testRestrictedRoute()
 {
 $this->get('/api/user')->assertResponseOk();
 }
 
 public function testUnrestrictedRoute()
 {
 $this->get('/api/restricted')->assertResponseStatus(401);
 }
}

Here are two tests; one that expects an OK response and another that expects a 401 forbidden response. Since we're using the overloaded get() function, the authentication headers will be getting sent with the requests. These routes would need to be defined in your routes for them to work.

You might also notice the $scopes variable defined near the top. This is an array of the scopes you want your test requests to have access to in this test case. If the application has none defined, you can ignore this.

And there you have it, you can now write tests for Passport authenticated users in your Laravel app.

Find the full example code in this Gist on GitHub.

Purpose First.

Ready to connect with your audience and inspire them into action?

Let's talk

Insights Digest

Receive a summary of the latest insights direct to your inbox every Tuesday.