Symfony Deprecation on SessionTokenStorage when generating a csrf token in phpunit functionnal tests

Viewed 508

I'm on symfony 5.4

I didn't understand what symfony really need in order to correct this deprecation:

Since symfony/security-csrf 5.3: Using the "Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage" without a session has no effect and is deprecated. It will throw a "Symfony\Component\HttpFoundation\Exception\SessionNotFoundException" in Symfony 6.0 1x in MeansCablesControllerTest::TestDatagridAdd from App\Tests\Controller

My function in tests/Controller/MeansBenchesControllerTest.php (WebTestCase) :

function datagridAddUpdate($controllerName, $dataArray)
    {
        $client = static::createClient();
        
        $usersRepository = static::getContainer()->get(UsersRepository::class);
        $testUserAdmin = $usersRepository->find(1);
        
        $client->loginUser($testUserAdmin);

        $csrfToken = $client->getContainer()->get('security.csrf.token_manager')->getToken($controllerName.'Token_item');

        $dataArray['_token'] = $csrfToken;

        $crawler = $client->request('POST', '/datagridAddUpdate/'.$controllerName,$dataArray, [], ['HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest']);

        $this->assertResponseIsSuccessful('Status code 2xx pour datagridAdd : '.$controllerName);

    }
2 Answers

Running this before building the form will make sure there is a session available for it:

$request = new Request();
$request->setSession(new Session(new MockArraySessionStorage()));

self::getContainer()->get(RequestStack::class)->push($request);

Unfortunately, @bart's answer didn't work for me. It effectively suppressed the deprecation warning, but it creates a separate session from the loginUser() session. My goal was to log in, and then be able to set session values on the common logged in session.

I figured out a workaround for my use case, documented here: https://github.com/symfony/symfony/discussions/46961

use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\User\UserInterface;

abstract class AbstractWebTestCase extends WebTestCase
{
    private KernelBrowser $client;
    private SessionInterface $session;
    
    public function setUp(): void
    {
        parent::setUp();
        $this->client = static::createClient();
        ...
    }

    ...

    /**
     * This replicates static::createClient()->loginUser()
     * Inspect that method, as there are additional checks there that may be necessary for your use case.
     * The magic here is tracking an internal $session object that can be updated as needed.
     */
    protected function loginUser(UserInterface $user): void
    {   
        $token = new TestBrowserToken($user->getRoles(), $user, $firewallContext);
        $container = static::getContainer();
        $container->get('security.untracked_token_storage')->setToken($token);

    $this->session = $container->get('session.factory')->createSession();
        $this->setLoginSessionValue('_security_'.$firewallContext, serialize($token));

        $domains = array_unique(array_map(function (Cookie $cookie) {
            return $cookie->getName() === $this->session->getName() ? $cookie->getDomain() : '';
        }, $this->client->getCookieJar()->all())) ?: [''];
        
        foreach ($domains as $domain) {
            $cookie = new Cookie($this->session->getName(), $this->session->getId(), null, null, $domain);
            $this->client->getCookieJar()->set($cookie);
        }

        return $this;
    }

    /** @param mixed $value */
    protected function setLoginSessionValue(string $name, $value): self
    {
        if (isset($this->session)) {
            $this->session->set($name, $value);
            $this->session->save();
            return $this;
        }
        throw new \LogicException("loginUser() must be called to initialize session");
    }

    ...
}

And now we can update the session:

use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage;

class MyWebTest extends AbstractWebTestCase
{
    public function testSomething(): void
    {
        $user = ...;
        $this->loginUser($user);
        
        // Technically, you don't need to generate a real token here, and instead could use any test string
        $tokenId = ...;
        $csrfToken = static::getContainer()->get('security.csrf.token_generator')->generateToken();
        $this->setLoginSessionValue(SessionTokenStorage::SESSION_NAMESPACE . "/$tokenId", $csrfToken);

        // Now you can make raw POST requests without crawling to the form page first!
    }
}
Related