Can i run php unit tests using Laravel Policy in route middleware? - laravel

I am using Laravel policies to control authorisation on my api routes. I want to only allow the current user to update their own post. This works fine when i manually run it through the application, but the unit tests fail. The unit tests, redirect to the login screen.
route:
Route::post('/posts/{post:reference}/editDetails', [PostDetailsApiController::class, 'update'])
->middleware('can:update,post');
policy:
public function update(User $user, Post $post)
{
return $post->user_id === $user->id;
}
unit test:
$this->user = User::factory()->create();
$post = Post::factory()->create(['user_id' => $this->user->id]);
Passport::actingAs($this->user);
$response = $this->call('POST', 'http://test/api/posts/' . $post->reference . '/editDetails');
$response->assertStatus(200);
but this test fails saying 'Failed asserting that 200 is identical to 302'. If i add followingRedirects() and actingAsClient then it passes. If i dd in the controller, it doesnt get fired, so i'm pretty sure the controller isnt getting hit? if i remove the middleware, it runs fine. any advice welcomed. thanks

This has nothing to do with the policy as the status code 302 is a redirect. The problem lies within this snippet.
$this->call('POST', 'http://test/api/posts/' . $post->reference . '/editDetails');
Normally you would call it with a relative URL.
$this->call('POST', 'api/posts/' . $post->reference . '/editDetails');
A better approach is to use the route helper for named routes. I have a example project, where i used this approach.
$this->call('POST', route('posts.edit-details', ['reference' => $post->reference]));
Remember to add a name to your route.
Route::post('/posts/{post:reference}/editDetails', [PostDetailsApiController::class, 'update'])
->middleware('can:update,post')
->name('posts.edit-details');

Related

Find a Controller by the Route in Laravel

I have a route and I need to know a controller that would be used for it.
I know how to find a controller for the current route:
Illuminate\Support\Facades\Route::currentRouteAction();
But how can I do the same for other routes?
Route Facade is the answer. It can return Illuminate\Routing\RouteCollection object.
and then you can get Illuminate\Routing\Route object by route name.
Every Route triggers multiple actions such as middleware and controller methods. So we need only the controller.
use Illuminate\Support\Facades\Route as RouteFacade;
/*#var $route Illuminate\Routing\Route*/
$name = 'admin.reports.my-report.get-filters'; // sample route name
$route = RouteFacade::getRoutes()->getByName($name);
$controllerAction = $route->action['controller'];
$controller = explode('#', $controllerAction)[0];
logger($controller);
P.S.
In cases like that - remember to make a unit test for this functionality to be sure it works as you upgrade your laravel.

Testing Laravel Nova

Currently I'm trying to write feature tests for laravel nova that assert that the page is loaded correctly and data can be seen.
However when I write the tests I can't find a way to assert that the correct text is shown due to way laravel nova's data is produce. Ontop of that I can't seem to test if a page loads correctly with laravel nova's 404 page coming back as a 200 response when a resource page that doesn't exist loads.
Has anyone found a good way to feature test nova?
TL;DR: check out this repo: https://github.com/bradenkeith/testing-nova. It has helped me find my way on how to test Laravel Nova.
Laravel Nova is basically a CRUD framework. So I'm assuming that, when you say
"that the page is loaded correctly and data can be seen"
You actually mean: my resources are loaded correctly. Or, I can create/update/delete a resource. This is because Nova is loading its resource info asynchronous via api calls.
So that's why, a good method to test your logic is to test the /nova-api/ routes.
For example:
<?php
namespace Tests\Feature\Nova;
use App\Note;
use Tests\TestCase;
class NoteTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
}
/** #test */
public function it_gets_a_note()
{
// given
$note = factory(Note::class)->create();
$response = $this->get('/nova-api/notes/' . $note->id)
->assertStatus(200)
->assertJson([
'resource' => [
'id' => [
'value' => $note->id
]
]
]);
}
}
By calling the route https://my-app.test/resources/notes/1, we can assert that we're getting a 200 response (successful) and that we're actually returning our newly created Note. This is a pretty trustworthy test to be sure a resource detail page is working fine.
If, however, you are talking about Browser Testing, you might want to take a look at Laravel Dusk:
Laravel Dusk provides an expressive, easy-to-use browser automation and testing API.
Once installed, you can start real browser testing:
$user = factory(User::class)->create();
$user->assignRole('admin');
$this->browse(function (Browser $browser) use ($user) {
$browser
->loginAs($user)
->visit('/')
->assertSee('Dashboard');
});
I had the same issue, I found out that the gate in App\Providers\NovaServiceProvider.php is not letting users pass, just return true when testing only and everything must work as expected
protected function gate()
{
Gate::define('viewNova', function ($user) {
return true;
});
}
Add on app/config file in your project directory:
App\Providers\NovaServiceProvider::class,

Laravel 5.3 dynamic routing to multiple controllers

I'm using Laravel 5.3. I have a bunch of urls that I'd like to handle with a single route, to multiple controllers.
e.g.
GET /admin/foo => FooController#index
GET /admin/foo/edit/1 => FooController#edit($id)
GET /admin/bar => BarController#index
GET /admin/bar/edit/1 => BarController#item($id)
GET /admin/baz => BazController#index
GET /admin/baz/edit/1 => BazController#item($id)
etc.
I want to be able to detect if the controller exists, and if not throw a 404 or route to a default controller (which may throw a 404).
Below is what I've got so far, but I'm not sure what I'm doing. Shouldn't I be instantiating the controller using the service container? I don't think I should be hardcoding namespaces like this. And my handling of the id parameter is sketchy. Perhaps I should have two routes for these two patterns or something?
Route::get('/admin/{entityType}/{action?}/{id?}', function ($entityType, $action = 'index', $id = null) {
$controllerClass = 'App\Http\Controllers\\' . ucfirst($entityType) . 'Controller';
$controller = new $controllerClass;
$route = app(\Illuminate\Routing\Route::class);
$container = app(\Illuminate\Container\Container::class);
return (new Illuminate\Routing\ControllerDispatcher($container))->dispatch($route, $controller, $action);
abort(404);
});
I'd recommend you to define a route for every controller explicitly. This is the best way to build a maintainable app.
Also, if using one route and one method is an option (with right architecure it is) use one route:
Route::get('/admin/{entityType}/{action?}/{id?}', 'Controller#method');
And one entry point:
public function method($entity, $action = null, $id = null)
{
// Handle request here.
https://laravel.com/docs/5.3/routing#parameters-optional-parameters

Laravel 5 and Socialite - New Redirect After Login

Another newb question here, but hopefully someone can shed some light:
I am using Socialite with Laravel 5, and I want to be able to redirect the user to a page on the site after they have logged in. The problem is that using
return redirect('any-path-I-put-here');
simply redirects back to 'social-site/login?code=afkjadfkjdslkfjdlkfj...' (where 'social-site' is whatever site is being used i.e. facebook, twitter, google, etc.)
So, what appears to me to be happening is that the redirect() function in the Socialite/Contracts/Provider interface is overriding any redirect that I attempt after the fact.
Just for clarification, my routes are set up properly. I have tried every version of 'redirect' you can imagine ('to', 'back', 'intended', Redirect::, etc.), and the method is being called from my Auth Controller (though I have tried it elsewhere as well).
The question is, how do I override that redirect() once I am done storing and logging in the user with socialite? Any help is appreciated! Thank you in advance.
The code that contains the redirect in question is:
public function socialRedirect( $route, $status, $greeting, $user )
{
$this->auth->login( $user, true );
if( $status == 'new_user' ) {
// This is a new member. Make sure they see the welcome modal on redirect
\Session::flash( 'new_registration', true );
return redirect()->to( $route );// This is just the most recent attempt. It originated with return redirect($route);, and has been attempted every other way you can imagine as well (as mentioned above). Hardcoding (i.e., 'home') returns the exact same result. The socialite redirect always overrides anything that is put here.
}
else {
return redirect()->to( $route )->with( [ 'greeting' => $greeting ] );
}
}
... The SocialAuth class that runs before this, however, is about 500 lines long, as it has to determine if the user exists, register new users if necessary, show forms for different scenarios, etc. Meanwhile, here is the function that sends the information through from the Social Auth class:
private function socialLogin( $socialUser, $goto, $provider, $status, $controller )
{
if( is_null( $goto ) ) {
$goto = 'backlot/' . $socialUser->profile->custom_url;
}
if( $status == 'new_user' ) {
return $controller->socialRedirect($goto, $status, null, $socialUser);
}
else {
// This is an existing member. Show them the welcome back status message.
$message = 'You have successfully logged in with your ' .
ucfirst( $provider ) . ' credentials.';
$greeting =
flash()->success( 'Welcome back, ' . $socialUser->username . '. ' . $message );
return $controller->socialRedirect($goto, $status, $greeting, $socialUser);
}
}
I managed to workaround this problem, but I am unsure if this is the best way to fix it. Similar to what is stated in question, I got authenticated callback from the social media, but I was unable to redirect current response to another url.
Based on the callback request params, I was able to create and authenticate the user within my Laravel app. It worked good so far but the problems occured after this step when I tried to do a return redirect()->route('dashboard');. I tried all the flavours of redirect() helper and Redirect facade but nothing helped.
The blank page just stared at my face for over 2 days, before I checked this question. The behaviour was very similar. I got redirect from social-media to my app but could not further redirect in the same response cycle.
At this moment (when the callback was recieved by the app and user was authenticated), if I refreshed the page manually (F5), I got redirected to the intended page. My interpretation is similar to what's stated in this question earlier. The redirect from social-media callback was dominating the redirects I was triggering in my controller (May be redirect within Laravel app got suppressed because the redirect from social-media was still not complete). It's just my interpretation. Experts can throw more light if they think otherwise or have a better explaination.
To fix this I issued a raw http redirect using header("Location /dashboard"); and applied auth middleware to this route. This way I could mock the refresh functionality ,redirect to dashboard (or intended url) and check for authentication in my DashboardController.
Once again, this is not a perfect solution and I am investigating the actual root of the problem, but this might help you to move ahead if you are facing similar problem.
I believe you are overthinking this. Using Socialite is pretty straight forward:
Set up config/services.php. For facebook I have this:
'facebook' => [
'client_id' => 'your_fb_id',
'client_secret' => 'your_fb_secret',
'redirect' => '>ABSOLUTE< url to redirect after login', //like: 'http://stuff'
],
Then set up two routes, one for login and one for callback (after login).
In the login controller method:
return \Socialize::with('facebook')->redirect();
Then in the callback function
$fb_user = \Socialize::with('facebook')->user();
// check if user exists, create it and whatnot
//dd($fb_user);
return redirect()->route('some.route');
It should be pretty much similar for all other providers.
We are using the Socialite login in our UserController as a trait. We simply overrode the AuthenticatesSocialiteLogin::loginSuccess() in our controller.
use Broco\SocialiteLogin\Auth\AuthenticatesSocialiteLogin;
class UserController extends BaseController
{
use AuthenticatesSocialiteLogin;
public function loginSuccess($user)
{
return redirect()->intended(url('/#login-success'));
}
....

Why aren't my controllers actions accessible with unit tests in Laravel

I have set up my route as such:
Route::controller('clients', 'Controllers\ClientsController');
Through this method I can easily access all the controller functions via post and get. However I cannot test them as easily.
public function testCantDeleteOtherAccountsClient()
{
Route::enableFilters();
$user = Models\User::find(1);
$this->be($user);
$response = $this->action('GET', 'ClientsController#getDelete');
$this->assertRedirectedToAction('ClientsController#getIndex');
}
This test results in the message
InvalidArgumentException: Route [ClientsController#getDelete] not defined.
The method accessible via url though. What am I missing?
Just tried this out myself (a route specified via the controller) your issue is that using action requires a named route. Controller routes do not currently support this as far as I'm aware of.
If you create a test route:
Route::get('test', array(
'as' => 'testName',
'uses' => 'ClientsController#getDelete'
));
And try
$this->action('GET', 'testName');
The test should pass, you can view all the routes with names via php artisan routes.
You may want to use $this->client->request() instead. You can check if a redirect occurred with:
$this->assertRedirectedTo("some\url");
Note that $this->call() is just an alias to $this->client->request().
I found changing it to use call instead of action worked for me:
$response = $this->call('GET', 'clients/delete/1');

Resources