How to let a unit test ignore a middleware in Laravel [duplicate] - laravel

This question already has answers here:
How to Disable Selected Middleware in Laravel Tests
(6 answers)
Closed 3 years ago.
I'm testing an endpoint in my Laravel app. However, I have a middleware that does complex logic to determine the location of the user (using ip reverse look up etc such as this code:
public function getOpCityByIP()
{
// Get the client's remote ip address
if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && $_SERVER['HTTP_X_FORWARDED_FOR']) {
$clientIpAddress = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
} else {
$clientIpAddress = $_SERVER['REMOTE_ADDR'];
}
$opCityArray = OpCityIP::get($clientIpAddress);
return $opCityArray;
}
I'm not interested in going inside the guts of such methods in said middleware and having to mock them etc. I'd rather simply skip the entire middleware during the unit testing, or at least mock its entire operations and instead hardcode the result to something predetermined. How do I do that?
I'm using Laravel 5.4
update
i need it to ignore a specific middleware, not all of them

You can use the withoutMiddleware() method on your test object. As of Laravel 5.5, this method accepts a parameter that lets you specify which middleware to disable, instead of just disabling them all.
In your test:
$this->withoutMiddleware([YourGeoIpMiddleware::class]);

To solve my problem of skipping a specific middleware, I simply put this code inside the handle method of said middleware:
public function handle($request, Closure $next)
{
if (config('app.env') === 'testing') {
return $next($request);
}
..

<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
class ExampleTest extends TestCase
{
use WithoutMiddleware;
// Stuff
}

Related

Laravel middleware to check user has access - if not do not proceed with controller method?

Googled and tried for hours now, i need help. Laravel 9.x here.
I use ajax calls which are handled by controllers.
Before the controller handles them, i need to check if the user has indeed access to the administration the user is requesting data for.
The problem is the return false in the middleware class. How can i return a response to the browser without executing any method inside the requested controller class?
The middleware i'm using:
namespace App\Http\Middleware;
use Closure;
class CheckUserAccessToAdministration {
public function handle($request, Closure $next)
{
// Check if the user has access rights to the requested administration
$allowedAdministrations = $request->session()->get('allowed_administrations');
$administration = $request->adm;
if(!in_array($administration, $allowedAdministrations)){
// the idea is that i stop execution of the call, not execute controller method
and just return a message to the browser. i use return false atm, but that isn't right,
i think.
return false;
}
return $next($request);
} }

Feature Test in Laravel 8 uses withHeader to set Host invalid?

Since my project has deployed multiple domain names, the API interface that needs to be tested is using the api.example.test domain name as the entrance.
Using $this->get('/v1/ping') in Feature Test will request to www.example.test, I hope to set up $this->withHeader('Host', config('domain.api_domain')) uniformly in setUp in ApiFeatureBaseTestCase to automatically request API related tests to api.example.test Go in.
However, in practice, I found that this is invalid. By tracing the code, I found two codes that may cause the invalid Host setting:
First place (Laravel):
Code in Laravel Framework src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:503, pass the parameter $uri ='/v1/ping' into $this->prepareUrlForRequest($uri) and get a complete Url with the default Host, the return value is http://www.example.test/v1/ping.
Second place (Symfony):
In the code of Symfony HttpFoundation Component Request.php:355, the parsing in $uri will be used first The coming out host is overwritten in the Header as the default Host.
The above two codes eventually caused the Host I set by withHeader to fail. Obviously, in this code logic, Symfony HttpFoundation Component's choice of Host in conflict cannot be considered wrong, but I submitted this question issue was closed when it was given to Laravel Framework.
I don't know if this issue is a bug or feature?
Finally, I am sorry that my question has interrupted everyone's time, but if there is a conclusion on this question, please tell me what should be more appropriate?
My current approach is $this->get($this->api_base_url . '/v1/ping'), but I don’t think this is elegant
3Q!!1
Code example
// File: config/domain.php
return [
'api_domain' => 'api.example.test',
'web_domain' => 'www.example.test',
];
// File: routes/demo.php
Route::domain(config('domain.api_domain'))
->middleware(['auth:api', 'api.sign.check'])
->namespace($this->namespace)
->group(function () {
Route::get('/v1/ping', function () {
return 'This Api v1';
});
});
Route::domain(config('domain.web_domain'))
->middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
// File: tests/ApiFeatureBaseTestCase.php
namespace Tests;
class ApiFeatureBaseTestCase extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->withHeader('Host', config('domain.api_domain'));
}
}
// File: tests/Feature/ApiPingTest.php
namespace Tests\Feature;
use Tests\ApiFeatureBaseTestCase;
class ApiPingTest extends ApiFeatureBaseTestCase
{
public function testPing()
{
$this->get('/v1/ping');
}
}
Can you create a wrapper method on your ApiFeatureBaseTestCase class?
public function get($uri, array $headers = [])
{
return parent::get(config('domain.api_domain') . $uri, $headers);
}
Then in your ApiPingTest class:
public function testPing()
{
$this->get('/v1/ping');
}

Is Laravel introducing new programming syntax?

What is this Closure $next. Ok I am guessing an object but how a object reference accepts an argument $request? Shouldn’t it call method. Closure in php are anonymous function but is it that the next is an anonymous function variable defined else where? I see the handle method is in Class Frameguard but the above example didn’t even instantiate Frameguard Class. Am I correct to say Laravel does not follow oop syntax?
public function handle($request, Closure $next)
{
if ($request->age <= 200) {
return redirect('home');
}
return $next($request);
}
Now, it does not. Laravel is just framework so it allows you to complete some every-day actions in easier and faster way.
Obviously Laravel follows OOP and there is no something like "OOP syntax" as far as I know. But there are parts in Laravel and every other framework that are not so obvious.
Basically in middleware you have access to Request and closure. You can read about middleware here. If you want to dig in dipper you can look at \Illuminate\Pipeline\Pipeline class - there is carry method in there that is responsible for looping over each used middleware.

Why won't this Laravel 5.4 service provider register?

I am trying to do a hello world service provider with the new Laravel 5.4.
I have created the following service provider file:
//File: app/TestProvider/TestServiceProvider.php
namespace App\TestProvider;
use Illuminate\Support\ServiceProvider;
class TestServiceProvider extends ServiceProvider
{
/**
* Register bindings in the container.
*
* #return void
*/
public function register()
{
$this->app->bind('Test', function ($app) {
return new Test();
});
}
}
I have created a simple class under the same namespace:
//File: app/TestProvider/Test.php
namespace App\TestProvider;
class Test
{
/**
* Register bindings in the container.
*
* #return void
*/
public function helloWorld()
{
echo "hello world";
}
}
The problem is, this is not registering. The register method is executing as when I put a breaker before the 'bind' method, it executes:
public function register()
{
dd("BREAKER");
$this->app->bind('Test', function ($app) {
return new Test();
});
}
So this outputs "BREAKER" as expected. However if I put the breaker in the closure, nothing happens which suggests for some reason, that 'bind' method isn't being executed??
Any ideas?
EDIT:
Just some further info: I know that the Test class is registered and in the correct namespace as I can do:
dd(new Test());
in the registration method, and it outputs the resource id as expected.
Explanation
The closure provided only runs when the binding is being resolved. That's why it's a closure, it can be saved in the service container and resolved at any time while the program runs.
Solution
To see the resolved binding, create a controller and resolve the class in that controller:
// File: app/Http/Controllers/TestController.php
namespace App\Http\Controllers;
// This isn't the best way, but it works. See the best way below
class TestController extends Controller {
public function index()
{
return \App::make('Test')->helloWorld();
}
}
Of course, don't forget to register the route:
// File: routes/web.php
Route::get('/', 'TestController#index');
The binding will resolve when you hit the homepage.
However, as I said, it's not the best way, so here I prepared a better way. Change the way you register the binding:
// File: app/Providers/TestProvider.php
namespace App\TestProvider;
use Illuminate\Support\ServiceProvider;
use App\TestProvider\Test;
// Better way
class TestServiceProvider extends ServiceProvider
{
/**
* Register bindings in the container.
*
* #return void
*/
public function register()
{
// Note: we bind the exact complete class name!
$this->app->bind(Test::class, function ($app) {
return new Test();
});
}
}
After this change the controller so that it looks like this:
namespace App\Http\Controllers;
use App\TestProvider\Test;
class TestController extends Controller {
/**
* #var Test $test
*/
private $test;
// Let Laravel resolve the dependency on constructing the class
public function __construct(Test $test)
{
$this->test = $test;
}
public function index()
{
return $this->test->helloWorld();
}
}
You will see that the exact same thing happens, but it looks more elegant and avoids conflicts.
Details
Laravel gives only a high level overview of the service container, which doesn't help to learn how it works on the inside. The best way to see that is to go down the call stack.
When you do that, you find that Laravel registers every class in the project in the service container. That means that whether you create a service provider or not, the class will be in the container. How exactly?
When you run php artisan optimize, Laravel creates files that have array with all the classes of the project. When you run the app, after registering everything from the service providers, Laravel registers the rest of the classes from that file.
That means that in your case, if you don't specifically register the Test class, it will still be resolvable. Basically, you only need to register classes that need some specific instructions to be resolved.
So how does Laravel resolve the dependencies?
When you run \App::make(Test::class) or inject dependency via type hinting in the constructor (the "better way" from my solution), Laravel looks for that dependency among the bindings.
When it finds the dependency, it resolves either the closure associated to it or the constructor of the class directly.
When it resolves the constructor directly, it looks for type hints among the constructor parameters and recursively resolves all of them until there's nothing else to resolve.
After that it returns the resolved class.
Of course, bear in mind that for Laravel to analyze the contructor of a class, it needs to be resolved via the service container in the first place. You can't just call $test = new Test(); and expect Laravel to do all the magic :)
Conclusion
This is a rather quick overview of Laravel's service container. The best way for you to learn it is, of course, studying the sources for yourself. It's truly elegant and it uses PHP's functionality to the fullest.
I really hope this shed some light on the service container for you and can help you in the future :)
The closure passed to the bind() method is not executed until you actually attempt to resolve the alias you are binding.
So, if you dd('breaker') inside the closure, this won't actually get executed until Test is resolved (whatever your preferred resolution method is):
Service provider:
// bind the closure to the 'Test' alias
public function register()
{
$this->app->bind('Test', function ($app) {
dd("BREAKER");
return new Test();
});
}
Code that resolve Test alias:
// different ways of resolving the alias out of the container.
// any of these will execute the bound closure.
$test = resolve('Test');
$test = app('Test');
$test = app()->make('Test');
$test = \App::make('Test');
try:
$this->app->bind(Test::class, function ($app) {
return new Test();
});

Laravel 5.3 SubstituteBindings middleware with withoutMiddleware issue

Since Laravel 5.3, the route implicit binding works as middleware called SubstituteBindings. I used to work with Laravel 5.2 and upgraded to 5.3.
I have some custom middlewares in my application and in my tests I need to disable them. So, until now I used $this->withoutMiddleware() in the test methods. But since the update to Laravel 5.3, withoutMiddleware stops the route implicit binding, and all my tests fails.
I don't know if this should be considered as bug, but it is a huge problem for me.
Is there any way to set the SubstituteBindings middleware as mandatory middleware? How can I still use implicit binding and test my tests without other middlewares?
Building on my comment above I had a look at registering a custom router which always adds SubstituteBindings to the list of middleware if middleware was disabled. You can achieve it by registering a custom RoutingServiceProvider and registering your own Router class. Unfortunately since the route is created fairly early on in the app bootstrap process you also need to create a custom App class and use that in bootstrap/app.php too.
RoutingServiceProvider
<?php namespace App\Extensions\Providers;
use Illuminate\Routing\RoutingServiceProvider as IlluminateRoutingServiceProvider;
use App\Extensions\ExtendedRouter;
class RoutingServiceProvider extends IlluminateRoutingServiceProvider
{
protected function registerRouter()
{
$this->app['router'] = $this->app->share(function ($app) {
return new ExtendedRouter($app['events'], $app);
});
}
}
Custom router
This adds the middleware, it just extends the default router but overrides the runRouteWithinStack method and, instead of returning an empty array if $this->container->make('middleware.disable') is true, it returns an array containing the SubstituteBindings class.
<?php namespace App\Extensions;
use Illuminate\Routing\Router;
use Illuminate\Routing\Route;
use Illuminate\Routing\Pipeline;
use Illuminate\Http\Request;
class ExtendedRouter extends Router {
protected function runRouteWithinStack(Route $route, Request $request)
{
$shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
$this->container->make('middleware.disable') === true;
// Make sure SubstituteBindings is always used as middleware
$middleware = $shouldSkipMiddleware ? [
\Illuminate\Routing\Middleware\SubstituteBindings::class
] : $this->gatherRouteMiddleware($route);
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(function ($request) use ($route) {
return $this->prepareResponse(
$request, $route->run($request)
);
});
}
}
Custom App Class
<?php namespace App;
use App\Extensions\Providers\RoutingServiceProvider;
class MyCustomApp extends Application
{
protected function registerBaseServiceProviders()
{
parent::registerBaseServiceProviders();
$this->register(new RoutingServiceProvider($this));
}
Using the custom app class
In bootstrap/app.php change the line where the app is instantiated to:
$app = new App\MyCustomApp(
realpath(__DIR__.'/../')
);
--
Warning! I haven't fully tested this, my app loads and my tests pass but there could be issues that I haven't discovered. It's also quite brittle since if the Laravel base Router class changes you might find things break randomly on future upgrades.
--
You might also want to refactor this so the list of middleware in the custom router always contains the SubstituteBindings class so there isn't so much of a difference in behaviour if middleware is disabled.

Resources