From 6345b8079317cd6b139a7d53e13f11339650d730 Mon Sep 17 00:00:00 2001 From: mmarchois Date: Tue, 11 Feb 2025 13:52:13 +0100 Subject: [PATCH] Delete invitation --- .../CreateInvitationCommand.xml | 2 +- .../CreateInvitationCommand.php | 2 +- .../CreateInvitationCommandHandler.php | 2 +- .../Invitation/DeleteInvitationCommand.php | 17 +++ .../DeleteInvitationCommandHandler.php | 38 ++++++ .../JoinOrganizationCommand.php | 2 +- .../JoinOrganizationCommandHandler.php | 2 +- .../User/AcceptInvitationController.php | 2 +- .../User/CreateInvitationController.php | 2 +- .../User/DeleteInvitationController.php | 65 +++++++++ .../Doctrine/Fixtures/InvitationFixture.php | 2 +- .../my_area/organization/user/index.html.twig | 28 ++++ .../User/DeleteInvitationControllerTest.php | 59 ++++++++ .../CreateInvitationCommandHandlerTest.php | 6 +- .../DeleteInvitationCommandHandlerTest.php | 129 ++++++++++++++++++ .../JoinOrganizationCommandHandlerTest.php | 6 +- translations/messages.fr.xlf | 12 ++ 17 files changed, 362 insertions(+), 14 deletions(-) rename config/validator/Application/User/{ => Invitation}/CreateInvitationCommand.xml (91%) rename src/Application/User/Command/{ => Invitation}/CreateInvitationCommand.php (89%) rename src/Application/User/Command/{ => Invitation}/CreateInvitationCommandHandler.php (97%) create mode 100644 src/Application/User/Command/Invitation/DeleteInvitationCommand.php create mode 100644 src/Application/User/Command/Invitation/DeleteInvitationCommandHandler.php rename src/Application/User/Command/{ => Invitation}/JoinOrganizationCommand.php (85%) rename src/Application/User/Command/{ => Invitation}/JoinOrganizationCommandHandler.php (97%) create mode 100644 src/Infrastructure/Controller/MyArea/Organization/User/DeleteInvitationController.php create mode 100644 tests/Integration/Infrastructure/Controller/MyArea/Organization/User/DeleteInvitationControllerTest.php rename tests/Unit/Application/User/Command/{ => Invitation}/CreateInvitationCommandHandlerTest.php (97%) create mode 100644 tests/Unit/Application/User/Command/Invitation/DeleteInvitationCommandHandlerTest.php rename tests/Unit/Application/User/Command/{ => Invitation}/JoinOrganizationCommandHandlerTest.php (97%) diff --git a/config/validator/Application/User/CreateInvitationCommand.xml b/config/validator/Application/User/Invitation/CreateInvitationCommand.xml similarity index 91% rename from config/validator/Application/User/CreateInvitationCommand.xml rename to config/validator/Application/User/Invitation/CreateInvitationCommand.xml index c3e3fa650..8c9545983 100644 --- a/config/validator/Application/User/CreateInvitationCommand.xml +++ b/config/validator/Application/User/Invitation/CreateInvitationCommand.xml @@ -2,7 +2,7 @@ - + diff --git a/src/Application/User/Command/CreateInvitationCommand.php b/src/Application/User/Command/Invitation/CreateInvitationCommand.php similarity index 89% rename from src/Application/User/Command/CreateInvitationCommand.php rename to src/Application/User/Command/Invitation/CreateInvitationCommand.php index 856ce7157..c3d55abcc 100644 --- a/src/Application/User/Command/CreateInvitationCommand.php +++ b/src/Application/User/Command/Invitation/CreateInvitationCommand.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Application\User\Command; +namespace App\Application\User\Command\Invitation; use App\Application\CommandInterface; use App\Domain\User\Organization; diff --git a/src/Application/User/Command/CreateInvitationCommandHandler.php b/src/Application/User/Command/Invitation/CreateInvitationCommandHandler.php similarity index 97% rename from src/Application/User/Command/CreateInvitationCommandHandler.php rename to src/Application/User/Command/Invitation/CreateInvitationCommandHandler.php index 80e295560..39a397f04 100644 --- a/src/Application/User/Command/CreateInvitationCommandHandler.php +++ b/src/Application/User/Command/Invitation/CreateInvitationCommandHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Application\User\Command; +namespace App\Application\User\Command\Invitation; use App\Application\DateUtilsInterface; use App\Application\IdFactoryInterface; diff --git a/src/Application/User/Command/Invitation/DeleteInvitationCommand.php b/src/Application/User/Command/Invitation/DeleteInvitationCommand.php new file mode 100644 index 000000000..b7c50da4b --- /dev/null +++ b/src/Application/User/Command/Invitation/DeleteInvitationCommand.php @@ -0,0 +1,17 @@ +invitationRepository->findOneByUuid($command->invitationUuid); + if (!$invitation instanceof Invitation) { + throw new InvitationNotFoundException(); + } + + $organizationUuid = $invitation->getOrganization()->getUuid(); + + if (!$this->canUserEditOrganization->isSatisfiedBy($invitation->getOrganization(), $command->user)) { + throw new InvitationNotOwnedException(); + } + + $this->invitationRepository->delete($invitation); + + return $organizationUuid; + } +} diff --git a/src/Application/User/Command/JoinOrganizationCommand.php b/src/Application/User/Command/Invitation/JoinOrganizationCommand.php similarity index 85% rename from src/Application/User/Command/JoinOrganizationCommand.php rename to src/Application/User/Command/Invitation/JoinOrganizationCommand.php index c7851495c..fa940fca4 100644 --- a/src/Application/User/Command/JoinOrganizationCommand.php +++ b/src/Application/User/Command/Invitation/JoinOrganizationCommand.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Application\User\Command; +namespace App\Application\User\Command\Invitation; use App\Application\CommandInterface; use App\Domain\User\User; diff --git a/src/Application/User/Command/JoinOrganizationCommandHandler.php b/src/Application/User/Command/Invitation/JoinOrganizationCommandHandler.php similarity index 97% rename from src/Application/User/Command/JoinOrganizationCommandHandler.php rename to src/Application/User/Command/Invitation/JoinOrganizationCommandHandler.php index 8907b55e2..0017e49f2 100644 --- a/src/Application/User/Command/JoinOrganizationCommandHandler.php +++ b/src/Application/User/Command/Invitation/JoinOrganizationCommandHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Application\User\Command; +namespace App\Application\User\Command\Invitation; use App\Application\IdFactoryInterface; use App\Domain\User\Exception\InvitationNotFoundException; diff --git a/src/Infrastructure/Controller/MyArea/Organization/User/AcceptInvitationController.php b/src/Infrastructure/Controller/MyArea/Organization/User/AcceptInvitationController.php index 8c7bb7614..98b767c54 100644 --- a/src/Infrastructure/Controller/MyArea/Organization/User/AcceptInvitationController.php +++ b/src/Infrastructure/Controller/MyArea/Organization/User/AcceptInvitationController.php @@ -5,7 +5,7 @@ namespace App\Infrastructure\Controller\MyArea\Organization\User; use App\Application\CommandBusInterface; -use App\Application\User\Command\JoinOrganizationCommand; +use App\Application\User\Command\Invitation\JoinOrganizationCommand; use App\Domain\User\Exception\InvitationNotFoundException; use App\Domain\User\Exception\InvitationNotOwnedException; use App\Domain\User\Exception\OrganizationUserAlreadyExistException; diff --git a/src/Infrastructure/Controller/MyArea/Organization/User/CreateInvitationController.php b/src/Infrastructure/Controller/MyArea/Organization/User/CreateInvitationController.php index 44abfe07f..7d4f7a80f 100644 --- a/src/Infrastructure/Controller/MyArea/Organization/User/CreateInvitationController.php +++ b/src/Infrastructure/Controller/MyArea/Organization/User/CreateInvitationController.php @@ -6,7 +6,7 @@ use App\Application\CommandBusInterface; use App\Application\QueryBusInterface; -use App\Application\User\Command\CreateInvitationCommand; +use App\Application\User\Command\Invitation\CreateInvitationCommand; use App\Application\User\Command\Mail\SendInvitationMailCommand; use App\Domain\User\Exception\InvitationAlreadyExistsException; use App\Domain\User\Exception\OrganizationUserAlreadyExistException; diff --git a/src/Infrastructure/Controller/MyArea/Organization/User/DeleteInvitationController.php b/src/Infrastructure/Controller/MyArea/Organization/User/DeleteInvitationController.php new file mode 100644 index 000000000..4938525d4 --- /dev/null +++ b/src/Infrastructure/Controller/MyArea/Organization/User/DeleteInvitationController.php @@ -0,0 +1,65 @@ + Requirement::UUID], + methods: ['DELETE'], + )] + #[IsCsrfTokenValid('delete-invitation')] + public function __invoke(Request $request, string $uuid): Response + { + /** @var FlashBagAwareSessionInterface */ + $session = $request->getSession(); + $user = $this->authenticatedUser->getSymfonyUser(); + + try { + $organizationUuid = $this->commandBus->handle(new DeleteInvitationCommand($uuid, $user)); + $session->getFlashBag()->add('success', $this->translator->trans('invitation.delete.success')); + + return new RedirectResponse( + url: $this->router->generate('app_users_list', ['uuid' => $organizationUuid]), + status: Response::HTTP_SEE_OTHER, + ); + } catch (InvitationNotFoundException) { + throw new NotFoundHttpException(); + } catch (InvitationNotOwnedException) { + $session->getFlashBag()->add('error', $this->translator->trans('invitation.delete.error')); + + return new RedirectResponse( + url: $this->router->generate('app_my_area'), + status: Response::HTTP_SEE_OTHER, + ); + } + } +} diff --git a/src/Infrastructure/Persistence/Doctrine/Fixtures/InvitationFixture.php b/src/Infrastructure/Persistence/Doctrine/Fixtures/InvitationFixture.php index bef90e6c6..586e1bdcc 100644 --- a/src/Infrastructure/Persistence/Doctrine/Fixtures/InvitationFixture.php +++ b/src/Infrastructure/Persistence/Doctrine/Fixtures/InvitationFixture.php @@ -35,7 +35,7 @@ public function load(ObjectManager $manager): void email: 'mathieu.marchois@beta.gouv.fr', role: OrganizationRolesEnum::ROLE_ORGA_CONTRIBUTOR->value, createdAt: new \DateTimeImmutable('2025-02-12'), - owner: $this->getReference('mainOrgUser', User::class), + owner: $this->getReference('mainOrgAdmin', User::class), organization: $this->getReference('mainOrg', Organization::class), ); diff --git a/templates/my_area/organization/user/index.html.twig b/templates/my_area/organization/user/index.html.twig index e6497b62d..3ffa1339e 100644 --- a/templates/my_area/organization/user/index.html.twig +++ b/templates/my_area/organization/user/index.html.twig @@ -5,6 +5,7 @@ {% endblock %} {% set deleteCsrfToken = csrf_token('delete-user') %} +{% set deleteInvitationCsrfToken = csrf_token('delete-invitation') %} {% block body %}
@@ -54,7 +55,26 @@ {% if is_granted(constant('App\\Infrastructure\\Security\\Voter\\OrganizationVoter::EDIT'), organization) %}
+
+ + + + + + +
{% endif %} @@ -120,4 +140,12 @@ { label: 'common.do_not_delete'|trans, attr: {value: 'close', class: 'fr-btn fr-btn--secondary'} }, ] } only %} + {% include 'common/confirmation_modal.html.twig' with { + id: 'invitation-delete-modal', + title: 'invitation.list.delete'|trans, + buttons: [ + { label: 'common.delete'|trans, attr: {type: 'submit', class: 'fr-btn'} }, + { label: 'common.do_not_delete'|trans, attr: {value: 'close', class: 'fr-btn fr-btn--secondary'} }, + ] + } only %} {% endblock %} diff --git a/tests/Integration/Infrastructure/Controller/MyArea/Organization/User/DeleteInvitationControllerTest.php b/tests/Integration/Infrastructure/Controller/MyArea/Organization/User/DeleteInvitationControllerTest.php new file mode 100644 index 000000000..a8fce06cd --- /dev/null +++ b/tests/Integration/Infrastructure/Controller/MyArea/Organization/User/DeleteInvitationControllerTest.php @@ -0,0 +1,59 @@ +login(UserFixture::MAIN_ORG_ADMIN_EMAIL); + $client->request('DELETE', '/mon-espace/invitations/' . InvitationFixture::INVITATION_ALREADY_JOINED_UUID . '/delete', [ + '_token' => $this->generateCsrfToken($client, 'delete-invitation'), + ]); + + $this->assertResponseStatusCodeSame(303); + $crawler = $client->followRedirect(); + + $this->assertResponseStatusCodeSame(200); + $this->assertEquals(['success' => ['Invitation supprimée.']], $this->getFlashes($crawler)); + $this->assertRouteSame('app_users_list'); + } + + public function testInvitationNotOwned(): void + { + $client = $this->login('mathieu.fernandez@beta.gouv.fr'); + $client->request('DELETE', '/mon-espace/invitations/' . InvitationFixture::UUID . '/delete', [ + '_token' => $this->generateCsrfToken($client, 'delete-invitation'), + ]); + $crawler = $client->followRedirect(); + + $this->assertEquals(['error' => ['Vous n\'avez pas les droits pour supprimer cette invitation.']], $this->getFlashes($crawler)); + $this->assertResponseStatusCodeSame(200); + $this->assertRouteSame('app_my_area'); + } + + public function testOrganizationOrUserNotFound(): void + { + $client = $this->login(); + $client->request('DELETE', '/mon-espace/invitations/d68fba17-fb22-490d-ae52-2a371f14ceb1/delete', [ + '_token' => $this->generateCsrfToken($client, 'delete-invitation'), + ]); + $this->assertResponseStatusCodeSame(404); + } + + public function testWithoutAuthenticatedUser(): void + { + $client = static::createClient(); + $client->request('DELETE', '/mon-espace/invitations/' . InvitationFixture::UUID . '/delete'); + $this->assertResponseRedirects('http://localhost/login', 302); + } +} diff --git a/tests/Unit/Application/User/Command/CreateInvitationCommandHandlerTest.php b/tests/Unit/Application/User/Command/Invitation/CreateInvitationCommandHandlerTest.php similarity index 97% rename from tests/Unit/Application/User/Command/CreateInvitationCommandHandlerTest.php rename to tests/Unit/Application/User/Command/Invitation/CreateInvitationCommandHandlerTest.php index 8cdbef629..8dc84184c 100644 --- a/tests/Unit/Application/User/Command/CreateInvitationCommandHandlerTest.php +++ b/tests/Unit/Application/User/Command/Invitation/CreateInvitationCommandHandlerTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace App\Tests\Unit\Application\User\Command; +namespace App\Tests\Unit\Application\User\Command\Invitation; use App\Application\DateUtilsInterface; use App\Application\IdFactoryInterface; use App\Application\StringUtilsInterface; -use App\Application\User\Command\CreateInvitationCommand; -use App\Application\User\Command\CreateInvitationCommandHandler; +use App\Application\User\Command\Invitation\CreateInvitationCommand; +use App\Application\User\Command\Invitation\CreateInvitationCommandHandler; use App\Domain\User\Enum\OrganizationRolesEnum; use App\Domain\User\Exception\InvitationAlreadyExistsException; use App\Domain\User\Exception\OrganizationUserAlreadyExistException; diff --git a/tests/Unit/Application/User/Command/Invitation/DeleteInvitationCommandHandlerTest.php b/tests/Unit/Application/User/Command/Invitation/DeleteInvitationCommandHandlerTest.php new file mode 100644 index 000000000..fd5d7d062 --- /dev/null +++ b/tests/Unit/Application/User/Command/Invitation/DeleteInvitationCommandHandlerTest.php @@ -0,0 +1,129 @@ +invitationRepository = $this->createMock(InvitationRepositoryInterface::class); + $this->canUserEditOrganization = $this->createMock(CanUserEditOrganization::class); + + $this->handler = new DeleteInvitationCommandHandler( + $this->invitationRepository, + $this->canUserEditOrganization, + ); + } + + public function testDelete(): void + { + $user = $this->createMock(SymfonyUser::class); + $organization = $this->createMock(Organization::class); + $organization + ->expects(self::once()) + ->method('getUuid') + ->willReturn('ca143c24-fa0f-4702-b0d4-876be006f8a7'); + + $invitation = $this->createMock(Invitation::class); + $invitation + ->expects(self::exactly(2)) + ->method('getOrganization') + ->willReturn($organization); + + $this->invitationRepository + ->expects($this->once()) + ->method('findOneByUuid') + ->with('12444823-fa32-477b-baf1-5a3cbcaf68a3') + ->willReturn($invitation); + + $this->invitationRepository + ->expects($this->once()) + ->method('delete') + ->with($invitation); + + $this->canUserEditOrganization + ->expects(self::once()) + ->method('isSatisfiedBy') + ->with($organization, $user) + ->willReturn(true); + + $command = new DeleteInvitationCommand('12444823-fa32-477b-baf1-5a3cbcaf68a3', $user); + $this->assertSame('ca143c24-fa0f-4702-b0d4-876be006f8a7', $this->handler->__invoke($command)); + } + + public function testInvitationNotOwned(): void + { + $this->expectException(InvitationNotOwnedException::class); + + $user = $this->createMock(SymfonyUser::class); + $organization = $this->createMock(Organization::class); + $organization + ->expects(self::once()) + ->method('getUuid') + ->willReturn('ca143c24-fa0f-4702-b0d4-876be006f8a7'); + $invitation = $this->createMock(Invitation::class); + $invitation + ->expects(self::exactly(2)) + ->method('getOrganization') + ->willReturn($organization); + + $this->invitationRepository + ->expects($this->once()) + ->method('findOneByUuid') + ->with('12444823-fa32-477b-baf1-5a3cbcaf68a3') + ->willReturn($invitation); + + $this->invitationRepository + ->expects($this->never()) + ->method('delete'); + + $this->canUserEditOrganization + ->expects(self::once()) + ->method('isSatisfiedBy') + ->with($organization, $user) + ->willReturn(false); + + $command = new DeleteInvitationCommand('12444823-fa32-477b-baf1-5a3cbcaf68a3', $user); + $this->handler->__invoke($command); + } + + public function testInvitationNotFound(): void + { + $this->expectException(InvitationNotFoundException::class); + $user = $this->createMock(SymfonyUser::class); + + $this->invitationRepository + ->expects($this->once()) + ->method('findOneByUuid') + ->with('12444823-fa32-477b-baf1-5a3cbcaf68a3') + ->willReturn(null); + + $this->invitationRepository + ->expects($this->never()) + ->method('delete'); + + $this->canUserEditOrganization + ->expects(self::never()) + ->method('isSatisfiedBy'); + + $command = new DeleteInvitationCommand('12444823-fa32-477b-baf1-5a3cbcaf68a3', $user); + $this->handler->__invoke($command); + } +} diff --git a/tests/Unit/Application/User/Command/JoinOrganizationCommandHandlerTest.php b/tests/Unit/Application/User/Command/Invitation/JoinOrganizationCommandHandlerTest.php similarity index 97% rename from tests/Unit/Application/User/Command/JoinOrganizationCommandHandlerTest.php rename to tests/Unit/Application/User/Command/Invitation/JoinOrganizationCommandHandlerTest.php index 65fad0c8f..fde1962c8 100644 --- a/tests/Unit/Application/User/Command/JoinOrganizationCommandHandlerTest.php +++ b/tests/Unit/Application/User/Command/Invitation/JoinOrganizationCommandHandlerTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace App\Tests\Unit\Application\User\Command; +namespace App\Tests\Unit\Application\User\Command\Invitation; use App\Application\IdFactoryInterface; -use App\Application\User\Command\JoinOrganizationCommand; -use App\Application\User\Command\JoinOrganizationCommandHandler; +use App\Application\User\Command\Invitation\JoinOrganizationCommand; +use App\Application\User\Command\Invitation\JoinOrganizationCommandHandler; use App\Domain\User\Exception\InvitationNotFoundException; use App\Domain\User\Exception\InvitationNotOwnedException; use App\Domain\User\Exception\OrganizationUserAlreadyExistException; diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index e6bda8b2c..b9fb18c7f 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -2787,6 +2787,18 @@ invitation.accept.error_not_owned Cette adresse email ne correspond pas à l'invitation reçue. + + invitation.delete.error + Vous n'avez pas les droits pour supprimer cette invitation. + + + invitation.delete.success + Invitation supprimée. + + + invitation.list.delete + Supprimer cette invitation +