Laravel by default disables VerifyCsrfToken middleware if running tests. As a result I didn't notice that api routes need csrf token to succeed. Is there a way to enable it back for some tests? Like
function withVerifyCsrfTokenMiddleware()
{
$this->app... // here we're passing something to the app
}
And make amendments in VerifyCsrfToken's handle method. In a custom one. The one, that overrides the method from the framework. But that's just my idea. Any better ones?
Okay, here's what I did:
app/Http/Middleware/VerifyCsrfToken.php:
function handle($request, Closure $next)
{
$dontSkipCsrfVerificationWhenRunningTests
= $this->app->bound('dontSkipCsrfVerificationWhenRunningTests')
&& $this->app->make('dontSkipCsrfVerificationWhenRunningTests');
if (
$this->isReading($request) ||
! $dontSkipCsrfVerificationWhenRunningTests && $this->runningUnitTests() ||
$this->shouldPassThrough($request) ||
$this->tokensMatch($request)
) {
return $this->addCookieToResponse($request, $next($request));
}
throw new TokenMismatchException;
}
tests/TestCase.php:
function withVerifyCsrfTokenMiddleware()
{
$this->app->instance('dontSkipCsrfVerificationWhenRunningTests', TRUE);
}
small change to the answer above, rather than changing the handle method I updated the runningUnitTests method to reduce the chance of an issue on upgrade:
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* #var array
*/
protected $except = [
];
/**
* Determine if the application is running unit tests.
*
* #return bool
*/
protected function runningUnitTests()
{
$dontSkip
= $this->app->bound('dontSkipCsrfVerificationWhenRunningTests')
&& $this->app->make('dontSkipCsrfVerificationWhenRunningTests');
return $this->app->runningInConsole() && $this->app->runningUnitTests() && !$dontSkip;
}
}
Related
I'm trying to get settings from the database and put them in the config,
my function need the user id so it can bring his settings only,
in the service provider ( boot function ) there is no authentication yet, can you please advise me to the right place to run my function, please note that I need it to run before the view get rendered because there are settings for the layout inside it, this is my function :
// public static becouse it's inside Class//
public static function getAppSettings(){
if (!config('settings') && Auth::check()) {
$user_id = Auth::user()->id;
$settings = AppSettings::where('user_id', $user_id)->get()->all();
$settings = Cache::remember('settings', 60, function () use ($settings) {
// Laravel >= 5.2, use 'lists' instead of 'pluck' for Laravel <= 5.1
return $settings->pluck('value', 'key')->all();
});
config()->set('settings', $settings);
}else{
// this is for testing//
dd('no');
}
}
without the auth, it can work inside the service provider ( boot function ) but it will bring all settings for all the users.
You can create middleware for this.Middleware calls after routes and before controller
php artisan make:middleware Settings
This will create below class
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class Settings
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle(Request $request, Closure $next)
{
// exicute your logic here
return $next($request);
}
}
You can call your method inside handle and before next
You can read more about this in
https://laravel.com/docs/8.x/middleware
In a right pickle with phpunit. I currently have a test class for a resource route with 9 tests in it. All but two of these tests pass, ironically what should be the two simplest; the tests for articles.index and articles.show (the last 2 tests in the code below).
<?php
namespace Tests\Feature;
use App\Article;
use Tests\TestCase;
use App\User;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use DB;
class ArticleTest extends TestCase
{
use RefreshDatabase;
// runs before any of the tests
protected function setUp(): void
{
parent::setUp();
// run tests without language(locale) middleware
$this->withoutMiddleware(\App\Http\Middleware\Language::class);
}
/** #test */
public function unauthenticated_users_can_not_access_create()
{
$this->get('/articles/create')->assertRedirect('/login');
}
/** #test */
public function admin_users_can_access_edit()
{
$article = factory(Article::class)->create();
$record = DB::table('articles')->where('id', $article->id)->first();
$user = factory(User::class)->create();
$user->isAdmin = 1;
$this->actingAs($user);
$this->get('/articles/' . $record->slug . '/edit?locale=en')->assertOK();
}
/** #test */
public function authenticated_but_not_admin_users_can_not_access_create()
{
$this->actingAs(factory(User::class)->create());
$this->get('/articles/create')->assertRedirect('home');
}
/** #test */
public function admin_can_access_create()
{
$user = factory(User::class)->create();
$user->isAdmin = 1;
$this->actingAs($user);
$this->get('/articles/create')->assertOk();
}
/** #test */
public function can_store_an_article()
{
$article = factory(Article::class)->create();
$record = DB::table('articles')->where('id', $article->id)->first();
$this->assertDatabaseHas('articles', ['slug' => $record->slug]);
}
/** #test */
public function admin_can_access_articles_admin_index()
{
$user = factory(User::class)->create();
$user->isAdmin = 1;
$this->actingAs($user);
$this->get('/admin/articles')->assertOk();
}
/** #test */
public function admin_can_delete_article_and_it_is_removed_from_the_database()
{
$user = factory(User::class)->create();
$user->isAdmin = 1;
$this->actingAs($user);
$article = factory(Article::class)->create();
$this->assertDatabaseHas('articles', ['slug' => $article->slug]);
$record = DB::table('articles')->where('id', $article->id)->delete();
$this->assertDatabaseMissing('articles', ['slug' => $article->slug]);
}
/** #test */
public function can_see_article_index_page()
{
$this->get('/articles')->assertOK();
}
/** #test */
public function can_only_access_articles_made_visible()
{
$article = factory(Article::class)->create();
$article->displayStatus = 2;
$this->assertDatabaseHas('articles', ['slug' => $article->slug]);
$this->get('/articles/' . $article->slug)->assertRedirect('/articles');
}
}
The test can_see_article_index_page should return a 200 status code yet it 404's and can_only_access_articles_made_visible should be a redirect status code yet 404's as well.
SEEN HERE
My application is multi-lingual, using the spatie/translatable package and I'm unsure if it's that that is interfering (doesn't really make sense for it to be as the withoutMiddleware line in setUp should prevent this) or something else entirely (i.e my stupidity). My app is built with Laravel 5.8, and I am running phpunit 7.5.
EDIT I found out the error was due to some specific rows not existing in my test database, where in my controller it would fail if it couldn't find them. By adding the line $this->withoutExceptionHandling() to the failing tests it told me this information.
Since all your routes are working but /articles that means that something is not ok with your routes.
You added this route lately: Every time you create a new route or make any changes in the route files of laravel you should run php artisan config:cache command.
I am pretty sure it's not a conflict in routes because of a variable {}, since the status code is a clear 404 meaning that no controller is even accessed that's why i won't elaborate further on possible routing conflicting issues.
If the error persists though get your route at the very top of your routes file and see if it works there. If that's the case that means that one of your routes, overlaps it.
A quick fix to that is to order your static naming routes above the routes using variables for example:
/articles
/{articles}
If you reverse above order your /articles route will never be accessed. This is a case of naming route conflicts.
I wrote a localization middleware in Laravel using the LaravelGettext package which looks like this:
<?php
namespace App\Http\Middleware;
use Closure;
class Locale {
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next) {
if ($request->method() === 'GET') {
$segment = $request->segment(1);
if (!in_array($segment, config('laravel-gettext.supported-locales'))) {
$segments = $request->segments();
$fallback = session('locale') ?: config('laravel-gettext.fallback-locale');
$segments = array_prepend($segments, $fallback);
return redirect()->to(implode('/', $segments));
}
session(['locale' => $segment]);
LaravelGettext::setLocale($segment);
}
return $next($request);
}
}
I am routing into the middleware via:
Route::prefix('{lang?}')->middleware('locale')->group(function () {
...
}
Running through the middleware gives me this error though:
"Class 'App\Http\Middleware\LaravelGettext' not found"
So I figured I might have to import the LaravelGettext package manually by adding:
use Xinax\LaravelGettext\LaravelGettext;
Which now gives me this Exception:
"Non-static method Xinax\LaravelGettext\LaravelGettext::setLocale() should not be called statically"
Which makes me wonder: Is there even a valid option to access the package inside a middleware? Or did I drive into a design flaw here?
Well, it just came to me that I had to import the Facade, not the actual class itself. So adding
use Xinax\LaravelGettext\Facades\LaravelGettext;
made it finally work!
On my app, I'm trying to make it so that if a user has a certain condition, he will ALWAYS be redirected to a certain page, no matter which route he tries to access. In this case, it's if he doesn't have a username (long story).
ComposerServiceProvider.php :
public function boot() {
View::composer('templates.default', function ($view) {
if(Auth::user()) {
if (Auth::user()->username == null || Auth::user()->username == "") {
return redirect()->route('auth.chooseUsername');
}
So I figured the place to do this would be
ComposerServiceProvider.php.
However, I'm noticing that my redirect don't work in ComposerServiceProvider.php. And laravel.log doesn't give me an error or reason why.
The if condition is being met. If I replace return redirect()->route('auth.chooseUsername'); with dd('test');, sure enough all my pages return 'test'.
Why is this happening?
Try this steps:
You can use middleware for this scenario like below:
Create middleware php artisan make:middleware CheckPoint
Inside App\Http\Middleware\CheckPoint.php File
use Closure;
class CheckPoint
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if($request->user()) {
if ($request->user()->username == null || $request->user()->username == "") {
return redirect()->route('auth.chooseUsername');
}
}
return $next($request);
}
}
2. Add the middleware inside the app\Http\kernel.php
protected $routeMiddleware = [
'checkPoint' => \App\Http\Middleware\CheckPoint::class,
];
Then you can use it inside your route file and controller like below
Route::get(...)->middleware('checkPoint');
Route::middleware('checkPoint')->group(function() {
//Group of routes
.....
});
More About Middleware
controller middleware
In App\Http\Middleware create a new middleware:
<?php
namespace App\Http\Middleware;
use Closure;
class CheckYourCondition
{
public function handle($request, Closure $next)
{
if (! $request->user()->yourCondition()) {
return redirect('your_target_routre');
}
return $next($request);
}
}
Register your middleware by adding it to protected $routeMiddleware in App\Http\Kernel.
and assing it to the 'web' middleware group in protected $middlewareGroups.
For details see
The ComposerServiceProvider has a different purpose. It is used to register View Composers.
View composers are callbacks or class methods that are called when a
view is rendered. If you have data that you want to be bound to a view
each time that view is rendered, a view composer can help you organize
that logic into a single location.
See View Composers.
It is common that errors from Authentication and CSRF arise when running phpunit.
In the TestCase we use:
use WithoutMiddleware;
The problem is when forms fail, it usually comes back with a Flash Message and Old Input. We have disabled all middleware so we have no access to Input::old('username'); or the flash message.
Furthermore our tests of this failed form post returns:
Caused by
exception 'RuntimeException' with message 'Session store not set on request.
Is there a way to enable the Session Middleware and disable everything else.
The best way I have found to do this isn't by using the WithoutMiddleware trait but by modifying the middleware you want to disable. For example, if you want to disable the VerifyCsrfToken middleware functionality in your tests you can do the following.
Inside app/Http/Middleware/VerifyCsrfToken.php, add a handle method that checks the APP_ENV for testing.
public function handle($request, Closure $next)
{
if (env('APP_ENV') === 'testing') {
return $next($request);
}
return parent::handle($request, $next);
}
This will override the handle method inside of Illuminate\Foundation\Http\Middleware\VerifyCsrfToken, disabling the functionality entirely.
Laravel >= 5.5
As of Laravel 5.5, the withoutMiddleware() method allows you to specify the middleware to disable, instead of disabling them all. So, instead of modifying all of your middleware to add env checks, you can just do this in your test:
$this->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class);
Laravel < 5.5
If you're on Laravel < 5.5, you can implement the same functionality by adding the updated method to your base TestCase class to override the functionality from the framework TestCase.
PHP >= 7
If you're on PHP7+, add the following to your TestCase class, and you'll be able to use the same method call mentioned above. This functionality uses an anonymous class, which was introduced in PHP7.
/**
* Disable middleware for the test.
*
* #param string|array|null $middleware
* #return $this
*/
public function withoutMiddleware($middleware = null)
{
if (is_null($middleware)) {
$this->app->instance('middleware.disable', true);
return $this;
}
foreach ((array) $middleware as $abstract) {
$this->app->instance($abstract, new class {
public function handle($request, $next)
{
return $next($request);
}
});
}
return $this;
}
PHP < 7
If you're on PHP < 7, you'll have to create an actual class file, and inject that into the container instead of the anonymous class.
Create this class somewhere:
class FakeMiddleware
{
public function handle($request, $next)
{
return $next($request);
}
}
Override the withoutMiddleware() method in your TestCase and use your FakeMiddleware class:
/**
* Disable middleware for the test.
*
* #param string|array|null $middleware
* #return $this
*/
public function withoutMiddleware($middleware = null)
{
if (is_null($middleware)) {
$this->app->instance('middleware.disable', true);
return $this;
}
foreach ((array) $middleware as $abstract) {
$this->app->instance($abstract, new FakeMiddleware());
}
return $this;
}
You can use trait in test:
use Illuminate\Foundation\Testing\WithoutMiddleware;
Laravel >= 5.7
I might be late, but what I've figured it out:
$this->withoutMiddleware([
'email-verified', //alias does NOT work
EnsureEmailIsVerified::class //Qualified class name DOES WORK
]);
The following worked for me:
use WithoutMiddleware;
public function setUp(): void
{
parent::setUp();
$this->withoutMiddleware();
}
The withoutMiddleware method can only remove route middleware and does not apply to global middleware.
From https://laravel.com/docs/8.x/middleware