I am trying to make a helper class for my codeception functional tests that makes it quicker to specify logging in as a super user. Here is my helper:
class Laravel5Helper extends Laravel5 {
public function amSuperAdmin() {
$this->amLoggedAs(['username' => 'super', 'password'=> 'password']);
}
}
But this doesn't seem to work. When I use the declaration $this->amLoggedAs(['username' => 'super', 'password'=> 'password']); in a normal test it logs in the user as expected, but when I use $I->amSuperAdmin() it seems to fail to do so. There are no errors; the method is visible in my generated FunctionalTester class. When I trace the code I can see the authentication passing in the Laravel5::amLoggedAs method, but the test gets redirected to my app's login page as though authentication failed.
What am I doing wrong?
Your helper is a different object than the Laravel5 module,
you should get the instance of Laravel5 module and call its method:
class Laravel5Helper extends \Codeception\Module {
public function amSuperAdmin() {
$this->getModule('Laravel5')->amLoggedAs(['username' => 'super', 'password'=> 'password']);
}
}
Related
I am trying to create a test for a feature I've written.
The logic is quite simple:
From the api.php I am calling the store method:
Route::group(['prefix' => '/study/{study}/bookmark_list'], function () {
...
Route::post('/{bookmarkList}/bookmark', 'BookmarkController#store');
...
});
thus I am injecting the study and the bookmark list.
My controller passes down the parameters
public function store(Study $study, BookmarkList $bookmarkList)
{
return $this->serve(CreateBookmarkFeature::class);
}
And I am using them in the Feature accordingly
'bookmark_list_id' => $request->bookmarkList->id,
class CreateBookmarkFeature extends Feature
{
public function handle(CreateBookmarkRequest $request)
{
//Call the appropriate job
$bookmark = $this->run(CreateBookmarkJob::class, [
'bookmark_list_id' => $request->bookmarkList->id,
'item_id' => $request->input('item_id'),
'type' => $request->input('type'),
'latest_update' => $request->input('latest_update'),
'notes' => $request->input('notes')
]);
//Return
return $this->run(RespondWithJsonJob::class, [
'data' => [
'bookmark' => $bookmark
]
]);
}
}
I am also using a custom request (CreateBookmarkRequest) which practically verifies if the user is authorised and imposes some rules on the input.
class CreateBookmarkRequest extends JsonRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return $this->getAuthorizedUser()->canAccessStudy($this->study->id);
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
"item_id" => ["integer", "required"],
"type" => [Rule::in(BookmarkType::getValues()), "required"],
"latest_update" => ['date_format:Y-m-d H:i:s', 'nullable'],
"text" => ["string", "nullable"]
];
}
}
Now, here comes the problem. I want to write a test for the feature that tests that the correct response is being returned (it would be good to verify the CreateBookmarkJob is called but not that important). The problem is that although I can mock the request, along with the input() method, I cannot mock the injected bookmarkList.
The rest of the functions are mocked properly and work as expected.
My test:
class CreateBookmarkFeatureTest extends TestCase
{
use WithoutMiddleware;
use DatabaseMigrations;
public function setUp(): void
{
parent::setUp();
// seed the database
$this->seed();
}
public function test_createbookmarkfeature()
{
//GIVEN
$mockRequest = $this->mock(CreateBookmarkRequest::class);
$mockRequest->shouldReceive('authorize')->once()->andReturnTrue();
$mockRequest->shouldReceive('rules')->once()->andReturnTrue();
$mockRequest->shouldReceive('input')->once()->with('item_id')->andReturn(1);
$mockRequest->shouldReceive('input')->once()->with('type')->andReturn("ADVOCATE");
$mockRequest->shouldReceive('input')->once()->with('latest_update')->andReturn(Carbon::now());
$mockRequest->shouldReceive('input')->once()->with('notes')->andReturn("acs");
$mockRequest->shouldReceive('bookmark_list->id')->once()->andReturn(1);
//WHEN
$response = $this->postJson('/api/recruitment_toolkit/study/1/bookmark_list/1/bookmark', [
"type"=> "ADVOCATE",
"item_id"=> "12",
"text"=> "My first bookmark"
]);
//THEN
$this->assertEquals("foo", $response['data'], "das");
}
One potential solution that I though would be to not mock the request, but this way I cannot find a way to mock the "returnAuthorisedUser" in the request.
Any ideas on how to mock the injected model would be appreciated, or otherwise any idea on how to properly test the feature in case I am approaching it wrong.
It is worth mentioning that I have separate unit tests for each of the jobs (CreateBookmarkJob and RespondWithJSONJob).
Thanks in advance
A feature test, by definition, will be imitating an end-user action. There's no need to mock the request class, you just make the request as a user would.
Assuming a Study with ID 1 and a BookmarkList with ID 1 have been created by your seeder, Laravel will inject appropriate dependencies via route model binding. If not, you should use a factory method to create models and then substitute the appropriate ID in the URL.
<?php
namespace Tests\Feature;
use Tests\TestCase;
class CreateBookmarkFeatureTest extends TestCase
{
use WithoutMiddleware;
use DatabaseMigrations;
public function setUp(): void
{
parent::setUp();
$this->seed();
}
public function TestCreateBookmarkFeature()
{
$url = '/api/recruitment_toolkit/study/1/bookmark_list/1/bookmark';
$data = [
"type"=> "ADVOCATE",
"item_id"=> "12",
"text"=> "My first bookmark"
];
$this->postJson($url, $data)
->assertStatus(200)
->assertJsonPath("some.path", "some expected value");
}
}
I agree with #miken32's response - that a feature should indeed imitate a user interaction - however the dependency injection via route model binding still did not work.
After spending some hours on it, I realised that the reason for it is that
use WithoutMiddleware;
disables all middleware, even the one responsible for route model binding, therefore the object models were not injected in the request.
The actual solution for this is that (for laravel >=7) we can define the middleware we want to disable, in this case:
$this->withoutMiddleware(\App\Http\Middleware\Authenticate::class);
Then we just use
$user = User::where('id',1)->first(); $this->actingAs($user);
And everything else works as expected.
DISCLAIMER: I am not implying that miken32's response was incorrect; it was definitely in the right direction - just adding this as a small detail.
I am trying to test a controller in Laravel that uses another class as a helper which calls an API and returns the result.
To avoid external API calls, I need to mock this helper.
I tried to mock the class inside the controller and run the test but I didn't get what I expected in mock class.
this is my controller method:
public function A(Request $request){
$helper = new TheHelper();
$result = $helper->getResult($request->email);
if($result){
return response()->json([
'success' => true,
'message' => "result found",
], 200);
}else{
return response()->json([
'success' => false,
'message' => "no result",
], 500);
}
}
My helper method simply calls an API and returns the result.
class TheHelper
{
public function getResult($email){
// some api calls
return $result;
}
}
Here is my test:
public function testExample()
{
$helperMock = Mockery::mock(TheHelper::class);
// Set expectations
$helperMock ->shouldReceive('getResult')
->once()
->with('testemail#test.com')
->andReturn([
'id' => '100'
]);
$this->app->instance(TheHelper::class, $helperMock);
$this->json(
'POST',
'/api/test_method',
['email' => 'testemail#test.com'])
->assertStatus(200);
}
My mock function never called. it only checks with the real API inside TheHelper method
Your test is creating a mock object and binding that mock object into the Laravel service container. However, your controller is not pulling a TheHelper instance from the Laravel service container; it is manually instantiating it with the new keyword. Using the new keyword is core PHP, and does not involve Laravel at all.
Your test is showing you an issue in your code. TheHelper is a dependency of your method, and should therefore be passed into the method instead of being created inside the method.
You either need to update your controller method to use dependency injection, so that Laravel can automatically resolve the TheHelper dependency from its container, or you need to replace your new keyword with a call into the Laravel container.
Using dependency injection:
public function A(Request $request, TheHelper $helper)
{
$result = $helper->getResult($request->email);
// rest of function...
}
Manually pull from the container:
public function A(Request $request)
{
$helper = app(TheHelper::class);
$result = $helper->getResult($request->email);
// rest of function...
}
Type hinted route parameter does not instantiate when called from a test.
I have a Laravel API Resource Route::apiResource('users', 'Api\UserController');
Here's my update method in the controller:
public function update(UpdateUserRequest $request, User $user)
{
//
}
Inside the UpdateUserRequest:
public function rules()
{
dd($this->route("user"));
}
If I call this endpoint from Postman, I get the full user object back. However, if I call it from a test:
$response = $this->actingAs($this->user)->
json('POST', '/api/users/'.$this->user->id, [
'_method' => 'PUT',
'data' => [
// ...
]
]);
I just get the string "1", not the instantiated User object.
This is probably caused by the \Illuminate\Foundation\Testing\WithoutMiddleware trait being used by your test case.
For posterity, should anyone come across this, route model binding is performed by the \Illuminate\Routing\MiddlewareSubstituteBindings middleware. The WithoutMiddleware trait therefore prevents it from running.
The base Laravel test case provides an undocumented withoutMiddleware() method via /Illuminate/Foundation/Testing/WithoutMiddleware which you can use to get around this, however it may be worth noting that the lead developer of Laravel, Taylor Otwell, recommends testing with all middleware active when possible.
Well, one thing that worked, and I don't know if this is the correct or the "Laravel" way of doing things is to force instantiate the model in the custom request constructor, and to bind the instance inside the test:
In the UpdateUserRequest:
private $user;
public function __construct(User $user)
{
$this->user = $user;
}
In the Test:
$this->user = factory(\App\Models\User::class)->create();
$this->app->instance(\App\Models\User::class, $this->user);
In my Laravel application, i have several policies working, but one will not work.
Controller
public function store(Project $project, CreateActionRequest $request)
{
$this->authorize('store', $project);
Action::create([
'name' => $request->name,
]);
return redirect()->route('projects.show', $project->id)->withSuccess('Massnahme erfolgreich gespeichert');
}
Policy
namespace App\Policies\Project;
use App\Models\Project\Project;
use App\Models\User;
use App\Models\Project\Action;
use Illuminate\Auth\Access\HandlesAuthorization;
class ActionPolicy
{
use HandlesAuthorization;
public function store(User $user, Project $project)
{
return $user->company_id === $project->company_id;
}
}
AuthServiceProvider
protected $policies = [
'App\Models\User' => 'App\Policies\CompanyAdmin\UserPolicy',
'App\Models\Company' => 'App\Policies\CompanyAdmin\CompanyPolicy',
'App\Models\Team' => 'App\Policies\CompanyAdmin\TeamPolicy',
'App\Models\Department' => 'App\Policies\CompanyAdmin\DepartmentPolicy',
'App\Models\Location' => 'App\Policies\CompanyAdmin\LocationPolicy',
'App\Models\Division' => 'App\Policies\CompanyAdmin\DivisionPolicy',
'App\Models\Costcenter' => 'App\Policies\CompanyAdmin\CostcenterPolicy',
'App\Models\Workplace' => 'App\Policies\CompanyAdmin\WorkplacePolicy',
'App\Models\Product' => 'App\Policies\CompanyAdmin\ProductPolicy',
'App\Models\Project\Action' => 'App\Policies\Project\ActionPolicy',
'App\Models\Project\Project' => 'App\Policies\Project\ProjectPolicy',
];
CreateActionRequest
namespace App\Http\Requests\Project;
use Illuminate\Foundation\Http\FormRequest;
class CreateActionRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => 'required|min:3',
];
}
}
All policies are working except ActionPolicy and ProjectPolicy.
I added in the policy a __construct() method to check if the policy is called. But ActionPolicy and ProjectPolicy are not working.
How can i search the error? I tried with dd() but i got only allways the message: This action is unauthorized
Since you are injecting CreateActionRequest instead of Request that means you are defining your own set of rules to authorize the FormRequest which comes inside of your method. Further it means that you gotta define a few rules which the "FormRequest" has to pass in order to EVEN reach your controller, this is a nice concept that I like about Laravel since the code is not centralized, but rather spread and every layer has it's own responsibility. Now, you don't have to call any method from your CreateActionRequest nor you have to write any code regarding that class in your controller, because Laravel runs authorize method by default before allowing the Request to reach your controller, before running authorizemethod in your CreateActionRequest it runs rules method which verifies that all the given fields pass the expressions you assigned them, so the execution is something like this CreateActionRequest => rules => authorize => IF(authorized) Controller ELSE Not authorized, hope that makes sense. In order to fix your code:
1.) Remove $this->authorize('store', $project);
This will allow you to pass not authorized error in case your name passes the truth test inside of rules method inside of your CreateActionRequest. If you wish to utilize your Action Policy you will need to hook up your custom Request(CreateActionRequest) with it and this is how:
public function authorize()
{
$store = $this->route('project');
//The above line will return Project object if your mapping is correct
//If it's not it will return the value you passed to your route for {project}
return $this->user() && $this->user()->can('store', $store);
}
EDIT:
Here is the link where you can see how to properly authorize and connect policy with CreateActionRequest
Do you have all your controller methods defined with the Request object last?
public function store(Project $project, CreateActionRequest $request)
The Request object should be the first parameter in the methods signature:
public function store(CreateActionRequest $request, Project $project)
Dependency Injection & Route Parameters
If your controller method is also expecting input from a route parameter you should list your route parameters after your other dependencies.
Most Laravel authorization mechanisms have identical method signatures allowing them to work across varying classes.
I have the following test file in my Laravel application:
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class ApiAuthControllerTest extends TestCase{
use DatabaseTransactions;
public function testLogin(){
// Test login success
$response = $this->json('POST', '/login', array(
'email' => 'hello#yahoo.com',
'password' => 'sometext'
))->decodeResponseJson();
return $response['token'];
}
/**
* #depends testLogin
*/
public function testLogout($token){
// Test logout success
$this->json('DELETE', '/logout', array(
'token' => $token
))->assertReponseStatus(200);
}
}
I am using the DatabaseTransactions class to wrap my tests as transactions so they don't write to my database. I noticed that using this class will wrap every individual test within my class as a transaction.
I would like to wrap the entire class as a transaction. In my example above, I need for the token that was generated from my login request to be persistent in the database as I test the logout request.
How would I do this with Laravel?
Unfortunately, I don't believe this is possible. Laravel refreshes the app instance on setUp/tearDown. In PHPUnit, these functions are run every test method. Thus, using transactions means there will be no persistence between test methods.
You can, however, generate the token again in your testLogout test. Since your logout test relies on a token to be present, there is nothing inherently wrong with that approach.