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);
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've seached a lot and haven't found a solution for this issue.
When calling my resource controller create method, I'm always getting 403.
Other policies that work: view, update, delete
On the model policy:
public function create(User $user)
{
$manager = app('impersonate');
return ($user->hasRole('Psycologist') || $user->hasRole('Intern')) && !$manager->isImpersonating();
}
On the controller:
public function create()
{
$this->authorize('create', User::class);
return view('personalarea::layouts.areas.employment.jobboard.employee.experience.create');
}
On the AuthServiceProvider:
\\'App\Models\Employment\CandidateExperience' => 'App\Policies\JobCandidateExperiencePolicy',
CandidateExperience::class => CandidateExperiencePolicy::class,
Tried both versions and no difference.
Also made sure registerPolicies as set on boot.
$this->registerPolicies();
I've tried to call die("test") on the policy method and it makes no difference, it seems not be even called.
Any ideas?
i think the problem is in this line:
$this->authorize('create', User::class);
you should not pass the User::class, the user parameter will be injected by laravel not by you, you should pass the model type you want to create:
$this->authorize('create', CandidateExperience::class);
more details in:
https://laravel.com/docs/7.x/authorization#via-the-user-model
In a Laravel Application, the JWT middleware doesn't work properly. I found out, that there is no auth check, when the controller has a __construct method.
class ProjectController extends Controller
{
public $company;
public $user;
public function __construct(Request $request)
{
$this->company = $request->user()->company;
$this->user = $request->user();
}
Api routes:
Route::group(['middleware' => 'jwt.auth'], function () {
Route::resource('/projects', 'Project\\ProjectController');
});
When i comment the __construct method, the system return a 401 as expected. But if the __construct method is not commented, the system returns a 500 because the company can not be found.
Why the __construct method doesn't work with jwt?
This has nothing to do with the jwt middleware, this is intended behavior from laravel, you can read more about it here.
Laravel collects all route specific middlewares first before running
the request through the pipeline, and while collecting the controller
middleware an instance of the controller is created, thus the
constructor is called, however at this point the request isn’t ready
yet.
You can find Taylor's reasoning behind it here:
It’s very bad to use session or auth in your constructor as no request
has happened yet and session and auth are INHERENTLY tied to an HTTP
request. You should receive this request in an actual controller
method which you can call multiple times with multiple different
requests. By forcing your controller to resolve session or auth
information in the constructor you are now forcing your entire
controller to ignore the actual incoming request which can cause
significant problems when testing, etc.
So the solution would be to get the user and company from the request in each controller method, but if you want to keep it in the constructor you could implement the following workaround:
class ProjectController extends Controller
{
public $company;
public $user;
public function __construct(Request $request)
{
$this->middleware(function ($request, $next) {
$this->company = $request->user()->company;
$this->user = $request->user();
return $next($request);
});
}
}
I have the following controller:
class Foo extends Controller
{
public function __construct ()
{
$this->middleware('bar', ['only' => ['store', 'update']]);
}
public function store ()
{
// some code to result $baz object (a model or null)
// pass $baz to Bar middleware for further use
}
}
and the following middleware that should be executed after controller code:
class Bar
{
public function handle($request, Closure $next)
{
$response = $next($request);
// I need to use $baz here
return $response;
}
}
However, I can pass data from a normal middleware to controller using $request->request->add(['baz'=>123]) , but this is not working for 'after' middleware. I think the reason is that $request is not passed by reference in constructor/handle function to be updated for further use, only for consume. I have also tried Session::put() and Session::pull(), but for unclear reasons to me, it won't work (is returning null anytime). To return $baz in Foo.store() method thinking that Bar.handle() method would receive it as result returned, won't work (is returning a Illuminate\Http\RedirectResponse object). I have searched around a lot, but google is returning me only normal(before) middleware case. How can I pass data from Controller to middleware with -after- execution in an elegant way?
I would prefer (if it is possible) a middleware approch for the sake of the question, not an alternative to middleware. Thanks!
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.