Symfony and PHPUnit: Exception thrown but not intercepted by setExpectedException - php

I wrote a test for a controller that saves in the database some data passed by a form.
I wrote the following test method to be sure that if the form is empty an exception is thrown:
public function testRegisterNewMerchantExceptionNoDataSubmitted()
{
$client = static::createClient();
$crawler = $client->request('GET', '/getstarted');
$form = $crawler->selectButton('getStarted[submit]')->form();
$form['getStarted[email]'] = '';
$this->setExpectedException('DomainException');
$client->submit($form);
$this->assertEquals(500, $client->getResponse()->getStatusCode());
//dump($client->getResponse());die;
}
The method i'm testing is the following:
public function endAction(Request $request)
{
$form = $this->createForm(new GetStartedType());
$form->handleRequest($request);
if ($form->isValid()) {
// Get data from form
$data = $form->getData();
} else {
throw new \DomainException('No data submitted.');
}
...
I'm sure that also during tests the exception is thrown because dumping the Response object the page is a 500 error reporting the exact message "No data submitted". More, the assertEquals test on the status code is successful, so there are no doubts that the exception is correctly thrown.
But the $this->setExpectedException() test doesn't intercept it and returns a fail of the test.
Any idea about why this happens?

Using $this->setExcpectedException() tells PHPUnit to expect the given exception type to be thrown from the test method, not just that an exception of that type is thrown at some point during execution.
When you throw an exception in a controller method, the Symfony controller catches
that exception and creates a 500 response. This means the exception will not be thrown from the test method, so the test fails. Your test looks reasonable otherwise, so removing $this->setExpectedException() should solve the problem and test the behavior you intended.

Related

Low-level exception class to target all exceptions

I'm trying to modify my App\Exceptions\Handler to pass the request (and therefore current URL) through to all exceptions. For this reason I need the lowest-level exception class I can get hold of to type-hint to the ->renderable() method.
Laravel/Symfony's HttpException works but only for HTTP errors, leaving out all non-HTTP exceptions. PHP's Exception class works when using getCode() instead of getStatusCode(), but always returns a "0" for both HTTP errors and exceptions. Is there another low-level exception class that will work for my purposes, or otherwise any other way to accomplish what I'm trying to do here?
public function register()
{
$this->renderable(function (Exception $exception, $request) {
$url = $request->fullUrl();
$status = $exception->getCode();
Log::warning("Error $status when trying to visit $url. Received the following message: " . $exception->getMessage());
return response()->view("errors.$status", [
"exception" => $exception
],
$status
);
});
}
}
For what it's worth, I'm using the following web routes to trigger exceptions and HTTP errors for testing:
if (app()->environment('local')) {
Route::get("/exception", function (){
throw new JsonException; // chosen because it's one of the few Laravel exceptions
// that doesn't seem to automatically resolve to a HTTP error
});
}
if (app()->environment('local')) {
Route::get("/fail/{status}", function ($status){
abort($status);
});
}
As requested, this is what I have in my Handler. I use some custom logging, and I want to make sure I grab the right code when it's an HTTP error.
public function report(Throwable $e)
{
$code = match (get_class($e)) {
'Symfony\Component\HttpKernel\Exception\NotFoundHttpException' => 404,
\HttpException::class => $e->getStatusCode(),
default => 'No Code',
};
// more stuff here
}
You can use $e->getCode() for your default as well
You can throw your JsonException and abort like so with a given code and the handler should grab it from getCode like so
// in your controller
throw new \JsonException('Something went wrong', 500);
// or
abort(500, 'Something went wrong')
// in your handler
$status = $e->getCode(); // 500
$message = $e->getMessage(); // "Something went wrong"
That said it's better to keep them as semantically separate as possible in my opinion, and let the handler do the handling depending on what it receives.
I finally managed to figure this out in the end. It's probably not the cleanest solution, but it works perfectly for my needs.
It works by inspecting each instance of the Exception class and using PHP's instanceof() to check whether it's a HTTP exception or not. If it is, it gets logged with the request URL and returns a view with a status code. If it's a generic non-HTTP exception, it gets logged with the request URL and returns another view with no status code (or you can keep the default exception behaviour by removing the return block, which renders a blank screen in production).
public function register()
{
$this->renderable(function (Exception $exception, $request) {
$url = $request->fullUrl();
if ($exception instanceof HttpException) {
$status = $exception->getStatusCode();
Log::warning("Error $status occurred when trying to visit $url. Received the following message: " . $exception->getMessage());
return response()->view("errors.error", [
"exception" => $exception,
"status" => $status
],
$status
);
} else {
$status = $exception->getCode();
Log::warning("Exception $status occurred when trying to visit $url. Received the following message: " . $exception->getMessage());
return response()->view("errors.exception", [
"exception" => $exception,
"status" => $status
]);
}
});
// Optionally suppress all Laravel's default logging for exceptions, so only your own logs go to the logfile
$this->reportable(function (Exception $e) {
})->stop();
}

passing exception to controller without try/catch

I have made my custom Exception defined in $this->exception($response['error']); .
So in my service method:
$response = $this->request('POST', $endpoint, $options);
if (array_key_exists('error', $response)) {
$this->exception($response['error']);
}
Which is returning error message if key error exists in response.
And in my controler:
$response = $this->serviceMethod();
if (!empty($response)) {
return $this->apiError(['error' => 'VIOLATION']);
}
If service method pass, response will return empty array which is fine.
Problem occurs that if service method catches error in if() statement and I am dumping controller method response it never gets to that part. It finishes with that custom exception.
How can I accomplish to pass response in both ways so I can handle in in my apiError() method which is made to write error from response.
It's like:
protected function apiError(array $errors = [], $message = ''): void
{
throw new ApiException($errors, $message)
}

How can I change a Symfony exception response code

Symfony 3.4
PHP 7.4
I am trying to map
Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException
to another response code. Basically like Laravel did: https://github.com/laravel/framework/pull/29000/files
Currently it creates a fatal error and 500 response:
PHP Fatal error: Uncaught Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException: ...
I want to return a 404 as response.
If you want to do like in Laravel, you should take a look at Event Listener.
public function onKernelException(ExceptionEvent $event)
{
// You get the exception object from the received event
$exception = $event->getThrowable();
// Customize your response object to display the exception details
$response = new Response();
if ($exception instanceof SuspiciousOperationException) {
$response->setStatusCode(404);
} else {
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
}
// sends the modified response object to the event
$event->setResponse($response);
}

How to remove default Laravel Exception message from Custom Exception message

I have this get organisations method in one project that talks to a central api project that handles all data like so:
public function searchOrganisations()
{
try {
return $this->client->request(.....);
} catch (Exception $ex) {
}
}
Within the api project a method is then hit and if a certain time frame criteria is hit I throw a custom exception like so:
public function searchOrganisations($searchRequest)
{
$experianCutOff = Carbon::createFromFormat('H:i:s', '06:00:00');
$now = Carbon::now()->setTime(02, 0, 0);
if (!$now->lt($experianCutOff)) {
return $data
} else {
throw new ExperianServiceException();
}
}
My custom exeption is as follows:
class ExperianServiceException extends Exception
{
public function render() {
return response()->json([
'message' => 'The Experian Service is currently unavailable, please try again at 0600 GMT'
], 503);
}
}
This works as expected and I catch the exception in the first method listed, I can access the status 503 and can see the message, however the message property of the exception always comes back in this format:
Server error: `POST http://docker.../search-organisations` resulted in a `503 Service Unavailable` response:
{"message":"The Experian Service is currently unavailable, please try again at 0600 GMT"}
It seems as though my supplied custom message has been concatenated with the standard Laravel Exception message (which I dont want). How can I make sure my message only contains what I supplied in my custom exception?
You could have your Exception implement Illuminate\Contracts\Support\Responsable and define the toResponse method to return that response. When you only have the render method the framework is still potentially going to do things to prepare the response. If it is Responsable it calls toResponse on it and returns that directly.
This causes a raw response from toResponse of your Exception to be returned without passing through any other parts of the Handler to be prepared in any way.
Maybe:
return Response::json(array(
'code' => 404,
'message' => $message
), 404);

Ignore and continue on specific Symfony2 kernel.exception event

Scenario:
The Symfony2 project works with main database and a secondary (remote) for remote data. The remote database can be down or the connection between them might be severed. In such case I still want the execution to continue as normal albeit with a notice that the remote is down.
I have so far tried this in my event listener:
public function onKernelException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
if (
$exception instanceof ConnectionException ||
$exception instanceof PDOException ||
$exception instanceof \PDOException ||
$exception instanceof DBALException
) {
$this->flash->flashMessage('remote_server_connection_error', array(), FlashBagUtil::TYPE_ALERT);
// $kernel = $event->getKernel();
// $request = $event->getRequest();
// $response = $kernel->handle($request, HttpKernelInterface::MASTER_REQUEST, true);
// $response->setStatusCode(Response::HTTP_OK);
// $event->setResponse($response);
$event->stopPropagation();
echo 'works';
}
}
If I uncomment the response part, I get the page retuned as expected but with a status 500, not 200 as I've explicitly set. "works" gets echoed so event part is working fine.
What's the correct way of dealing with an exception and ignoring it for the rest of the request from within Symfony2 kernel.exception listener?
Many thanks
As said in the documentation, for kernel.exception event :
As Symfony ensures that the Response status code is set to the most appropriate one depending on the exception, setting the status on the response won't work. If you want to overwrite the status code (which you should not without a good reason), set the X-Status-Code header
So you will have to set this header :
$response->headers->set('X-Status-Code', Response::HTTP_OK);

Categories