Search code examples
symfony

"There is currently no session available" error when I test Symfony Live Component


I encounter a problem during a test on a Symfony LiveComponent with a form.

here is the test file :

// /tests/SomeTest.php
class SomeTest extends WebTestCase
{
    use InteractsWithLiveComponents;

    public function testSomething(): void
    {
        $client = static::createClient();
        $productRepository = static::getContainer()->get(ProductRepository::class);
        $userRepository = static::getContainer()->get(UserRepository::class);

        $product = $productRepository->findOneBy(['name' => 'Chino Beige']);
        $user = $userRepository->findOneBy(['username' => 'admin']);

        $component = $this->createLiveComponent(AddToCart::class, [
            'product' => $product,
        ])->actingAs($user);

        $this->assertInstanceOf(TestLiveComponent::class, $component);
        $this->assertEquals(200, $component->response()->getStatusCode());
    }
}

here is the function that instantiates the form into the LiveComponent :

// /src/Twig/Components/Form/AddToCart.php
protected function instantiateForm(): FormInterface
    {
        $cartItem = new CartItem();

        return $this->createForm(AddToCartType::class, $cartItem, [
            'product' => $this->product,
            'cart' => $this->cartRepository->findOneBy(['customer' => $this->getUser(), 'catalog' => $this->product->getCatalog()]),
            'view_mode' => $this->product->getCatalog()->getViewMode(),
        ]);
    }

The AddToCartType form :

<?php

namespace App\Form;

// use [...]

class AddToCartType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('product', EntityType::class, [
                'class' => Product::class,
                'data' => $options['product'],
                'required' => true,
                'constraints' => [
                    new NotBlank(),
                ],
            ])
            ->add('cart', EntityType::class, [
                'class' => Cart::class,
                'data' => $options['cart'],
                'choice_label' => 'id',
                'required' => true,
                'constraints' => [
                    new NotBlank(),
                ],
            ])
        ;

        if ($options['view_mode'] === 'grid') {
            foreach ($options['product']->getMeasures() as $measure) {
                $builder->add('quantity_' . $measure->getId(), IntegerType::class, [
                    'mapped' => false,
                    'label' => $measure->getName(),
                    'data' => 0,
                    'required' => false,
                    'attr' => [
                        'min' => 0,
                    ],
                    'constraints' => [
                        new PositiveOrZero(),
                    ],
                ]);
            }
            $builder->add('total_quantity', HiddenType::class, [
                'mapped' => false,
                'label' => false,
                'required' => true,
                'data' => 0,
                'constraints' => [
                    new TotalQuantity(),
                ],
            ]);
        } else {
            $builder
                ->add('measure', EntityType::class, [
                    'label' => 'app.add_to_cart.measures',
                    'class' => Measure::class,
                    'query_builder' => function (MeasureRepository $measureRepository) use ($options): QueryBuilder {
                        $qb = $measureRepository->createQueryBuilder('m');

                        return $qb->andWhere($qb->expr()->in('m.id', ':ids'))
                            ->setParameter('ids', $options['product']->getMeasures()->map(fn($measure) => $measure->getId()));
                    },
                    'expanded' => true,
                    'required' => true,
                    'placeholder' => false,
                    'constraints' => [
                        new NotBlank(),
                    ],
                ])
                ->add('quantity', IntegerType::class, [
                    'label' => 'app.add_to_cart.quantity',
                    'data' => 0,
                    'attr' => [
                        'min' => 0,
                    ],
                    'required' => true,
                    'constraints' => [
                        new Positive(),
                    ],
                ])
                ->add('receiver', TextType::class, [
                    'label' => 'app.add_to_cart.receiver',
                    'attr' => [
                        'placeholder' => 'app.add_to_cart.receiver',
                    ],
                    'required' => true,
                    'constraints' => [
                        new NotBlank(),
                    ],
                ])
            ;
        }
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => CartItem::class,
            'product' => null,
            'cart' => null,
            'view_mode' => 'classic',
        ]);

        $resolver->setAllowedTypes('product', Product::class);
        $resolver->setAllowedTypes('cart', Cart::class);
        $resolver->setAllowedTypes('view_mode', 'string');
    }
}

the stack trace :

Symfony\Component\HttpFoundation\Exception\SessionNotFoundException : There is currently no session available.
 .../vendor/symfony/http-foundation/RequestStack.php:105
 ...:vendor/symfony/security-csrf/TokenStorage/SessionTokenStorage.php:104
 .../vendor/symfony/security-csrf/TokenStorage/SessionTokenStorage.php:71
 .../vendor/symfony/security-csrf/CsrfTokenManager.php:69
 .../vendor/symfony/form/Extension/Csrf/Type/FormTypeCsrfExtension.php:78
 .../vendor/symfony/form/ResolvedFormType.php:125
 .../vendor/symfony/form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php:86
 .../vendor/symfony/form/ResolvedFormType.php:119
 .../vendor/symfony/form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php:86
 .../vendor/symfony/form/Form.php:904
 .../vendor/symfony/ux-live-component/src/ComponentWithFormTrait.php:118
 .../vendor/symfony/ux-live-component/src/ComponentWithFormTrait.php:93
 .../vendor/symfony/ux-twig-component/src/ComponentFactory.php:205
 .../vendor/symfony/ux-twig-component/src/ComponentFactory.php:91
 .../vendor/symfony/ux-twig-component/src/ComponentFactory.php:65
 .../vendor/symfony/ux-live-component/src/Test/TestLiveComponent.php:174
 .../vendor/symfony/ux-live-component/src/Test/TestLiveComponent.php:123
 .../vendor/symfony/ux-live-component/src/Test/TestLiveComponent.php:51
 .../tests/SomeTest.php:31

From what I could see, the error would be when Symfony tries to generate a CSRF token for the form. in the file /vendor/symfony/security-csrf/CsrfTokenManager.php

public function getToken(string $tokenId): CsrfToken  
{  
    $namespacedId = $this->getNamespace().$tokenId;  
    if ($this->storage->hasToken($namespacedId)) {  
        $value = $this->storage->getToken($namespacedId);  
    } else {  
        $value = $this->generator->generateToken();  
  
        $this->storage->setToken($namespacedId, $value);  
    }  
  
    return new CsrfToken($tokenId, $this->randomize($value));  
}

it fails to retrieve the token from $this->storage because the namespaceId does not exist, but I don't know why !

I also point out that the component works very well on a web page and that the form options in unit test mode are also recoverable well.

Env : Symfony 7.0.6 PHP 8.3 php unit 9.6.19 symfony ux live component 2.17

Do you have any idea what this could come from?

thanks in advance


Solution

  • I had the same error, and so far I could just apply this workaround. Do this before the $this->createLiveComponent() call

    /* Workaround to have a session from where the form can get the CSRF token */
    $session = new Session(new MockFileSessionStorage());
    $request = new Request();
    $request->setSession($session);
    $stack = self::getContainer->get(RequestStack::class);
    $stack->push($request);