2 minute read

Behat is a very nice behaviour-driven framework well suited for Symfony.

A workaround is always some hack you need to solve a problem not in an optimal way, but in my case I found it good enough to solve my problem in a timely fashion.

This is about user authentication for Behat and how you could do It.

/**
 * @Given /^I am logged in as "([^"]*)"$/
 */
public function loggedIn($email)
{
    // Find a user
    $user = $this->myservice->find(
        'User',
        array('email' => $email)
    );

    if ($user) {
        $token = new UsernamePasswordToken(
            $user, null, 'main', $user->getRoles()
        );

        $this->security->setToken($token);

        $granted = $this->security->isGranted('ROLE_USER');

        if (!$granted) {
            throw new Exception('User is not logged in');
        }
    }
}

I tried to solve it the way it should, creating a token inside a Context and It seemed to work (isGranted was returning an authenticated valid value), but the security token wasn’t used in the Browser client launching the headless browser.

I tried to do the same inside the client and following other tutorials, checking if something was missing, but didn’t work and at some point I didn’t want to spend more time.

I could solve the problem in a behavioral way, without jumping out the flow of the site. People recommend to use the web navigation flow on Behat (filling out forms and such) to authenticate, but when you are trying to login with social networks, the strategy has to be a bit different.

In my case I could have just created some logic inside our login process and trigger the testing mock authentication when the test Symfony environment is active, but I didn’t want to set that logic inside the login process of our service.

I decided to create a TestController to log in the user of our choosing while in the test environment, It works more or less like the impersonating symfony feature.

We create a new /login_test route linking to Test:login action.

# routing.yml
test:
    resource: "@MainBundle/Resources/config/routing_test.yml"
# routing_test.yml
login_test:
    pattern: /login_test
    defaults: { _controller: MainBundle:Test:login }

Inside the controller we just create the authentication token depending on the user retrieved by email.

# TestController
<?php

namespace Acme\MainBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

use Acme\CoreBundle\Model\Environment;

class TestController extends Controller
{
    public function loginAction()
    {
        $this->checkTestEnvironment();
        $email = $this->getRequest()->query->get('email');

        // Gets a user by email or whatever you want
        $user = $this->getObjectOr404('User', $email);

        $this->authenticateUser($user);

        return $this->setRedirect('homepage');
    }

    private function checkTestEnvironment()
    {
        $env = $this->get('kernel')->getEnvironment();

        // Environment::TEST is 'test'
        if ($env != Environment::TEST) {
            throw new AccessDeniedHttpException('No access');
        }
    }

    /**
     * Authenticates a user
     *
     * @param User $user User to be authenticated
     */
    private function authenticateUser($user)
    {
        $token = new UsernamePasswordToken(
            $user,
            null,
            // The firewall name in your security.yml
            Environment::DEFAULT_FIREWALL,
            $user->getRoles()
        );
        $this->container->get('security.context')
            ->setToken($token);
    }
}

Easy enough if you know how symfony security layer works.

Then in you Behat context, you could just do:

// Inside the Behat Context

const LOGIN_VIEW = '/login_test';

...

/**
 * @Given /^I am logged in as "([^"]*)"$/
 */
public function loggedIn($email)
{
    $user = $this->myservice->find(
        'User',
        array('email' => $email)
    );

    if ($user) {
        $this->getSession()->visit(
            $this->baseUrl . self::LOGIN_VIEW . '?email=' . $email
        );
    }
}

That’s It! In our scenarios:

# user.feature

Scenario: Can view header menus with all the options
    Given I am logged in as "fake@email.com"
    And I am on "/pepe"

Questions? shoot!