3

Intro: Custom user implementation to be able to use and Wordpress users:

In our project, we have implemented a custom user provider (for Wordpress users - implements UserProviderInterface) with corresponding custom user (WordpressUser implements UserInterface, EquatableInterface). I have setup a firewall in the security.yml and implemented several voters.

# app/config/security.yml
security:
    providers:
        wordpress:
            id: my_wordpress_user_provider

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        default:
            anonymous: ~
            http_basic: ~
            form_login:
                login_path: /account

Functional phpunit testing:

So far so good - but now the tricky part: mocking authenticated (Wordpress) users in functional phpunit tests. I have succeeded mocking the WordpressUserProvider so a mocked WordpressUser will be returned on loadUserByUsername(..). In our BaseTestCase (extends WebTestCase) the mocked WordpressUser gets authenticated and the token is stored to session.

//in: class BaseTestCase extends WebTestCase

/**
 * Login Wordpress user
 * @param WordpressUser $wpUser
 */
private function _logIn(WordpressUser $wpUser)
{
    $session = self::get('session');

    $firewall = 'default';
    $token = new UsernamePasswordToken($wpUser, $wpUser->getPassword(), $firewall, $wpUser->getRoles());
    $session->set('_security_' . $firewall, serialize($token));
    $session->save();

    $cookie = new Cookie($session->getName(), $session->getId());
    self::$_client->getCookieJar()->set($cookie);
}

The problem: losing session data on new request:

The simple tests succeed on the authentication part. Until tests with a redirect. The user is only authenticated one request, and 'forgotten' after a redirect. This is because the Symfony2 test client will shutdown() and boot() the kernel on each request, and in this way, the session gets lost.

Workarounds/solutions:

In a solution provided in question 12680675 only user ID should be used for the UsernamePasswordToken(..) to solve this. Our project needs the full user object.

In the solution provided in Unable to simulate HTTP authentication in functional test the basic HTTP authentication is used. In this case the full user object - including roles - cannot be used.

As suggested by Isolation of tests in Symfony2 you can persist instances by overriding the doRequest() method in the test client. As suggested I have created a custom test client and made an override on the doRequest() method.

Custom test client to 'store' session data between requests:

namespace NS\MyBundle\Tests;

use Symfony\Bundle\FrameworkBundle\Client as BaseClient;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Class Client
 * Overrides and extends the default Test Client
 * @package NS\MyBundle\Tests
 */
class Client extends BaseClient
{
    static protected $session;

    protected $requested = false;

    /**
     * {@inheritdoc}
     *
     * @param Request $request A Request instance
     *
     * @return Response A Response instance
     */
    protected function doRequest($request)
    {
        if ($this->requested) {
            $this->kernel->shutdown();
            $this->kernel->boot();
        }

        $this->injectSession();
        $this->requested = true;

        return $this->kernel->handle($request);
    }

    /**
     * Inject existing session for request
     */
    protected function injectSession()
    {
        if (null === self::$session) {
            self::$session = $this->getContainer()->get('session');
        } else {
            $this->getContainer()->set('session', self::$session);
        }
    }
}

Without the if statement holding the shutdown() and boot() calls, this method is working more or less. There are some weird problems where $_SERVER index keys cannot be found so I would like to properly re-instantiate the kernel container for other aspects of the system. While keeping the if statement, users cannot be authenticated, though the session data is the same before and during/after the request (checked by var_export to log).

Question(s):

What am I missing in this this approach that causes the authentication to fail? Is the authentication (and session check) done directly on/after kernel boot() or am I missing something else? Does anyone has another/better solution to keep the session intact so users will be authenticated in functional tests? Thank you in advance for your answer.

--EDIT--

In addition: the session storage for the test environment is set to session.storage.mock_file. In this way, the session should already be persisted between requests as describe by Symfony2 components here. When checked in the test after a (second) request, the session seems to be intact (but somehow ignored by the authentication layer?).

# app/config/config_test.yml
# ..
framework:
    test: ~
    session:
        storage_id: session.storage.mock_file
    profiler:
        collect: false

web_profiler:
    toolbar: false
    intercept_redirects: false
# ..
Community
  • 1
  • 1

1 Answers1

2

My assumptions were close; it was not the session that was not persisted, the problem was in the case that mocked services are 'erased' by the kernel at a fresh request. This is the basic behaviour of functional phpunit testing...

I found out that this had to be the problem while debugging in the Symfony\Component\Security\Http\Firewall\AccessListener. There the token was found, and the (not anymore) mocked custom WordpressUser was there - empty. This explains why setting the username only instead of user object worked in the suggested workarounds stated above (no need of the mocked User class).

Solution

First of all, you don't need to override the Client as suggested in my question above. To be able to persist your mocked classes, you will have to extend the AppKernel and make some sort of kernel-modifier override with a closure as parameter. There is an explanation here on LyRiXx Blog. After injecting with a closure, you could restore the service mock after a request.

// /app/AppTestKernel.php

/**
 * Extend the kernel so a service mock can be restored into the container
 * after a request.
 */

require_once __DIR__.'/AppKernel.php';

class AppTestKernel extends AppKernel
{
    private $kernelModifier = null;

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

        if ($kernelModifier = $this->kernelModifier) {
            $kernelModifier($this);
        };
    }

    /**
     * Inject with closure
     * Next request will restore the injected services
     *
     * @param callable $kernelModifier
     */
    public function setKernelModifier(\Closure $kernelModifier)
    {
        $this->kernelModifier = $kernelModifier;
    }
}

Usage (in your functional test):

$mock = $this->getMockBuilder(..);
..
static::$kernel->setKernelModifier(function($kernel) use ($mock) {
    $kernel->getContainer()->set('bundle_service_name', $mock);
});

I still have to tweak the class and extended WebTestCase class, but this seems to work for me. I hope I can point someone else in the right(?) direction with this answer.