Issue programmatically authenticating Users for PhpUnit functional test – Unmanaged by Doctrine – Symfony 4.3

huangapple go评论60阅读模式
英文:

Issue programmatically authenticating Users for PhpUnit functional test - Unmanaged by Doctrine - Symfony 4.3

问题

我正在尝试让网站的一部分进行身份验证的简单的“200 Response”测试工作。我认为我已经让会话的创建工作了,在调试期间,控制器函数被调用并且用户被检索到(使用 $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检索的内容,那就会有道理。记录是在每次运行/每次测试时使用Fixtures创建的。这就是为什么在控制器中 $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;
}

创建会话:

// 基于 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 时,它进入了终端点,确认用户确实已经进行了身份验证。

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

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

我期望这也会确保会话用户被替换为Doctrine管理的用户对象,以防止上面的错误。

我应该如何解决会话中的用户在PhpUnit测试期间变为受管理的用户?

(注意:生产代码没有任何问题,这个问题仅在测试时出现(现在开始进行测试的旧代码))

英文:

I'm trying to get a simple "200 Response" test to work for a part of a website requiring an authenticated user. I think I've got the creation of the Session working, as during debugging the Controller function is called and a User is retrieved (using $this-&gt;getUser()).

However, afterwards the function fails with the following message:

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

 * A new entity was found through the relationship &#39;App\Entity\User#role&#39; 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={&quot;persist&quot;}).
 * A new entity was found through the relationship &#39;App\Entity\User#secret_property&#39; 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={&quot;persist&quot;}). (500 Internal Server Error)

Failed asserting that 500 matches expected 200.

This would make sense if this was not already stored in the (MySQL) database and retrieved with Doctrine. The records are created using Fixtures on each run/for each test. This is why in the Controller $this-&gt;getUser() functions as expected.

The test I'm wanting to work:

public function testIndex200Response(): void
{
    $client = $this-&gt;getAuthenticatedSecretUserClient();

    $this-&gt;checkPageLoadResponse($client, &#39;http://localhost/secret_url/&#39;);
}

Get a user:

protected function getAuthenticatedSecretUserClient(): HttpKernelBrowser
{
    $this-&gt;loadFixtures(
        [
            RoleFixture::class,
            SecretUserFixture::class,
        ]
    );

    /** @var User $user */
    $user = $this-&gt;entityManager-&gt;getRepository(User::class)-&gt;findOneBy([&#39;username&#39; =&gt; &#39;secret_user&#39;]);

    $client = self::createClient(
        [],
        [
            &#39;PHP_AUTH_USER&#39; =&gt; $user-&gt;getUsername(),
            &#39;PHP_AUTH_PW&#39;   =&gt; $user-&gt;getPlainPassword(),
        ]
    );

    $this-&gt;createClientSession($user, $client);

    return $client;
}

Create a session:

// 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, &#39;chain_provider&#39;, $user-&gt;getRoles());
    $tokenStorage            = new TokenStorage();
    $tokenStorage-&gt;setToken($authenticatedGuardToken);

    $session = self::$container-&gt;get(&#39;session&#39;);
    $session-&gt;set(&#39;_security_&lt;security_context&gt;&#39;, serialize($authenticatedGuardToken));
    $session-&gt;save();

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

    self::$container-&gt;set(&#39;security.token_storage&#39;, $tokenStorage);
}

This works for the creating of the client, session and cookie.

When the Request is executed to the $url in the first function, it gets into the endpoint, confirming the User is indeed authenticated.

According to the documentation here a User should be "refreshed" from via the configured provider (using Doctrine in this case) to check if a given object matches a stored object.

> [..] At the beginning of the next request, it's deserialized and then passed to your user provider to "refresh" it (e.g. Doctrine queries for a fresh user).

I would expect this would also ensure that the session User is replaced with a Doctrine managed User object to prevent the error above.

How can I go about solving that the User in the session becomes a managed User during PhpUnit testing?

(Note: the production code works without any issue, this problem only arises during testing (legacy code now starting to get tests))

答案1

得分: 0

好的,以下是翻译好的部分:

首先,由于密码不正确而创建了一个客户端,我在固定装置中创建了具有相同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对象。

这对其他人可能有点多余,但我们的设置(遗留)需要一些自定义工作来提供用于身份验证的用户(尽管与我的当前问题范围相关,但似乎非常相关)。

再次提醒,请注意,这可能不适用于您的解决方案。也许您需要进行一些修改,也许完全不相关。无论如何,我想为未来的使用者留下一个解决方案。

英文:

Ok, had multiple issues, but got it working doing the following:

First, was creating a Client using incorrect password, I was creating (in Fixtures) User entities with username and password being identical. The function getPlainPassword, though present in an interface, was not something stored, so was a blank value.

Corrected code:

$client = self::createClient(
    [],
    [
        &#39;PHP_AUTH_USER&#39; =&gt; $user-&gt;getUsername(),
        &#39;PHP_AUTH_PW&#39;   =&gt; $user-&gt;getUsername(),
    ]
);

Next, a User not being refreshed took some more.

In config/packages/security.yaml, add the following:

security:
  firewalls:
    test:
      security: ~

This is to create the "test" key, as creating that immediately in the next file will cause a permission denied error. In config/packages/test/security.yaml, create the following:

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

This adds a custom UserProvider specifically for testing purposes (hence usage App\Tests\ namespace). You must register this service in your config/services_test.yaml:

services:
    App\Tests\Functional\Security\:
        resource: &#39;../tests/Functional/Security&#39;

Not sure you'll need it, but I added in config/packages/test/routing.yaml the following:

parameters:
    protocol: http

As PhpUnit is testing via CLI, there by default is no secure connection, can vary by environment so see if you need it.

Lastly, config for test framework in config/packages/test/framework.yaml:

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

All of the above config (apart from the http bit) is to ensure that a custom UserProvider will be used to provider User objects during testing.

This might excessive for others, but our setup (legacy) has some custom work for providing Users for authentication (which seems very related but far out of my current issue scope).

Back on to the UserProvider, it's setup like so:

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-&gt;userRepository = $userRepository;
    }

    public function loadUserByUsername($username)
    {
        try {
            return $this-&gt;userRepository-&gt;getByUsername($username);
        } catch (UserNotFoundException $e) {
            throw new UsernameNotFoundException(&quot;Username: $username unknown&quot;);
        }
    }

    public function refreshUser(UserInterface $user)
    {
        return $this-&gt;loadUserByUsername($user-&gt;getUsername());
    }

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

Note: should you use this, you need to have a getByUsername function in your UserRepository.


Please note, this might not be the solution for you. Maybe you need to change it up, maybe it's completely off. Either way, thought to leave a solution for any future souls.

huangapple
  • 本文由 发表于 2020年1月3日 17:09:56
  • 转载请务必保留本文链接:https://go.coder-hub.com/59575751.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定