From 8cf90062272c8cfa86724a34ba0d7597518ccd55 Mon Sep 17 00:00:00 2001 From: Vidar Langseid Date: Thu, 27 Aug 2026 06:59:57 +0200 Subject: [PATCH 1/3] IBX-12340: Pass the required struct option when registering from invitation --- .../Controller/UserRegisterController.php | 1 + .../Controller/UserRegisterControllerTest.php | 75 +++++++++++++++++++ tests/integration/IbexaTestKernel.php | 14 ++++ 3 files changed, 90 insertions(+) create mode 100644 tests/integration/Controller/UserRegisterControllerTest.php diff --git a/src/bundle/Controller/UserRegisterController.php b/src/bundle/Controller/UserRegisterController.php index 5ce6f7b..986043d 100644 --- a/src/bundle/Controller/UserRegisterController.php +++ b/src/bundle/Controller/UserRegisterController.php @@ -85,6 +85,7 @@ public function registerFromInvitationAction(Request $request): Response|FormVie [ 'languageCode' => $language, 'mainLanguageCode' => $language, + 'struct' => $data, 'intent' => 'invitation', ] ); diff --git a/tests/integration/Controller/UserRegisterControllerTest.php b/tests/integration/Controller/UserRegisterControllerTest.php new file mode 100644 index 0000000..3a97b9e --- /dev/null +++ b/tests/integration/Controller/UserRegisterControllerTest.php @@ -0,0 +1,75 @@ +set(UserDispatcher::class, new UserDispatcher()); + } + + public function testRegisterFromInvitationBuildsForm(): void + { + $invitation = self::getInvitationService()->createInvitation( + new InvitationCreateStruct(self::INVITEE_EMAIL, $this->getCurrentSiteAccessName()) + ); + + $controller = self::getServiceByClassName(UserRegisterController::class); + + $request = new Request(attributes: ['inviteHash' => $invitation->getHash()]); + // the form has CSRF protection, which reads the token from the session + $request->setSession(new Session(new MockArraySessionStorage())); + self::getServiceByClassName(RequestStack::class)->push($request); + + $view = $controller->registerFromInvitationAction($request); + + self::assertInstanceOf(FormView::class, $view); + } + + protected function tearDown(): void + { + // Fixtures are imported once per run, so without this the invitation survives into + // InvitationServiceTest, which asserts absolute findInvitations() counts and fails + $connection = self::getDoctrineConnection(); + $connection->executeStatement( + 'DELETE FROM ibexa_user_invitation_assignment WHERE invitation_id IN ' + . '(SELECT id FROM ibexa_user_invitation WHERE email = :email)', + ['email' => self::INVITEE_EMAIL] + ); + $connection->delete('ibexa_user_invitation', ['email' => self::INVITEE_EMAIL]); + } + + private function getCurrentSiteAccessName(): string + { + $siteAccess = self::getServiceByClassName(SiteAccessServiceInterface::class)->getCurrent(); + self::assertNotNull($siteAccess); + + return $siteAccess->name; + } +} diff --git a/tests/integration/IbexaTestKernel.php b/tests/integration/IbexaTestKernel.php index d328e52..8cafbe8 100644 --- a/tests/integration/IbexaTestKernel.php +++ b/tests/integration/IbexaTestKernel.php @@ -10,15 +10,18 @@ use Ibexa\Bundle\ContentForms\IbexaContentFormsBundle; use Ibexa\Bundle\Notifications\IbexaNotificationsBundle; +use Ibexa\Bundle\User\Controller\UserRegisterController; use Ibexa\Bundle\User\IbexaUserBundle; use Ibexa\ContentForms\Form\ActionDispatcher\UserDispatcher; use Ibexa\Contracts\Core\Test\IbexaTestKernel as BaseIbexaTestKernel; use Ibexa\Contracts\User\Invitation\InvitationService; +use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use LogicException; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\HttpFoundation\RequestStack; final class IbexaTestKernel extends BaseIbexaTestKernel { @@ -51,6 +54,9 @@ protected static function getExposedServicesByClass(): iterable yield InvitationService::class; yield FormFactoryInterface::class; + yield UserRegisterController::class; + yield SiteAccessServiceInterface::class; + yield RequestStack::class; } #[\Override] @@ -63,6 +69,14 @@ public function registerContainerConfiguration(LoaderInterface $loader): void self::createSyntheticService($container); + $container->loadFromExtension('ibexa', [ + 'system' => [ + 'default' => [ + 'languages' => ['eng-GB'], + ], + ], + ]); + $container->loadFromExtension('framework', [ 'router' => [ 'resource' => __DIR__ . '/Resources/routing.yaml', From ea9b24f96846c66415e3456b2b574d0562154254 Mon Sep 17 00:00:00 2001 From: Vidar Langseid Date: Thu, 3 Sep 2026 10:46:52 +0200 Subject: [PATCH 2/3] Clarified why UserRegisterControllerTest::testRegisterFromInvitationBuildsForm() removes its invitation --- tests/integration/Controller/UserRegisterControllerTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/integration/Controller/UserRegisterControllerTest.php b/tests/integration/Controller/UserRegisterControllerTest.php index 3a97b9e..bff993a 100644 --- a/tests/integration/Controller/UserRegisterControllerTest.php +++ b/tests/integration/Controller/UserRegisterControllerTest.php @@ -54,8 +54,9 @@ public function testRegisterFromInvitationBuildsForm(): void protected function tearDown(): void { - // Fixtures are imported once per run, so without this the invitation survives into - // InvitationServiceTest, which asserts absolute findInvitations() counts and fails + // Database is imported once per run and there is no per-test rollback, so without this the + // invitation survives into InvitationServiceTest, which asserts absolute findInvitations() + // counts and fails $connection = self::getDoctrineConnection(); $connection->executeStatement( 'DELETE FROM ibexa_user_invitation_assignment WHERE invitation_id IN ' From ea4cd834b5af2d894ce970dc6d84eb9bc58fdb40 Mon Sep 17 00:00:00 2001 From: Vidar Langseid Date: Thu, 3 Sep 2026 13:37:40 +0200 Subject: [PATCH 3/3] Changed the invitation registration test to assert on the HTTP response --- composer.json | 1 + .../Controller/UserRegisterControllerTest.php | 44 ++++++------------- tests/integration/IbexaTestKernel.php | 6 --- 3 files changed, 15 insertions(+), 36 deletions(-) diff --git a/composer.json b/composer.json index 9caff30..343ffd1 100644 --- a/composer.json +++ b/composer.json @@ -55,6 +55,7 @@ "phpstan/phpstan-phpunit": "^2.0", "phpstan/phpstan-symfony": "^2.0", "phpunit/phpunit": "^9.6", + "symfony/browser-kit": "^7.4", "symfony/phpunit-bridge": "^7.4" }, "scripts": { diff --git a/tests/integration/Controller/UserRegisterControllerTest.php b/tests/integration/Controller/UserRegisterControllerTest.php index bff993a..4391be7 100644 --- a/tests/integration/Controller/UserRegisterControllerTest.php +++ b/tests/integration/Controller/UserRegisterControllerTest.php @@ -8,16 +8,11 @@ namespace Ibexa\Tests\Integration\User\Controller; -use Ibexa\Bundle\User\Controller\UserRegisterController; use Ibexa\ContentForms\Form\ActionDispatcher\UserDispatcher; use Ibexa\Contracts\User\Invitation\InvitationCreateStruct; -use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use Ibexa\Tests\Integration\User\IbexaKernelTestCase; -use Ibexa\User\View\Register\FormView; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpFoundation\Session\Session; -use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; +use Symfony\Bundle\FrameworkBundle\KernelBrowser; +use Symfony\Component\HttpFoundation\Response; /** * @covers \Ibexa\Bundle\User\Controller\UserRegisterController::registerFromInvitationAction @@ -26,30 +21,27 @@ final class UserRegisterControllerTest extends IbexaKernelTestCase { private const string INVITEE_EMAIL = 'invitee@ibexa.co'; - protected function setUp(): void + public function testRegisterFromInvitationRespondsWithOk(): void { self::setAdministratorUser(); - - // the test kernel declares the dispatcher synthetic; the form is never submitted here self::getContainer()->set(UserDispatcher::class, new UserDispatcher()); - } - public function testRegisterFromInvitationBuildsForm(): void - { + // InvitationService::isValid() compares against the SiteAccess the request resolves to + // which is the default one, not what SiteAccessServiceInterface::getCurrent() returns here + $siteAccess = self::getContainer()->getParameter('ibexa.site_access.default'); + self::assertIsString($siteAccess); + $invitation = self::getInvitationService()->createInvitation( - new InvitationCreateStruct(self::INVITEE_EMAIL, $this->getCurrentSiteAccessName()) + new InvitationCreateStruct(self::INVITEE_EMAIL, $siteAccess) ); - $controller = self::getServiceByClassName(UserRegisterController::class); - - $request = new Request(attributes: ['inviteHash' => $invitation->getHash()]); - // the form has CSRF protection, which reads the token from the session - $request->setSession(new Session(new MockArraySessionStorage())); - self::getServiceByClassName(RequestStack::class)->push($request); + $kernel = self::$kernel; + self::assertNotNull($kernel); - $view = $controller->registerFromInvitationAction($request); + $client = new KernelBrowser($kernel); + $client->request('GET', '/from-invite/register/' . $invitation->getHash()); - self::assertInstanceOf(FormView::class, $view); + self::assertSame(Response::HTTP_OK, $client->getResponse()->getStatusCode()); } protected function tearDown(): void @@ -65,12 +57,4 @@ protected function tearDown(): void ); $connection->delete('ibexa_user_invitation', ['email' => self::INVITEE_EMAIL]); } - - private function getCurrentSiteAccessName(): string - { - $siteAccess = self::getServiceByClassName(SiteAccessServiceInterface::class)->getCurrent(); - self::assertNotNull($siteAccess); - - return $siteAccess->name; - } } diff --git a/tests/integration/IbexaTestKernel.php b/tests/integration/IbexaTestKernel.php index 8cafbe8..2403fc6 100644 --- a/tests/integration/IbexaTestKernel.php +++ b/tests/integration/IbexaTestKernel.php @@ -10,18 +10,15 @@ use Ibexa\Bundle\ContentForms\IbexaContentFormsBundle; use Ibexa\Bundle\Notifications\IbexaNotificationsBundle; -use Ibexa\Bundle\User\Controller\UserRegisterController; use Ibexa\Bundle\User\IbexaUserBundle; use Ibexa\ContentForms\Form\ActionDispatcher\UserDispatcher; use Ibexa\Contracts\Core\Test\IbexaTestKernel as BaseIbexaTestKernel; use Ibexa\Contracts\User\Invitation\InvitationService; -use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; use LogicException; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\Form\FormFactoryInterface; -use Symfony\Component\HttpFoundation\RequestStack; final class IbexaTestKernel extends BaseIbexaTestKernel { @@ -54,9 +51,6 @@ protected static function getExposedServicesByClass(): iterable yield InvitationService::class; yield FormFactoryInterface::class; - yield UserRegisterController::class; - yield SiteAccessServiceInterface::class; - yield RequestStack::class; } #[\Override]