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
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);