Codeception / module-yii2

Codeception module for Yii2 framework
MIT License
16 stars 36 forks source link

Fix parallel sessions with `$I->haveFriend()` when no session is active #91

Closed michaelarnauts closed 9 months ago

michaelarnauts commented 9 months ago

When using $I->haveFriend(), my tests would fail with the following error.

[yii\base\UnknownPropertyException] Getting unknown property: yii\console\Application::session

This is because the code in _backupSession() didn't check if we actually have a Yii::$app->session before accessing Yii::$app->session->useCustomStorage.

This PR fixes this and allows me to run my tests.

See #2 and #3

SamMousa commented 9 months ago

I'm a bit confused though. This would resolve the exception for sure, but in your specific test case it will not work as expected. Since sessions are not actually used in the console application.

As far as I can tell the code should work for custom storage backends, any reason why you explicitly check for it and exclude it?

michaelarnauts commented 9 months ago

I'm a bit confused though. This would resolve the exception for sure, but in your specific test case it will not work as expected. Since sessions are not actually used in the console application.

As far as I can tell the code should work for custom storage backends, any reason why you explicitly check for it and exclude it?

I'm testing a REST application with enableSession set to false;

        'user' => [
            'identityClass' => common\models\UserIdentity::class,
            'enableSession' => false,
            'loginUrl' => null,
        ],

In my tests, I set a Bearer token to login as a specific user. I can now use this like this (simplified).

        $I->haveHttpHeader('Content-Type', 'application/json');
        $I->setUpAuthentication('admin');
        // make calls as admin

        $friend = $I->haveFriend('friend');
        $friend->does(function (FunctionalTester $I) {
            $I->haveHttpHeader('Content-Type', 'application/json');
            $I->setUpAuthentication('user');
            // make calls as user
        });

The $I->setupAuthentication() generates a token and eventually does a $I->amBearerAuthenticated($token)

SamMousa commented 9 months ago

Hmm, but if you are not really using sessions, why even use the friend workflow?

 $I->haveHttpHeader('Content-Type', 'application/json');
        $I->setUpAuthentication('admin');
        // make calls as admin

        $I->setUpAuthentication('user');
        // make calls as user

This would work just as well right? I'm happy merging this btw, but I feel it's not very descriptive of what's happening. Since there is actually no session stuff and I'm pretty sure that even outside the haveFriend closure your bearer authentication will persist and you'll not be an admin.

In big caps my expectation of what'll happen:

        $I->haveHttpHeader('Content-Type', 'application/json');
        $I->setUpAuthentication('admin');
        // YOU ARE NOW ADMIN

        $friend = $I->haveFriend('friend');
        $friend->does(function (FunctionalTester $I) {
            $I->haveHttpHeader('Content-Type', 'application/json');
            $I->setUpAuthentication('user');
            // YOU ARE NOW USER
        });
        // YOU ARE STILL USER
michaelarnauts commented 9 months ago

Hmm, it actually works as expected. I'm using another token inside the closure. I suppose inside the closure, there is a new FunctionalTester instance with fresh empty headers.

Some (simplified) debug log:

 I send post "/api/admin-call"
  [Request] POST http://localhost:8080/index-test.php/api/admin-call []
  [Request Headers] {"Authorization":"Bearer ey...4EgaIJAGeikPCriXHo"}

 FRIEND DOES ---
 I have http header "Content-Type","application/json"
 Codeception\Lib\Friend: does "Closure"
 I am bearer authenticated "ey..."

 I send post "/api/user-call"
  [Request] POST http://localhost:8080/index-test.php/api/user-call []
  [Request Headers] {"Content-Type":"application/json","Authorization":"Bearer ey.._OtZ0K5fFi21o"}

 --- FRIEND FINISHED
 I send post "/api/admin-call"
  [Request] POST http://localhost:8080/index-test.php/api/admin-call []
  [Request Headers] {"Authorization":"Bearer ey...4EgaIJAGeikPCriXHo"}
SamMousa commented 9 months ago

Ah, of course; the backup session function not only backs up the session but also the client context and headers:

    /**
     * Return the session content for future restoring. Implements MultiSession.
     * @return array backup data
     */
    public function _backupSession(): array
    {
        if (isset(Yii::$app) && Yii::$app->session->useCustomStorage) {
            throw new ModuleException($this, "Yii2 MultiSession only supports the default session backend.");
        }
        return [
            'clientContext' => $this->client->getContext(),
            'headers' => $this->headers,
            'cookie' => isset($_COOKIE) ? $_COOKIE : [],
            'session' => isset($_SESSION) ? $_SESSION : [],
        ];
    }
SamMousa commented 9 months ago

@samdark can you trigger / do a release? It's not automated and I don't know the release process.

michaelarnauts commented 9 months ago

@samdark @SamMousa Can we do a release with this change?