I'm having odd behaviour in a laravel 5.5 project. I have some feature tests set up and working, and need to test that a specific route will return a 404 if the id passed in doesn't exist. I have explicit model binding set up in the RouteServiceProvider for my Note model
Route::bind('note', function($value){
return Note::where('id', $value)->first() ?? abort(404);
});
This works for my get route test. This test below passes as expected. ($this->headers is just some bits I set in a setUp method that is needed for a lot of the tests)
/** #test */
public function error_received_if_note_does_not_exist()
{
$this->withExceptionHandling();
$response = $this->json('GET', '/api/v1/note/1', [], $this->headers);
$response->assertStatus(404);
}
but this one for the delete route fails ...
/**
* #test
* #expectedException \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function error_received_if_note_not_found()
{
$this->withExceptionHandling();
$response = $this->json('DELETE', '/api/v1/note/1', [], $this->headers);
$response->assertStatus(404);
}
with message
Failed asserting that exception of type "\Illuminate\Database\Eloquent\ModelNotFoundException" is thrown.
I get that technically the exception is correct, but I want to assert that I get a 404 error code.
Here is the routes/api.php file
Route::apiResource('note', 'NoteController')->only([
'show',
'destroy'
]);
I'm pulling my hair out. Any ideas welcome.
I think Ive figured this out encase anyone else has this problem.
In the delete test, I removed the $this->withExceptionHandling(); line. Which then allows the test to pass.
If I do the same in the get test it fails. So the get test needs it, but the delete test doesn't.
Randomly.
Related
TL;DR
I would like to know how I should handle an exception that occurs in an event and subsequently rollback changes made to the database prior to the exception, provide feedback to the user and write a test for all of this too.
Full Details
In my Laravel web application I have four models: Applicant, Application, ApplicationAction and Document. An Applicant can have one Application which can have many ApplicationActions. Also, an Application can have many Documents.
Conceptually, within the application when an ApplicationAction is created it is applied to an Application which in turn has its status attribute updated. For example, if an ApplicationAction is of type reject the Application's status then becomes rejected.
Getting into the code where this all happens, I have an ActionController with a store() method that creates a new ApplicationAction:
ActionController.php
public function store(StoreActionFormRequest $request)
{
$actionDetails = (new ApplicationAction)->createFromRequest($request);
}
ApplicationAction.php
public function createFromRequest(Request $request)
{
return $this->create([
'action' => $request->action,
'action_meta' => $this->actionMeta($request),
'details' => $request->details,
'details_external' => $request->details_external,
'action_by' => $request->user()->id,
'application_id' => $request->application->id,
]);
}
On the creation of a new ApplicationAction the ApplicationActionCreated event is fired with a listener that modifies the Application's status attribute based on ApplicationAction. I am not including the code in as it's not relevant.
When the Application is updated in this process (i.e. its status is changed) an event is triggered: ApplicationUpdated. A listener for this event is the NotifyApplicationWasUpdated listener which sends an email to the Applicant:
NotifyApplicationWasUpdated.php
/**
* Handle the event.
*
* #param ApplicationUpdated $event
* #return void
*/
public function handle(ApplicationUpdated $event)
{
if ($mailable = MailableType::resolve($event->application)) {
$to = $event->application->applicant->user->email;
$from = $event->application->applicant->dispatchEmail('verification');
Mail::to($to)
->bcc($from)
->send(new $mailable($event->application));
}
}
The mailable is determined dynamically (code not relevant) and within the mailable an attachment is added to the email:
A dynamically determined MailableClass
/**
* Build the message.
*
* #return $this
*/
public function build()
{
//... some code
$documents = $this->application->documents->whereIn('type', ['some-document', 'some-other-document']);
$documents->each(function ($document, $key) {
// TODO: check the file exists first
$this->attach(storage_path($document->file_path));
});
return $this->from($from)->view($view);
}
I would like to handle the FileNotFound exception if the file to be used as an attachment cannot be found. If this exception occurs then the email would not be sent, so I thought it would be a good idea to rollback the changes made to the database prior to the exception which would include deleting the newly made ApplicationAction and returning the Application's status to its previous value.
I would also like to provide the user appropriate feedback such as the file intended to be used as an attachment couldn't be found and also that the ApplicationAction was not created and the Application status was not modified.
The approaches I have considered to handling the FileNotFound exception in the event that the Document cannot be found when executing this line of code $this->attach(storage_path($document->file_path)); is to check if the file exists first and if it doesn't then to rollback changes and respond to user with an error:
if(!file_exists(storage_path($document->file_path))) {
// roll back DB changes and show user error page
$this->application->rollback(); // some method that sets the status to its previous value, not implemented it yet
$this->application->lastAction()->delete(); // delete the last action
// then throw an exception to be handled by Laravel exception handler
// which in turn responds to user with an appropriate message?
}
$this->attach(storage_path($document->file_path));
...but I'm a bit unsure on the details of how to rollback the DB changes and show the error to the user. I could either use the above approach, having a Application rollback() method etc. or instead wrapping the controller store() action in a try-block and DB::transaction()/commit(), which would look something like this:
ActionController.php
public function store(StoreActionFormRequest $request)
{
DB::beginTransaction();
try {
$actionDetails = (new ApplicationAction)->createFromRequest($request);
} catch(FileNotFoundException $e) {
DB::rollback();
throw $e;
}
DB::commit();
}
Then, as mentioned before, use the Laravel exception handler which in turn responds to user with an appropriate error message:
Handler.php
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $exception
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $exception)
{
if ($exception instanceof FileNotFoundException) {
return response()->view('errors.file_not_found', [], 500);
}
return parent::render($request, $exception);
}
If I did use this approach would it be possible to pass some specific text explaining to the user the email was not sent and why? Would I be better off creating my own EmailAttachmentFileNotFoundException and have it hardcoded in the view and using this in Handlers render() function?
Finally, I would most importantly like to ask how may I write a test for all the above? As most of the logic I'm trying to implement is around an event, which is currently occuring synchronously, how would I write a test for this specific event/listener because I already have something testing the controller action in the form of an integration test but it all it tests for is if the event is fired. How would I then write an isolated test of a test that goes smoothly, i.e. the email is sent successfully and secondly how would I write a test that goes wrong and check it's handled correctly, i.e. email attachment is not found and FileNotFound exception is triggered then check the DB stuff is rolled back and the user gets the correct error page?
Any help would be appreciated and if this question can be made any shorter please feel free to edit away!
Hi I'm trying to create a test for a laravel controller not dependent on the view. I have the following method in my controller class:
public function store(Request $request)
{
$this->validate($request,[
'name'=>'required',
'login'=>'required',
'password'=>'required'
]);
//TODO: Store to database
return redirect('usuario/nuevo');
}
And I found the following code to test whether the request had any errors:
public function testStore()
{
$response=$this->call('GET','usuario.store');
$this->assertSessionHasErrors();
}
As it is this test should pass since I'm sending a request without the required field filled out however PhpUnit returns the following message:
Session missing key: errors
Failed asserting that false is true.
The only way I can make it work is to try to "see" the error message on the response page by making the test like:
public function testStore()
{
$this->visit('/usuario/nuevo')
->press('Crear')
->see('Whoops');
}
This doesn't work for me for two reasons: 1)The test depends on the view to "press" the button and send the request(I wish to test this in a different test and keep this one strictly for the controller) and 2)The test depends on the 'Whoops' string to be present which is obviously bad practice.
I have found several answers claiming that the fist test code should work, but it simply doesn't.
Try it with session start and send empty parameters
$this->startSession();
$this->call('GET','usuario.store', array(
'name' => '',
'login' => '',
'password' => ''
));
$errors = session('errors');
$this->assertSessionHasErrors();
$this->assertEquals($errors->get('name')[0],""your custom error message);// only for checking exact message
Your problem is you are not testing what you think you are testing.
To execute the controller's store() you should use a POST method and not GET unless your routes are really weird.
I defined a test which tests the creation of a user. The controller is set to redirect back to the same page on error (using validation through a generated App\Http\Requests\Request). This works correctly when manually clicking in a browser, but fails during a test. Instead of being redirected to:
http://localhost/account/create
The test redirects to (missing a slash):
http://localhostaccount/create
Neither of these urls are what I have setup in the .htaccess or in the $url variable in config/app.php. Which is (On OSX Yosemite):
http://~username/laravel_projects/projectname/public
I finally pinpointed the issue to have something to do with how the result of Request::root() is generated. Making a call to this outside of a test results in the expected value defined in .htaccess and $url. Inside the test it results in:
http://localhost
What configuration needs to change in order to get this function to return the correct value in both contexts?
I should also mention I made the painful upgrade from Laravel 4 to the current version 5.0.27.
****** UPDATE *******
I was able to figure out an acceptable solution/workaround to this issue!
In Laravel 5, FormRequests were introduced to help move validation logic out of controllers. Once a request is mapped to the controller, if a FormRequest (or just Request) is specified, this is executed before hitting the controller action.
This FormRequest by default handles the response if the validation fails. It attempts to construct a redirect based on the route you posted the form data to. In my case, possibly related to an error of mine updating from Laravel 4 to 5, this default redirect was being constructed incorrectly. The Laravel System code for handling the response looks like this:
/**
* Get the proper failed validation response for the request.
*
* #param array $errors
* #return \Symfony\Component\HttpFoundation\Response
*/
public function response(array $errors)
{
if ($this->ajax() || $this->wantsJson())
{
return new JsonResponse($errors, 422);
}
return $this->redirector->to($this->getRedirectUrl())
->withInput($this->except($this->dontFlash))
->withErrors($errors, $this->errorBag);
}
Notice how the returned redirect is NOT the same as calling Redirect::route('some_route'). You can override this response function by including use Response in your Request class.
After using Redirect::route() to create the redirect, the logic in my tests passed with the expected results. Here is my Request code that worked:
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use App\Http\Requests\Request;
use Response;
class AccountRequest extends FormRequest {
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required|max:50|email|unique:users',
'password' => 'required|min:6',
'password_confirmation' => 'required|same:password'
];
}
public function response(array $errors){
return \Redirect::route('account_create');
}
}
The important part is that I called Redirect::route instead of letting the default response code execute.
Override the response function in the FormRequest validation handler to force the redirect to be constructed with Redirect::route('named_route') instead of allowing the default redirect.
You need to change config/app.php file's url value. Default value is http://localhost
Doc from config/app.php
This URL is used by the console to properly generate URLs when using the Artisan command line tool. You should set this to the root of your application so that it is used when running Artisan tasks.
I know this isn't an exact answer to your question since it is not a configuration update that solves the problem. But I was struggling with a related problem and this seems to be the only post on the internet of someone dealing with something similar - I thought I'd put in my two cents for anyone that wants a different fix.
Please note that I'm using Laravel 4.2 at the moment, so this might have changed in Laravel 5 (although I doubt it).
You can specify the HTTP_HOST header when you're testing a controller using the function:
$response = $this->call($method, $uri, $parameters, $files, $server, $content);
To specify the header just provided the $server variable as an array like so:
array('HTTP_HOST' => 'testing.mydomain.com');
When I did the above, the value produced for my Request::root() was http://testing.mydomain.com.
Again, I know this isn't a configuration update to solve you're issue, but hopefully this can help someone struggling with a semi-related issue.
If you tried changine config/app.php and it did not help.
it is better to use $_ENV - global variable in phpunit.
say, you want Request::root() to return 'my.site'
but you cannot touch phpunit.xml
you can simply set an env param like so
$_ENV['APP_URL'] = 'my.site';
and call $this->refreshApplication(); in your unittest.
viola, your request()->root() is giving you my.site now.
I am encountering a very strange thing while doing testing with Laravel 4. It looks like a bug but there's probably a logical explanation.
I have replicated the "bug" in a clean Laravel install and here's my code:
My resource controller /app/controllers/TestController.php:
(Created with php artisan controller:make TestController)
class TestController extends \BaseController {
/**
* Display a listing of the resource.
*
* #return Response
*/
public function index()
{
return Response::json(array());
}
// The end of the file is unchanged
In my app/routes.php:
Route::get('/', function()
{
return View::make('hello');
});
// Note the composed part of the URL.
// My problem isn't present if I just use `myapi` or `path` as a url
Route::resource('myapi/path', 'TestController');
Added in /app/test/ExampleTest.php:
public function testTest()
{
$res = $this->call('GET', 'myapi/path');
// Until here everything works right
$this->assertEquals(json_decode($res->getContent()), array());
// Now I call the same URL a second time
$res = $this->call('GET', 'myapi/path');
$this->assertEquals(json_decode($res->getContent()), array());
}
Now I run phpunit and here's what I get:
There was 1 error:
1) ExampleTest::testTest
Symfony\Component\HttpKernel\Exception\NotFoundHttpException:
/home/me/Web/laraveltest/bootstrap/compiled.php:5531
/home/me/Web/laraveltest/bootstrap/compiled.php:4848
/home/me/Web/laraveltest/bootstrap/compiled.php:4836
/home/me/Web/laraveltest/bootstrap/compiled.php:4828
/home/me/Web/laraveltest/bootstrap/compiled.php:721
/home/me/Web/laraveltest/bootstrap/compiled.php:702
/home/me/Web/laraveltest/vendor/symfony/http-kernel/Symfony/Component/HttpKernel/Client.php:81
/home/me/Web/laraveltest/vendor/symfony/browser-kit/Symfony/Component/BrowserKit/Client.php:332
/home/me/Web/laraveltest/vendor/laravel/framework/src/Illuminate/Foundation/Testing/ApplicationTrait.php:51
/home/me/Web/laraveltest/app/tests/ExampleTest.php:25
In my other project I get a slightly different backtrace, but I have the impression that's the same problem: (but I have no idea of why the other is compiled and this one not)
2) UnitModelTest::testOther
Symfony\Component\HttpKernel\Exception\NotFoundHttpException:
/home/me/Web/my-project/vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php:148
/home/me/Web/my-project/vendor/laravel/framework/src/Illuminate/Routing/Router.php:1049
/home/me/Web/my-project/vendor/laravel/framework/src/Illuminate/Routing/Router.php:1017
/home/me/Web/my-project/vendor/laravel/framework/src/Illuminate/Routing/Router.php:996
/home/me/Web/my-project/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:775
/home/me/Web/my-project/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:745
/home/me/Web/my-project/vendor/symfony/http-kernel/Symfony/Component/HttpKernel/Client.php:81
/home/me/Web/my-project/vendor/symfony/browser-kit/Symfony/Component/BrowserKit/Client.php:327
/home/me/Web/my-project/vendor/laravel/framework/src/Illuminate/Foundation/Testing/ApplicationTrait.php:51
/home/me/Web/my-project/app/tests/UnitModelTest.php:32
In both case the line given in the trace for the test file corresponds to the second call of the test.
As I noted in the comments of the routes.php file, if I use a simple url with no slash, the test passes without problem.
I have no problem when I use the api from the browser.
I found many topics related to the NotFoundHttpException on StackOverflow, but none looks like mine. This one is specifically present when testing and only trigger an error at the second call.
So what am I doing wrong here ? Or is it really a bug ?
The problem is that calls made with the same client will use the provided URI relatively. That means what you actually call is:
myapi/path
myapi/myapi/path
You can fix this if you add a preface the urls with a / to make them absolute to the root of the application.
public function testTest()
{
$res = $this->call('GET', '/myapi/path');
// Until here everything works right
$this->assertEquals(json_decode($res->getContent()), array());
// Now I call the same URL a second time
$res = $this->call('GET', '/myapi/path');
$this->assertEquals(json_decode($res->getContent()), array());
}
If you experience other issues like that it often helps to call
$this->refreshApplication();
(This would also create a new client and therefore solve this issue as well)
In my case this error happened because i change public directory name to public_html my solution was put this in the \App\Providers\AppServiceProvider register method.
public function register()
{
// ...
$this->app->bind('path.public', function() {
return base_path('public_html');
});
}
I am using a route in Silex to delete an object from the database. If an object does not exist, a 404 error should be thrown. This works fine in the browser, and the response is received accordingly.
This is my source:
$app->delete("/{entity}/{id}", function(\Silex\Application $app, HttpFoundation\Request $request, $entity, $id) {
// some prep code is here
$deleteObject = $this->em->getRepository($entityClass)->find($id);
if (empty($deleteObject))
$app->abort(404, "$ucEntity with ID $id not found");
// other code comes here...
}
This is my test case:
// deleting the same object again should not work
$client->request("DELETE", "/ccrud/channel/$id");
$this->assertTrue($response->getStatusCode() == 404);
Now phpunit fails with the following error:
1) CrudEntityTest::testDelete
Symfony\Component\HttpKernel\Exception\HttpException: Channel with ID 93 not found
I can see from the message that the 404 was thrown, but I cannot test the response object as planned. I know that in theory I could assert for the exception itself, but that is not what I want to do, I want to get the response (as a browser would do as well) and test for the status code itself.
Anybody any ideas how to reach that or if there is a better way to test this?
Thanks,
Daniel
This is how it is being done in the tests of Silex itself (see here):
public function testErrorHandlerNotFoundNoDebug()
{
$app = new Application();
$app['debug'] = false;
$request = Request::create('/foo');
$response = $app->handle($request);
$this->assertContains('<title>Sorry, the page you are looking for could not be found.</title>', $response->getContent());
$this->assertEquals(404, $response->getStatusCode());
}