以编程方式对用户进行身份验证以进行 PhpUnit 功能测试 - 不受 Doctrine 管理 - Symfony 4.3
Issue programmatically authenticating Users for PhpUnit functional test - Unmanaged by Doctrine - Symfony 4.3
我正在尝试获得一个简单的“200 响应”测试,以用于需要经过身份验证的用户的网站部分。我想我已经创建了 Session,因为在调试期间调用了 Controller 函数并检索了一个 User(使用 $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 检索,这将是有意义的。这些记录是在每个 run/for 每个测试中使用 Fixtures 创建的。这就是为什么在 Controller $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 用户应该 "refreshed" 通过配置的提供程序(在这种情况下使用 Doctrine)来检查给定对象是否与存储的对象匹配。
[..] 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).
我希望这也能确保会话 User 被 Doctrine 管理的 User 对象替换,以防止上述错误。
如何解决会话中的用户在 PhpUnit 测试期间成为托管用户的问题?
(注意:生产代码没有任何问题,这个问题只在测试期间出现(遗留代码现在开始测试))
好的,有多个问题,但通过执行以下操作使其正常工作:
首先,我使用不正确的密码创建了一个客户端,我正在创建(在 Fixtures 中)username
和 password
相同的用户实体。函数 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
函数。
请注意,这可能不是适合您的解决方案。也许你需要改变它,也许它完全关闭了。无论哪种方式,都想为任何未来的灵魂留下解决方案。
我正在尝试获得一个简单的“200 响应”测试,以用于需要经过身份验证的用户的网站部分。我想我已经创建了 Session,因为在调试期间调用了 Controller 函数并检索了一个 User(使用 $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 检索,这将是有意义的。这些记录是在每个 run/for 每个测试中使用 Fixtures 创建的。这就是为什么在 Controller $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 用户应该 "refreshed" 通过配置的提供程序(在这种情况下使用 Doctrine)来检查给定对象是否与存储的对象匹配。
[..] 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).
我希望这也能确保会话 User 被 Doctrine 管理的 User 对象替换,以防止上述错误。
如何解决会话中的用户在 PhpUnit 测试期间成为托管用户的问题?
(注意:生产代码没有任何问题,这个问题只在测试期间出现(遗留代码现在开始测试))
好的,有多个问题,但通过执行以下操作使其正常工作:
首先,我使用不正确的密码创建了一个客户端,我正在创建(在 Fixtures 中)username
和 password
相同的用户实体。函数 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
函数。
请注意,这可能不是适合您的解决方案。也许你需要改变它,也许它完全关闭了。无论哪种方式,都想为任何未来的灵魂留下解决方案。