Laravel 5 / Codeception not routing correctly - php

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.

Related

Laravel 9 edit route with {id} parameter returns Not Found (404) despite being defined

Below is an example of the route definition in my routes/web.php file.
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\WidgetController;
Route::controller(WidgetController::class)->name('widgets.')->prefix('widgets')->group(function () {
Route::get('{id}/{slug}', 'show')->name('show');
Route::get('create', 'create')->name('create')->middleware('auth');
Route::post('store', 'store')->name('store')->middleware('auth');
Route::get('{id}/edit', 'edit')->name('edit')->middleware('auth');
Route::post('update', 'update')->name('update')->middleware('auth');
});
All the routes listed work fine except for one.
Route::get('{id}/edit', 'edit')->name('edit')->middleware('auth');
For whatever reason, it works completely fine when it's written like this:
Route::get('{id}', 'edit')->name('edit')->middleware('auth');
or this:
Route::get('{id}//edit', 'edit')->name('edit')->middleware('auth');
With "Route::resource", the default would be the first "edit" route. However, I get a 404 when defining it like that. My associated controller method is written like this:
public function edit($id)
{
// Rest of code here
}
Could it be that Laravel is mistaking "edit" as a second parameter? But that doesn't really make much sense considering it's the default for the resource method.
Try putting
Route::get('{id}/edit', 'edit')->nam
Above
Route::get('{id}/{slug}'
Since {slug} could be anything it's more then likely getting executed first. I would suggest putting that last.

Declare same route twice but expect different behaviour according to a middleware

I started creating a REST API using the lumen framework and wanted to set up a particular behaviour for my GET /user route. Behaviour is the following:
If the request come from an authenticated user (using auth middleware), the method getAllFields from UserController is called and return all the data from the user
If it's not the case, the method get from UserController is called and return some of the data from the user
It seems logic to me to just write it like that in my web.php using a simple middleware:
<?php
$router->group(['middleware' => 'auth'], function () use ($router) {
$router->get('/user/{id}', [
'uses' => 'UserController#getAllFields'
]);
});
$router->get('/user/{id}', [
'uses' => 'UserController#get'
]);
But for some reason, even if the middleware is correct, I always get the response of the second route declaration (that call get()). I precise that if I remove the second route declaration, the one in the middleware work as expected.
Have someone an idea how I can achieve something similar that work?
Router will check if your request matches to any declared route. Middleware will run AFTER that match, so You cannot just return to router and try to find another match.
To fallow Laravel and Routes pattern - You should have single route that will point to method inside controller. Then inside that You can check if user is logged or not and execute getAllFields() from that controller. It will be not much to rewrite since You are currently using UserController in both routes anyway.
web.php
$router->get('/user/{id}', 'UserController#get');
UserController.php
public function get()
{
return auth()->check() ? YourMethodForLogged() : YourMethodForNotLogged();
}
Or if there is not much logic You can keep this in single method.
Also it is good idea to fallow Laravels REST standards (so use show instead of get, "users" instead of "user" etc - read more https://laravel.com/docs/7.x/controllers)
web.php
$router->get('/users/{user}', 'UserController#show');
UserController.php
public function show(User $user)
{
if (auth()->check()) {
//
} else {
//
}
}
To summary - for your needs use Auth inside controller instead of middleware.
To check if user is logged You can use Facade Auth::check() or helper auth()->check(), or opposite Auth::guest() or auth()->guest().
If you are actually using Lumen instead of full Laravel then there is not auth helper by default (You can make own or use package like lumen-helpers) or just keep it simple and use just Facades instead (if You have then enabled in Lumen).
Read more https://laravel.com/docs/7.x/authentication and https://lumen.laravel.com/docs/7.x/authentication
This pattern is against the idea of Laravel's routing. Each route should be defined once.
You can define your route without auth middleware enabled and then define your logic in the controller.

Laravel 5 redirect to path with parameters (not route name)

I've been reading everywhere but couldn't find a way to redirect and include parameters in the redirection.
This method is for flash messages only so I can't use this.
return redirect('user/login')->with('message', 'Login Failed');
This method is only for routes with aliases my routes.php doesn't currently use an alias.
return redirect()->route('profile', [1]);
Question 1
Is there a way to use the path without defining the route aliases?
return redirect('schools/edit', compact($id));
When I use this approach I get this error
InvalidArgumentException with message 'The HTTP status code "0" is not valid.'
I have this under my routes:
Route::get('schools/edit/{id}', 'SchoolController#edit');
Edit
Based on the documentation the 2nd parameter is used for http status code which is why I'm getting the error above. I thought it worked like the URL facade wherein URL::to('schools/edit', [$school->id]) works fine.
Question 2
What is the best way to approach this (without using route aliases)? Should I redirect to Controller action instead? Personally I don't like this approach seems too long for me.
I also don't like using aliases because I've already used paths in my entire application and I'm concerned it might affect the existing paths if I add an alias? No?
redirect("schools/edit/$id");
or (if you prefer)
redirect("schools/edit/{$id}");
Just build the path needed.
'Naming' routes isn't going to change any URI's. It will allow you to internally reference a route via its name as opposed to having to use paths everywhere.
Did you watch the class Illuminate\Routing\Redirector?
You can use:
public function route($route, $parameters = [], $status = 302, $headers = [])
It depends on the route you created. If you create in your app\Http\Routes.php like this:
get('schools/edit/{id}', 'SchoolController#edit');
then you can create the route by:
redirect()->action('SchoolController#edit', compact('id'));
If you want to use the route() method you need to name your route:
get('schools/edit/{id}', ['as' => 'schools.edit', 'uses' => 'SchoolController#edit']);
// based on CRUD it would be:
get('schools/{id}/edit', ['as' => 'schools.edit', 'uses' => 'SchoolController#edit']);
This is pretty basic.
PS. If your schools controller is a resource (CRUD) based you can create a resource() and it will create the basic routes:
Route::resource('schools', 'SchoolController');
// or
$router->resource('schools', 'SchoolController');
PS. Don't forget to watch in artisan the routes you created

Unable to run multiple controller tests in Laravel

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()

laravel 4 route not found - defaults to show method

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.

Categories