Question

I'm using Behat in Symfony2 / Doctrine2. Now, I have this scenario that boils down to the fact that "if I'm logged in and I go to /login, I shoud go to / instead":

@login
Scenario: Go to the login page while being logged in
  Given I am logged in
  When I go to "/login"
  Then I should be on "/"

For the @login, I created the following:

/**
 * @BeforeScenario @login
 */
public function loginUser()
{
    $doctrine = $this->getContainer()->get('doctrine');
    $userRepository = $doctrine->getRepository('MyTestBundle:User');
    $user = $userRepository->find(1); // 1 = id

    $token = new UsernamePasswordToken($user, NULL, 'main', $user->getRoles());
    $this->getContainer()->get('security.context')->setToken($token);
}

In the "when I go to /login" code (the controller gets called), the token seems gone (not what I intended):

/**
 * @Route("/login", name="login")
 */
public function loginAction()
{
    $token = $this->get('security.context')->getToken();
    $fd = fopen('/tmp/debug.log', 'a');
    fwrite($fd, $token);

    // prints 'AnonymousToken(user="anon.", authenticated=true, roles="")'
    ...

But in the FeatureContext, it seems to stick around (the way I hoped it would work). In the "Given I am logged in":

/**
 * @Given /^I am logged in$/
 */
public function iAmLoggedIn()
{        
    $token = $this->getContainer()->get('security.context')->getToken();
    $fd = fopen('/tmp/debug.log', 'a');
    fwrite($fd, $token);

    // prints 'UsernamePasswordToken(user="admin", authenticated=true, roles="ROLE_ADMIN")'
    ...

I run behat like this:

app/console -e=test behat

I also did this in the controller to be sure it's test:

fwrite($fd, $this->get('kernel')->getEnvironment());
// prints 'test'

Any clue how to authenticate a user? I will have to test a lot of admin pages, so it would be nice if I could hook the login into @BeforeSuite, @BeforeFeature (or @BeforeScenario ...) so that I don't get blocked.

(Suggestions on disabling the authentication mechanism for testing, or a way to stub/mock a user are also welcome.)

Was it helpful?

Solution

Oh my. It doesn't work because the DIC inside your FeatureContext isn't shared with your app - your app has separate kernel and DIC. You can get it through Mink. Or, you can simply do it right way :-)

Right way means, that every part of behavior, that is observable by the enduser, should be described inside *.feature, not inside FeatureContext. It means, that if you want to login a user, you should simply describe it with steps (like: "i am on /login", "and i fill in username ...", "i fill in password" and stuf). If you want to do it in multiple times - you should create a metastep.

Metasteps are simply steps, that describe multiple other steps, for example - "i am logged in as everzet". You could read bout them here: http://docs.behat.org/guides/2.definitions.html#step-execution-chaining

OTHER TIPS

Here is an solution for login with OAuth I've used. After number of times of searching for the answer and landing on this page I thought it would be great to share the solution. Hopefully it will help someone.

Background: Symfony2 App using HWIOAuthBundle, hooked up to some OAuth2 provider.

Problem: How do I implement Given I'm logged in when Behat context in not shared with Symfony context?

Solution:

HWIOAuthBundle uses @buzz service for all API calls to OAuth providers. So all you need to do is replace Buzz client with your implementation which doesn't call external services, but returns the result straight away. This is my implementation:

<?php

namespace Acme\ExampleBundle\Mocks;

use Buzz\Client\ClientInterface;
use Buzz\Message\MessageInterface;
use Buzz\Message\RequestInterface;

class HttpClientMock implements ClientInterface
{
    public function setVerifyPeer()
    {
        return $this;
    }

    public function setTimeout()
    {
        return $this;
    }

    public function setMaxRedirects()
    {
        return $this;
    }

    public function setIgnoreErrors()
    {
        return $this;
    }

    public function send(RequestInterface $request, MessageInterface $response)
    {
        if(preg_match('/\/oauth2\/token/', $request->getResource()))
        {
            $response->setContent(json_encode([
                'access_token' => 'valid',
                'token_type' => 'bearer',
                'expires_in' => 3600
            ]));
        }
        elseif(preg_match('/\/oauth2\/me/', $request->getResource()))
        {
            $response->setContent(json_encode([
                'id' => 1,
                'username' => 'doctor',
                'realname' => 'Doctor Who'
            ]));
        }
        else throw new \Exception('This Mock object doesn\'t support this resource');
    }
}

Next step is to hijack the class used by HWIOAuthBundle/Buzz and replace it with the implementation above. We need to do it only for test environment.

# app/config/config_test.yml
imports:
    - { resource: config_dev.yml }

parameters:
    buzz.client.class: Acme\ExampleBundle\Mocks\HttpClientMock

And finally, you need to set require_previous_session to false for test environment - therefore I suggest to pass it as parameter.

# app/config/security.yml
security:
    firewalls:
        secured_area:
            oauth:
                require_previous_session: false

Now you can implement your step like this.

Specification:

Feature: Access restricted resource

  Scenario: Access restricted resource
    Given I'm logged in
    When I go to "/secured-area"
    Then I should be on "/secured-area"
    And the response status code should be 200

Implementation:

<?php
/**
 * @Given /^I\'m logged in$/
 */
public function iMLoggedIn()
{
    $this->getSession()->visit($this->locatePath('/login/check-yourOauthProvider?code=validCode'));
}

The code you're passing is not relevant, anything you pass will be OK as it's not being checked. You can customise this behaviour in HttpClientMock::send method.

http://robinvdvleuten.nl/blog/handle-authenticated-users-in-behat-mink/ is simple, clean article on how to create a login session and set the Mink session cookie so that the Mink session is logged in. This is much better than using the login form every time to login a user.

It’s ok to call into the layer “inside” the UI layer here (in symfony: talk to the models). And for all the symfony users out there, behat recommends using a Given step with a tables arguments to set up records instead of fixtures. This way you can read the scenario all in one place and make sense out of it without having to jump between files:

Given there are users:
| username | password | email |
| everzet | 123456 | everzet@knplabs.com |
| fabpot | 22@222 | fabpot@symfony.com |

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top