Session in Symfony (5.3/5.4) functional tests - phpunit

Background
I updated from Symfony 5.3 to Symfony 5.4.
Since then I get an error message, when I start my PHPunit-tests.
The error message
Since symfony/framework-bundle 5.3: Accessing the "session"
service directly from the container is deprecated, use
dependency injection instead.
The code of my test
class BookingControllerTest extends WebTestCase
{
public function testBookingStep4()
{
// ...
// create the CLIENT
self::ensureKernelShutdown();
$this->client = static::createClient();
// generate CART (simulating previous steps)
$cart = $this->generateCart($person, $items);
// put CART into SESSION
$session = $this->client->getContainer()->get('session'); // <-- THROWS THE WARNING
$session->set('cart', $cart);
// make the REQUEST (which is using the data in the session)
$this->client->request('GET', "booking/finalization");
$this->assertResponseStatusCodeSame(200);
// ...
}
}
What I did so far ...
// tried to replace this line
$session = $this->client->getContainer()->get('session');
// by this and ...
$session = $this->client->getContainer()->get('request_stack')->getSession();
// by this and ...
$session = $this->client->getContainer()->get(RequestStack::class)->getSession();
// by that
$session = new Session(new MockFileSessionStorage());
No solutions solved the issue.
Solution (open)
What can I do? Anybody an idea?
Appendix (the controller that gets tested)
class BookingController extends AbstractController
{
private $cart;
/**
* #Route("booking/finalization", name="app_booking_finalization")
*/
public function booking_finalization(Request $request, RequestStack $requestStack)
{
$session = $requestStack->getSession();
$this->cart = $session->get('cart');
// check requirements
$isValid = $this->bookingHelper->isCartValid($this->cart);
// redirect if requirements are not fulfilled
if (!$isValid) {
return $this->redirectToRoute('app_booking_checkout');
}
// ...
}

Related

Vich UploadedFile, FOSUserBundle and Events

I added a VichImageType field on edit profile twig template...so im trying to check image dimensions using vich_uploader.pre_upload as Event.
In my class i got the image properties and if their dimensions arent bigger enough i tried to stop propagation and flashed a message to the twig template but, i dont know why, the event keeps propagating and redirects to fos_user_profile_show showing the image setup. Also, i tried to redirect again to fos_user_profile_edit but i cant use $event because "Vich\UploaderBundle\Event\Event" doesnt implement setController(). How can achieve this?
This is the method of the Listener class:
namespace BackendBundle\EventListener;
use Vich\UploaderBundle\Event\Event;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
class ComprobarDimensionesDeImagen
{
private $requestStack;
private $router;
public function __construct(RequestStack $requestStack, RouterInterface $router)
{
$this->requestStack = $requestStack;
$this->router = $router;
}
public function onVichUploaderPreUpload(Event $event)
{
$object = $event->getObject();
$mapping = $event->getMapping();
$imagen = getimagesize($object->getImageFile());
if (250 > $imagen[0] || 250 > imagen[1]) {
$request = $this->requestStack->getCurrentRequest();
$session = $request->getSession();
$event->stopPropagation();
$session->getFlashBag()->add('error', "Minimum dimensions: 250x250 \n");
$url = $this->router->generate('fos_user_profile_edit');
/*
* Testing different methods of redirect
*
* $response = new RedirectResponse($url);
* $event->setResponse($response);
*/
$event->setController(function() use ($request) {
return new RedirectResponse($url);
});
}
}
}
When i edit the profile again, I can see the flash message and the image setup in VichImageType field (i didnt expect that stopping propagation). Any help will be very welcome.
SOLVED: Just using #Assert\Image in my Entity class did the validation. No service neither listener needed
The argument for ->setController must be a callable . In your case, the function you pass as an argument returns an object of type Response. In order to be callable, the method should have the suffix Action. See also this post.

Why PHPUnitTest WebTestCase takes into account previous test?

I have a Symfony 3.2 project with a backend. Each entity has its CRUD Controllers, Views etc. I have prepared an
abstract class AbstractControllerTest extends WebTestCase that is a base for tests for each entity. For each entity I use a simple test that asserts that list, show, edit and new returns HTTP 200.
So when I run all test it test list, show etc for each Entity. The problem is that in list Controller I use KNPPaginator with default order. The Controller works OK but when I run tests and it gets to the second entity I get 500 error because of a missing entity field. It turns out that the test takes a list Query for Pager from previous test.
So Entity A is ordered by default with a position field. Entity B doesn't have position field and that cause the error. So when PHPUnit goes to test A Entity it is OK, then it moves to test B Entity and then there is an error.
I don't know what is going on because ordering is not saved in session so there is no way that PHPUnit gets query from session from previous Entity.
Any ideas what is going on?
AbstractControllerTest
abstract class AbstractControllerTest extends WebTestCase
{
/** #var Client $client */
public $client = null;
protected $user = '';
protected $prefix = '';
protected $section = '';
protected $entityId = '';
public function setUp()
{
$this->client = $this->createAuthorizedClient();
}
/**
* #return Client
*/
protected function createAuthorizedClient()
{
$client = static::createClient();
$client->setServerParameter('HTTP_HOST', $client->getContainer()->getParameter('test_info_domain'));
$client->setServerParameter('HTTPS', true);
$client->followRedirects();
$container = $client->getContainer();
$session = $container->get('session');
/** #var $userManager \FOS\UserBundle\Doctrine\UserManager */
$userManager = $container->get('fos_user.user_manager');
/** #var $loginManager \FOS\UserBundle\Security\LoginManager */
$loginManager = $container->get('fos_user.security.login_manager');
$firewallName = $this->section;
/** #var UserInterface $userObject */
$userObject = $userManager->findUserBy(array('username' => $this->user));
$loginManager->logInUser($firewallName, $userObject);
// save the login token into the session and put it in a cookie
$container->get('session')->set('_security_' . $firewallName,
serialize($container->get('security.token_storage')->getToken()));
$container->get('session')->save();
$client->getCookieJar()->set(new Cookie($session->getName(), $session->getId()));
return $client;
}
public function testIndex()
{
//CRUD index
$this->client->request('GET', sprintf('/%s/%s',$this->section,$this->prefix));
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
public function testShow()
{
//CRUD show
$this->client->request('GET', sprintf('/%s/%s/%s/show',$this->section,$this->prefix, $this->entityId));
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
public function testEdit()
{
//CRUD edit
$this->client->request('GET', sprintf('/%s/%s/%s/edit',$this->section,$this->prefix, $this->entityId));
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
public function testNew()
{
//CRUD new
$this->client->request('GET', sprintf('/%s/%s/new',$this->section,$this->prefix));
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
}
And an example of one of the test class for Controller for one Entity
class AgendaCategoryControllerTest extends AbstractControllerTest
{
protected $user = 'tom#test.com';
protected $section = 'admin';
protected $prefix = 'agenda-category';
protected $entityId = '40';
}
If I run separately
php phpunit.phar src/Bundle/Tests/Controller/Admin/AControllerTest.php
and
php phpunit.phar src/Bundle/Tests/Controller/Admin/BControllerTest.php
it is OK.
If run together there is this weird bug
php phpunit.phar -c phpunit.xml.dist --testsuite=Admin
You can reset your test client between tests by doing the following in your setUp-method:
public function setUp()
{
$this->client = $this->createAuthorizedClient();
$this->client->restart();
}
You might have to move the restart into your createAuthorizedClient-method to ensure it does not reset your auth info.

FOSUserBundle - PHPUnit - Mock a user

I am using Symfony with the FOSUserBundle and now I like to test some things like:
Doctrine lifecycle
Controller behind firewall
For those tests I need to be a specific user or at least in a user group.
How do I mock a user session so that ...
The lifecycle field like "createdAt" will use the logged in user
The Controller act like some mocked user is logged in
Example:
class FooTest extends ... {
function setUp() {
$user = $this->getMock('User', ['getId', 'getName']);
$someWhereGlobal->user = $user;
// after this you should be logged in as a mocked user
// all operations should run using this user.
}
}
You can do this with LiipFunctionalTestBundle. Once you have installed and configured the Bundle, creating and user and log in in tests is easy.
Create a fixture for your user
This creates a user which will be loaded during tests:
<?php
// Filename: DataFixtures/ORM/LoadUserData.php
namespace Acme\MyBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Acme\MyBundle\Entity\User;
class LoadUserData extends AbstractFixture implements FixtureInterface
{
public function load(ObjectManager $manager)
{
$user = new User();
$user
->setId(1)
->setName('foo bar')
->setEmail('foo#bar.com')
->setPassword('12341234')
->setAlgorithm('plaintext')
->setEnabled(true)
->setConfirmationToken(null)
;
$manager->persist($user);
$manager->flush();
// Create a reference for this user.
$this->addReference('user', $user);
}
}
If you want to use groups of users, you can see the official documentation.
Log in as this user in your test
As explained in LiipFunctionalTestBundle's documentation, here is how to load the user in the database and log in as this user:
/**
* Log in as the user defined in the Data Fixture.
*/
public function testWithUserLoggedIn()
{
$fixtures = $this->loadFixtures(array(
'Acme\MyBundle\DataFixtures\ORM\LoadUserData',
));
$repository = $fixtures->getReferenceRepository();
// Get the user from its reference.
$user = $repository->getReference('user')
// You can perform operations on this user.
// ...
// And perform functional tests:
// Create a new Client which will be logged in.
$this->loginAs($user, 'YOUR_FIREWALL_NAME');
$this->client = static::makeClient();
// The user is logged in: do whatever you want.
$path = '/';
$crawler = $this->client->request('GET', $path);
}
What I would do in this case is to create a CustomWebTestCase which extends the Symfony WebTestCase. In the class I would create a method which does the authentication for me.
Here is an example code:
namespace Company\MyBundle\Classes;
use Symfony\Bundle\FrameworkBundle\Client;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\User\User;
abstract class CustomWebTestCase extends WebTestCase
{
/**
* #param array|null $roles
* #return \Symfony\Bundle\FrameworkBundle\Client
*/
protected static function createAuthenticatedClient(array $roles = null) {
// Assign default user roles if no roles have been passed.
if($roles == null) {
$role = new Role('ROLE_SUPER_ADMIN');
$roles = array($role);
} else {
$tmpRoles = array();
foreach($roles as $role)
{
$role = new Role($role, $role);
$tmpRoles[] = $role;
}
$roles = $tmpRoles;
}
$user = new User('test_super_admin', 'passwd', $roles);
return self::createAuthentication(static::createClient(), $user);
}
private static function createAuthentication(Client $client, User $user) {
// Read below regarding config_test.yml!
$session = $client->getContainer()->get('session');
// Authenticate
$firewall = 'user_area'; // This MUST MATCH the name in your security.firewalls.->user_area<-
$token = new UsernamePasswordToken($user, null, $firewall, $user->getRoles());
$session->set('_security_'.$firewall, serialize($token));
$session->save();
// Save authentication
$cookie = new Cookie($session->getName(), $session->getId());
$client->getCookieJar()->set($cookie);
return $client;
}
}
The code above will directly create a valid user session and will skip the firewall entirely. Therefore you can create whatever $user you want and it will still be valid. The important part of the code is located in the method createAuthentication. This is what does the authentication magic.
One more thing worth mentioning - make sure you have set framework.session.storage_id to session.storage.mock_file in your config_test.yml so that Symfony will automatically mock sessions instead of you having to deal with that in each test case:
framework:
session:
storage_id: session.storage.mock_file
Now in your test case you would simply extend MyWebTestCase and call the createAuthenticatedClient() method:
class MyTest extends CustomWebTestCase {
public function testSomething() {
//Create authoried and unauthorized clients.
$authenticatedClient = self::createAuthenticatedClient(array("ROLE_SUPER_ADMIN"));
$unauthorizedClient = self::createAuthenticatedClient(array("ROLE_INSUFFICIENT_PERMISSIONS"));
// Check if the page behaves properly when the user doesn't have necessary role(s).
$unauthorizedClient->request('GET', '/secured-page');
$response = $unauthorizedClient->getResponse();
$this->assertFalse($response->isSuccessful());
$this->assertEquals(403, $response->getStatusCode(), "This request should have failed!");
// Check if the page behaves properly when the user HAS the necessary role(s)
$authenticatedClient->request('GET', '/secured-page');
$response = $authenticatedClient->getResponse();
$this->assertTrue($response->isSuccessful());
$this->assertEquals(200, $response->getStatusCode(), "This request should be working!");
}
}
You can see an example in the Symfony official documentation as well.
You can easily do that with LiipFunctionalTestBundle which authorize you lot of shortcut for create Unit Test.
If already you have a form user for create or edit you can use this for your test unit workflow user in your application :
use the makeClient method for logging test
$credentials = array(
'username' => 'a valid username',
'password' => 'a valid password'
);
$client = static::makeClient($credentials);
use your form for test your creation
$crawler = $client->request('GET', '/profile');
$form = $crawler->selectButton('adding')->form();
$form['fos_user_profile_form[firstName]'] = 'Toto';
$form['fos_user_profile_form[lastName]'] = 'Tata';
$form['fos_user_profile_form[username]'] = 'dfgdgdgdgf';
$form['fos_user_profile_form[email]'] = 'testfgdf#grgreger.fr';
$form['fos_user_profile_form[current_password]'] = 'gfgfgdgpk5dfgddf';
testing "createdAt" with just call findOneBy in repository user like this
$user = $this->getObjectManager()
->getRepository('AcmeSecurityBundle:User')
->findOneBy(array('username' => 'testCreateUserUsername'));
$this->assertTrue($user->getCreatedAt() == now());

Symfony2 custom Voter: cannot have access to getDoctrine from inside the Voter

I'm trying to implement a custom Voter.
From the controller I call it this way:
$prj = $this->getDoctrine()->getRepository('AppBundle:Project')->findOneById($id);
if (false === $this->get('security.authorization_checker')->isGranted('responsible', $prj)) {
throw new AccessDeniedException('Unauthorised access!');
}
The first line properly retrieves the Project object (I checked with a dump).
The problem occurs inside the voter
<?php
namespace AppBundle\Security\Authorization\Voter;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class ProjectVoter implements VoterInterface
{
const RESPONSIBLE = 'responsible';
const ACCOUNTABLE = 'accountable';
const SUPPORT = 'support';
const CONSULTED = 'consulted';
const INFORMED = 'informed';
public function supportsAttribute($attribute)
{
return in_array($attribute, array(
self::RESPONSIBLE,
self::ACCOUNTABLE,
self::SUPPORT,
self::CONSULTED,
self::INFORMED,
));
}
public function supportsClass($class)
{
$supportedClass = 'AppBundle\Entity\Project';
return $supportedClass === $class || is_subclass_of($class, $supportedClass);
}
/**
* #var \AppBundle\Entity\Project $project
*/
public function vote(TokenInterface $token, $project, array $attributes)
{
// check if class of this object is supported by this voter
if (!$this->supportsClass(get_class($project))) {
return VoterInterface::ACCESS_ABSTAIN;
}
// check if the voter is used correct, only allow one attribute
// this isn't a requirement, it's just one easy way for you to
// design your voter
if (1 !== count($attributes)) {
throw new \InvalidArgumentException(
'Only one attribute is allowed'
); //in origin it was 'for VIEW or EDIT, which were the supported attributes
}
// set the attribute to check against
$attribute = $attributes[0];
// check if the given attribute is covered by this voter
if (!$this->supportsAttribute($attribute)) {
return VoterInterface::ACCESS_ABSTAIN;
}
// get current logged in user
$user = $token->getUser();
// make sure there is a user object (i.e. that the user is logged in)
if (!$user instanceof UserInterface) {
return VoterInterface::ACCESS_DENIED;
}
$em = $this->getDoctrine()->getManager();
$projects = $em->getRepository('AppBundle:Project')->findPrjByUserAndRole($user, $attribute);
foreach ($projects as $key => $prj) {
if ($prj['id'] === $project['id'])
{
$granted = true;
$index = $key; // save the index of the last time a specifif project changed status
}
}
if($projects[$index]['is_active']===true) //if the last status is active
return VoterInterface::ACCESS_GRANTED;
else
return VoterInterface::ACCESS_DENIED;
}
}
I get the following error
Attempted to call method "getDoctrine" on class
"AppBundle\Security\Authorization\Voter\ProjectVoter".
I understand that the controller extends Controller, that is why I can use "getDoctrine" there. How can I have access to my DB from inside the Voter?
I solved it. This is pretty curious: I spend hours or days on a problem, then post a question here, and I solve it myself within an hour :/
I needed to add the following in my voter class:
public function __construct(EntityManager $em)
{
$this->em = $em;
}
I needed to add the following on top:
use Doctrine\ORM\EntityManager;
I also needed to add the arguments in the service.yml
security.access.project_voter:
class: AppBundle\Security\Authorization\Voter\ProjectVoter
arguments: [ #doctrine.orm.entity_manager ]
public: false
tags:
- { name: security.voter }

how to write flash message from symfony2 service?

Could any of you symfony2 gurus enlighten me as to how I can write a flash message from a symfony2 service?
I thought I had what I needed when I injected the container as below, but apparently not, I get error
Fatal error: Call to undefined method appDevDebugProjectContainer::getRequest() in /var/www/cloudsign_beta/src/BizTV/CommonBundle/Helper/globalHelper.php on line 135
So apparently I can not access the request... If I have to pass that as well from the controller I will soon loose the point of a service, it being unable to do anything by itself =)
<?php
namespace BizTV\CommonBundle\Helper;
use Symfony\Component\DependencyInjection\ContainerInterface as Container;
use Doctrine\ORM\EntityManager as EntityManager;
use BizTV\CommonBundle\Entity\Log;
class globalHelper {
private $container;
private $em;
public function __construct(Container $container, EntityManager $em) {
$this->container = $container;
$this->em = $em;
}
public function log($type,$message) {
// currently $type can be 'success', 'fail' or 'error'.
$currentUser = $this->container->get('security.context')->getToken()->getUser();
$currentCompany = $this->container->get('security.context')->getToken()->getUser()->getCompany();
//if the $type is one that we want to write to the log then create log entity (we don't log failed attempts at operations, but we do log errors.
if ($type == 'success') {
$em = $this->em;
$now = new \DateTime("now");
$entity = new Log();
$entity->setCompany($currentCompany);
$entity->setExecutor($currentUser);
$entity->setTime($now);
$entity->setEventType($type);
$entity->setEventMessage($message);
$em->persist($entity);
$em->flush();
}
//flash out the $message message text
$container = $this->container;
$session = $container->getRequest()->getSession()->setFlash($type, $message);
}
}
$session = $container->get('request')->getSession()->setFlash($type, $message);
But be careful as you may not be aware of whether request exists or not. A proper way of managing this would be by restricting your service to the request scope.

Resources