codeception functional Авторизация (symfony) через loginUser() не работает после первого запроса в функциональных тестах

Проблема

Решение через loginUser() для программного логина пользователя, почему-то не работает у нас (symfony 7) в ситуации, когда в цепочке более двух тестов, например :

    /**
     * @before authAsSimpleUserVasya // первая авторизация
      этот метода успешно создает статью
     */
    public function testPostCreatingBySimpleUserVasya(FunctionalTester $I)
    {
        $client = $this->client;
        $client->request(
            'POST',
            '/api/' . $this->getApiVersion() . '/post/new',
// .........  
    }

    /**
      Зависим от метода создания статьи, который перед собой  
     проводит авторизацию и все работет нормально
     * @before testPostCreatingBySimpleUserVasya
     * @before authAsAdmin // метода ниже не сработает 
              -- даже если авторизуемся еще раз
     */
    public function canEditAndDeleteByAdminArticleCreatedByOtherSimpleUser(FunctionalTester $I)
    {
        $client = $this->client;
        $client->request( // получаем ошибку
            'PUT',
            '/api/' . $this->getApiVersion() . '/post/edit/' . $this->postId,
// .......................
    }
}

- в нашем случае второй метода в цепоке (зависящий от первого через @before) не проходит проверку на роль IS_GRANTED (symfony) (напр. как в примерах кода тут)

План поиска причины проблемы

  1. Убедиться, что между методами (тестами) в цепочке не вызывается каких то "сбросов" напр. смены объекта клиента в initMe() (или вашем его аналоге), работающем через определения _before()
  2. Разобраться, где и как Symfony вообще проверят доступ для IS_GRANTED

Ключевые слова по теме (поисковые запросы):
symfony loginUser fail auth after one request

Возможно связанные темы

Возможные решения

  1. Применять способ (если он там есть, надо вникать): https://symfony.com/doc/current/testing....
  2. Использовать костыль с перезагрузкой ядра, чтобы для второго запроса оно было "как для первого"
vedro-compota's picture

Вот тут: vendor/symfony/security-core/Authorization/AuthorizationChecker.php

/**
 * AuthorizationChecker is the main authorization point of the Security component.
 *
 * It gives access to the token representing the current user authentication.
 *
 * @author Fabien Potencier <fabien @symfony.com>
 * @author Johannes M. Schmitt <schmittjoh @gmail.com>
 */
class AuthorizationChecker implements AuthorizationCheckerInterface
{
    public function __construct(
        private TokenStorageInterface $tokenStorage,
        private AccessDecisionManagerInterface $accessDecisionManager,
    ) {
    }

    final public function isGranted(mixed $attribute, mixed $subject = null): bool
    {
        $token = $this->tokenStorage->getToken(); // сюда точку останова

_____________
матфак вгу и остальная классика =)

vedro-compota's picture

Функция перезагрузи это

Symfony\Component\HttpKernel\Kernel::boot

Дла поиска:

public function boot(): void

_____________
матфак вгу и остальная классика =)

vedro-compota's picture

Вариант с пересозданием (перелогином) каждвый раз перед вызовом с подходом:

self::ensureKernelShutdown();
$client = self::createClient()->loginUser($user);

Как предлагают тут https://github.com/symfony/symfony/issue...

Другое дело, что что если унаследовать Cest-класс от WebTestCase - начнет запускаться куча публичных методов в родителях, а это не то, что нам нужно)

Поэтому можно просто написать отдельный класс-наследние, который будет отдавать клиент с залогиненным пользователем:

use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Костыль для получения клиента с залогиненным пользователем 
 * с предварительной (что важно) перезагрузкой ядра
 */
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Костыль для получения клиента с залогиненным пользователем 
 * с предварительной (что важно) перезагрузкой ядра
 */
class ClientGettingCrutch extends WebTestCase {

    /**
     * @param UserInterface $user Пользователь, под которым залогиним клиент
     */
    public static function get(UserInterface $user): KernelBrowser {
        self::ensureKernelShutdown();
        return self::createClient()->loginUser($user); 
    }

}

-- просто получайте заново клиент перед каждым запросом и stateless: true файервол начнет пропускать не только первый запрос

Если в тестах вам нужно в тестах получать исключения, то выключить их перехват клиентом:

class ClientGettingCrutch extends WebTestCase {

    /**
     * @param UserInterface $user Пользователь, под которым залогиним клиент
     */
    public static function get(UserInterface $user): KernelBrowser {
        self::ensureKernelShutdown();
        $client = self::createClient()->loginUser($user);
        $client->catchExceptions(false); // выключаем перехват
        return $client; 
    }

_____________
матфак вгу и остальная классика =)