如何在 PHPUnit WebTestCase (Symfony 5) 中访问会话

发布于 2025-01-16 04:56:52 字数 1732 浏览 3 评论 0原文

我正在尝试在 PHPUnit WebTestCase 中测试需要会话的方法,但没有成功。

PHP 8.0,Symfony 5.4

这是我的代码:

当用户登录时,我在会话中保存自定义信息:

public function methodCalledAfterLoginSuccess(int $id_internal_network, SessionInterface $session): Response
    {
        $session->set('current_internal_network',$id_internal_network);
        return $this->redirectToRoute("dashboard");
    }

在某些控制器中,我得到这样的值:

#[Route('/contract/list', name: 'list_contract')]
public function listContracts(Request $request, SessionInterface $session): Response
    {
        $currentInternalNetwork = $session->get('current_internal_network');
        (...)

一切都很好。然后,我正在设置功能测试:

class SomeController extends WebTestCase

public function setUp(): void
    {
        $this->client = static::createClient([], ['HTTPS' => true]);
        parent::setUp();
    }

public function testShowContractSearchForm(): void
    {
        $session = new Session(new MockFileSessionStorage());
        $session->start();
        $this->login('admin');
        dd($session->get('current_internal_network'));
        $this->client->request('GET', '/contract/list');
        self::assertResponseIsSuccessful();
    }

但是 $session->get('current_internal_network') 为空

方法 $this->login('admin'); 将提交包含正确信息的登录表单,因此我在测试中“登录”,这部分没问题。

我的framework.yaml:

when@test:
    framework:
        test: true
        session:
            storage_factory_id: session.storage.factory.mock_file

我不需要在测试中专门访问$session,但方法listContracts()需要有一个会话,其中填充了登录部分的正确信息。

我缺少什么?

I'm trying to test methods which requires Sessions in my PHPUnit WebTestCase, with no success.

PHP 8.0, Symfony 5.4

Here's my code:

When user log-in, I'm saving custom info in session:

public function methodCalledAfterLoginSuccess(int $id_internal_network, SessionInterface $session): Response
    {
        $session->set('current_internal_network',$id_internal_network);
        return $this->redirectToRoute("dashboard");
    }

In some controllers, I get this value like this:

#[Route('/contract/list', name: 'list_contract')]
public function listContracts(Request $request, SessionInterface $session): Response
    {
        $currentInternalNetwork = $session->get('current_internal_network');
        (...)

Everything works great. Then, I'm setting my functional tests:

class SomeController extends WebTestCase

public function setUp(): void
    {
        $this->client = static::createClient([], ['HTTPS' => true]);
        parent::setUp();
    }

public function testShowContractSearchForm(): void
    {
        $session = new Session(new MockFileSessionStorage());
        $session->start();
        $this->login('admin');
        dd($session->get('current_internal_network'));
        $this->client->request('GET', '/contract/list');
        self::assertResponseIsSuccessful();
    }

But $session->get('current_internal_network') is empty

The method $this->login('admin'); will submit a login form with correct info, so I'm "logged" in my tests, this part is ok.

my framework.yaml:

when@test:
    framework:
        test: true
        session:
            storage_factory_id: session.storage.factory.mock_file

I do not need specifically to access $session in my tests BUT the method listContracts() need to have a session filled with correct info from the login part.

What I'm missing?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

阿楠 2025-01-23 04:56:52

我遇到了同样的问题,最终实现了我自己的替代方案 KernelBrowser::loginUser()。就我而言,我有一个多租户应用程序,并且我将活动租户的 ID 保留在用户会话中。

仅作为上下文,这是我的用户案例:

我使用一些事件订阅者来验证(1)当前用户有权访问用户会话中的租户,以及(2)所有转换为 Doctrine 实体的请求参数都属于当前处于活动状态的租户。

这是我防止人们试图访问他们不应该访问的内容的第一道防线。

在我的功能测试中,我使用 KernelBrowser 来调用类似 /task/1 的 URL。 “任务”属于单个租户,并且需要经过身份验证的用户才能访问它,因此测试用例必须与经过身份验证的用户以及存储在会话中的租户一起使用。

我不想引入仅测试的方式如何以其他方式指定租户 ID(例如 /task/1?tenant=1)。我认为这样的事情是一场即将发生的安全灾难。

无论如何,在客户端实例中设置正确的会话 cookie 之前,这里是创建会话并向其添加一些参数的帮助器方法。

它的工作方式就像一个魅力,我正在考虑提交一个 PR,其中 KernelBrowser::loginUser() 将接受一组应注入到模拟会话中的属性的键/值对。

<?php

namespace App\Tests;

use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\TestBrowserToken;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\BrowserKit\AbstractBrowser;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\Request;

abstract class BaseWebTestCase extends WebTestCase
{
    protected function loginUser(KernelBrowser $client, OrganisationUser|User|string $user, string $firewallContext = 'main'): KernelBrowser
    {
        // If the $user is just a fixture key, we'll try to convert it into an actual entity.
        if (is_string($user)) {
            $fixture = $this->getFixture($user);

            if (!$fixture instanceof User && !$fixture instanceof OrganisationUser) {
                throw new Error(sprintf(
                    'The fixture "%s" must be an instance of User or OrganisationUser, "%s" found.',
                    $user,
                    get_debug_type($fixture)
                ));
            }

            $user = $fixture;
            unset($fixture);
        }

        $securityUser = $user instanceof User ? $user : $user->getUser();

        $token = new TestBrowserToken($securityUser->getRoles(), $securityUser, $firewallContext);

        $container = $client->getContainer();
        $container->get('security.untracked_token_storage')->setToken($token);

        if ($container->has('session.factory')) {
            $session = $container->get('session.factory')->createSession();
        } elseif ($container->has('session')) {
            $session = $container->get('session');
        } else {
            return $client;
        }
        $session->set('_security_'.$firewallContext, serialize($token));

        // The magic happens here: If the $user is a OrganisationUser, store the Organisation ID in the session that gets picked up by the client later
        if ($user instanceof OrganisationUser) {
            $session->set(OrganisationService::ACTIVE_ORGANISATION, $user->getOrganisation()->getId());
        }


        $session->save();

        // IMPORTANT: the domain name must be set to localhost, otherwise it does not work
        $cookie = new Cookie($session->getName(), $session->getId(), null, null, 'localhost');
        // End of magic

        $client->getCookieJar()->set($cookie);

        return $client;
    }
}

关键是在设置会话ID的同时添加与安全相关的东西。

我确信稍后有一种正确的方法可以从代码的任何地方访问客户端会话,但我还没有找到它。如果有人知道的话,我很想知道该怎么做。

我希望它有帮助。

I had the same issue and ended up implementing my own alternative to KernelBrowser::loginUser(). In my case, I have a multi-tenant application, and I am keeping the ID of the active tenant in the user session.

Just for context, here is my user case:

I'm using some event subscribers to verify the (1) the current user has access to the tenant in the user's session, and (2) all request parameters converted into Doctrine Entities belong to the tenant that is currently active.

This is my first-line defense against people trying to access things they should not.

In my functional tests, I'm using the KernelBrowser to call URLs like /task/1. A "Task" belongs to a single tenant and requires an authenticated user to access it, so the test case has to work with an authenticated user, and with a tenant stored in the session.

I did not want to introduce a test-only way how to specify the tenant ID some other way (such as /task/1?tenant=1). I think of things like this as a security disaster waiting to happen.

Anyway, here is the helper method that creates the session and adds some parameters to it, before setting the right session cookie in the client instance.

It works like a charm and I'm thinking of submitting a PR where KernelBrowser::loginUser() would accept an array of key/value pairs of properties that should be injected into the mocked session.

<?php

namespace App\Tests;

use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\TestBrowserToken;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\BrowserKit\AbstractBrowser;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\Request;

abstract class BaseWebTestCase extends WebTestCase
{
    protected function loginUser(KernelBrowser $client, OrganisationUser|User|string $user, string $firewallContext = 'main'): KernelBrowser
    {
        // If the $user is just a fixture key, we'll try to convert it into an actual entity.
        if (is_string($user)) {
            $fixture = $this->getFixture($user);

            if (!$fixture instanceof User && !$fixture instanceof OrganisationUser) {
                throw new Error(sprintf(
                    'The fixture "%s" must be an instance of User or OrganisationUser, "%s" found.',
                    $user,
                    get_debug_type($fixture)
                ));
            }

            $user = $fixture;
            unset($fixture);
        }

        $securityUser = $user instanceof User ? $user : $user->getUser();

        $token = new TestBrowserToken($securityUser->getRoles(), $securityUser, $firewallContext);

        $container = $client->getContainer();
        $container->get('security.untracked_token_storage')->setToken($token);

        if ($container->has('session.factory')) {
            $session = $container->get('session.factory')->createSession();
        } elseif ($container->has('session')) {
            $session = $container->get('session');
        } else {
            return $client;
        }
        $session->set('_security_'.$firewallContext, serialize($token));

        // The magic happens here: If the $user is a OrganisationUser, store the Organisation ID in the session that gets picked up by the client later
        if ($user instanceof OrganisationUser) {
            $session->set(OrganisationService::ACTIVE_ORGANISATION, $user->getOrganisation()->getId());
        }


        $session->save();

        // IMPORTANT: the domain name must be set to localhost, otherwise it does not work
        $cookie = new Cookie($session->getName(), $session->getId(), null, null, 'localhost');
        // End of magic

        $client->getCookieJar()->set($cookie);

        return $client;
    }
}

The key is to set the session ID at the same time the security-related things are added to it.

I'm sure there is a proper way to access the client session later on, anywhere from the code, but I have not found it yet. I'd love to know how to do it, if anybody knows.

I hope it helps.

云柯 2025-01-23 04:56:52

这应该可以解决问题(例如,将区域设置保存到会话中):

public function testSomeSessionValue() {
     // ...

     $client->jsonRequest('GET', '/api/set-locale', ['locale' => 'en_US']);

     $sessionLocale = $client->getRequest()->getSession()->get('locale');

     $this->assertEquals('en_US', $sessionLocale);
}

This should do the trick (just as an example, saving a locale into the session):

public function testSomeSessionValue() {
     // ...

     $client->jsonRequest('GET', '/api/set-locale', ['locale' => 'en_US']);

     $sessionLocale = $client->getRequest()->getSession()->get('locale');

     $this->assertEquals('en_US', $sessionLocale);
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文