I am trying to run Unit Tests for Laravel using PHPUnit.
My calls to the functions seem to get to the controller, however, they seem to bypass my filters. The filters are used in order to do user validation, session tokens and so not. The call seems to go directly to the controller and forgets about the filters.
public function testBasicExample(){
$response = $this->call('GET', 'URL_Here', array('param_1' => 'value_1', 'param_2' => 'value_2' ));
echo $response->getContent(); /**This should not be returning true if values are wrong and it is **/
$this->assertTrue($this->client->getResponse()->isOk());
}
Any ideas?
According to the Laravel documentation, the route filters are disabled when in the testing environment. You can use the following call to enable them:
Route::enableFilters();
You can read more about this here. The Note block at the end of that chapter answers your question.
Related
I'm writing a Feature test for an API and I want to test custom auth logic.
I know that when I call the login API endpoint, Laravel caches the fact that the user is logged in so the next API call in the test would consider the user already authenticated...
So for one test, how do I disable this Laravel magic of auth caching so I can manually provide the Bearer auth token to check if my custom authentication logic works?
I'm thinking something along the lines of clearing the guards in AuthManager, or clearing AuthManager entirely so Laravel would be force to reinitialize it. But I'm having no luck in figuring out how to do that in a way that works.
Here's some pseudo-example code:
public function testLogin()
{
$responseData = $this->post('/login', ['email' => $this->user->email, 'password' => 'password'])->json();
$this->get('/endpoint-requiring-auth')->assertOk();
//
// $this->logicToResetAuth() ?
//
$this->get('/endpoint-requiring-auth')->assertUnauthorized();
// I want to manually have to pass the token to get it to work now.
$this->get('/endpoint-requiring-auth', ['Authorization' => "Bearer $responseData[access_token]"])
->assertOk();
}
Also the unknown reset logic should affect multiple guards.
Oh yeah, I'm writing custom logic around the JWT-Auth library.
For Laravel 8.x (and maybe newer)
auth()->forgetGuards();
But for JWT you may need to additionally do:
app('tymon.jwt')->unsetToken();
Or
app()->instance('tymon.jwt', null);
For Laravel 7.x (and maybe older)
As ->forgetGuards() is not invented yet in this version, and guards-array is protected, do something like:
/** #var \Illuminate\Auth\AuthManager $auth */
$auth = app('auth');
$mirror = new \ReflectionObject( $auth );
$prop = $mirror->getProperty( 'guards' );
$prop->setAccessible(true);
$prop->setValue($auth, []);
$prop->setAccessible(false); // Back to protected.
But for JWT, do same as above Laravel 8 section.
For jwt-auth, you can do this to clear the auth user:
auth()->forgetGuards();
app()->instance('tymon.jwt', null);
The first line, discards the existing cached guards. And the second, ensures when the guard is re-created, it wont reuse the same underlying jwt instance.
Good question. I was also struggling with this too. Yesterday I performed a deep dive on this subject. Turned out that TokenGuard has en implementation problem as the authenticated user is not refreshed after a new request is loaded. In order to fix this the following is needed:
Extends TokenGuard class into your own class and overload method setRequest. Add '$this->user = null;' before calling the parent implementation by 'return parent::setRequest($request);'
Use Auth::extend to be able to use your custom TokenGuard
After that, every new request resets the authenticated user automatically
See also https://code.tutsplus.com/tutorials/how-to-create-a-custom-authentication-guard-in-laravel--cms-29667
I have this test:
public function test_user_can_access_the_application_page(
{
$user=[
'email'=>'user#user.com',
'password'=>'user1234',
];
$response=$this->call('POST','/login',$user);
$this->assertAuthenticated();
$response->assertStatus(302)
->assertRedirect('/dashboard')
->assertLocation('/dashboard');
$response=$this->call('GET','/application/index');
$response->assertLocation('/application/index');
}
After I log in, it directs me to the dashboard ok until now, but if I want to access the other page after that, I cant. This error comes up.
Expected :'http://mock.test/application/index'
Actual :'http://mock.test'
Aren't multiple calls allowed in the same test, or is another way to access other pages after login?
(Note: It's not possible to use factories for the actingAs so I need to login).
If you can't use factories for actingAs, then you should try with cookie.
Look at the https://github.com/firebase/php-jwt library.
I guess you will need to call the function as an user, since you can only access it logged in. Laravel provides the actingAs() method for such cases.
https://laravel.com/docs/7.x/http-tests#session-and-authentication
You can create a random User who has the permission to log into your app or take a seeded one and call the function acting as the chosen User.
$response=$this->actingAs($user)->call('GET','/application/index');
If you call it without actingAs(), your middleware will redirect you back to the login or home screen (what you defined in the LoginController ).
In my opinion this test case should have its own testing method. I recommend using a test method per route or per use case. It makes your tests clearly arranged and easy to understand.
If you want to be authenticated, the easiest way is to have PHPUnit simulate authentication using the actingAs() method.
This method makes the user authenticated, so you wouldn't want to test the login method with it. You should write your login tests separate from testing the other pages.
To answer your question, yes you can make multiple requests in the same test, but in this case linking the login test to the 'application/index' page likely does not make much sense.
public function test_the_user_can_login()
{
$user = [
'email'=>'user#user.com',
'password'=>'user1234',
];
$response = $this->call('POST','/login',$user);
$this->assertAuthenticated();
$response->assertStatus(302)
->assertRedirect('/dashboard')
->assertLocation('/dashboard');
}
public function test_user_can_access_the_application_page()
{
$user = User::where($email, "user#user.com")->first();
$response = $this->actingAs($user)
->call('GET','/application/index');
$response->assertLocation('/application/index');
}
I experienced that in laravel 8 I use comment #test and involved second test!!! I mean if you use two function for test you must us #test that php artisan test, test both of them.
I'm writing unit tests for an API using PHPUnit and Laravel. Most functions I'm testing require that the user is authenticated before the function can be ran. The user data is stored in one table, and their permissions are stored inside of another table. I can fake the user object inside of Laravel, but I need to be able to also pull the corresponding permissions from the other table without having to hit the database like the dingo router currently is doing.
Currently running Laravel 5.8 and PHPUnit 8.1.5. I currently have the users object that I generated from a Laravel factory saved to a text file. I am able to pass that to a function called "actingAsApi" (found on Github, code below) and that allows me to authenticate as that user. However, the function is still going out and getting all permissions for that user from the database. I'm trying to mock or fake the permissions object it is pulling somewhere so that it doesn't need to hit the database at all. I also tried using the built in Passport functions for Passport::actingAs, and those did not work either as they were still hitting the DB (and not really working anyways).
actingAsApi (inside of TestCase.php)
protected function actingAsApi($user)
{
// mock service middleware
$auth = Mockery::mock('Dingo\Api\Http\Middleware\Auth[handle]',
[
Mockery::mock('Dingo\Api\Routing\Router'),
Mockery::mock('Dingo\Api\Auth\Auth'),
]);
$auth->shouldReceive('handle')
->andReturnUsing(function ($request, \Closure $next) {
return $next($request);
});
$this->app->instance('Dingo\Api\Http\Middleware\Auth', $auth);
$auth = Mockery::mock('Dingo\Api\Auth\Auth[user]',
[
app('Dingo\Api\Routing\Router'),
app('Illuminate\Container\Container'),
[],
]);
$auth->shouldReceive('user')
->andReturnUsing(function () use ($user) {
return $user;
});
$this->app->instance('Dingo\Api\Auth\Auth', $auth);
return $this;
}
Test inside of my Test file
public function testActAs() {
$user = 'tests/users/user1.txt';
$this->actingAsApi($user);
$request = new Request;
$t = new TestController($request);
$test = $t->index($request);
}
I expect the actingAsApi function to allow me to also pass in the mock permissions data that corresponds to my mock user object data from the file, but instead it is hitting the database to pull from the permissions table.
EDIT:
So i've been playing around with doing mock objects, and i figured out how to mock the original controller here:
$controlMock = Mockery::mock('App\Http\Controllers\Controller', [$request])->makePartial();
$controlMock->shouldReceive('userHasPermission')
->with('API_ACCESS')
->andReturn(true);
$this->app->instance('App\Http\Controllers\Controller', $controlMock);
but now I can't figure out how to get my call from the other controllers to hit the mocked controller and not a real one. Here is my code for hitting an example controller:
$info = $this->app->make('App\API\Controllers\InfoController');
print_r($info->getInfo('12345'));
How can i make the second block of code hit the mocked controller and not standup a real one like it does in its constructor method?
Finally came on an answer, and it is now fixed. Here's how I did it for those wondering:
$request = new Request;
$controlMock = m::mock('App\API\Controllers\InfoController', [$request])->makePartial();
$controlMock->shouldReceive('userHasPermission')
->with('API_ACCESS')
->andReturn(true);
print_r($controlMock->getInfo('12345'));
Basically, I was trying to Mock the original API controller, and then catch all of the calls thrown at it. Instead, I should've been mocking the controller I'm testing, in this case the InfoController. I can then catch the call 'userHasPermission', which should reach out to the Controller, but I am automatically returning true. This eliminates the need for hitting the database to receive permissions and other info. More information on how I solved it using Mockery can be found here: http://docs.mockery.io/en/latest/cookbook/big_parent_class.html. As you can see, this is referred to as a 'Big Parent Class'. Good luck!
I have an API controller, where I always return JSON. Returned status will always be 200, because for other cases I'm throwing exceptions and handle them globally. I used to return response()->json($content); in each controller's method, but I changed it to form JSON in the middleware. Now, I'm having return response($content); everywhere. I was thinking about simplifying it even more and just return $content; instead. It works, but I am not sure if it's reasonable solution.
Are there any traps behind this idea?
I believe this is a good solution since you are using a middleware. In routes you don't want to return data as JSON you can simply exclude them from running your middleware.
In the future if you want to return your data in another format you can even pass the format as an argument when running the middleware. For example:
Route::get('resource/{id}', ['middleware' => 'format:xml', function ($id) {
//
}]);
Im writing some unit tests and bear with me I am still very new to unit testing.
The issue I am having is a lot of my saves invoke a behaviour that requires
the users id from Yii::app()->user->id.
However when I run the UnitTest I get problems as the user isn't logged in.
Is there anyway I can either ignore the behaviour by a flag (e.g. if ($isInTestingMode)) or log the user in within the testing class?
I would probably build a user object that you use in your tests. And then in the appropriate tests (as part of the setup method, like ernie describes in his comment), swap in the testing user object.
The test user object would then have a method that works like this:
public function getId() {
return 12;
}
public function getIsGuest() {
return false;
}
The above is what they call a 'Fake' object.
In your setup method you'd use the following lines:
Yii::app()->configure(array(
'components' => array(
'user' => array(
'class' => 'path.to.FakeUser',
)
)
));
You can also add that to your test config file if you want that to be the default user (and then swap in the normal CWebUser/WebUser model in tests that need to have a non-logged in user.
Or you could have a flag you set for your FakeUser (isLoggedIn = true/false) in each unit test. I'd probably go with this option myself ...
Tests like these are not unit tests. Unit tests are for testing individual functions in a class. A "logged in user" covers a large swath of code (logging in, session management, navigating to a specific page, etc.). I believe you're looking for functional testing. You can learn more about that here: http://www.yiiframework.com/doc/guide/1.1/en/test.functional