symfony / panther

A browser testing and web crawling library for PHP and Symfony
MIT License
2.93k stars 221 forks source link

Panther with Authentication #283

Open Sethbass opened 4 years ago

Sethbass commented 4 years ago

Hello,

First I would like to thank you to improve this part of symfony and helping us to get rid of Behat.

I have created a symfony app which needs authentication. This means that / is protected by the firewall main and every URL needs an authenticated user. On top of that I have custom authenticator which extends AbstractGuardAuthenticator. The authentication process is based on CAS (not my choice I have to cope with it).

I have tried to start my functional tests using Panther and followed the documentation here : https://github.com/symfony/panther

So far so good :) Then I have to deal immediately with the authentication, so I found this tutorial : https://symfony.com/doc/4.3/testing/http_authentication.html

First thing I have discovered is that I need to visit a page on my website otherwise I get this error :

Facebook\WebDriver\Exception\InvalidCookieDomainException: invalid cookie domain
  (Session info: chrome=79.0.3945.117)
  (Driver info: chromedriver=78.0.3904.70 (edb9c9f3de0247fd912a77b7f6cae7447f6d3ad5-refs/branch-heads/3904@{#800}),platform=Mac OS X 10.15.2 x86_64)

Ok then I have created a test page accessible anonymously and got rid of the error. Not sure this is a best practice.

Then I was able to run the following code :

<?php

namespace App\Tests;

use App\Entity\User;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\Panther\PantherTestCase;
use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken;

class HomeControllerTest extends PantherTestCase
{
    private $client = null;

    public function setUp()
    {
        $this->client = static::createPantherClient();
    }

    public function testHome()
    {

        $this->logIn();
        $crawler = $this->client->request('GET', '/test');

        sleep(5);
        $user = self::$container->get('security.helper')->getUser();
        echo ('The user is :'.$user);
        $crawler = $this->client->request('GET', '/');

    }

    private function logIn()
    {

        $doctrine = self::$container->get('doctrine.orm.default_entity_manager');

        /** @var User $user */
        $user = $doctrine->getRepository(User::class)->findOneBy(['email' => 'sebastien@code-it.fr']);

        $session = self::$container->get('session');

        // you may need to use a different token class depending on your application.
        // for example, when using Guard authentication you must instantiate PostAuthenticationGuardToken
        $token = new PostAuthenticationGuardToken($user, 'main', $user->getRoles());
        $session->set('_security_main', serialize($token));
        $session->save();
        $cookie = new Cookie($session->getName(), $session->getId());
        $this->client->getCookieJar()->set($cookie);
    }

}

The problem is that I am still redirected to the CAS authentication page calling $crawler = $this->client->request('GET', '/'); My user is always NULL which means I am not authenticated.

I thought Session Storage Mock file could be a suspect but actually all my tests were unsuccessful.

Help :)

Thanks a lot for reading

ioniks commented 4 years ago

I have the same issue.

Sethbass commented 4 years ago

Hello,

If anyone is getting the same error about user authentication in functional tests implementing the token, I found the issue.

This was not related to Panther but to the security component, indeed I did not notice the logs telling me that the token needed to be refresh and then the user was automatically deauthenticated => cannot refresh the token because the user has changed. This means that symfony was not able to compare properly the user with the authentication system which I guess is by default comparing username password and salt. But in the case of CAS authentication you do not have password or salt but only the username.

In order to solve this, you need to implement Symfony\Component\Security\Core\User\EquatableInterface and the related method isEqualTo.

public function isEqualTo(UserInterface $user)
{
        if ($this->username !== $user->getUsername()) {
            return false;
        }

        return true;
}

So when the security component will check if it needs a refresh, it will work properly.

I am now able to run a classic WebTestCase and to access my home page.

The only issue remaining is the invalid cookie domain which is preventing me to use Panther for now. I will investigate on this side and let you know in case I fund something.

Cheers

Sethbass commented 4 years ago

Hello,

I have tried many things on my side, and this is not possible to get rid of the "invalid cookie domain" error, unless you create a new page (simple page with nothing or an h1 is enough) to initiate the process. This is not related to Panther but to php webdriver. I found many entries about it on stackoverflow.

The real issue is that my code to log in is working with absolutely no issue doing a WebTestCase but is not working with Panther.

    private function logIn($email)
    {
        /** @var User $user */
        $user = self::$container->get('doctrine')->getRepository(User::class)->findOneBy(['email' => $email]);
        $session = self::$container->get('session');

        $firewallName = 'main';
        // if you don't define multiple connected firewalls, the context defaults to the firewall name
        // See https://symfony.com/doc/current/reference/configuration/security.html#firewall-context
        $firewallContext = 'main';

        // you may need to use a different token class depending on your application.
        // for example, when using Guard authentication you must instantiate PostAuthenticationGuardToken
        $token = new PostAuthenticationGuardToken($user, $firewallName, ['ROLE_ADMIN']);
        $session->set('_security_'.$firewallContext, serialize($token));
        $session->save();

        $cookie = new Cookie($session->getName(), $session->getId());
        $this->client->getCookieJar()->set($cookie);
    }

As far as I understand, despite I had the cookie to Panther Cookie Jar it is not used to authenticate and it keeps sending me to the CAS authentication page.

Here is the content of the cookie Jar of my Panther client:

array(1) {
  [0]=>
  object(Symfony\Component\BrowserKit\Cookie)#602 (9) {
    ["name":protected]=>
    string(10) "MOCKSESSID"
    ["value":protected]=>
    string(64) "a1c18866bc13ca0b2233feb351b02788a93961672793420bfa24dcfc25a93923"
    ["expires":protected]=>
    NULL
    ["path":protected]=>
    string(1) "/"
    ["domain":protected]=>
    string(9) "127.0.0.1"
    ["secure":protected]=>
    bool(false)
    ["httponly":protected]=>
    bool(true)
    ["rawValue":protected]=>
    string(64) "a1c18866bc13ca0b2233feb351b02788a93961672793420bfa24dcfc25a93923"
    ["samesite":"Symfony\Component\BrowserKit\Cookie":private]=>
    NULL
  }
}

Based on the log in my test environment I can see no entry from the Security component like using the WebTestCase. So my conclusion is that the cookie is not even considered and used for authentication in Symfony. PantherClient is just redirecting me to the authentication page.

Does anyone have a clue ?

Thanks a lot,

andrescevp commented 4 years ago

here same problem... some glue? thanks!

gponty commented 4 years ago

Same problem here... any idea ?

Sethbass commented 4 years ago

@andrescevp , @gponty , I ended up using classic classic Web test case and the Crawler which are working perfectly with the code above. I even found a way to add items to the collection without using JS. Maybe I will look into Panther once we have a solution...

JohnstonCode commented 4 years ago

This it what i had to throw together to get it working. I had to set readinessPath in the option to make sure the client wasn't redirected to the login page as I use SAML SSO. Then you need to make another request to your site before it will allow you to set a cookie. It doesn't matter if the page returns a 404 response.

class HomeControllerTest extends PantherTestCase
{
    private $client;

    protected function setUp()
    {
        $this->client = static::createPantherClient(['readinessPath' => '/error']);
    }

    public function testHome()
    {
        $this->logIn();

        $this->client->request('GET', '/');

        $this->assertPageTitleContains('Home Page');
    }

    private function logIn()
    {
        $this->client->request('GET', '/error');

        $doctrine = self::$container->get('doctrine.orm.default_entity_manager');

        /** @var User $user */
        $user = $doctrine->getRepository(User::class)->findOneBy(['email' => 'test@example.com']);

        $session = self::$container->get('session');

        // you may need to use a different token class depending on your application.
        // for example, when using Guard authentication you must instantiate PostAuthenticationGuardToken
        $token = new PostAuthenticationGuardToken($user, 'main',$user->getRoles());
        $session->set('_security_main', serialize($token));
        $session->save();
        $cookie = new Cookie($session->getName(), $session->getId());
        $this->client->getCookieJar()->set($cookie);
    }
}
Sethbass commented 4 years ago

Hi @JohnstonCode, you should check the upcoming changes on SF 5.1 :) There will be a new auth system for the tests. As far as I understood there will be no need for us anymore to generate this code. https://symfony.com/blog/new-in-symfony-5-1-simpler-login-in-tests

Cheers :)

JohnstonCode commented 4 years ago

Thanks, looks good hopefully this resolves our issue.

antoine1003 commented 4 years ago

Same issue here :/

bastien70 commented 4 years ago

Same here with :

        $client = static::createPantherClient();

        /** @var User $user */
        $user = $this->getSuperAdminAccount();

        $session = self::$container->get('session');

        $token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
        $session->set('_security_main', serialize($token));
        $session->save();

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

Any solution ?

nizarRebhi commented 4 years ago

For setting-up cookie's domain, you have to request firstly on the domain, then set-up the cookie...

$client->request('GET', '/');

kolvin commented 4 years ago

@nizarRebhi thanks for the comment, working a treat for me now

shadowc commented 3 years ago

So I've been investigating the login process as well and it seems $client->loginUser is not present in the PantherClient. I'm not sure if this should be implemented or not, but if not implemented, I suppose the project should provide an equally simple and efficient method to authenticate.

Us cypress users are used to just send a POST request to the login Url that gets us logged in for the e2e tests without going through the UI login process. May be panther can allow for POST calls in their client (I've tried this and it just plainly said POST calls are not supported) or provide a shortcut function like loginUser

njsa04 commented 3 years ago

@shadowc Have you been able to login yourself? I have tried methods outlined earlier - but continue to get CSRF errors. The createClient and then ->loginUser() method works fine. But using the pantherClient and setting the cookies with a generated token value doesn't seem to work. I suspect it is because the test is running in test env but the URL it's hitting is in the dev environment, but I haven't figured out where to go from here.

@Sethbass have you found a solution to getting PantherClient to work with authentication?

@JohnstonCode did you say you got it working with PantherClient and authentication?

Would love to get this working.

So I've been investigating the login process as well and it seems $client->loginUser is not present in the PantherClient. I'm not sure if this should be implemented or not, but if not implemented, I suppose the project should provide an equally simple and efficient method to authenticate.

Us cypress users are used to just send a POST request to the login Url that gets us logged in for the e2e tests without going through the UI login process. May be panther can allow for POST calls in their client (I've tried this and it just plainly said POST calls are not supported) or provide a shortcut function like loginUser

drupol commented 3 years ago

Dear SethBass,

I'm trying to do functional tests on an app protected by CAS and I'm not able to pass the authentication yet.

I have a few questions, could you please help me?

  1. Could you show me your security.yaml ?
  2. Is there something else, besides implements EquatableInterface to do to get this working?
  3. Could you please tell me which CAS bundle you're using?

Thanks!

madxmind commented 3 years ago

Hi,

Has anyone found a solution to use : static::createClient()->loginUser($myUser) in a PanterClient : static::createPantherClient()->loginUser($myUser)

PS : it doesn't work so (Error: Call to undefined method Symfony\Component\Panther\Client::loginUser())

Symfony 5.3 PphUnit 9.5

Thanks!

hutnikau commented 3 years ago

For me it was the issue with empty cookies when env switched to "test"

Try to comment out mock session storage factory:

#framework.yaml
when@test:
    framework:
        test: true
#        session:
#            storage_factory_id: session.storage.factory.mock_file
holema commented 2 years ago

I want to use panther to test an application which uses a SSO to authenticate. Unfortunantaly up to now it is not possible to test this application with panther because of the missing login fuction wich is implemented in the WebTestCase of the kernel application. When I want to login into the application I get sveral redirects ant he redirect url has to be correct as well. so the effort is so huge to simulate it with a SSO provider.

I would be so great if here is an update planned.

chadcasey commented 2 years ago

I'm also experiencing this same issue.

// Assuming an existing user:
$user = iterator_to_array($userRepo->findByRole(User::ROLE_SUPER_ADMIN, 1))[0];

// Get the Panther client:
$client = static::createPantherClient();

// Get a new token:
$token = new OAuthToken('test', $user->getRoles());
$token->setUser($user);

// Assign it to the session:
$session = static::getContainer()->get('session');
$session->set('_security_main', serialize($token));
$session->save();

// Add cookies to the client:
$client->request(Request::METHOD_GET, '/'); // Need to hit the domain first before we can set cookies
$client->getCookieJar()->set(new Cookie($session->getName(), $session->getId()));
$client->request(Request::METHOD_GET, '/'); // <-- Still redirected back to the login page

Previously, I tried creating the panther client with the hostname and port configured so that I can use PhpStorm breakpoints with traffic coming from the Panther browser, and I could see that the cookies are being set as expected. But the $_SESSION global doesn't have any of the security token information that I'm setting as shown above. Is there a better way to set session properties that will be used by the Panther browser? Could that be the issue preventing SSO authentication?

alexandre-mace commented 2 years ago

Hi,

Has anyone found a solution to use : static::createClient()->loginUser($myUser) in a PanterClient : static::createPantherClient()->loginUser($myUser)

PS : it doesn't work so (Error: Call to undefined method Symfony\Component\Panther\Client::loginUser())

Symfony 5.3 PphUnit 9.5

Thanks!

Still facing this problem, any solution ?

arderyp commented 2 years ago

I can authenticate in Panther with CAS, but I am not using a bundle but rather my own Guard implementation.

I can navigate to pages that require authentication, and click through things, but I have hit a wall where my Ajax POST requests are not working. They get triggered but return 404 not found. Posting to json endpoints provided 404, but to HTML endpoints gives a 302 redirect to my login page.

More info on my POST issue and my authentication implementation here: https://github.com/symfony/panther/issues/547

While I am not using a CAS bundle, I based by Guard implementation on this (its a few years old so probably a bit out of date, but the same basic idea): https://github.com/PRayno/CasAuthBundle/blob/master/Security/CasAuthenticator.php

P.S. I don't implement EquatableInterface, and I do have to hit / before setting my login token. I've tested implementing EquatableInterface just now and it does not solve my POST issue.

arderyp commented 2 years ago

@shadowc, you said

May be panther can allow for POST calls in their client (I've tried this and it just plainly said POST calls are not supported)

Where is this documented?

drupol commented 2 years ago

I can authenticate in Panther with CAS, but I am not using a bundle but rather my own Guard implementation.

I can navigate to pages that require authentication, and click through things, but I have hit a wall where my Ajax POST requests are not working. They get triggered but return 404 not found. Posting to json endpoints provided 404, but to HTML endpoints gives a 302 redirect to my login page.

More info on my POST issue and my authentication implementation here: https://github.com/symfony/panther/issues/547

While I am not using a CAS bundle, I based by Guard implementation on this (its a few years old so probably a bit out of date, but the same basic idea): https://github.com/PRayno/CasAuthBundle/blob/master/Security/CasAuthenticator.php

P.S. I don't implement EquatableInterface, and I do have to hit / before setting my login token. I've tested implementing EquatableInterface just now and it does not solve my POST issue.

How about using a bundle for CAS ? Wouldn't it be a bit more practical?

arderyp commented 2 years ago

Interestingly, other ajax posts seem to work, so maybe there is something about this specific implementation that's wonky. I will test further and report back.

arderyp commented 2 years ago

@drupol no, I've found it is not. The CAS protocol is very simple and easy to implement (at least on the consumer side). My coworkers who are depending on phpCas and various CAS bundles have experienced breakage from updates to our CAS servers, due to their package implementations. I, on the other hand, have not :)

In addition, if something breaks or changes (hasn't happened in 4 years), its much easier/quicker for me to fix it myself rather than wait on third party developers to acknowledge and fix the issue or accept my pull request. In general, programming has become way too reliant on dependencies for simple functionality that can be implemented/controlled in house. This comes with all sorts of problems. I can't say my software if free from this problem altogether, not by a long shot. But, thankfully, it is in the context of CAS.

If CAS were more complex, I'd reach for a library, but its very simple XML parsing.

Out of curiosity, which CAS bundle do you recommend?

P.S. I've resolved my ajax POST mystery, and it turned out authentication was not the problem.

drupol commented 2 years ago

@drupol no, I've found it is not. The CAS protocol is very simple and easy to implement (at least on the consumer side). My coworkers who are depending on phpCas and various CAS bundles have experienced breakage from updates to our CAS servers, due to their package implementations. I, on the other hand, have not :)

In addition, if something breaks or changes (hasn't happened in 4 years), its much easier/quicker for me to fix it myself rather than wait on third party developers to acknowledge and fix the issue or accept my pull request. In general, programming has become way too reliant on dependencies for simple functionality that can be implemented/controlled in house. This comes with all sorts of problems. I can't say my software if free from this problem altogether, not by a long shot. But, thankfully, it is in the context of CAS.

If CAS were more complex, I'd reach for a library, but its very simple XML parsing.

Out of curiosity, which CAS bundle do you recommend?

P.S. I've resolved my ajax POST mystery, and it turned out authentication was not the problem.

Try this one: https://github.com/ecphp/cas-bundle

Sorry for the brevity, replying from the smartphone.

arderyp commented 2 years ago

all good, thanks for the recommendation

EDIT: I see you're the primary contributor, nice! Thanks for the FOSS offering.

shadowc commented 2 years ago

@shadowc, you said

May be panther can allow for POST calls in their client (I've tried this and it just plainly said POST calls are not supported)

Where is this documented?

Not documented. I believe I was referring to an error message that came out in the console. This message is a year old so things could have changed since!

drupol commented 2 years ago

all good, thanks for the recommendation

EDIT: I see you're the primary contributor, nice! Thanks for the FOSS offering.

You're welcome! I developed this bundle for the European Commission and we are using everyday in every Symfony app. Feel free to let me know if anything goes wrong, we're quite responsive!

bastoune commented 2 years ago

Hi, Has anyone found a solution to use : static::createClient()->loginUser($myUser) in a PanterClient : static::createPantherClient()->loginUser($myUser) PS : it doesn't work so (Error: Call to undefined method Symfony\Component\Panther\Client::loginUser()) Symfony 5.3 PphUnit 9.5 Thanks!

Still facing this problem, any solution ?

Hi, did anybody has update about this ?

Using Symfony 5.4 and still facing the issue.

mhujer commented 2 years ago

Hi! I'm using the following base class for our E2E tests (it is based on previous comments in this discussion and code in KernelBrowser::loginUser) and it works fine on SF 6.1.

//...
use Symfony\Component\Panther\Client as PantherClient;
//...

abstract class PantherTestCase extends KernelTestCase
{

    use WebTestAssertionsTrait;

    /**
     * @param string[] $options An array of options to pass to the createKernel class
     * @param string[] $kernelOptions
     * @param string[] $managerOptions
     */
    protected static function createAuthenticatedPantherClient(
        User $user,
        array $options = [],
        array $kernelOptions = [],
        array $managerOptions = []
    ): PantherClient
    {
        $client = self::createPantherClient();

        // without this request, the session cookie could not be set
        $client->request('GET', '/');

        // Inspired by \Symfony\Bundle\FrameworkBundle\KernelBrowser::loginUser()
        $token = new UsernamePasswordToken($user, 'main', $user->getRoles());

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

        $session = $container->get('session.factory')->createSession();
        $session->set('_security_main', serialize($token));
        $session->save();

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

        return $client;
    }
tacman commented 2 years ago

Thanks, @mhujer , this looks very promising.

When I make the first call after getting the client, I get


Testing /home/tac/survos/demos/symfony-demo/tests

Facebook\WebDriver\Exception\InvalidCookieDomainException : invalid cookie domain
  (Session info: headless chrome=103.0.5060.53)

With the $client->request('GET', '/'); line, I made some progress, but the authenticated route returns the login page.

With the I tried @hutnikau idea and got rid of the session in framework when test:

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

And got the curious error:


Testing /home/tac/survos/demos/symfony-demo/tests

RuntimeException : Failed to start the session because headers have already been sent by "/home/tac/survos/demos/symfony-demo/vendor/bin/.phpunit/phpunit-9.5-0/src/Util/Printer.php" at line 104.
 /home/tac/survos/demos/symfony-demo/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php:135
 /home/tac/survos/demos/symfony-demo/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php:296
 /home/tac/survos/demos/symfony-demo/vendor/symfony/http-foundation/Session/Session.php:258
 /home/tac/survos/demos/symfony-demo/vendor/symfony/http-foundation/Session/Session.php:278
 /home/tac/survos/demos/symfony-demo/vendor/symfony/http-foundation/Session/Session.php:86
 /home/tac/survos/demos/symfony-demo/tests/E2eTest.php:41
 /home/tac/survos/demos/symfony-demo/tests/E2eTest.php:58

The test itself is using the latest (6.1) symfony demo.


use App\Entity\User;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\Panther\Client;
use Symfony\Component\Panther\PantherTestCase;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;

class E2eTest extends PantherTestCase
{
    /**
     * @param string[] $options An array of options to pass to the createKernel class
     * @param string[] $kernelOptions
     * @param string[] $managerOptions
     */
    protected static function createAuthenticatedPantherClient(
//        User $user,
        ?string $username=null,
        array $options = [],
        array $kernelOptions = [],
        array $managerOptions = []
    ): Client
    {
        $client = self::createPantherClient();
        if ($username) {

          $client->request('GET', '/'); // removing this line shows a different error

            $container = self::getContainer();
            $user = $container->get('doctrine')->getRepository(User::class)->findOneBy(['username' => $username]);
            assert($user, "Invalid user $username, not in user database");

            // Inspired by \Symfony\Bundle\FrameworkBundle\KernelBrowser::loginUser()
            $token = new UsernamePasswordToken($user, 'main', $user->getRoles());

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

            $session = $container->get('session.factory')->createSession();
            $session->set('_security_main', serialize($token));
            $session->save();

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

        }

        return $client;
    }
    public function testMyApp(): void
    {
//        static $client = static::createPantherClient(); // Your app is automatically started using the built-in web server
        $client = $this->createAuthenticatedPantherClient('jane_admin');
        $client->request('GET', '/');

You mentioned it was working for you -- can you post the entire test in a gist or repo somewhere? Or here? Thanks.

shadydealer commented 2 years ago

@tacman This no longer works since https://github.com/symfony/symfony/discussions/45662 . Unfortunately I cannot find ANY workaround, since that change. I can't even login a user using the login form. For some reason it submits it but nothing happens... I literally cannot get Authentication to work with panther

arderyp commented 2 years ago

@shadydealer I've worked around this in 5.4. Here are my related links.

https://github.com/symfony/panther/issues/547

https://github.com/symfony/symfony/discussions/46961#discussioncomment-3194094

I made a PR to allow sessions in tests for persistent interactions, but no one at Symfony has chimed in:

https://github.com/symfony/symfony/pull/47001

My implementation may need work, but if people want this feature, they should comment on and upvote the PR.

shadydealer commented 2 years ago

@arderyp thanks for the reply.

I managed to get my login form to work. What I had missed and am now realizing is not really part of the documentation and probably should be is that I hadn't set up my panther environment correctly. I had to: add PANTHER_APP_ENV=panther to my .env.test environment. What this does is start the panther server using a .env.panther environment that had to be set up, but I had missed for some reason. Otherwise it will use your .env.local which, in my case, caused my panther environment using my dev database and my test environment using my test database, so when I tried logging in a user using the login form it always returned incorrect credentials. Furthermore, you have to create a config/packages/panther/framework.yaml along with any other configurations you need (in my case I copied my config/packages/test files over. Once that's done your config/packages/panther/framework.yaml needs to not have the lines I've commented below (or just keep them commented like I've shown in the example):

framework:
    ...
    #
    # remove or comment these 2 lines below.
    #session:
    #  storage_factory_id: session.storage.factory.mock_file

why this is needed is explained in the following issue: https://github.com/symfony/panther/issues/159

Once all of these changes are in place I can successfully log in a user using the login form, so at least that works. But I think it should be explained in the documentation a bit better.

I realize this solution is not ideal for your case (I think probably everybody would prefer not having to login using their login form) but at least it's something that others might find useful, I think.

Unfortunately I've abandoned trying to set panther up and can no longer be of help on this particular issue, but I did try to replicate the loginUser() method, like you did, but did not test it after finally setting my environment up correctly, so I don't know if that would've changed anything.

plotbox-io commented 2 years ago

Hi all. I've got a workable solution for Symfony 6.1. May not be perfect for everyone but maybe can be useful for some people.

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

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

Mock session storage is the default mechanism when running tests to help avoid complications with native sessions. It essentially dumps the session to a flat file (i.e., "/app/var/cache/{APP_ENV}/sessions"). So by ensuring that both panther and test environments use the same file, we can make the automated authentication work as expected.

See below working example of helper method

protected function logInUser(Client $panther, User $user): void
{
    // Make a single request to avoid 'Invalid cookie domain' error
    $panther->request('GET', '/login');

    // This is the key part - I've just hardcoded the mock session file to be where I know the panther one will
    // be stored in the other (panther env) webserver
    $session = new Session(new MockFileSessionStorage('/app/var/cache/panther/sessions'));

    $token = new UsernamePasswordToken($user, 'main', $user->getRoles());
    $session->set('_security_main', serialize($token));
    $session->save();

    $cookie = new Cookie($session->getName(), $session->getId());
    $panther->getCookieJar()->set($cookie);
}
arderyp commented 2 years ago

@plotbox-io this is a very interesting alternative to my approach. Great write up and clarity. Your solution feels a little more "proper" than mine, so I may move over to your approach in the coming months

shadydealer commented 2 years ago

In config/packages/framework.yaml ensure that both test and panther environments use 'mock session storage' (see example below)

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

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

Mock session storage is the default mechanism when running tests to help avoid complications with native sessions. It essentially dumps the session to a flat file (i.e., "/app/var/cache/{APP_ENV}/sessions"). So by ensuring that both panther and test environments use the same file, we can make the automated authentication work as expected.

I think this was exactly the piece that I was missing in order to get this to work, nice work @plotbox-io

shadydealer commented 2 years ago

@plotbox-io maybe you can create a pull request and try to get the loginUser method merged?

I would also suggest the following changes:

protected function logInUser(object $user, String $firewallContext): void
{
    if (!interface_exists(UserInterface::class)) {
        throw new \LogicException(sprintf('"%s" requires symfony/security-core to be installed.', __METHOD__));
    }

    if (!$user instanceof UserInterface) {
        throw new \LogicException(sprintf('The first argument of "%s" must be instance of "%s", "%s" provided.', __METHOD__, UserInterface::class, get_debug_type($user)));
    }
    // Make a single request to avoid 'Invalid cookie domain' error
    $this->request('GET', '/login');

    // This is the key part - I've just hardcoded the mock session file to be where I know the panther one will
    // be stored in the other (panther env) webserver
    $session = new Session(new MockFileSessionStorage('/app/var/cache/panther/sessions'));

    $token = new UsernamePasswordToken($user, $firewallContext, $user->getRoles());
    $session->set('_security_'.$firewallContext, serialize($token));
    $session->save();

    $cookie = new Cookie($session->getName(), $session->getId());
    $this->getCookieJar()->set($cookie);
}
  1. Using $this instead of Client $panther so it could be baked into the Panther Client
  2. Using $firewallContext
  3. Adding some of the checks for the $user and making it an object, because not all project might have a User entity defined.

(2 and 3 are taken from here)

plotbox-io commented 2 years ago

@shadydealer Thanks for the updated code. Probably would want to inject SessionStorageFactoryInterface as well rather than hardcode. A client of the library could feasibly use a database, redis, or some other method to share the session between the test/panther environment.

I've made a PR here. Let's see what the Symfony team thinks.

Regards, Richard

arderyp commented 1 year ago

I like these ideas. Likewise, it would be nice to have an optional string $loginPath = null argument to logInUser() for those of us who don't use /login.