Symfony Panther: HttpFoundation Response object is not available when using WebDriver - php

I just start testing in Symfony. And I am using Symfony/Panther to achieve testing.
But I'm facing an error which is :
LogicException: HttpFoundation Response object is not available when using WebDriver.
Here is my test code:
<?php
namespace App\Tests\Controller;
use App\Repository\UserRepository;
use Liip\TestFixturesBundle\Services\DatabaseToolCollection;
use Symfony\Component\Panther\PantherTestCase;
use Symfony\Component\Panther\Client;
class UserManagmentControllerTest extends PantherTestCase
{
/** #var AbstractDatabaseTool */
protected $databaseTool;
private ?Client $client = null;
public function setUp(): void
{
parent::setUp();
if (null === $this->client) {
$this->client = static::createPantherClient();
}
$this->databaseTool = static::getContainer()->get(DatabaseToolCollection::class)->get();
}
public function testShouldLoadAllUsersList()
{
$userRepository = static::getContainer()->get(UserRepository::class);
// retrieve the test user
$testUser = $userRepository->findOneByEmail('user1#domaine.ci');
// simulate $testUser being logged in
// $this->client->loginUser($testUser->setRoles("[\"ROLE_ADMIN\"]"));
$this->client->request('GET', '/admin/liste-des-utilisateurs');
// $this->assertResponseIsSuccessful();
// $this->client->getWebDriver()->findElement(WebDriverBy::name('rgpd'))->click();
$this->assertResponseRedirects();
}
protected function tearDown(): void
{
parent::tearDown();
$this->client->close();
unset($this->databaseTool);
}
}
In Panther documentation on github it is said that we can Use any PHPUnit assertion, including the ones provided by Symfony.
So why do I get this error ?
Thank you in advance
Php version: 8.1.7
Symfony version: 6.1.3
Panther version: 2.0

It's seems it is currently not possible if you use Panther. For simple HTTP requests they recommend to use WebTestCase. Something like this:
use App\Entity\Event;
use App\Repository\EventRepository;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class EventControllerTest extends WebTestCase
{
private KernelBrowser $client;
private EventRepository $repository;
private string $path = '/tester/';
protected function setUp(): void
{
$this->client = static::createClient();
}
public function testIndex(): void
{
$crawler = $this->client->request('GET', $this->path);
self::assertResponseStatusCodeSame(200);
self::assertPageTitleContains('Event index');
// Use the $crawler to perform additional assertions e.g.
// self::assertSame('Some text on the page', $crawler->filter('.p')->first());
}

Related

Symfony Functional Testing - How to mock controller injected service with request(submit)

How can I mock a service in a functional test use-case where a "request"(form/submit) is being made. After I make the request all the changes and mocking I made to the container are lost.
I am using Symfony 4 or 5. The code posted here can be also found here: https://github.com/klodoma/symfony-demo
I have the following scenario:
SomeActions service is injected into the controller constructor
in the functional unit-tests I try to mock the SomeActions functions in order to check that they are executed(it sends an email or something similar)
I mock the service and overwrite it in the unit-tests:
$container->set('App\Model\SomeActions', $someActions);
Now in the tests I do a $client->submit($form); which I know that it terminates the kernel.
My question is: HOW can I inject my mocked $someActions in the container after $client->submit($form);
Below is a sample code I added to the symfony demo app
https://github.com/symfony/demo
in services.yaml
App\Model\SomeActions:
public: true
SomeController.php
<?php
namespace App\Controller;
use App\Model\SomeActions;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* Controller used to send some emails
*
* #Route("/some")
*/
class SomeController extends AbstractController
{
private $someActions;
public function __construct(SomeActions $someActions)
{
//just dump the injected class name
var_dump(get_class($someActions));
$this->someActions = $someActions;
}
/**
* #Route("/action", methods="GET|POST", name="some_action")
* #param Request $request
* #return Response
*/
public function someAction(Request $request): Response
{
$this->someActions->doSomething();
if ($request->get('send')) {
$this->someActions->sendEmail();
}
return $this->render('default/someAction.html.twig', [
]);
}
}
SomeActions
<?php
namespace App\Model;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
class SomeActions
{
private $mailer;
public function __construct(MailerInterface $mailer)
{
$this->mailer = $mailer;
}
public function doSomething()
{
echo 'doSomething';
}
public function sendEmail()
{
echo 'sendEmail';
$email = (new Email())
->from('hello#example.com')
->to('you#example.com')
->subject('Time for Symfony Mailer!')
->text('Sending emails is fun again!')
->html('<p>See Twig integration for better HTML integration!</p>');
$this->mailer->send($email);
}
}
SomeControllerTest.php
<?php
namespace App\Tests\Controller;
use App\Model\SomeActions;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class SomeControllerTest extends WebTestCase
{
public function testSomeAction()
{
$client = static::createClient();
// gets the special container that allows fetching private services
$container = self::$container;
$someActions = $this->getMockBuilder(SomeActions::class)
->disableOriginalConstructor()
->getMock();
//expect that sendEmail will be called
$someActions->expects($this->once())
->method('sendEmail');
//overwrite the default service: class: Mock_SomeActions_e68f817a
$container->set('App\Model\SomeActions', $someActions);
$crawler = $client->request('GET', '/en/some/action');
//submit the form
$form = $crawler->selectButton('submit')->form();
$client->submit($form);
//after submit the default class injected in the controller is "App\Model\SomeActions" and not the mocked service
$response = $client->getResponse();
$this->assertResponseIsSuccessful($response);
}
}
The solution is to disable the kernel reboot:
$client->disableReboot();
It makes sense if ones digs deep enough to understand what's going on under the hood;
I am still not sure if there isn't a more straight forward answer.
public function testSomeAction()
{
$client = static::createClient();
$client->disableReboot();
...

How can I use doctrine method inside my service (Symfony 4)?

I created my first own service in Symfony :
// src/Service/PagesGenerator.php
namespace App\Service;
class PagesGenerator
{
public function getPages()
{
$page = $this->getDoctrine()->getRepository(Pages::class)->findOneBy(['slug'=>$slug]);
$messages = [
'You did it! You updated the system! Amazing!',
'That was one of the coolest updates I\'ve seen all day!',
'Great work! Keep going!',
];
$index = array_rand($messages);
return $messages[$index];
}
}
But I get the error message:
Attempted to call an undefined method named "getDoctrine" of class
"App\Service\PagesGenerator".
I tried then to add in my services.yaml:
PagesGenerator:
class: %PagesGenerator.class%
arguments:
- "#doctrine.orm.entity_manager"
But then I get the error message:
The file "/Users/work/project/config/services.yaml" does not contain
valid YAML in /Users/work/project/config/services.yaml (which is
loaded in resource "/Users/work/project/config/services.yaml").
So, in comments I was saying that is better to let Symfony doing his job and autowiring EntityManager. This is what you should do. Also, can you tell us what Symfony version are you using and if autowiring is enabled (check services.yaml for that)?
<?php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
class PagesGenerator
{
public function __construct(EntityManagerInterface $em) {
$this->em = $em;
}
public function getPages()
{
$page = $this->em->getRepository(Pages::class)->findOneBy(['slug'=>$slug]);
$messages = [
'You did it! You updated the system! Amazing!',
'That was one of the coolest updates I\'ve seen all day!',
'Great work! Keep going!',
];
$index = array_rand($messages);
return $messages[$index];
}
}
With Symfony 4 and the new autowiring you can easily inject certain number of class
To find out, which classes/interface you can use for autowiring, use this command:
bin/console debug:autowiring
We are going to use this one :
Doctrine\ORM\EntityManagerInterface
(doctrine.orm.default_entity_manager)
So let's make it, add this just before getPages function
/**
* #var EntityManagerInterface
*/
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
Then you can use it like this:
$page = $this->em->getRepository(Pages::class)->findOneBy(['slug'=>$slug]);
Hope it helps !
make sure you use proper indent using "spaces" for YAML.
A YAML file use spaces as indentation, you can use 2 or 4 spaces for
indentation, but no tab
read more about this
Before symfony 3.3
for example we have service sms_manager in AppBundle/FrontEndBundle/Services
services:
AppBundle.sms_manager:
class: AppBundle\FrontEndBundle\Services\SmsManager
arguments: [ '#service_container' ,'#doctrine.orm.entity_manager' ]
then your service can receive your arguments in constructor
<?php
namespace AppBundle\FrontEndBundle\Services;
use Symfony\Component\DependencyInjection\ContainerInterface as Container;
class SmsManager {
private $container;
private $DM;
public function __construct( Container $container, \Doctrine\ORM\EntityManager $DM )
{
$this->container = $container;
$this->DM = $DM;
}
/**
* #return \Doctrine\ORM\EntityManager
*/
public function getDoctrine() {
return $this->DM;
}
}
With Symfony 3.3 or more,
Is there a way to inject EntityManager into a service
use Doctrine\ORM\EntityManagerInterface
class PagesGenerator
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
// ...
}

How can I log all requests and responses

Zend Expressive 2
I need all inputs and all outputs.
My, pipeline.php
$app->pipe(\Zend\Stratigility\Middleware\ErrorHandler::class);
$app->pipe(\Zend\Expressive\Helper\ServerUrlMiddleware::class);
$app->pipe(\App\Middleware\LogRequestsMiddleware::class);
$app->pipeRoutingMiddleware();
$app->pipe(\Zend\Expressive\Middleware\ImplicitHeadMiddleware::class);
$app->pipe(\Zend\Expressive\Middleware\ImplicitOptionsMiddleware::class);
$app->pipe(\Zend\Expressive\Helper\UrlHelperMiddleware::class);
$app->pipeDispatchMiddleware();
$app->pipe(\App\Middleware\LogResponseMiddleware::class);
$app->pipe(\Zend\Expressive\Middleware\NotFoundHandler::class);
LogResponseMiddleware dont execute. If Error it need too.
You can log the request and response very easy, just create a middleware:
<?php
namespace Acme\Middleware;
use Psr\Log\LoggerInterface;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class LoggingMiddleware implements MiddlewareInterface
{
/**
* #var Psr\Log\LoggerInterface $logger
*/
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function process(ServerRequestInterface $request, DelegateInterface $delegate) {
// log the request
$this->logger->log(Logger::INFO, 'Request', []);
$response = $delegate->process($request);
// log the response
$this->logger->log(Logger::INFO, 'Response', []);
return $response;
}
}
In your factory you can use any PSR logger like monolog:
<?php
namespace Acme\Middleware;
use Interop\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
class LoggingMiddlewareFactory
{
public function __invoke(ContainerInterface $container)
{
$logger = $container->get(LoggerInterface::class);
return new LoggingMiddleware($logger);
}
}
Finally, the logging middleware must be added to the pipe exactly after ServerUrlMiddleware:
<?php
// config/pipeline.php
$app->pipe(ErrorHandler::class);
$app->pipe(ServerUrlMiddleware::class);
$app->pipe(LoggingMiddleware::class);
That's it.
Correct order:
$app->pipe(\App\Middleware\LogResponseMiddleware::class);
$app->pipe(\Zend\Stratigility\Middleware\ErrorHandler::class);
$app->pipe(\Zend\Expressive\Helper\ServerUrlMiddleware::class);
$app->pipeRoutingMiddleware();
$app->pipe(\Zend\Expressive\Middleware\ImplicitHeadMiddleware::class);
$app->pipe(\Zend\Expressive\Middleware\ImplicitOptionsMiddleware::class);
$app->pipe(\Zend\Expressive\Helper\UrlHelperMiddleware::class);
$app->pipeDispatchMiddleware();
$app->pipe(\App\Middleware\LogRequestsMiddleware::class);
$app->pipe(\Zend\Expressive\Middleware\NotFoundHandler::class);
Details here: https://framework.zend.com/blog/2017-03-15-nested-middleware-in-expressive.html

Check user in every controller in Symfony

I have a user object that has a property 'enabled'. I want every action to first check if the user is enabled before continuing.
Right now I have solved it with a Controller that every other controller extends, but using the setContainer function to catch every Controller action feels really hacky.
class BaseController extends Controller{
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
$user = $this->getUser();
// Redirect disabled users to a info page
if (!$user->isEnabled() && !$this instanceof InfoController) {
return $this->redirectToRoute('path_to_info');
}
}
I have tried building this using a before filter (http://symfony.com/doc/current/event_dispatcher/before_after_filters.html), but could not get the User object..any tips?
EDIT:
This is my solution:
namespace AppBundle\Security;
use AppBundle\Controller\AccessDeniedController;
use AppBundle\Controller\ConfirmController;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Bundle\TwigBundle\Controller\ExceptionController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
class UserEnabledListener
{
private $tokenStorage;
private $router;
public function __construct(TokenStorage $tokenStorage, Router $router)
{
$this->tokenStorage = $tokenStorage;
$this->router = $router;
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
/*
* $controller passed can be either a class or a Closure.
* This is not usual in Symfony but it may happen.
* If it is a class, it comes in array format
*/
if (!is_array($controller)) {
return;
}
$controller = $controller[0];
// Skip enabled check when:
// - we are already are the AccessDenied controller, or
// - user confirms e-mail and becomes enabled again, or
// - Twig throws error in template
if ($controller instanceof AccessDeniedController ||
$controller instanceof ConfirmController ||
$controller instanceof ExceptionController) {
return;
}
$user = $this->tokenStorage->getToken()->getUser();
// Show info page when user is disabled
if (!$user->isEnabled()) {
$redirectUrl = $this->router->generate('warning');
$event->setController(function() use ($redirectUrl) {
return new RedirectResponse($redirectUrl);
});
}
}
}
EDIT 2:
Ok so turns out checking for each controller manually is really bad, as you will miss Controllers from third party dependencies. I'm going to use the Security annotation and do further custom logic in a custom Exception controller or template etc.
You can use an event listener to listen for any new request.
You'll need to inject the user and then do your verification:
<service id="my_request_listener" class="Namespace\MyListener">
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" />
<argument type="service" id="security.token_storage" />
</service>
Edit: Here is a snippet to give an example
class MyRequestListener {
private $tokenStorage;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function onKernelRequest(GetResponseEvent $event)
{
if (!$event->getRequest()->isMasterRequest()) {
// don't do anything if it's not the master request
return;
}
if ($this->tokenStorage->getToken()) {
$user = $this->tokenStorage->getToken()->getUser();
//do your verification here
}
}
In your case I would use the #Security annotation, which can be very flexible if you use the expression language.
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
/**
* #Security("user.isEnabled()")
*/
class EventController extends Controller
{
// ...
}
In the end it's only 1 line in each of your controller files, and it has the advantage of being very readable (a developer new to the project would know immediately what is going on without having to go and check the contents of a BaseController or any potential before filter...)
More documentation on this here.
You can override also getuser() function in your BaseController also.
/**
* Get a user from the Security Token Storage.
*
* #return mixed
*
* #throws \LogicException If SecurityBundle is not available
*
* #see TokenInterface::getUser()
*/
protected function getUser()
{
if (!$this->container->has('security.token_storage')) {
throw new \LogicException('The SecurityBundle is not registered in your application.');
}
if (null === $token = $this->container->get('security.token_storage')->getToken()) {
return;
}
if (!is_object($user = $token->getUser())) {
// e.g. anonymous authentication
return;
}
// Redirect disabled users to a info page
if (!$user->isEnabled() && !$this instanceof InfoController) {
return $this->redirectToRoute('path_to_info');
}
return $user;
}

FOSUserBundle : get repository from inside FormHandler

I need to set a default value to a new user before saving it.
The problem is that I can't find a way to get an object through its repository from inside the FormHandler.
<?php
namespace Acme\UserBundle\Form\Handler;
use FOS\UserBundle\Form\Handler\RegistrationFormHandler as BaseHandler;
use FOS\UserBundle\Model\UserInterface;
class RegistrationFormHandler extends BaseHandler
{
protected function onSuccess(UserInterface $user, $confirmation)
{
$repository = $this->container->get('doctrine')->getEntityManager()->getRepository('AcmeUserBundle:Photo');
if($user->isMale()){
$photo = $repository->getDefaultForMale();
$user->setPhoto($photo);
}
else {
$photo = $repository->getDefaultForFemale();
$user->setPhoto($photo);
}
parent::onSuccess($user, $confirmation);
}
}
The problem comes from the following line :
$repository = $this->container->get('doctrine')->getEntityManager()->getRepository('AcmeUserBundle:Photo');
... and I can't find a way to get this repository, or the entity manager from this FormHandler.
Many thanks for your help !
A
You have to define a service that reference your extended handler class and point it in app/config.yml. e.g
The class,
//namespace definitions
class MyHandler extends RegistrationFormHandler{
private $container;
public function __construct(Form $form, Request $request, UserManagerInterface $userManager, MailerInterface $mailer, ContainerInterface $container)
{
parent::__construct($form, $request, $userManager, $mailer);
$this->container = $container;
}
protected function onSuccess(UserInterface $user, $confirmation)
{
$repository = $this->container->get('doctrine')->getEntityManager()->getRepository('AcmeUserBundle:Photo');
// your code
}
The service,
my.registration.form.handler:
scope: request
class: FQCN\Of\MyHandler
arguments: [#fos_user.registration.form, #request, #fos_user.user_manager, #fos_user.mailer, #service_container]
Lastly in app/config.yml,
fos_user:
#....
registration:
#...
form:
handler: my.registration.form.handler
FOS got his own UserManager. Try to use this.

Categories