Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Login before scenario with Behat and Symfony2

Tags:

php

symfony

behat

First of all I've read this related question and this other approach (blog post).

This is my behat.yml:

default:
    suites:
        users:
            contexts:
                - DoctrineFixturesContext
                - FeatureContext
                - Behat\MinkExtension\Context\MinkContext
                - Sanpi\Behatch\Context\RestContext
                - Sanpi\Behatch\Context\JsonContext
            paths: [%paths.base%/features/users]
    extensions:
        Behat\Symfony2Extension: 
            kernel:
                env: "test"
                debug: "true"
        Behat\MinkExtension:
            base_url: ''
            sessions:
                default:
                    symfony2: ~
        Sanpi\Behatch\Extension: ~

Now what I want is to reproduce the behavior of a user, which is:

  • User logs in once
  • Requests have then an Authorization header with the token

The easiest way I've found is to simply mimic the user behavior:

class FeatureContext implements Context, SnippetAcceptingContext
{
    private $request;

    public function __construct(Request $request)
    {
        $this->request = $request; 
    }

    /**
     * @BeforeScenario @Login
     */
    public function login() 
    {
        $d = $this->request->send('POST', '/login_check', ['_username' => 'test', '_password' => 'test']);
        $data = json_decode($d->getContent());
        $this->request->setHttpHeader('Authorization', 'Bearer '.$data->token);
    }
}

The user is previously loaded through fixtures. This works well except that the Authorization header is not kept through the next steps:

Feature: Get my availables menus
  @Login
  Scenario: Get my menus
    When I send a "GET" request to "/api/menus"
    Then the response status code should be 200

The response is 401.

Why is the Authorization header not kept through the scenario? How may I achieve this simple task?

like image 402
soyuka Avatar asked Nov 10 '15 16:11

soyuka


2 Answers

As you can read here, following here, the Behatch/RestContext is quite useful (a lot of shortcuts and handful methods) but it's quite messy with the symfony2 integration.

As @stof says in this comment:

The rest-context is a huge mistake altogether. Mink is not a REST client (browsers are not REST clients either). There is no point using the Mink driver to perform the REST API calls. It is the wrong tool for the job.

Longo story short: it seems that the request headers you set are not passed with the request.

Having said that, you can try with this workaround:

Feature: Get my availables menus
  @Login
  Scenario: Get my menus
    # note the change in the following line
    # When I send a "GET" request to "/api/menus"
    When I am on "/api/menus"
    Then the response status code should be 200

Basically, using the When I am on... you are using the MinkContext and the request headers are properly passed with the request.

Of course I know this is not the perfect solution (eg: how to handle POST request??) but at least the specific test you described should pass.

Let me know. Ciao

like image 169
dop3 Avatar answered Oct 14 '22 00:10

dop3


Why is the Authorization header not kept through the scenario? How may I achieve this simple task?

I would first of all what is this Request object that you're injecting in your context? If your idea is to inject the request object that will be used in your Symfony application, then that won't work as the object will be created by the front controller when Mink will "visit the page".

Moreover, I would recommend to use directly the Symfony's kernel in your context files to send requests to your application. With the Symfony2Extension of Behat, you can instanciate your FeatureContext with the kernel by mentioning the service you want to inject in the behat.yml file:

default:
    suites:
        users:
            contexts:
                - FeatureContext:
                      - @kernel

Then, you can create request objects manually and send them to the Symfony kernel. Here's an example of sending the almost the same POST request than in your example:

class FeatureContext implements Context, SnippetAcceptingContext
{
    private $kernel;

    public function __construct(KernelInterface $kernel)
    {
        $this->kernel = $kernel; 
    }

    /**
     * @What user :username is authenticating itself with :password 
     */
    public function userIsAuthenticatingItselfWith($username, $password) 
    {
        $response = $this->kernel->handle(Request::create(
            '/login_check',
            'POST',
            [],
            [],
            [],
            [
                'CONTENT_TYPE' => 'application/x-www-form-urlencoded'
            ],
            http_build_query([
                '_username' => $username,
                '_password' => $password,
            ])
        ));

        // Do whatever you want with the `$response` object to check that
        // the user is successfully authenticated for instance..
    }
}

But you can see that I changed the method signature and actually didn't store the token or anything else: this is intended to actually test this authentication, but not as a Given step.

The Symfony authentication system contains a TokenStorage object that stores the current authentication token. So if you want to "fake" an authentication, you can simply set a token in the object, with a step definition that will probably look like that:

class SecurityContext implements Context
{
    /**
     * @var TokenStorageInterface
     */
    private $tokenStorage;

    /**
     * @param TokenStorageInterface $tokenStorage
     */
    public function __construct(TokenStorageInterface $tokenStorage)
    {
        $this->tokenStorage = $tokenStorage;
    }

    /**
     * @Given I am authenticated as :username
     */
    public function iAmAuthenticated()
    {
        $token = new YourTokenClass(['ROLE_USER']);
        $token->setUser(new YouUserClass($username));

        $this->tokenStorage->setToken($token);
    }
}

And then, instead of scenario hooks, you can simply use the Given I am authenticated as :username in your scenario or even with the Backgrounds.

like image 2
Samuel ROZE Avatar answered Oct 14 '22 01:10

Samuel ROZE