Unit test: Simulate a timeout with Guzzle 5 - php

I am using Guzzle 5.3 and want to test that my client throws a TimeOutException.
Then, how can I do a mock of Guzzle Client that throw a GuzzleHttp\Exception\ConnectException?
Code to test.
public function request($namedRoute, $data = [])
{
try {
/** #noinspection PhpVoidFunctionResultUsedInspection */
/** #var \GuzzleHttp\Message\ResponseInterface $response */
$response = $this->httpClient->post($path, ['body' => $requestData]);
} catch (ConnectException $e) {
throw new \Vendor\Client\TimeOutException();
}
}
Update:
The right question was: how to throw a Exception with Guzzle 5? or, how to test a catch block with Guzzle 5?

You can test code inside a catch block with help of the addException method in the GuzzleHttp\Subscriber\Mock object.
This is the full test:
/**
* #expectedException \Vendor\Client\Exceptions\TimeOutException
*/
public function testTimeOut()
{
$mock = new \GuzzleHttp\Subscriber\Mock();
$mock->addException(
new \GuzzleHttp\Exception\ConnectException(
'Time Out',
new \GuzzleHttp\Message\Request('post', '/')
)
);
$this->httpClient
->getEmitter()
->attach($mock);
$this->client = new Client($this->config, $this->routing, $this->httpClient);
$this->client->request('any_route');
}
In the unit test, I add the GuzzleHttp\Exception\ConnectException to the mock. After, I add the mock to the emitter and, finally, I call the method I want test, request.
Reference:
Source Code
Mockito test a void method throws an exception

Related

Invalid JSON in Tests Authentifications PHPUnit

Good morning all , I did a complete migration of my symfony 2.8 application to version 5.4. I am now at the unit testing stage. I copied the tests to my new project, however I'm having some difficulties with the API authentication. I launched the unit tests without modification with the classic configuration of rest bundle. In my tests, I test the authentication of a user upstream in order to recover the rights necessary to test the different endpoints. When I want to authenticate with _username, and _password, I get the following error in my response content:
<!-- Invalid JSON. (400 Bad Request) -->
Here is the content of my authUser function of my abstract class which allows to authenticate the user :
<?php
namespace WORD\UserBundle\Tests\Model;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\FrameworkBundle\Client;
abstract class AbstractAuthTestCase extends WebTestCase
{
/**
* #var Client
*/
protected $client = null;
/**
* ContainerInterface
*/
//protected $container;
// public function setUp(): void
// {
// self::ensureKernelShutdown();
// $this->client = static::createClient();
// // $this->container = $this->client->getContainer();
// $this->client->setServerParameter('HTTPS', true);
// $this->client->setServerParameter('HTTP_HOST', self::$container->getParameter('api_host'));
// $this->client->setServerParameter('HTTP_CONTENT_TYPE', 'application/json');
// }
public function authUser($username='stack.fr', $password='overflow', $referer='https://stack.over.local/')
{
$this->newClient();
$this->client->setServerParameter('HTTP_REFERER', $referer);
$this->client->request('POST', '/api/login_check', array(
'_username' => $username,
'_password' => $password,
));
$response = json_decode($this->client->getResponse()->getContent(), true);
if (isset($response['token'])) {
$this->client->setServerParameter('HTTP_AUTHORIZATION', 'Bearer ' . $response['token']);
}
return $response;
}
private function newClient()
{
$this->client = static::createClient();
//$this->container = $this->client->getContainer();
$this->client->setServerParameter('HTTPS', true);
$this->client->setServerParameter('HTTP_HOST', self::$container->getParameter('api_host'));
$this->client->setServerParameter('HTTP_CONTENT_TYPE', 'application/json');
}
}
Could you tell me if I forgot something please? I'm working on Symfony 5.4 with the FOS/RESTBundle 3.4
According to the documentation, this test method is still functional on symfony. 5 version. Thank you for your help
I finally found the solution to my problem.
I had to change the format of the query:
$this->client->request(
'POST',
'/api/login_check',
[],
[],
['CONTENT_TYPE' => 'application/json'],
'{"_username":"'.$username.'","_password": "'.$password.'" }'
);

How can I set the response code from the DataPersister

I'm working with Symfony 4.4 / API Platform, and I'm trying to return the response from the DataPersister or to set up its code.
In my DataPersister, I test if the Admin->isManager() is true, So Admin can never be deleted, So in this case I want to return a custom status code in my response 414, and a message "thisAdminIsManager"
AdminDataPersister:
final class AdminDataPersister implements ContextAwareDataPersisterInterface
{
/* #var EntityManagerInterface */
private $manager;
public function __construct(
EntityManagerInterface $manager
){
$this->manager = $manager;
}
public function supports($data, array $context = []): bool
{
return $data instanceof Admin;
}
public function persist($data, array $context = [])
{
$this->manager->persist($data);
$this->manager->flush();
}
public function remove($data, array $context = [])
{
/* #var Admin $data */
#The Manager can never be deleted:
if( $data->getManager() ){
return; //here I want to return the custom response
}
$this->manager->remove($data);
$this->manager->flush();
}
You should throw an exception, then you should configurate your api_platform to handle this exception. ApiPlatform will convert exception into a response with message and the specified code.
Step1: Create dedicated exception class
<?php
// api/src/Exception/ProductNotFoundException.php
namespace App\Exception;
final class AdminNonDeletableException extends \Exception
{
}
Step2: In your data persister, throw exception:
public function remove($data, array $context = [])
{
/* #var Admin $data */
#The Manager can never be deleted:
if( $data->getManager() ){
throw new AdminNonDeletableException('thisAdminIsManager');
}
$this->manager->remove($data);
$this->manager->flush();
}
Step3: Add your exception in the config/package/api_platform.yaml file and declare the code number (414)
# config/packages/api_platform.yaml
api_platform:
# ...
exception_to_status:
# The 4 following handlers are registered by default, keep those lines to prevent unexpected side effects
Symfony\Component\Serializer\Exception\ExceptionInterface: 400 # Use a raw status code (recommended)
ApiPlatform\Core\Exception\InvalidArgumentException: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST
ApiPlatform\Core\Exception\FilterValidationException: 400
Doctrine\ORM\OptimisticLockException: 409
# Custom mapping
App\Exception\AdminNonDeletableException: 414 # Here is the handler for your custom exception associated to the 414 code
You can find more information in error handling chapter

Adding new route to test client in Symfony

I am trying to add new route in test case extending Symfony\Bundle\FrameworkBundle\Test\KernelTestCase that will throw exception, to test EventListener for it. Simplified tests will looks like this:
public function testHandlingException()
{
$kernel = $this->createKernel(['environment' => 'test', 'debug' => true]);
$kernel->boot();
$client = $kernel->getContainer()->get('test.client');
$controller = new class extends Controller {
public function testAction()
{
throw new \Exception();
}
};
$route = new Route(
'route_500',
['_controller' => get_class($controller).'::testAction']
);
$client
->getContainer()
->get('router')
->getRouteCollection()
->add('route_500', $route);
$this->expectException(\Exception::class);
$client->request('GET', '/route_500');
}
It fails with:
Failed asserting that exception of type "Exception" is thrown.
How can I add route in-fly so calling it will work?
You can catch exceptions in unit tests. Not in end2end ones. If a GET request throw an exception you can get it: but you must catch that status code is 500 (for example). Another check could be that the response is containing the exception message.

PHPUnit How to constraint mock stub method input with array subset containing instanceOf?

I want to test pretty specific piece of code, but I can't find a good way to do it. I have such code:
public function foo()
{
try {
//...some code
$this->service->connectUser();
} catch (\OAuth2Exception $e) {
$this->logger->error(
$e->getMessage(),
['exception' => $e]
);
}
}
And I want to test if the exception was thrown and logged to $this->logger. But I can't find a good way to do it. Here is how I do it currently.
public function testFoo()
{
$oauthException = new \OAuth2Exception('OAuth2Exception message');
//This is a $service Mock created with $this->getMockBuilder() in test case injected to AuthManager.
$this->service
->method('connectUser')
->will($this->throwException($oauthException));
//This is a $logger Mock created with $this->getMockBuilder() in test case injected to AuthManager.
$this->logger
->expects($this->once())
->method('error')
->with(
$this->isType('string'),
$this->logicalAnd(
$this->arrayHasKey('exception'),
$this->contains($oauthException)
)
);
//AuthManager is the class beeing tested.
$this->authManager->foo($this->token);
}
This will test if error method was called with certain parameters, but array key 'exception' and exception object can exist in different parts of the array. What I mean is that test will pass for such error method call:
$this->logger->error(
$e->getMessage(),
[
'exception' => 'someValue',
'someKey' => $e,
]
);
I would like to make sure that error method will always receive such subset ['exception' => $e]. Something like this would be perfect:
$this->logger
->expects($this->once())
->method('error')
->with(
$this->isType('string'),
$this->arrayHasSubset([
'exception' => $oauthException,
])
);
Is it possible to achieve with PHPUnit?
You can use the callback() constraint:
public function testFoo()
{
$exception = new \OAuth2Exception('OAuth2Exception message');
$this->service
->expects($this->once())
->method('connectUser')
->willThrowException($exception);
$this->logger
->expects($this->once())
->method('error')
->with(
$this->identicalTo($exception->getMessage()),
$this->logicalAnd(
$this->isType('array'),
$this->callback(function (array $context) use ($exception) {
$expected = [
'exception' => $exception,
];
$this->assertArraySubset($expected, $context);
return true;
})
)
);
$this->authManager->foo($this->token);
}
See https://phpunit.de/manual/current/en/test-doubles.html#test-doubles.mock-objects:
The callback() constraint can be used for more complex argument verification. This constraint takes a PHP callback as its only argument. The PHP callback will receive the argument to be verified as its only argument and should return true if the argument passes verification and false otherwise.
Also note how I adjusted setting up your test doubles:
expect the method connectUser() to be invoked exactly once
use $this->willThrowException() instead of $this->will($this->throwException())
use $this->identicalTo($exception->getMessage()) instead of the more loose $this->isType('string')
I always try to make an argument to be as specific as possible, and only loosen constraints on intention.
You can try PHPUnit spies as described in https://lyte.id.au/2014/03/01/spying-with-phpunit/
With spy you can do something like
$this->logger->expects($spy = $this->any())->method('error');
$invocations = $spy->getInvocations();
/**
* Now $invocations[0]->parameters contains arguments of first invocation.
*/

Symfony: How to return a JSON response from a Before Filter (Listener)?

From following this example I have managed to set up the below Listener/Before Filter to parse all requests to my API endpoint (ie. /api/v1) before any controller actions are processed. This is used to validate the request type and to throw an error if certain conditions are not met.
I would like to differentiate the error response based on the applications environment state. If we are in development or testing, I simply want to throw the error encountered. Alternatively, if in production mode I want to return the error as a JSON response. Is this possible? If not, how could I go about something similar?
I'm using Symfony v3.1.*, if that makes any difference.
namespace AppBundle\EventListener;
use AppBundle\Interfaces\ApiAuthenticatedController;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class ApiBeforeListener
{
/**
* #var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* Validates a request for API resources before serving content
*
* #param \Symfony\Component\HttpKernel\Event\FilterControllerEvent $event
* #return mixed
*/
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller))
return;
if ($controller[0] instanceof ApiAuthenticatedController) {
$headers = $event->getRequest()->headers->all();
// only accept ajax requests
if(!isset($headers['x-requested-with'][0]) || strtolower($headers['x-requested-with'][0]) != 'xmlhttprequest') {
$error = new AccessDeniedHttpException('Unsupported request type.');
if (in_array($this->container->getParameter("kernel.environment"), ['dev', 'test'], true)) {
throw $error;
} else {
// return json response here for production environment
//
// For example:
//
// header('Content-Type: application/json');
//
// return json_encode([
// 'code' => $error->getCode(),
// 'status' => 'error',
// 'message' => $error->getMessage()
// ]);
}
}
}
}
}
Unlike most events, the FilterControllerEvent does not allow you to return a response object. Be nice if it did but oh well.
You have two basic choices.
The best one is to simply throw an exception and then add an exception listener. The exception listener can then return a JsonResponse based on the environment.
Another possibility to to create a controller which only returns your JsonResponse then use $event->setController($jsonErrorController) to point to it.
But I think throwing an exception is probably your best bet.
More details here: http://symfony.com/doc/current/reference/events.html

Categories