In Symfony 4.4 I am attempting simulate authenticating a user so that I can write PHPUnit tests for a secured area of my application. I am using the ZenstruckFoundry to create a factory for my User that will be authenticated.
I have also followed the Symfony docs for Creating an Authentication Token and originally opened an issue in the zenstruck/foundry repo. Since this doesn't actually seem to be an issue with Foundry I'm asking here on SO.
My test to an endpoint behind my main firewall simply looks like this:
/**
* #test
*/
public function it_displays_the_dashboard()
{
$user = UserFactory::new()->createOne();
$this->auth($user);
$this->appClient->request('GET', '/'); // <- this requires an auth'd user
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('[data-test="user-profile"]', $user->getUsername());
}
/**
* Simulate authentication
*
* #param Proxy $user
*/
protected function auth(Proxy $user)
{
$session = self::$container->get('session');
$firewallName = 'main';
// **********************************************
// **********************************************
// $user = $user->object(); <-- If I uncomment to use the actual User model then auth doesn't work
// **********************************************
// **********************************************
$token = new UsernamePasswordToken($user, null, $firewallName, $user->getRoles());
$session->set('_security_'.$firewallName, serialize($token));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$this->appClient->getCookieJar()->set($cookie);
}
The issue I'm encountering is this: If I pass the Proxy user to UsernamePasswordToken then the token is created successfully and the user is authenticated. However, this causes Twig to not be able to use its magic methods to resolve getter attributes. For example, my Twig template has {{ user.username }}, Behind the scenes Twig resolves this to the getUsername() method on my User instance. But since the Proxy class doesn't have such a method, I get this error:
Error: Call to undefined method App\Model\User::username()
If I modify my template to be {{ user.getUsername }} then my test passes.
But, if I pass the User class directly to UsernamePasswordToken using $user->object() then the token is initially created, but then seemingly doesn't exist by the time authentication takes place and my test fails because I'm not authenticated.
If I look at the stacktrace I can see that the User is no longer the User that was used when creating the token and I see the following:
/Users/myuser/code/adminapp/vendor/symfony/symfony/src/Symfony/Component/Security/Http/Firewall/AccessListener.php:84:
class Symfony\Component\Security\Core\Authentication\Token\AnonymousToken#847 (6) {
private $secret =>
string(7) "OxIzLMV"
private $user =>
string(5) "anon."
private $roles =>
array(0) {
}
private $roleNames =>
array(0) {
}
private $authenticated =>
bool(true)
private $attributes =>
array(0) {
}
}
I would like to be able to pass the actual User class using $user->object() so that I don't have to manipulate my Twig templates by using the getters directly.
Related
[SETTINGS]
Symfony 3.4
FosUserBundle 2.0
RESTRICTION: Avoid bundle inheritance (This is also bundle inheritance)
[PROBLEM]
While reading the Symfony doc about how to override any part if a bundle,
I met those lines:
If the controller is a service, see the next section on how to override it. Otherwise, define a new route + controller with the same path associated to the controller you want to override (and make sure that the new route is loaded before the bundle one).
And somehow felt overjoyed seeing how the doc was still as incomplete as ever on some of the most important sections... Right, this part got no code example, can't even be sure of what to do.
Would someone be kind enough to give me an example on how to override the FosUserBundle? Just one section like the login part will be enough. As the same logic will apply for the other sections.
Also, as a side questions:
Is it worth using FosUserBundle?
Is there a bundle easier to use than FosUserBundle?
Wouldn't it be more worth and faster to make my own logic to handle login?
What I understand : simply create your controller and then add a route for it in your configuration with the same path as the one you want to override, making sure it's loaded before.
For example, to override the login action:
// AppBundle\Controller\UserController.php
/**
* #route("/login", name="login_override")
* #param Request $request
* #return Response
*/
public function loginAction(Request $request)
{
/** #var $session Session */
$session = $request->getSession();
$authErrorKey = Security::AUTHENTICATION_ERROR;
$lastUsernameKey = Security::LAST_USERNAME;
// get the error if any (works with forward and redirect -- see below)
if ($request->attributes->has($authErrorKey)) {
$error = $request->attributes->get($authErrorKey);
} elseif (null !== $session && $session->has($authErrorKey)) {
$error = $session->get($authErrorKey);
$session->remove($authErrorKey);
} else {
$error = null;
}
if (!$error instanceof AuthenticationException) {
$error = null; // The value does not come from the security component.
}
// last username entered by the user
$lastUsername = (null === $session) ? '' : $session->get($lastUsernameKey);
$tokenManager = $this->container->get('security.csrf.token_manager');
$csrfToken = $tokenManager
? $tokenManager->getToken('authenticate')->getValue()
: null;
return $this->render('#FOSUser/Security/login.html.twig', array(
'last_username' => $lastUsername,
'error' => $error,
'csrf_token' => $csrfToken,
));
}
#app\config\routing.yml
app:
resource: '#AppBundle/Controller/'
type: annotation
fos_user:
resource: "#FOSUserBundle/Resources/config/routing/all.xml"
I'm using symfony 2.8 and FOSUserBundle. I want to allow admins to edit users' usernames and emails. If the new username or email is already taken then the database gives an error
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry
which is good, but I don't know how to communicate that back to the admin who tried to change it to tell them what went wrong (the production version of the app will just give an error 500). What I want to do is show an error message of some kind (preferable like the one FOSUserBundle has in its forms) to say the username (or email) is taken.
The relevant portions of the form is built here:
$userManager = $this->get('fos_user.user_manager');
$user = $userManager->findUserBy(array('id' => $id));
$form = $this->createFormBuilder()
->add('username', TextType::class, array(
'label' => 'Username',
'data' => $user->getUsername(),
))
->add('email', EmailType::class, array(
'label' => 'Email',
'data' => $user->getEmail(),
))
->getForm();
and the database is handled here:
if ($form->isSubmitted() and $form->isValid()) {
// set new username if different
$newUsername = $form['username']->getData();
if ($user->getUsername() !== $newUsername) {
$user->setUsername($newUsername);
}
// set new email if different
$newEmail = $form['email']->getData();
if ($user->getEmail() !== $newEmail) {
$user->setEmail($newEmail);
}
$userManager->updateUser($user);
}
I have tried a number of things, like also setting username_canonical and email_canonical, or adding #UniqueEntity in my User.php class, but they haven't helped (which makes sense since the error is correct - I just can't translate it into a useful message).
If you doesn't want override anymore for make some validation, you need to implement an EventListener that catch the exceptions of your need by listening on the onKernelResponse event.
DBALExceptionResponseListener.php
// src/AcmeBundle/EventListner/DBALExceptionResponseListener.php
<?php
namespace AcmeBundle\EventListener;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Doctrine\DBAL\DBALException;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpFoundation\Router\RouterInterface;
class DBALExceptionResponseListener
{
public function __construct(SessionInterface $session, RouterInterface $router)
{
$this->session = $session;
$this->router = $router;
}
/**
* #param GetResponseForExceptionEvent $event
*/
public function onKernelResponse(GetResponseForExceptionEvent $event)
{
$request = $event->getRequest();
$exception = $event->getException();
$message = $exception->getMessage();
// Maybe some checks on the route
if ($request->get('_route') !== 'your_route' || $request->headers->get('referer') !== 'your_referer') {
return;
}
// Listen only on the expected exception
if (!$exception instanceof DBALException) {
return;
}
// You can make some checks on the message to return a different response depending on the MySQL error given.
if (strpos($message, 'Integrity constraint violation')) {
// Add your user-friendly error message
$this->session->getFlashBag()->add('error', 'SQL Error: '.$message);
}
// Create your custom response to avoid the error page.
$response = new RedirectResponse($this->router->generate('your_route'));
// Update the Event Response with yours
$event->setResponse($response);
}
}
services.yml
# app/config/services.yml
services:
acme.kernel.listener.dbal_exception_response_listener:
class: AcmeBundle\EventListener\DBALExceptionResponseListener
tags:
- {name: kernel.event_listener, event: kernel.exception, method: onKernelResponse}
arguments:
session: "#session"
router: "#router"
By looking more at the Exception::$message, you can easily find which property causes the problem.
The most common message contains something like :
... column 'propertyname' cannot be null ...
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());
I'm trying to run a console command in symfony2 in which some properties of a certain class are being updated. One of the properties has got a corresponding reviewedBy-property which is being set by the blameable-behaviour like so:
/**
* #var bool
* #ORM\Column(name="public_cmt", type="boolean", nullable=true)
*/
private $publicCmt;
/**
* #var User $publicCmtReviewedBy
*
* #Gedmo\Blameable(on="change", field="public_cmt")
* #ORM\ManyToOne(targetEntity="My\Bundle\EntityBundle\Entity\User")
* #ORM\JoinColumn(name="public_cmt_reviewed_by", referencedColumnName="id", nullable=true)
*/
private $publicCmtReviewedBy;
When i run the task there's no user which can be 'blamed' so I get the following exception:
[Doctrine\ORM\ORMInvalidArgumentException]
EntityManager#persist() expects parameter 1 to be an entity object, NULL given.
However I can also not disable blameable because it's not registered as a filter by the time i start the task and programmatically trying to set the user through:
// create the authentication token
$token = new UsernamePasswordToken(
$user,
null,
'main',
$user->getRoles());
// give it to the security context
$this->getService('security.context')->setToken($token);
doesn't work. Anyone got an idea?
If you use the StofDoctrineExtensionsBundle you can simply do :
$this->container->get('stof_doctrine_extensions.listener.blameable')
->setUserValue('task-user');
see : https://github.com/stof/StofDoctrineExtensionsBundle/issues/197
First of all, I'm not sure if 'field' cares if you use the database column or the property, but you might need to change it to field="publicCmt".
What you should do is override the Blameable Listener. I'm going to assume you are using the StofDoctrineExtensionsBundle. First override in your config:
# app/config/config.yml
stof_doctrine_extensions:
class:
blameable: MyBundle\BlameableListener
Now just extend the existing listener. You have a couple options - either you want to allow for NULL values (no blame), or, you want to have a default user. Say for example you want to just skip the persist and allow a null, you would override as such:
namespace MyBundle\EventListener;
use Gedmo\Blameable\BlameableListener;
class MyBlameableListener extends BlameableListener
{
public function getUserValue($meta, $field)
{
try {
$user = parent::getUserValue($meta, $field);
}
catch (\Exception $e) {
$user = null;
return $user;
}
protected function updateField($object, $ea, $meta, $field)
{
if (!$user) {
return;
}
parent::updateField($object, $ea, $meta, $field);
}
}
So it tries to use the parent getUserValue() function first to grab the user, and if not it returns null. We must put in a try/catch because it throws an Exception if there is no current user. Now in our updateField() function, we simply don't do anything if there is no user.
Disclaimer - there may be parts of that updateField() function that you still need...I haven't tested this.
This is just an example. Another idea would be to have a default database user. You could put that in your config file with a particular username. Then instead of returning null if there is no user from the security token, you could instead grab the default user from the database and use that (naturally you'd have to inject the entity manager in the service as well).
Slight modification of the above answer with identical config.yml-entry: we can check if a user is set and if not: since we have access to the object-manager in the updateField-method, get a default-user, set it and then execute the parent-method.
namespace MyBundle\EventListener;
use Gedmo\Blameable\BlameableListener;
class MyBlameableListener extends BlameableListener
{
protected function updateField($object, $ea, $meta, $field)
{
// If we don't have a user, we are in a task and set a default-user
if (null === $this->getUserValue($meta, $field)) {
/* #var $ur UserRepository */
$ur = $ea->getObjectManager()->getRepository('MyBundle:User');
$taskUser = $ur->findOneBy(array('name' => 'task-user'));
$this->setUserValue($taskUser);
}
parent::updateField($object, $ea, $meta, $field);
}
}
I am developing an application using Symfony2 and doctrine 2. I would like to know how can I get the currently logged in user's Id.
Current Symfony versions (Symfony 4, Symfony >=3.2)
Since Symfony >=3.2 you can simply expect a UserInterface implementation to be injected to your controller action directly. You can then call getId() to retrieve user's identifier:
class DefaultController extends Controller
{
// when the user is mandatory (e.g. behind a firewall)
public function fooAction(UserInterface $user)
{
$userId = $user->getId();
}
// when the user is optional (e.g. can be anonymous)
public function barAction(UserInterface $user = null)
{
$userId = null !== $user ? $user->getId() : null;
}
}
You can still use the security token storage as in all Symfony versions since 2.6. For example, in your controller:
$user = $this->get('security.token_storage')->getToken()->getUser();
Note that the Controller::getUser() shortcut mentioned in the next part of this answer is no longer encouraged.
Legacy Symfony versions
The easiest way to access the user used to be to extend the base controller, and use the shortcut getUser() method:
$user = $this->getUser();
Since Symfony 2.6 you can retrieve a user from the security token storage:
$user = $this->get('security.token_storage')->getToken()->getUser();
Before Symfony 2.6, the token was accessible from the security context service instead:
$user = $this->get('security.context')->getToken()->getUser();
Note that the security context service is deprecated in Symfony 2 and was removed in Symfony 3.0.
In symfony2, we can get this simpler by this code:
$id = $this->getUser()->getId();
You can get the variable with the code below:
$userId = $this->get('security.context')->getToken()->getUser()->getId();
This can get dressed in the method:
/**
* Get user id
* #return integer $userId
*/
protected function getUserId()
{
$user = $this->get('security.context')->getToken()->getUser();
$userId = $user->getId();
return $userId;
}
And induction $this->getUserId()
public function example()
{
print_r($this->getUserId());
}