jwt-auth icon indicating copy to clipboard operation
jwt-auth copied to clipboard

how to test token JWT with phpunit in Laravel

Open alihossein opened this issue 7 years ago • 25 comments

HI. I want test login API in Laravel but instead of return 200 or 401, test return 302 always.

$info = ['email'=>'[email protected]','password'=>'123456']; $response = $this->json('POST', '/login', $info) ->assertStatus(200);

what is problem ??

thanks.

alihossein avatar Jul 11 '17 05:07 alihossein

Same problem here.

When I make a request from a browser or postman it works, but when I make a request from phpunit, the $headers property inside the JWTAuth.php on the method parseToken doesn't have the authorization key. (ONLY ON TEST ENV, when running phpunit).

JWT

emersonbroga avatar Jul 30 '17 16:07 emersonbroga

Found this, https://stackoverflow.com/questions/31309098/laravel-testcase-not-sending-authorization-headers-jwt-token, not a definitive solution, but maybe it helps.

emersonbroga avatar Jul 30 '17 19:07 emersonbroga

Same problem here.
UPDATE:
I have found some mysterious behavior in my system, it might be specific for only my system but gonna give more info since any body else might encounter this type of problem:

the way I generated token in my tests for a specific user was like as follows:

 public function admin_can_get_all_degrees()
{
    $token=JWTAuth::fromUser($this->adminUser);
    $this->get($this->DEGREES_ROUTE, ['Authorization' => "Bearer $token"])->assertStatus(200);
}

This was throwing 401 status code for me. tried getting token from the end point I had specified and it worked!
The way I'm authenticating a user in my controller is as follows:

if (Auth::once(compact('email', 'password')))
    {
        $user = Auth::user();

            $token = JWTAuth::fromUser($user);
            return $this->item(new Token($token), new TokenTransformer());

    }  

Is this a bug(problem) related to tymon's jwt or I was generating token wrong?

mehrdad-shokri avatar Aug 14 '17 11:08 mehrdad-shokri

thanks @mehrdaad @emersonbroga

alihossein avatar Aug 29 '17 10:08 alihossein

@emersonbroga @alihossein I'm having the same issue, but I'd like to use the $this->actingAs($user, 'api') method, but it's not working.

For now I created a AttachJwtToken trait, so inside the test class I just use AttachJwtToken and all calls send the JWT token for me. The trait code is here: https://gist.github.com/jgrossi/4b1364e20418eca3ca937e70550c1823.

Using it you can also do:

$this->loginAs($user)
    ->json('GET', '/foo');

This loginAs() method make the JWT working for testing purposes. It's fine for me. If you don't use the loginAs() method but is using the trait, it creates a user using factories and send its JWT.

If anyone made the JWT Auth working with ->actingAs() method please let me know.

jgrossi avatar Sep 20 '17 12:09 jgrossi

@jgrossi , I am also facing this issue. I found this as a good solution. Basically, It extends the TestCase class for use specifically with JWT and includes the token to the header

Api Testing With Laravel 5.1 (jwt-auth)

sakibulalam avatar Sep 30 '17 05:09 sakibulalam

@jgrossi actingAs or loginAs is not what you want to use, if your API does not use sessions you simply send token along all the time.

protected function apiAs($user, $method, $uri, array $data = [], array $headers = [])
{
    $headers = array_merge([
        'Authorization' => 'Bearer '.\JWTAuth::fromUser($user),
    ], $headers);

    return $this->api($method, $uri, $data, $headers);
}


protected function api($method, $uri, array $data = [], array $headers = [])
{
    return $this->json($method, $uri, $data, $headers);
}

Kyslik avatar Oct 06 '17 10:10 Kyslik

@Kyslik exactly... the trait I shared in gist does exactly that, sending the token in the header, but if the user is missing, it just creates one for me and sends his token.

jgrossi avatar Oct 06 '17 10:10 jgrossi

Another trick on what could be done is instead of passing Auth through the headers. Directly pass the token in your URI. I'm not sure if its the right approach but this works for me. For eg, in my FooTest.php

$token = \JWTAuth::fromUser( $user ); $this->call("POST","api/endpoint/?token=".$token);

usamamuneerchaudhary avatar Dec 11 '17 12:12 usamamuneerchaudhary

I was getting a 401 token_invalid. Turns out I hadn't set my application key yet, which the library requires to work.

chrisrocco avatar Dec 19 '17 07:12 chrisrocco

I modified the parseAuthHeader method in JWTAuth.php from $header = $this->request->headers->get($header) to $header = request()->headers->get($header);. This has fixed the problem.

vjrngn avatar Jan 27 '18 07:01 vjrngn

I was using custom middelware and i was accessing JWTAuth facade directly which faced me with the issue @emersonbroga mentioned in second comment.

What i did is i simply extended the Tymon\JWTAuth\Middleware\BaseMiddleware which accepts JWTAuth in constructor and stores in the $auth variable. Than in handle function i set the request to the $auth $this->auth->setRequest($request); which sets the request with our token. Same thing jwt.auth middelware is doing in handle method. In the controller we can normally use JWTAuth facade to retrieve logged in user.

Please not that i'm not using without middelware trait in my test. Using jwt.auth middelware should work as well.

cuca24 avatar Feb 03 '18 11:02 cuca24

Facing the same issue, and none of the workaround above work for me, Plus one for resolving this issue.

kalemdzievski avatar Sep 29 '18 13:09 kalemdzievski

protected function apiAs($user, $method, $uri, array $data = [], array $headers = []) { $headers = array_merge([ 'Authorization' => 'Bearer '.\JWTAuth::fromUser($user), ], $headers); return $this->api($method, $uri, $data, $headers); }

I'm using Laravel 5.7 and edited the above code a bit. Basically use the existing json call for unauthenticated routes, and below for authenticated routes:

protected function apiAs($method, $uri, array $data = [], array $headers = [], $user = null)
    {
        $user = $user ? $user : factory(User::class)->create();

        $headers = array_merge(
            ['Authorization' => 'Bearer '.\JWTAuth::fromUser($user)],
            $headers
        );

        return $this->json($method, $uri, $data, $headers);
    }

Which means unless you want to log in as a specific user, just change any of your test calls from json to apiAs and you're done.

EDIT: Add this to your base TestCase class.

iforwms avatar Sep 30 '18 00:09 iforwms

This answer on stackoverflow, provided by @emersonbroga helped me, but still, this is just a workaround.

kalemdzievski avatar Oct 01 '18 07:10 kalemdzievski

I use a trait in all my tests. Hope this helps someone.

<?php

namespace Tests;


use App\User;
use Tymon\JWTAuth\Facades\JWTAuth;

trait AttachesJWT {
    /**
     * @var User
     */
    protected $loginUser;

    /**
     * @param User $user
     *
     * @return $this
     */
    public function loginAs (User $user) {
        $this->loginUser = $user;

        return $this;
    }

    /**
     * @return string
     */
    protected function getJwtToken () {
        return is_null($this->loginUser) ? null : JWTAuth::fromUser($this->loginUser);
    }

    /**
     * @param string $method
     * @param string $uri
     * @param array $parameters
     * @param array $cookies
     * @param array $files
     * @param array $server
     * @param string $content
     *
     * @return \Illuminate\Http\Response
     */
    public function call ($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null) {
        if ($this->requestNeedsToken($method, $uri)) {
            $server = $this->attachToken($server);
        }

        return parent::call($method, $uri, $parameters, $cookies, $files, $server, $content);
    }

    /**
     * @param string $method
     * @param string $uri
     *
     * @return bool
     */
    protected function requestNeedsToken ($method, $uri) {
        return !($method === "POST" && ($uri === "/auth" || $uri === "/route-without-auth"));
    }

    /**
     * @param array $server
     *
     * @return string
     */
    protected function attachToken (array $server) {
        return array_merge($server, $this->transformHeadersToServerVars([
            'Authorization' => 'Bearer ' . $this->getJwtToken(),
        ]));
    }
}``

vjrngn avatar Oct 01 '18 07:10 vjrngn

Did somebody had the trouble of getting the user from the JWTAuth facade in some part of the application that is created on boot?

kalemdzievski avatar Oct 01 '18 08:10 kalemdzievski

@tymondesigns can somebody please check this issue, its really painful for testing. Thanks!

kalemdzievski avatar Oct 01 '18 11:10 kalemdzievski

@kalemdzievski just an update here. I'm using an easier way to add the header to the call. Just add this to a trait that overrides how the actingAs() method works:

public function actingAs(Authenticatable $user, $driver = null)
{
    $token = JWTAuth::fromUser($user);
    $this->withHeader('Authorization', 'Bearer ' . $token);

    return $this;
}

jgrossi avatar Oct 03 '18 16:10 jgrossi

@jgrossi didn't worked for me. The jwt request doesn't contain any authorization key. But the request()->header('authorization') is valid!

khalilst avatar Sep 18 '19 14:09 khalilst

Hello everyone, sorry for my english.

1 JWT set a response on boot app and parse it 2 Test can send request after 1 step 3 JWT does not pay attention on test request (2 step)

the following describes how I solved this problem

create new middleware

public function handle($request, Closure $next)
{
    if ($this->runningUnitTests()) {
        JWTAuth::setRequest($request);
    }

    return $next($request);
}

private function runningUnitTests()
{
    return $this->app->runningInConsole() && $this->app->runningUnitTests();
}

in kernel.php

protected $middleware = [
    ...,
    ...,
    OurMiddleware::class,
];

move the below code into trait Tests\LoginAPITrait;

public function signInAPI()
{
    /** @var \Illuminate\Foundation\Testing\TestResponse $response */
    $response = $this->postJson('api/v1/login', [
         'email' => 'email',
         'password' => 'password'
    ]);

    $response->assertStatus(200);
    $responseData = json_decode($response->getContent(), true);
    $this->assertTrue(isset($responseData['data']['access_token']));

    return $responseData['data']['access_token'];
}

and test


use LoginAPITrait;

/**
 * @var string
 */
private $token;

public function setUp()
{
    parent::setUp();

    $this->token = $this->signInAPI();
}

public function testExample()
{
    $response =
        $this->postJson(
            'api/v1/someurl',
            [ data ],
            [
                'Accept' => 'application/json',
                'Authorization' => 'Bearer ' . $this->token,
            ]
        );

    $response->assertStatus(201);
    .....
}

victor-ilichev avatar Dec 20 '19 21:12 victor-ilichev

For @jgrossi solution to work, I had to use this as route middleware in ^1.0:

'jwt.auth' => \Tymon\JWTAuth\Http\Middleware\Authenticate::class,
'jwt.refresh' => \Tymon\JWTAuth\Http\Middleware\RefreshToken::class,

Flayshon avatar Feb 12 '20 03:02 Flayshon

I solved this by making a getTokenForUser method in TestCase that gets the token for arbitrary users.

TestCase.php

<?php

namespace Tests;

use App\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use JWTAuth;
use Spatie\Permission\Models\Role;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication, DatabaseTransactions;

    const AUTH_PASSWORD = 'password';

    public function setUp() : void
    {
        parent::setUp();
    }

    public function tearDown() : void
    {
        parent::tearDown();
    }

    public function getTokenForUser(User $user) : string
    {
        return JWTAuth::fromUser($user);
    }

    public function adminUser() : User
    {
        $user = User::query()->firstWhere('email', '[email protected]');

        if ($user) {
            return $user;
        }

        $user = User::generate('Test Admin', '[email protected]', self::AUTH_PASSWORD);

        $user->assignRole(Role::findByName('admin'));

        return $user;
    }

    public function user() : User
    {
        $user = User::query()->firstWhere('email', '[email protected]');

        if ($user) {
            return $user;
        }

        // this is a wrapper around User::create() that handles default values and overrides such as `$user->status`
        $user = User::generate('Test User', '[email protected]', self::AUTH_PASSWORD);

        return $user;
    }
}

Then I can make authenticated requests in unit tests via syntax such as:

SomeTest.php

    /** @test */
    public function it_can_logout_as_admin()
    {
        $token = $this->getTokenForUser($this->adminUser());

        $this->postJson(route('logout'), [], ['Authorization' => "Bearer $token"])
            ->assertStatus(200)
            ->assertJsonStructure(['success']);
    }

For me this is close enough to $this->actingAs($user)->postJson(). I will argue that it makes it easy for me to test specific middleware error statuses such as 401 expired token because I can pass in a token with specific qualities.

agm1984 avatar May 25 '20 05:05 agm1984

@agm1984 It works perfectly, thx. :+1:

I'm only change "User::generate" to standard Eloquent:

$user = new User();
...
$user->save();

MichalFurman avatar Nov 09 '20 20:11 MichalFurman

@agm1984 this works but I have a question can we create token only once and use it over all tests in file rather than creating again. $token = $this->getTokenForUser($this->adminUser()); as this should get called in all test methods to pass as token and with each call it will create a token.

aroraank11 avatar Jan 24 '23 08:01 aroraank11