Symfony2 phpunit 功能测试自定义用户身份验证在重定向后失败(会话相关)

Symfony2 phpunit functional test custom user authentication fails after redirect (session related)

简介:能够使用的自定义用户实现和 Wordpress 用户:

在我们的项目中,我们实现了自定义用户提供程序(对于 Wordpress 用户 - 实现了 UserProviderInterface)和相应的自定义用户(WordpressUser 实现了 UserInterface、EquatableInterface).我在 security.yml 中设置了防火墙并实施了多个选民。

# 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

功能性 phpunit 测试:

到目前为止一切顺利 - 但现在是棘手的部分:在功能性 phpunit 测试中模拟经过身份验证的 (Wordpress) 用户。我已经成功模拟了 WordpressUserProvider,因此模拟的 WordpressUser 将在 loadUserByUsername(..) 上返回。在我们的 BaseTestCase(扩展 WebTestCase)中,模拟的 WordpressUser 获得身份验证并将令牌存储到会话中。

//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);
}

问题:在新请求时丢失会话数据:

认证部分简单测试成功。直到使用重定向进行测试。用户仅通过一次请求验证,并且在重定向后 'forgotten'。这是因为 Symfony2 测试客户端会在每次请求时关闭()和启动()内核,这样,session 就会丢失。

Workarounds/solutions:

question 12680675 中提供的解决方案中,只有用户 ID 应该用于 UsernamePasswordToken(..) 才能解决此问题。我们的项目需要完整的用户对象。

在中提供的解决方案中使用了基本的HTTP认证。在这种情况下,不能使用完整的用户对象(包括角色)。

正如 Isolation of tests in Symfony2 所建议的那样,您可以通过覆盖测试客户端中的 doRequest() 方法来持久化实例。按照建议,我创建了一个自定义测试客户端并重写了 doRequest() 方法。

自定义测试客户端到 'store' 请求之间的会话数据:

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);
        }
    }
}

如果没有包含 shutdown() 和 boot() 调用的 if 语句,此方法或多或少会起作用。有一些奇怪的问题,找不到 $_SERVER 索引键,所以我想为系统的其他方面正确地重新实例化内核容器。在保留 if 语句的同时,无法对用户进行身份验证,尽管会话数据在 during/after 请求之前是相同的(由 var_export 检查以记录)。

问题:

我在这种导致身份验证失败的方法中遗漏了什么?身份验证(和会话检查)是直接完成 on/after kernel boot() 还是我遗漏了其他东西?有没有人有 another/better 解决方案来保持会话完整,以便用户在功能测试中进行身份验证?预先感谢您的回答。

--编辑--

此外:测试环境的会话存储设置为session.storage.mock_file。通过这种方式,会话应该已经在请求之间保持不变,如 Symfony2 组件 here 所描述的那样。在(第二个)请求后检查测试时,会话似乎完好无损(但不知何故被身份验证层忽略了?)。

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

web_profiler:
    toolbar: false
    intercept_redirects: false
# ..

我的假设很接近;不是会话没有持久化,问题是内核根据新请求 'erased' 模拟服务。这是功能性 phpunit 测试的基本行为...

我在 Symfony\Component\Security\Http\Firewall\AccessListener 中调试时发现这一定是问题所在。在那里找到了令牌,并且(不再)模拟的自定义 WordpressUser 在那里 - 空。这解释了为什么在上述建议的解决方法中只设置用户名而不是用户对象(不需要模拟用户 class)。

解决方案

首先,您不需要按照我上面的问题中的建议覆盖客户端。为了能够持久化您的模拟 classes,您将必须扩展 AppKernel 并使用闭包作为参数进行某种内核修改器覆盖。有解释here on LyRiXx Blog。注入闭包后,您可以在请求后恢复服务模拟。

// /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;
    }
}

用法(在您的功能测试中):

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

我仍然需要调整 class 和扩展的 WebTestCase class,但这似乎对我有用。我希望我可以用这个答案为其他人指明正确的(?)方向。