以编程方式对用户进行身份验证以进行 PhpUnit 功能测试 - 不受 Doctrine 管理 - Symfony 4.3

Posted

技术标签:

【中文标题】以编程方式对用户进行身份验证以进行 PhpUnit 功能测试 - 不受 Doctrine 管理 - Symfony 4.3【英文标题】:Issue programmatically authenticating Users for PhpUnit functional test - Unmanaged by Doctrine - Symfony 4.3 【发布时间】:2020-04-21 20:25:38 【问题描述】:

我正在尝试让一个简单的“200 响应”测试适用于需要经过身份验证的用户的网站的一部分。我想我已经创建了会话工作,因为在调试期间调用了控制器函数并检索了一个用户(使用$this->getUser())。

但是,随后该函数失败并显示以下消息:

1) App\Tests\Controller\SecretControllerTest::testIndex200Response
expected other status code for 'http://localhost/secret_url/':
error:
    Multiple non-persisted new entities were found through the given association graph:

 * A new entity was found through the relationship 'App\Entity\User#role' that was not configured to cascade persist operations for entity: ROLE_FOR_USER. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade
persist this association in the mapping for example @ManyToOne(..,cascade="persist").
 * A new entity was found through the relationship 'App\Entity\User#secret_property' that was not configured to cascade persist operations for entity: test123. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade pe
rsist this association in the mapping for example @ManyToOne(..,cascade="persist"). (500 Internal Server Error)

Failed asserting that 500 matches expected 200.

如果这还没有存储在 (mysql) 数据库中并使用 Doctrine 检索,这将是有意义的。记录是在每次运行/每次测试时使用夹具创建的。这就是为什么在控制器$this->getUser() 中按预期运行的原因。

我想做的测试:

public function testIndex200Response(): void

    $client = $this->getAuthenticatedSecretUserClient();

    $this->checkPageLoadResponse($client, 'http://localhost/secret_url/');

获取用户:

protected function getAuthenticatedSecretUserClient(): HttpKernelBrowser

    $this->loadFixtures(
        [
            RoleFixture::class,
            SecretUserFixture::class,
        ]
    );

    /** @var User $user */
    $user = $this->entityManager->getRepository(User::class)->findOneBy(['username' => 'secret_user']);

    $client = self::createClient(
        [],
        [
            'php_AUTH_USER' => $user->getUsername(),
            'PHP_AUTH_PW'   => $user->getPlainPassword(),
        ]
    );

    $this->createClientSession($user, $client);

    return $client;

创建会话:

// Based on https://symfony.com/doc/current/testing/http_authentication.html#using-a-faster-authentication-mechanism-only-for-tests
protected function createClientSession(User $user, HttpKernelBrowser $client): void

    $authenticatedGuardToken = new PostAuthenticationGuardToken($user, 'chain_provider', $user->getRoles());
    $tokenStorage            = new TokenStorage();
    $tokenStorage->setToken($authenticatedGuardToken);

    $session = self::$container->get('session');
    $session->set('_security_<security_context>', serialize($authenticatedGuardToken));
    $session->save();

    $cookie = new Cookie($session->getName(), $session->getId());
    $client->getCookieJar()->set($cookie);

    self::$container->set('security.token_storage', $tokenStorage);

这适用于创建客户端、会话和 cookie。

当请求在第一个函数中执行到$url 时,它会进入端点,确认用户确实已通过身份验证。

根据the documentation here,应该通过配置的提供者(在这种情况下使用 Doctrine)“刷新”用户,以检查给定对象是否与存储的对象匹配。

[..] 在下一个请求开始时,它被反序列化,然后传递给您的用户提供程序以“刷新”它(例如,新用户的 Doctrine 查询)。

我希望这也可以确保会话用户被一个 Doctrine 管理的用户对象替换,以防止上述错误。

如何解决会话中的用户在 PhpUnit 测试期间成为托管用户的问题?

(注意:生产代码正常工作,这个问题只在测试期间出现(遗留代码现在开始测试))

【问题讨论】:

【参考方案1】:

好的,有多个问题,但它可以通过以下方式工作:

首先,使用不正确的密码创建客户端,我正在创建(在 Fixtures 中)用户实体,usernamepassword 相同。函数getPlainPassword,虽然存在于接口中,但不是存储的东西,所以是一个空白值。

更正代码:

$client = self::createClient(
    [],
    [
        'PHP_AUTH_USER' => $user->getUsername(),
        'PHP_AUTH_PW'   => $user->getUsername(),
    ]
);

接下来,未刷新的用户需要更多时间。

config/packages/security.yaml 中,添加以下内容:

security:
  firewalls:
    test:
      security: ~

这是为了创建“test”键,因为立即在下一个文件中创建它会导致权限被拒绝错误。在config/packages/test/security.yaml 中,创建以下内容:

security:
  providers:
    test_user_provider:
      id: App\Tests\Functional\Security\UserProvider
  firewalls:
    test:
      http_basic:
        provider: test_user_provider

这会添加一个自定义 UserProvider 专门用于测试目的(因此使用 App\Tests\ 命名空间)。您必须在您的config/services_test.yaml 中注册此服务:

services:
    App\Tests\Functional\Security\:
        resource: '../tests/Functional/Security'

不确定你是否需要它,但我在config/packages/test/routing.yaml 中添加了以下内容:

parameters:
    protocol: http

由于 PhpUnit 正在通过 CLI 进行测试,默认情况下没有安全连接,可能因环境而异,因此请查看是否需要它。

最后,在config/packages/test/framework.yaml中配置测试框架:

framework:
    test: true
    session:
        storage_id: session.storage.mock_file

上述所有配置(除了http 位)是为了确保在测试期间使用自定义UserProvider 来提供User 对象。

这对其他人来说可能太过分了,但我们的设置(旧版)有一些自定义工作来为用户提供身份验证(这似乎非常相关,但远远超出了我当前的问题范围)。

回到 UserProvider,它的设置如下:

namespace App\Tests\Functional\Security;

use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class UserProvider implements UserProviderInterface

    /** @var UserRepository */
    private $userRepository;

    public function __construct(UserRepository $userRepository)
    
        $this->userRepository = $userRepository;
    

    public function loadUserByUsername($username)
    
        try 
            return $this->userRepository->getByUsername($username);
         catch (UserNotFoundException $e) 
            throw new UsernameNotFoundException("Username: $username unknown");
        
    

    public function refreshUser(UserInterface $user)
    
        return $this->loadUserByUsername($user->getUsername());
    

    public function supportsClass($class)
    
        return User::class === $class;
    

注意:如果你使用它,你需要在你的 UserRepository 中有一个getByUsername 函数。


请注意,这可能不适合您。也许你需要改变它,也许它完全关闭了。无论哪种方式,都想为未来的灵魂留下一个解决方案。

【讨论】:

以上是关于以编程方式对用户进行身份验证以进行 PhpUnit 功能测试 - 不受 Doctrine 管理 - Symfony 4.3的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 DaoAuthenticationProvider 以编程方式使用 Spring Security 对用户进行身份验证

如何使用用户默认凭据以编程方式对受 Cloud Identity-Aware Proxy (Cloud IAP) 保护的资源进行身份验证?

从 C# 以编程方式进行 Firebase 身份验证用户管理

如何以编程方式检查某个 URL 是不是需要使用 Spring Security 进行身份验证?

如何使用 Spring-security 以编程方式登录用户?

是否可以通过 Spring Security 以编程方式进行身份验证并使其持久化?