Skip to main content

Symfony3: Authentication in controller testing

Submitted by on Mon, 05/09/2016 - 14:27

Moving more and more from Drupal to Symfony, I've stumbled upon some parts, which I found not that well documented, and not very self-explanatory.

One of these are testing a controller, which requires an authenticated user. Like so many others, I'm using the FOS user bundle.

A part of the Symfony docs covers this area, but in my case I found that, other than showing it can be done, it quickly raised a couple of questions.

So here's my full solution, with some notes that others devs comming to Symfony might find useful.

Controller

Say we have this very basic controller we want to test:

/**
 * @Route("/secure", name="secure")
 * @Security("has_role('ROLE_USER')")
 */
public function secureAction() {
  return $this->render('AppBundle:secure.html.twig');
}

The only thing to notice is the @Security("has_role('ROLE_USER')") annotation line.
This tells Symfony to only allow this route for authenticated users.

What to test?

So it's very clear, we need to authenticate before we test this route - well infact, testing that the route is only available when authenticated would be a pretty descent test, so let's write it.

Prerequisite

When writing these functional tests, I use the DoctrineFixtureBundle to load data into my test DB.

The docs above and the answer by Andrea Sprega on this StackOverflow thread, gives a pretty decent workflow of how to use fixtures in your tests.

The ControllerTest class

namespace Tests\AppBundle\Controller;

use Symfony\Component\HttpFoundation\Response;
use AppBundle\DataFixtures\ORM\LoadUserData;
use Tests\AppBundle\FixtureAwareTestCase;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\BrowserKit\Cookie;

class ControllerTest extends FixtureAwareTestCase
{

  /**
   * @var \Symfony\Bundle\FrameworkBundle\Client
   */
  protected $client;

  public function setUp()
  {
    parent::setUp();

    // Base fixture for all tests
    $this->addFixture(new LoadUserData());
    $this->executeFixtures();

    $this->client = static::$kernel->getContainer()->get('test.client');
    $this->client->setServerParameters([]);
  }

}

The class extends the FixtureAwareTestCase class, which is a copy/paste from the answer on StackOverflow mentioned above. For good orders sake, I've copy/pasted it in the end of this article.

In the setUp() method, I add a LoadUserData fixture. This sets up a user, which later will be used for authenticating. 

Since this test class doesn't extend the WebTestCase, the client needs to be created slightly different, which is what happens at the end of the method.

The LoadUserData class

namespace AppBundle\DataFixtures\ORM;

use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use AppBundle\Entity\User;

class LoadUserData implements FixtureInterface
{
  public function load(ObjectManager $manager)
  {
    $userOne = new User();
    $userOne->setUsername('user1@example.com');
    $userOne->setPlainPassword('user1');
    $userOne->setEnabled(TRUE);
    $userOne->setEmail('user1@example.com');
    $userOne->setFullname('User One');

    $manager->persist($userOne);

    $manager->flush();
  }
}

A useful note

In the example at the Symfony docs, the setPassword() method is used to set the password. This is not very useful, since this stores the password as plaintext, which in turn will make an otherwise correct login invalidate. Make sure to use the setPlainPassword() as this example.

 

Another useful note

Remember to call the setEnabled() method with a TRUE argument. Otherwise you won't be able to authenticated either.

Writing the first test

With this done, we are ready to write the first test - We'll make sure an authnticated user is redirected to.. well not anything specific, we just make sure he's not allowed access. Where he's redirected to, is implementation details, which we do not and should not really care about in our test.

public function testUnauthenticated() {
  $this->client->request('GET', '/secure');
  $this->assertEquals(Response::HTTP_FOUND, $this->client->getResponse()->getStatusCode());
}

Run the test, and se it passing. Read more careful, and you'll notice that it's checks for the 302 HTTP_FOUND status code. This is Symfony's way (or maybe more correct, FOS' way) of saying that a redirect is about to happen.

Writing the second test

Okay so the first test passed. Now it's time to test that an authenticated user can infact access the route.

The first step is to write a logIn() method. The following is a small rewrite of the method from the docs.

private function logIn()
{
  $session = $this->client->getContainer()->get('session');

  $firewall = 'main';
  $userManager = static::$kernel->getContainer()->get('fos_user.user_manager');
  /** @var \AppBundle\Entity\User $user */
  $user = $userManager->findUserByEmail('user1@example.com');
  $token = new UsernamePasswordToken($user, $user->getPassword(), $firewall, $user->getRoles());
  $session->set('_security_'.$firewall, serialize($token));
  $session->save();

  $cookie = new Cookie($session->getName(), $session->getId());
  $this->client->getCookieJar()->set($cookie);
}

A useful note

It's very important to update $firewall = 'main'; line. In the docs it's set to "secured_area", which at first glance, doesn't seem like it should be changed.

"Main" is from the FOS user bundle docs.

So with the login method in place, it's now pretty easy to write the test:

public function testAuthenticated() {
  $this->logIn();
  $this->client->request('GET', '/secure');
  $this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
}

Re-run the test suite, and now you have two passing tests.

We also have a great way to test controllers which requires authentication. Sweet, right?

Appendix

FixtureAwareTestCase.php

namespace Tests\Testaviva;

use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

abstract class FixtureAwareTestCase extends KernelTestCase
{
  /**
   * @var ORMExecutor
   */
  private $fixtureExecutor;

  /**
   * @var ContainerAwareLoader
   */
  private $fixtureLoader;

  public function setUp()
  {
    self::bootKernel();
  }

  /**
   * Adds a new fixture to be loaded.
   *
   * @param FixtureInterface $fixture
   */
  protected function addFixture(FixtureInterface $fixture)
  {
    $this->getFixtureLoader()->addFixture($fixture);
  }

  /**
   * Executes all the fixtures that have been loaded so far.
   */
  protected function executeFixtures()
  {
    $this->getFixtureExecutor()->execute($this->getFixtureLoader()->getFixtures());
  }

  /**
   * @return ORMExecutor
   */
  private function getFixtureExecutor()
  {
    if (!$this->fixtureExecutor) {
      /** @var \Doctrine\ORM\EntityManager $entityManager */
      $entityManager = self::$kernel->getContainer()->get('doctrine')->getManager();
      $this->fixtureExecutor = new ORMExecutor($entityManager, new ORMPurger($entityManager));
    }
    return $this->fixtureExecutor;
  }

  /**
   * @return ContainerAwareLoader
   */
  private function getFixtureLoader()
  {
    if (!$this->fixtureLoader) {
      $this->fixtureLoader = new ContainerAwareLoader(self::$kernel->getContainer());
    }
    return $this->fixtureLoader;
  }

}

 

Comment? Tweet me