I'm attempting to clean up an existing application by writing unit tests for some legacy code (and updating it along the way). I've rewritten a number of libraries and I've really been loving the TDD approach. However, now it's time to move on to testing some controllers, and I've run into a problem on the very first set of tests. I'm following the explanations in Jeffery Way's Laravel Testing Decoded.
The goal here is to test my login route: http://my-development-server/login. The code should work like this: first, check to see if someone is already logged in - if they are, redirect them to the dashboard (the main page in the app). Otherwise, render the login page. Pretty straight forward.
Here are the routes involved:
Route::get('login', array(
'as' => 'login',
'uses' => 'MyApp\Controllers\AccountController#getLogin',
));
Route::get('/', array(
'as' => 'dashboard',
'uses' => 'MyApp\Controllers\DashboardController#showDashboard',
'before' => 'acl:dashboard.view',
));
Here's the AccountController::getLogin method:
public function getLogin()
{
// Are we logged in?
if (\Sentry::check())
return Redirect::route('dashboard');
// Show the page.
return View::make('account.login');
}
I'm using the Sentry library for user authentication.
And here's my first test:
class AccountControllerTest extends TestCase {
public function tearDown()
{
Mockery::close();
}
public function test_login_alreadyLoggedIn()
{
// arrange
//
\Sentry::shouldReceive("check")
->once()
->andReturn(true);
// act
//
$response = $this->call("GET", "/login");
// assert
//
$this->assertRedirectedToRoute("dashboard");
}
}
This test emulates the "user attempts to log in when they're already logged in" case. check returns true (already logged in) and the user is redirected to the route named dashboard. It works perfectly, test passes.
Next, I add a new test, to test the "user attempts to log in when nobody's logged in" case. Here's the code for that test:
public function test_login_notLoggedIn()
{
// arrange
//
\Sentry::shouldReceive("getUser")
->twice()
->andReturn(null);
\Sentry::shouldReceive("check")
->once()
->andReturn(false);
// act
//
$response = $this->client->request("GET", "/login");
// assert
//
$h2 = $response->filter("h2");
$this->assertEquals("Please sign in", $h2->text());
}
When I run this test, the first test passes but I get a NotFoundHttpException in the second test method.
There are a couple of strange things about this problem:
if I comment out the first test method (test_login_alreadyLoggedIn), the second test passes.
if I reverse the order of the two test methods, test_login_notLoggedIn passes and test_login_alreadyLoggedIn fails (ie. the first method in the class passes and the second fails).
This looks to me like some sort of configuration issue - after the first test (which passes), something is messed up and it cannot make the request in the second test - but I'm at a loss. I've gone over the book several times and google but can't find any reference to any configuration that I'm missing.
Any suggestions?
For anyone who may run into this problem in the future...
This was caused by problems with my routes.php file. In particular, I organized by routes into a set of files:
accounts.php - contains routes related to "account" functions
admin.php - contains routes related to "admin" functions
etc...
These files were then included in my routes.php file with require_once. This works fine for my application, but for some reason, the routes were not loaded properly during testing. The solution is to move my routes back to the routes.php file until I can find a better way to organize them.
You can solve this problem by replacing your require_once() to require()
Related
I am building my first Laravel app with the Metronic 8 Laravel theme. It uses Breeze for authentication. I changed a couple of things around - created a welcome page for non-logged-in users, and moved the main template that was the index to an auth protected "/dashboard". The problem is that it still tries to load the dashboard Blade template, regardless of authentication, resulting in an error.
Route
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth'])->name('dashboard');
Here's Authenticate, where it should redirect non-authenticated users to the login page.
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('login');
}
}
When I'm not logged in and navigate to the dashboard URL, it attempts to load the dashboard Blade template, which calls a menu function that checks the user permissions for menu items. Unfortunately, since there is no user, the application blows up from passing a null value to a method expecting a user array/object.
Any ideas on where to look for the problem? It seems to me that the auth middleware should redirect to the login page before trying to load the Blade template when not logged in.
I would put the middleware at the beginning of the route like this, though I'm sure it's not causing the problem-
Route::middleware(['auth'])->get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
Aside from that, please provide some information on the error itself like what the error is about/what is says..etc...
First of all, make sure you have a login named route defined in your routes/web.php file. It should look something like:
Route::get('/login', '<controller>#<method>')->name('login');
The important bit is ->name('login') so that the Authenticate middleware can correctly identify the route to redirect to. Change <controller>#<method> appropriately to route to the login method of your app.
Wakil's answer is irrelevant and actually opposite of the documentation. Your syntax is correct.
I figured out the issue. Keen Themes put a call to a method to build an array of menu items in the web routes file. That was making the call to the offending code. After I wrapped that in an auth check the error was fixed, and everything works as expected.
I'm building a package called under-construction. When this package is activated in a config
file the site will be underconstruction only people with the right code can access the
application.
https://github.com/larsjanssen6/underconstruction
The problem that I have right now:
When the code is entered I make an ajax call that hit's this controller method (called check):
https://github.com/larsjanssen6/underconstruction/blob/master/src/Controllers/CodeController.php
If the code is correct a session variable is being set:
session(['can_visit' => true]);
Then in my vue.js code I redirect to /. And it will hit my middleware again. Here I check if a session called can_visit exists.
return session()->has('can_visit');
https://github.com/larsjanssen6/underconstruction/blob/master/src/UnderConstruction.php
But the session variable can_visit is always gone! How is that possible?
Thanks for your time.
You're not loading the session middleware, so session is not started and no values are persisted.
As was mentioned in the comments, even though your protected routes (/) are within the web middleware (read session), your service provider's routes (/under/construction, /under/check) are not (no write session).
The simple fix is to add the session, or even better, the whole web middleware.
$routeConfig = [
'namespace' => 'LarsJanssen\UnderConstruction\Controllers',
'prefix' => 'under',
'middleware' => [
'web', // add this
// DebugbarEnabled::class, // leaving this dead code behind despite vcs
],
];
However, you might quickly run into trouble with infinite redirect loops if a user adds your middleware to their web middleware group. So I would add a check of some sort to make sure you're not on one of the existing underconstruction routes.
public function handle($request, Closure $next)
{
// check this isn't one of our routes
// too bad router hasn't loaded named routes at this stage in pipeline yet :(
// let's hope it doesn't conflict with user's routes
if ($request->is('under/*')) {
return $next($request);
}
if (! $this->config['enabled']) {
return $next($request);
}
if (!$this->hasAccess($request)) {
return new RedirectResponse('/under/construction');
}
return $next($request);
}
And ultimately guessing from the context of this project, I'd expect most people would want to stick this in the global middleware. However, you're going to run into the same session-hasn't-started-yet issues because that doesn't run in the global middleware. So there's more to chew on. Happy coding!
I'm trying to write an API test case for a controller function using codeception, and I'm hitting an issue where the route to the controller function does not appear to be evaluated correctly, and the evaluation seems to be different depending on what I have in my test case.
Here is a code sample from my test case:
use \ApiTester;
class CustomerRegisterCest
{
// tests
public function testGetRegister(ApiTester $I)
{
$I->sendGET('register');
$I->seeResponseCodeIs(200);
}
public function testPostRegister(ApiTester $I)
{
$I->sendPOST('register', [
// set the data in here
]);
$I->seeResponseCodeIs(200);
}
I have a routes.php file containing these routes:
Route::get('/', ['as' => 'home', 'uses' => 'HomeController#getIndex']);
Route::get('register', ['as' => 'getRegister', 'uses' =>'RegistrationController#getRegister']);
Route::post('register', ['as' => 'postRegister', 'uses' => 'RegistrationController#postRegister']);
I have inserted some debug statements into my controller classes so that I can see what routes get run, like this:
Log::debug('GET register'); // or GET index or POST register, etc
At the moment I have stripped down everything from my controller classes so that ONLY the debug statements are included.
When I run the test case as above, I get the following debug output:
GET register
GET index
... so it appears that sendPOST('register', ...) actually routes to the GET route for "/" instead of the POST route for "/register". Outside of the test case everything works normally -- I can POST to the register routes fine, routing appears to work OK, the problem only appears inside a codeception test case.
If I change the test case so that I am doing the sendGET and the sendPOST inside the same function call, for example like this:
// tests
public function testPostRegister(ApiTester $I)
{
$I->sendGET('register');
$I->seeResponseCodeIs(200);
$I->sendPOST('register', [
// set the data in here
]);
$I->seeResponseCodeIs(200);
}
then I see this debug output:
GET register
GET register
... so that by inserting the sendGET into the same function as the sendPOST, it has changed the sendPOST behaviour so that it now routes to the GET route for register instead of the GET route for index (but still won't route to the correct POST route).
I have tried turning xdebug on and don't have any clues from the xdebug output as to what's going on either.
I think I found the answer after a lot of command line debugging (using phpstorm):
The POST register route handling function in the controller was declared like this:
public function postRegister(RegistrationRequest $request)
{
... requiring an instance of Request to be passed in via dependency injection. That request contained some validation code and if for some reason the validation code could not complete (e.g. throws an exception) then the controller function never gets called -- because building the request fails.
This, in browser-land, throws a 500 error but in codeception land the exception is trapped differently and it returns a redirect to / with no data. This all happens outside of the controller function rather than inside it, so that the Log statement in the controller function never runs because the function never gets called. The exception handler in codeception is a generic trap.
The implicit suggestion is that maybe dependency injections in controllers are a bad idea. Or, maybe, that generic exception handlers are a bad idea.
I've had this working but now a route is no longer found and I can't see why.
In a javascript function I am making an ajax post to the function with this url:
url: '/customers/storeajax',
In my routes.php file I have the following routes:
Route::post('customers/storeajax', array('as'=>'storeajax', 'uses' => 'CustomersController#storeAjax'));
Route::post('customers/updateajax/{id}', array('as'=>'updateajax','uses' => 'CustomersController#updateAjax'));
Route::resource('customers', 'CustomersController');
Now when I try to POST to the storeajax route I get a ModelNotFoundException which to me means the route could not be found so it defaults to the default customers controller show method - in the error log I can see the following entry:
#1 [internal function]: CustomersController->show('storeajax')
confirming its treating the storeajax as a parameter.
I've placed my additional routes above the default resource route
I've had this working before I can't see where I've gone wrong.
In addition these routes are placed in a group:
Route::group(array('before' => 'sentryAuth'), function () {}
which simply ensures user is logged on. To test though I've removed outside the group and at the top of the file but still they don't work.
The url in my browser is coming up correctly as: http://greenfees.loc/customers/storeajax (which I can see in firebug console
I'm using POST as the ajax method - just to confirm
Can anyone see why this route doesn't work and what I've missed?
Update:
Here's the method inside the controller:
public function storeAjax()
{
$input = Input::all();
$validation = Validator::make($input, Customer::$rules);
if ($validation->passes())
{
$customer = $this->customer->create($input);
return $customer;
}
return Redirect::route('customers.create')->withInput()
->withErrors(validation)
->with('message', 'There were validation errors.');
}
I'm 99% certain though that my route is not reaching this method (i've tested with a vardump inside the method) and the issue relates to my route customer/storeajax cannot be found.
What I think is happening is as customer/storeajax is not found in the list of routes starting with customer it is then defaulting to the resource route that appears on the list and thinks this is a restful request and translating it as customer route which defualts to the show method and using the storeajax as the parameter which then throws the error modelnotfoundexception because it cant find a customer with an id of 'storeajax'
This is evidence by the log detailing a call to the show method as above.
So for some reason my route for '/customers/storeajax' cannot be found even though it appears to be valid and appears before the customers resource. The modelnotfoundexception is a red herring as the cause is because of the routes defaulting to the resource constroller of customers when it cant find a route.
A route not being found raises a NotFoundHttpException.
If you are getting a ModelNotFoundException is because your route is firing and your logic is trying to find a Model, wich it can't somehow, and it is raising a not found error.
Are you using FindOrFail()? This is an example of method that raises this exception. BelongsToMany() is another one that might raise it.
I solved this by renaming the method in the controller to 'newAjax' and also updating the route to:
Route::post('customers/new', array('as'=>'newajax','uses' => 'CustomersController#newAjax'));
the terms store I assume is used by the system (restful?) and creating unexpected behaviour. I tested it in a number of other functions in my controller - adding the term store as a prefix to the method then updating the route and each time it failed.
Something learned.
There will be several high profile links for customers to focus on, for example:
Contact Us # domain.com/home/contact
About the Service # domain.com/home/service
Pricing # domain.com/home/pricing
How It Works # domain.com/home/how_it_works
Stuff like that. I would like to hide the home controller from the URL so the customer only sees /contact/, not /home/contact/. Same with /pricing/ not /home/pricing/
I know I can setup a controller or a route for each special page, but they will look the same except for content I want to pull from the database, and I would rather keep my code DRY.
I setup the following routes:
Route::get('/about_us', 'home#about_us');
Route::get('/featured_locations', 'home#featured_locations');
Which work well, but I am afraid of SEO trouble if I have duplicate content on the link with the controller in the URL. ( I don't plan on using both, but I have been known to do dumber things.)
So then made routes like these:
Route::get('/about_us', 'home#about_us');
Route::get('/home/about_us', function()
{
return Redirect::to('/about_us', 301);
});
Route::get('/featured_locations', 'home#featured_locations');
Route::get('/home/featured_locations', function()
{
return Redirect::to('/featured_locations', 301);
});
And now I have a redirect. It feels dumb, but it appears to be working the way I want. If I load the page at my shorter URL, it loads my content. If I try to visit the longer URL I get redirected.
It is only for about 8 or 9 special links, so I can easily manage the routes, but I feel there must be a smart way to do it.
Is this even an PHP problem, or is this an .htaccess / web.config problem?
What hell have I created with this redirection scheme. How do smart people do it? I have been searching for two hours but I cannot find a term to describe what I am doing.
Is there something built into laravel 4 that handles this?
UPDATE:
Here is my attempt to implement one of the answers. This is NOT working and I don't know what I am doing wrong.
application/routes.php
Route::controller('home');
Route::controller('Home_Controller', '/');
(you can see the edit history if you really want to look at some broken code)
And now domain.com/AboutYou and domain.com/aboutUs are returning 404. But the domain.com/home/AboutYou and domain.com/home/aboutUs are still returning as they should.
FINAL EDIT
I copied an idea from the PongoCMS routes.php (which is based on Laravel 3) and I see they used filters to get any URI segment and try to create a CMS page.
See my answer below using route filters. This new way doesn't require that I register every special route (good) but does give up redirects to the canonical (bad)
Put this in routes.php:
Route::controller('HomeController', '/');
This is telling you HomeController to route to the root of the website. Then, from your HomeController you can access any of the functions from there. Just make sure you prefix it with the correct verb. And keep in mind that laravel follows PSR-0 and PSR-1 standards, so methods are camelCased. So you'll have something like:
domain.com/aboutUs
In the HomeController:
<?php
class HomeController extends BaseController
{
public function getAboutUs()
{
return View::make('home.aboutus');
}
}
I used routes.php and filters to do it. I copied the idea from the nice looking PongoCMS
https://github.com/redbaron76/PongoCMS-Laravel-cms-bundle/blob/master/routes.php
application/routes.php
// automatically route all the items in the home controller
Route::controller('home');
// this is my last route, so it is a catch all. filter it
Route::get('(.*)', array('as' => 'layouts.locations', 'before' => 'checkWithHome', function() {}));
Route::filter('checkWithHome', function()
{
// if the view isn't a route already, then see if it is a view on the
// home controller. If not, then 404
$response = Controller::call('home#' . URI::segment(1));
if ( ! $response )
{
//didn't find it
return Response::error('404');
}
else
{
return $response;
}
});
They main problem I see is that the filter basically loads all the successful pages twice. I didn't see a method in the documentation that would detect if a page exists. I could probably write a library to do it.
Of course, with this final version, if I did find something I can just dump it on the page and stop processing the route. This way I only load all the resources once.
applicaiton/controllers/home.php
public function get_aboutUs()
{
$this->view_data['page_title'] = 'About Us';
$this->view_data['page_content'] = 'About Us';
$this->layout->nest('content', 'home.simplepage', $this->view_data);
}
public function get_featured_locations()
{
$this->view_data['page_title'] = 'Featured Locations';
$this->view_data['page_content'] = 'Featured properties shown here in a pretty row';
$this->layout->nest('content', 'home.simplepage', $this->view_data);
}
public function get_AboutYou()
{
//works when I return a view as use a layout
return View::make('home.index');
}