Laravel 4: Why is my partial mock of Auth failing? - laravel-4

I'm doing some unit tests in a controller that handles some authentication stuff in my web app. I am testing that a redirect happens on a valid login. The relevant controller code is:
if ( Auth::attempt(Input::only(array('username', 'password')),
Input::has('remember_me')) )
{
Session::put('client.id', Auth::user()->client->id);
Session::put('client.name', Auth::user()->client->name);
return Redirect::intended('admin/listings');
}
And my test looks like this:
public function testValidCredentialsRedirectToDashboard() {
Auth::shouldReceive('attempt')->once()->andReturn(true);
// Auth::makePartial();
$this->call('POST', 'login',
['username'=>'foo', 'password'=>'bar']);
$this->assertRedirectedTo('admin/dashboard');
}
When I leave the makePartial call commented, phpunit complains that
Method Mockery_1_Illuminate_Auth_AuthManager::user() does not exist on this mock object
But if I uncomment it xdebug complains about too much recursion:
Maximum function nesting level of '100' reached, aborting!
in /.../vendor/mockery/mockery/library/Mockery/Loader/EvalLoader.php(16) :
eval()'d code on line 307
(Turning xdebug off just yields a segmentation fault instead.)
I could just mock the user method, but I feel like that ties the test too close to the implementation. I don't want to test the Session stuff going on there, just that a login attempt is made and a redirect results.
Any ideas why makePartial is failing so miserably here?

Related

Mocking a controller enters controller instead of mock

The bounty expires in 5 days. Answers to this question are eligible for a +50 reputation bounty.
Chris Rockwell wants to draw more attention to this question.
I am trying to get comfortable with tests in Laravel and playing around with Dusk.
Given I have the following controller:
class CoursesController extends Controller {
private ApiServiceProvider $api;
public function __construct(ApiServiceProvider $apiServiceProvider) {
$this->api = $apiServiceProvider;
}
public function getCoursesCache(array $cIds = []) : array {
// Breakpoint here - always gets hit when running tests
if (empty($cIds)) {
$cIds = Request::capture()->query('cIds');
$cIds = explode(',', $cIds);
}
return $this->api->getCoursesCache($cIds);
}
}
Which is used by a route:
Route::get('/api/v1/courses/cache', 'App\Http\Controllers\Api\CoursesController#getCoursesCache')->name('courses.cache');
This route is used internally by a VueJS component, which is ultimately what I'd like to test.
I am using Dusk to do some browser based testing and I want to mock the controller response for getCoursesCache. However, when I use the following (with a breakpoint in the controller method) I always enter the controller instead of just returning the mock.
$courseController = $this->mock(CoursesController::class)->makePartial();
$item = new CourseCacheItem();
$item->name = $course->name;
$courseController->shouldReceive('getCoursesCache')
->with([$course->getKey()])
->andReturn([$item]);
$this->browse(function (Browser $browser) use ($course) {
$browser->visit('/')
->waitFor('.course-card-container--data-loaded', 10)
->screenshot('filename')
->assertSee($course->name);
});
I've also tried this to create the mock:
$cc = $this->createMock(CoursesController::class);
$item = new CourseCacheItem();
$item->name = $course->name;
$cc->expects($this->once())->method('getCoursesCache')->with([$course->getKey()])->willReturn([$item]);
Edit: I've also now tried Mocking and Spying on the injected service ApiServiceProvider but the code enters that real class during the test run as well.
My expectation is that my breakpoint within the actual CoursesController would never be hit - what am I doing wrong?
Your issue here is that you are dealing with two separate Laravel Runtime.
You can check this issue for more infos.
Basically, when you are making a Dusk test with an Http call, Dusk will make a real Http call to a fresh Laravel instance/runtime (using Chrome Headless).
So you end up having one runtime where you are lunching the test, and the second one where dusk is making an http call.
The second Laravel instance is not running your Mock and doesnt know about it. That's why you end up in the actual Controller.
One solution i found in the past is making a route responsible to mock what i need.
This route should be called in the the html page before the assertion is happening.
So when you make a Dusk http call some Js (in the second Laravel instance) will call the route and the mock will be setup.
Fortunately for you there is great package that can handle this for you. https://github.com/NoelDeMartin/laravel-dusk-mocking

Customise the "method is not supported for this route" error handling in Laravel

Route::post('order', 'OrderController#store')->name('order');
When I browse to the URL http://127.0.0.1:8000/order it shows the error:
The GET method is not supported for this route. Supported methods: POST.
Which is the correct.
But I want to redirect user to home page instead of showing this error.
First of all note that what you are trying to do seems like an anti-pattern for Laravel. Accessing a route with the wrong method should be denied!
I currently do not know about altering the default way of handling the wrong method error and I wouldn't advise to do that. But you can work around it:
Patching
Method 1
Keep your routes file clean but alter the original route line and add some lines to the beggining of the controller method
Route::match(['get', 'post'], 'order', [OrderController::class, 'store'])->name('order');
public function store(Request $request)
{
if ($request->isMethod('get')) {
return to_route('home');
}
// ...
Method 2
Keep your controller clean, but add a line to your routes file
Route::get('order', fn () => to_route('home'));

Laravel: Capture a difference between a Controller __construct()'s stage and other methods run stage

I am extending Laravel's controller for a package.
So I know Laravel controllers run their constructor and method at different stages of the app.
public function __construct()
{
//Middlewares have not run yet
//auth()->check() or auth()->user() do not work yet
}
And in any other method in your Controller
public function anyOtherMethod()
{
//All good, everything has booted.
}
I am looking a way of distinguishing between the two stages. For example, is there a method that says?
app()->middlewaresHaveBeenHandled(); //returns true or false
//or
app()->authIsBootedYouMayUseIt(); //returns true or false
The session from the request is null if the latter wan't yet handled by the app. So a good check would be
if(request()->route() && !request()->hasSession()){
//request has not been handled
}else{
//request has been handled
}
The check for the request route is necessary to make sure it is an HTTP request and it won't interfere with Tests or Console actions

How to do unit testing with Laravel Localization?

I'm using mcamara/laravel-localization package and I can't figure out how to make it work with my unit tests. Both of the following fail with red:
// 1. This one results in "Redirecting to http://myapp.dev/en"
$this->get('/')->assertSee('My App Homepage');
// 2. This one results in 404
$this->get('/en')->assertSee('My App Homepage');
In the browser, http://myapp.dev returns 302 with a redirect to http://myapp.dev/en, fair enough. However, http://myapp.dev/en returns 200. So both cases work 100% fine on the front-end, but not with unit tests.
I do have some customization however, which once again, works like charm in the browser.
// in web.php
Route::group([
'prefix' => app('PREFIX'), // instead of LaravelLocalization::setLocale()
'middleware' => ['localeSessionRedirect', 'localizationRedirect']],
function() {
Route::get('/', function() {
return view('home');
});
}
]);
// in AppServiceProvider.php
public function boot()
{
// This, unlike LaravelLocalization::setLocale(), will determine the
// language based on URL, rather than cookie, session or other
$prefix = request()->segment(1); // expects 'en' or 'fr'
$this->app->singleton('PREFIX', function($app) use ($prefix) {
return in_array($prefix, ['en', 'fr']) ? $prefix : null;
});
}
Hopefully this code makes sense to you. Thanks!
UPDATE
I addressed this problem with the package in a GitHub issue #435.
UPDATE 2
Insofar as I could figure it out, it seems that you can safely test your localized routes as long as you specify the locale in the base URL in your phpunit XML file:
<env name="APP_URL" value="http://myapp.dev/en"/>
However, this would work for your localized GET endpoints (which start with a locale prefix, e.g. 'en'), but not for non-localized POST, PUT, etc. (which don't have any prefix). Hence, you can't really test both kinds of endpoints at the same time, unless you use Dusk (which I don't, as it's an overkill and much slower, almost the same as doing it manually).
I found that if you dump the request URL during testing, it is always http://myapp.dev no matter what endpoint you're accessing. So both LaravelLocalization::setLocale() and my custom app('PREFIX') return null, meaning that not a single route is ever localized during testing. You are screwed either way because if you try to access a route without a locale prefix, you get a 302, but if you do specify the locale, the framework can't find a definition for that route.
One article helped me discover a temporary solution: you need to hideDefaultLocaleInURL to true in laravellocalization.php. This way, the routes matching your default locale won't have any prefix, so you can test them as if they were non-localized.
However, the problem still persists, because how are you supposed to test your application when it is localized? (For ex., when you have language-specific routes that need to be tested). This poses the question whether this package is even compatible with unit testing per se...
The problem
Using mcamara / laravel-localization when I test a show route I get a 404 error.
For instance, testing this route returns me a 404:
Route::get('/posts/{post:slug}', [PostController::class, 'show'])->name('posts.show');
The test:
/** #test */
public function itShouldDisplayThePostsShowViewToGuestUser()
{
$response = $this->get("/posts/{$this->post1->slug}");
$response->assertStatus(200);
$response->assertViewIs('posts.show');
}
The solution
I solved hiding the locale from the URL while testing.
Creating this env variable at the end of phpunit.xml.
...
<env name="LOCALIZATION_HIDE_DEFAULT_LOCALE" value="true"/>
</php>
</phpunit>
And in config/laravellocalization.php setting hideDefaultLocaleInURL like this:
'hideDefaultLocaleInURL' => env('LOCALIZATION_HIDE_DEFAULT_LOCALE', false)
This solution was inspired by this this post:
https://github.com/mcamara/laravel-localization/issues/161#issuecomment-381367191

Laravel 4 route-model binding exceptions doesn't work despite docs and examples

I read a lot about Laravel4 Route-model binding (L4 docs, tutorials, etc.) but still exceptions (i.e. the model is not found) don't work for me
These are my basic files
routes.php:
Route::model('game', 'Game', function(){
// override default 404 behavior if model not found, see Laravel docs
return Redirect::to('/games');
});
...
Route::get('/games/edit/{game}', 'GamesController#edit');
GamesController.php
class GamesController extends BaseController {
...
public function edit(Game $game){
return View::make('/games/edit', compact('game'));
}
}
Pretty straight, but I get this error: Argument 1 passed to GamesController::edit() must be an instance of Game, instance of Illuminate\Http\RedirectResponse given
If I type http://mysite.dev/games/edit/1 all is fine (model with ID = 1 exists)
If I type http://mysite.dev/games/edit/12345 (no model with that ID) the ugly error above is triggered instead of the redirect I specified
I also looked at this (the bottom part where a Redirect closure is suggested: that is just what I am doing!) but no way to make it work: laravel 4 handle not found in Route::model
What's wrong with it? Please any help?
Thanks in advance
In Route::model you declare which variable will be a model instance, you shouldn't use it to do a redirection that way. Instead of that, specify that $game is of type Game and then work with your routes:
Route::model('game', 'Game');
...
Route::get('/games/edit/{game}', 'GamesController#edit');
Then if you access to /games/edit/3 GamesController::edit will receive an instance of Game class whose id=3
I ended up by setting a general "Not Found" error catcher, like this:
// routes.php
App::error(function(Symfony\Component\HttpKernel\Exception\NotFoundHttpException $e) {
return Response::make('Not Found', 404);
});
...
Route::model('game', 'Game');
...
Route::get('/games/edit/{game}', 'GamesController#edit');
What I understand is that if I want a custom redirect and not a general 404 page (i.e. take the user to games' list if model not found), I CAN'T use the route-model-binding
In other words, I have to use Route::get('/games/edit/{id}', 'GamesController#edit'); and then do my application logic inside the 'edit' method:
public function edit($id){
$game = Game::findOrFail($id);
// if fails then redirect to custom page, else go on saving
}
I'm very new to Laravel, but as far as I can see this has nothing to do with the closure, but with the use of "Redirect::to" inside that closure. Using "App::abort( 404 );" works.

Resources