Behat authenticate Symfony2 user
Asked Answered
C

4

12

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.)

Crossjack answered 12/11, 2011 at 22:21 Comment(0)
U
20

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

Undergarment answered 12/11, 2011 at 22:44 Comment(4)
much better (and way simpler) indeed. Just pushed the changes into my project. Thanks!Crossjack
what to do if talk about authorization with oAuth? I'm just need authenticate user inside feature context, just flad it as logging in without any additional steps, cuase in such case it not helpHonorarium
You mention that it's possible to get the app's kernel and DIC, how would you that?Regina
Just because a user must use the login page to access a certain area doesn't mean that all test should also use the login form. By doing this, you are now coupling every test scenario to the login form. In many applications there is also 'remember me' functionality and email link login (email verification, forgotten password, etc). Just because it is called Behaviour Driven Development doesn't mean end user behaviour. It means expected system behaviour from all roles, users, business groups, etc.Pome
C
1

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.

Celestyna answered 3/10, 2013 at 16:30 Comment(0)
P
1

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.

Pome answered 8/12, 2014 at 8:29 Comment(0)
T
0

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 | [email protected] |
| fabpot | 22@222 | [email protected] |

Trevar answered 21/10, 2013 at 4:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.