2

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?

SilentBob
  • 51
  • 4

1 Answers1

0

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.

Jan Klan
  • 667
  • 6
  • 16
  • Hi, Thank you for your response and sorry for the (very) long delay. Unfortunatly, it doesn't seems to work for me. As I can understand, your code use the guard authenticator method which is deprecated since Sf 5.3. I'm not using it and so, it does not work (I can no longer log-in with your method). PS : if it can help you, you should not use "return $this" as it will return a "BaseWebTestCase" instead of "KernelBrowser" as expected by your method. – SilentBob May 09 '22 at 13:57
  • Hey Bob, nope, I’m on SF 6+ and so I definitely am not using Guard. The code I sent you is a copy of the standard login function that comes with Symfony, but with the few extra lines that make the session values be available to the rest if the test case. You’re right about the $this return value. In my case it’s a dead code I never tripped yet, so I didn’t even notice. Anyway, it’s not the reason for the login method to fail. If the standard method works for you bar the session parameters, maybe you can get the stock code and tweak it too, but keep it working in your context. – Jan Klan May 10 '22 at 20:31
  • I updated the code with the current & complete snippet that works in my SF 6.1 project. The upside? I can guarantee it works. The downside? It's not as much aligned with what you are asking. Search for the word `magic`, it's the only bit that really matters the way I see it. Notice the `localhost` domain in the cookie too! Also, apparently my past self replaced `return $this` with `return $client` a day after I posted this answer. PHPStan must have complained. Good spot, though. Still, I'd say invalid return statement in dead code would not have made the login method to fail. – Jan Klan May 10 '22 at 20:41