jwt-auth
jwt-auth copied to clipboard
how to test token JWT with phpunit in Laravel
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.
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).
Found this, https://stackoverflow.com/questions/31309098/laravel-testcase-not-sending-authorization-headers-jwt-token
, not a definitive solution, but maybe it helps.
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?
thanks @mehrdaad @emersonbroga
@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 , 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
@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 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.
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);
I was getting a 401 token_invalid
. Turns out I hadn't set my application key yet, which the library requires to work.
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.
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.
Facing the same issue, and none of the workaround above work for me, Plus one for resolving this issue.
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.
This answer on stackoverflow, provided by @emersonbroga helped me, but still, this is just a workaround.
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(),
]));
}
}``
Did somebody had the trouble of getting the user from the JWTAuth facade in some part of the application that is created on boot?
@tymondesigns can somebody please check this issue, its really painful for testing. Thanks!
@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 didn't worked for me.
The jwt request
doesn't contain any authorization key
.
But the request()->header('authorization')
is valid!
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);
.....
}
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,
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 It works perfectly, thx. :+1:
I'm only change "User::generate"
to standard Eloquent:
$user = new User();
...
$user->save();
@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.