diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml index 4274aeec6..45802f50b 100644 --- a/config/packages/messenger.yaml +++ b/config/packages/messenger.yaml @@ -66,6 +66,7 @@ framework: 'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority 'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async 'Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage': async + 'Chill\TicketBundle\Messenger\PostTicketUpdateMessage': async # end of routes added by chill-bundles recipes # Route your messages to the transports # 'App\Message\YourMessage': async diff --git a/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/ChangeEmergencyStateCommandHandler.php b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/ChangeEmergencyStateCommandHandler.php index 2a5440916..0ff810d07 100644 --- a/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/ChangeEmergencyStateCommandHandler.php +++ b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/ChangeEmergencyStateCommandHandler.php @@ -14,14 +14,20 @@ namespace Chill\TicketBundle\Action\Ticket\Handler; use Chill\TicketBundle\Action\Ticket\ChangeEmergencyStateCommand; use Chill\TicketBundle\Entity\EmergencyStatusHistory; use Chill\TicketBundle\Entity\Ticket; +use Chill\TicketBundle\Event\EmergencyStatusUpdateEvent; +use Chill\TicketBundle\Event\TicketUpdateEvent; use Symfony\Component\Clock\ClockInterface; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * Handler for changing the emergency status of a ticket. */ class ChangeEmergencyStateCommandHandler { - public function __construct(private readonly ClockInterface $clock) {} + public function __construct( + private readonly ClockInterface $clock, + private readonly EventDispatcherInterface $eventDispatcher, + ) {} public function __invoke(Ticket $ticket, ChangeEmergencyStateCommand $command): Ticket { @@ -30,6 +36,8 @@ class ChangeEmergencyStateCommandHandler return $ticket; } + $previous = $ticket->getEmergencyStatus(); + // End the current emergency status history (if any) foreach ($ticket->getEmergencyStatusHistories() as $emergencyStatusHistory) { if (null === $emergencyStatusHistory->getEndDate()) { @@ -44,6 +52,12 @@ class ChangeEmergencyStateCommandHandler $this->clock->now(), ); + // Dispatch event about the toggle + if (null !== $previous) { + $event = new EmergencyStatusUpdateEvent($ticket, $previous, $command->newEmergencyStatus); + $this->eventDispatcher->dispatch($event, TicketUpdateEvent::class); + } + return $ticket; } } diff --git a/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/ReplaceMotiveCommandHandler.php b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/ReplaceMotiveCommandHandler.php index a39186efe..985031156 100644 --- a/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/ReplaceMotiveCommandHandler.php +++ b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/ReplaceMotiveCommandHandler.php @@ -15,8 +15,11 @@ use Chill\TicketBundle\Action\Ticket\ChangeEmergencyStateCommand; use Chill\TicketBundle\Action\Ticket\ReplaceMotiveCommand; use Chill\TicketBundle\Entity\MotiveHistory; use Chill\TicketBundle\Entity\Ticket; +use Chill\TicketBundle\Event\MotiveUpdateEvent; +use Chill\TicketBundle\Event\TicketUpdateEvent; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Clock\ClockInterface; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class ReplaceMotiveCommandHandler { @@ -24,6 +27,7 @@ class ReplaceMotiveCommandHandler private readonly ClockInterface $clock, private readonly EntityManagerInterface $entityManager, private readonly ChangeEmergencyStateCommandHandler $changeEmergencyStateCommandHandler, + private readonly EventDispatcherInterface $eventDispatcher, ) {} public function handle(Ticket $ticket, ReplaceMotiveCommand $command): void @@ -32,6 +36,8 @@ class ReplaceMotiveCommandHandler throw new \InvalidArgumentException('The new motive cannot be null'); } + $event = new MotiveUpdateEvent($ticket); + // will add if there are no existing motive $readyToAdd = 0 === count($ticket->getMotiveHistories()); @@ -45,6 +51,8 @@ class ReplaceMotiveCommandHandler continue; } + // collect previous active motives before closing + $event->previousMotive = $history->getMotive(); $history->setEndDate($this->clock->now()); $readyToAdd = true; } @@ -52,6 +60,7 @@ class ReplaceMotiveCommandHandler if ($readyToAdd) { $history = new MotiveHistory($command->motive, $ticket, $this->clock->now()); $this->entityManager->persist($history); + $event->newMotive = $command->motive; // Check if the motive has makeTicketEmergency set and update the ticket's emergency status if needed if ($command->motive->isMakeTicketEmergency()) { @@ -59,5 +68,9 @@ class ReplaceMotiveCommandHandler ($this->changeEmergencyStateCommandHandler)($ticket, $changeEmergencyCommand); } } + + if ($event->hasChanges()) { + $this->eventDispatcher->dispatch($event, TicketUpdateEvent::class); + } } } diff --git a/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/SetPersonsCommandHandler.php b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/SetPersonsCommandHandler.php index e0a7cfbd0..cab723e7f 100644 --- a/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/SetPersonsCommandHandler.php +++ b/src/Bundle/ChillTicketBundle/src/Action/Ticket/Handler/SetPersonsCommandHandler.php @@ -15,9 +15,12 @@ use Chill\MainBundle\Entity\User; use Chill\TicketBundle\Action\Ticket\SetPersonsCommand; use Chill\TicketBundle\Entity\PersonHistory; use Chill\TicketBundle\Entity\Ticket; +use Chill\TicketBundle\Event\PersonsUpdateEvent; +use Chill\TicketBundle\Event\TicketUpdateEvent; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Clock\ClockInterface; use Symfony\Component\Security\Core\Security; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; final readonly class SetPersonsCommandHandler { @@ -25,10 +28,13 @@ final readonly class SetPersonsCommandHandler private ClockInterface $clock, private EntityManagerInterface $entityManager, private Security $security, + private EventDispatcherInterface $eventDispatcher, ) {} public function handle(Ticket $ticket, SetPersonsCommand $command): void { + $event = new PersonsUpdateEvent($ticket); + // remove existing addresses which are not in the new addresses foreach ($ticket->getPersonHistories() as $personHistory) { if (null !== $personHistory->getEndDate()) { @@ -40,6 +46,7 @@ final readonly class SetPersonsCommandHandler if (($user = $this->security->getUser()) instanceof User) { $personHistory->setRemovedBy($user); } + $event->personsRemoved[] = $personHistory->getPerson(); } } @@ -51,6 +58,11 @@ final readonly class SetPersonsCommandHandler $history = new PersonHistory($person, $ticket, $this->clock->now()); $this->entityManager->persist($history); + $event->personsAdded[] = $person; + } + + if ($event->hasChanges()) { + $this->eventDispatcher->dispatch($event, TicketUpdateEvent::class); } } } diff --git a/src/Bundle/ChillTicketBundle/src/Event/EmergencyStatusUpdateEvent.php b/src/Bundle/ChillTicketBundle/src/Event/EmergencyStatusUpdateEvent.php new file mode 100644 index 000000000..934c38dde --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Event/EmergencyStatusUpdateEvent.php @@ -0,0 +1,26 @@ + + */ + private array $toDispatch = []; + + public function __construct(private readonly MessageBusInterface $messageBus) {} + + public static function getSubscribedEvents(): array + { + return [ + TicketUpdateEvent::class => ['onTicketUpdate', 0], + KernelEvents::TERMINATE => ['onKernelTerminate', 8096], + ]; + } + + public function onTicketUpdate(TicketUpdateEvent $event): void + { + $this->toDispatch[] = new PostTicketUpdateMessage($event->ticket, $event->updateKind); + } + + public function onKernelTerminate(TerminateEvent $event): void + { + foreach ($this->toDispatch as $message) { + $this->messageBus->dispatch($message); + } + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Event/MotiveUpdateEvent.php b/src/Bundle/ChillTicketBundle/src/Event/MotiveUpdateEvent.php new file mode 100644 index 000000000..59aadf054 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Event/MotiveUpdateEvent.php @@ -0,0 +1,31 @@ +newMotive || null !== $this->previousMotive; + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Event/PersonsUpdateEvent.php b/src/Bundle/ChillTicketBundle/src/Event/PersonsUpdateEvent.php new file mode 100644 index 000000000..6d1672dc3 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Event/PersonsUpdateEvent.php @@ -0,0 +1,38 @@ + + */ + public $personsAdded = []; + + /** + * @var list + */ + public $personsRemoved = []; + + public function hasChanges(): bool + { + return count($this->personsAdded) > 0 || count($this->personsRemoved) > 0; + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Event/PostTicketUpdateEvent.php b/src/Bundle/ChillTicketBundle/src/Event/PostTicketUpdateEvent.php new file mode 100644 index 000000000..d4e8fd738 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Event/PostTicketUpdateEvent.php @@ -0,0 +1,29 @@ +ticketRepository->find($event->ticketId); + + if (null === $ticket) { + throw new UnrecoverableMessageHandlingException('Ticket not found'); + } + + $this->eventDispatcher->dispatch(new PostTicketUpdateEvent($event->updateKind, $ticket)); + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Messenger/PostTicketUpdateMessage.php b/src/Bundle/ChillTicketBundle/src/Messenger/PostTicketUpdateMessage.php new file mode 100644 index 000000000..ac6f9c2d5 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Messenger/PostTicketUpdateMessage.php @@ -0,0 +1,27 @@ +ticketId = $ticket->getId(); + } +} diff --git a/src/Bundle/ChillTicketBundle/src/config/services.yaml b/src/Bundle/ChillTicketBundle/src/config/services.yaml index 1089b574e..5e2b8e1b3 100644 --- a/src/Bundle/ChillTicketBundle/src/config/services.yaml +++ b/src/Bundle/ChillTicketBundle/src/config/services.yaml @@ -17,6 +17,9 @@ services: tags: - controller.service_arguments + Chill\TicketBundle\Event\EventSubscriber\: + resource: '../Event/EventSubscriber/' + Chill\TicketBundle\Repository\: resource: '../Repository/' @@ -35,6 +38,9 @@ services: Chill\TicketBundle\Menu\: resource: '../Menu/' + Chill\TicketBundle\Messenger\Handler\: + resource: '../Messenger/Handler' + Chill\TicketBundle\Validation\: resource: '../Validation/' diff --git a/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/ChangeEmergencyStateCommandHandlerTest.php b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/ChangeEmergencyStateCommandHandlerTest.php index c80204490..fe3e85f43 100644 --- a/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/ChangeEmergencyStateCommandHandlerTest.php +++ b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/ChangeEmergencyStateCommandHandlerTest.php @@ -16,9 +16,13 @@ use Chill\TicketBundle\Action\Ticket\Handler\ChangeEmergencyStateCommandHandler; use Chill\TicketBundle\Entity\EmergencyStatusEnum; use Chill\TicketBundle\Entity\EmergencyStatusHistory; use Chill\TicketBundle\Entity\Ticket; +use Chill\TicketBundle\Event\EmergencyStatusUpdateEvent; +use Chill\TicketBundle\Event\TicketUpdateEvent; use PHPUnit\Framework\TestCase; +use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Clock\MockClock; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * @internal @@ -36,7 +40,10 @@ final class ChangeEmergencyStateCommandHandlerTest extends TestCase // Create a YES emergency status history new EmergencyStatusHistory(EmergencyStatusEnum::YES, $ticket); - $handler = new ChangeEmergencyStateCommandHandler(new MockClock()); + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch(Argument::type(EmergencyStatusUpdateEvent::class), TicketUpdateEvent::class)->shouldNotBeCalled(); + + $handler = new ChangeEmergencyStateCommandHandler(new MockClock(), $eventDispatcher->reveal()); $command = new ChangeEmergencyStateCommand(EmergencyStatusEnum::YES); $result = $handler->__invoke($ticket, $command); @@ -57,7 +64,17 @@ final class ChangeEmergencyStateCommandHandlerTest extends TestCase // Create a YES emergency status history new EmergencyStatusHistory(EmergencyStatusEnum::YES, $ticket); - $handler = new ChangeEmergencyStateCommandHandler(new MockClock()); + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch( + Argument::that( + fn ($e) => $e instanceof EmergencyStatusUpdateEvent + && EmergencyStatusEnum::YES === $e->previousEmergencyStatus + && EmergencyStatusEnum::NO === $e->newEmergencyStatus + ), + TicketUpdateEvent::class + )->shouldBeCalled(); + + $handler = new ChangeEmergencyStateCommandHandler(new MockClock(), $eventDispatcher->reveal()); $command = new ChangeEmergencyStateCommand(EmergencyStatusEnum::NO); $result = $handler->__invoke($ticket, $command); @@ -90,7 +107,17 @@ final class ChangeEmergencyStateCommandHandlerTest extends TestCase // Create a NO emergency status history new EmergencyStatusHistory(EmergencyStatusEnum::NO, $ticket); - $handler = new ChangeEmergencyStateCommandHandler(new MockClock()); + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch( + Argument::that( + fn ($e) => $e instanceof EmergencyStatusUpdateEvent + && EmergencyStatusEnum::NO === $e->previousEmergencyStatus + && EmergencyStatusEnum::YES === $e->newEmergencyStatus + ), + TicketUpdateEvent::class + )->shouldBeCalled(); + + $handler = new ChangeEmergencyStateCommandHandler(new MockClock(), $eventDispatcher->reveal()); $command = new ChangeEmergencyStateCommand(EmergencyStatusEnum::YES); $result = $handler->__invoke($ticket, $command); diff --git a/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/ReplaceMotiveCommandHandlerTest.php b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/ReplaceMotiveCommandHandlerTest.php index 17b1e88d9..4a97ebb13 100644 --- a/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/ReplaceMotiveCommandHandlerTest.php +++ b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/ReplaceMotiveCommandHandlerTest.php @@ -19,16 +19,19 @@ use Chill\TicketBundle\Entity\EmergencyStatusEnum; use Chill\TicketBundle\Entity\Motive; use Chill\TicketBundle\Entity\MotiveHistory; use Chill\TicketBundle\Entity\Ticket; +use Chill\TicketBundle\Event\MotiveUpdateEvent; +use Chill\TicketBundle\Event\TicketUpdateEvent; use Doctrine\ORM\EntityManagerInterface; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Clock\MockClock; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * @internal * - * @coversNothing + * @covers \Chill\TicketBundle\Action\Ticket\Handler\ReplaceMotiveCommandHandler */ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase { @@ -37,14 +40,18 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase private function buildHandler( EntityManagerInterface $entityManager, ?ChangeEmergencyStateCommandHandler $changeEmergencyStateCommandHandler = null, + ?EventDispatcherInterface $eventDispatcher = null, ): ReplaceMotiveCommandHandler { $clock = new MockClock(); if (null === $changeEmergencyStateCommandHandler) { $changeEmergencyStateCommandHandler = $this->prophesize(ChangeEmergencyStateCommandHandler::class)->reveal(); } + if (null === $eventDispatcher) { + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class)->reveal(); + } - return new ReplaceMotiveCommandHandler($clock, $entityManager, $changeEmergencyStateCommandHandler); + return new ReplaceMotiveCommandHandler($clock, $entityManager, $changeEmergencyStateCommandHandler, $eventDispatcher); } public function testHandleOnTicketWithoutMotive(): void @@ -61,7 +68,16 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase return $arg->getMotive() === $motive; }))->shouldBeCalled(); - $handler = $this->buildHandler($entityManager->reveal()); + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch( + Argument::that(fn ($event) => $event instanceof MotiveUpdateEvent + && $event->newMotive === $motive + && null === $event->previousMotive + && $event->hasChanges()), + TicketUpdateEvent::class + )->shouldBeCalled(); + + $handler = $this->buildHandler($entityManager->reveal(), null, $eventDispatcher->reveal()); $handler->handle($ticket, new ReplaceMotiveCommand($motive)); @@ -83,7 +99,17 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase return $arg->getMotive() === $motive; }))->shouldBeCalled(); - $handler = $this->buildHandler($entityManager->reveal()); + $previous = $history->getMotive(); + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch( + Argument::that(fn ($event) => $event instanceof MotiveUpdateEvent + && $event->newMotive === $motive + && $previous === $event->previousMotive + && $event->hasChanges()), + TicketUpdateEvent::class + )->shouldBeCalled(); + + $handler = $this->buildHandler($entityManager->reveal(), null, $eventDispatcher->reveal()); $handler->handle($ticket, new ReplaceMotiveCommand($motive)); @@ -106,7 +132,10 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase return $arg->getMotive() === $motive; }))->shouldNotBeCalled(); - $handler = $this->buildHandler($entityManager->reveal()); + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch(Argument::any(), TicketUpdateEvent::class)->shouldNotBeCalled(); + + $handler = $this->buildHandler($entityManager->reveal(), null, $eventDispatcher->reveal()); $handler->handle($ticket, new ReplaceMotiveCommand($motive)); @@ -134,10 +163,15 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase Argument::that(fn (ChangeEmergencyStateCommand $command) => EmergencyStatusEnum::YES === $command->newEmergencyStatus) )->shouldBeCalled(); + // Expect event dispatch for motive update + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch(Argument::type(MotiveUpdateEvent::class), TicketUpdateEvent::class)->shouldBeCalled(); + // Create the handler with our mocks $handler = $this->buildHandler( $entityManager->reveal(), - $changeEmergencyStateCommandHandler->reveal() + $changeEmergencyStateCommandHandler->reveal(), + $eventDispatcher->reveal() ); // Handle the command @@ -166,10 +200,15 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase Argument::cetera() )->shouldNotBeCalled(); + // Expect event dispatch for motive update + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch(Argument::type(MotiveUpdateEvent::class), TicketUpdateEvent::class)->shouldBeCalled(); + // Create the handler with our mocks $handler = $this->buildHandler( $entityManager->reveal(), - $changeEmergencyStateCommandHandler->reveal() + $changeEmergencyStateCommandHandler->reveal(), + $eventDispatcher->reveal() ); // Handle the command diff --git a/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/SetPersonsCommandHandlerTest.php b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/SetPersonsCommandHandlerTest.php index 5c609b543..6097ba993 100644 --- a/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/SetPersonsCommandHandlerTest.php +++ b/src/Bundle/ChillTicketBundle/tests/Action/Ticket/Handler/SetPersonsCommandHandlerTest.php @@ -17,12 +17,15 @@ use Chill\TicketBundle\Action\Ticket\Handler\SetPersonsCommandHandler; use Chill\TicketBundle\Action\Ticket\SetPersonsCommand; use Chill\TicketBundle\Entity\PersonHistory; use Chill\TicketBundle\Entity\Ticket; +use Chill\TicketBundle\Event\PersonsUpdateEvent; +use Chill\TicketBundle\Event\TicketUpdateEvent; use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Clock\MockClock; use Symfony\Component\Security\Core\Security; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * @internal @@ -42,7 +45,16 @@ final class SetPersonsCommandHandlerTest extends TestCase $entityManager->persist(Argument::that(fn ($arg) => $arg instanceof PersonHistory && $arg->getPerson() === $person1))->shouldBeCalledOnce(); $entityManager->persist(Argument::that(fn ($arg) => $arg instanceof PersonHistory && $arg->getPerson() === $group1))->shouldBeCalledOnce(); - $handler = $this->buildHandler($entityManager->reveal()); + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch( + Argument::that(fn ($arg) => $arg instanceof PersonsUpdateEvent + && in_array($person1, $arg->personsAdded, true) + && in_array($group1, $arg->personsAdded, true) + && [] === $arg->personsRemoved), + TicketUpdateEvent::class + )->shouldBeCalled(); + + $handler = $this->buildHandler($entityManager->reveal(), $eventDispatcher->reveal()); $handler->handle($ticket, $command); @@ -59,7 +71,15 @@ final class SetPersonsCommandHandlerTest extends TestCase $entityManager = $this->prophesize(EntityManagerInterface::class); $entityManager->persist(Argument::that(fn ($arg) => $arg instanceof PersonHistory && $arg->getPerson() === $person))->shouldNotBeCalled(); - $handler = $this->buildHandler($entityManager->reveal()); + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch( + Argument::that( + fn ($arg) => $arg instanceof PersonsUpdateEvent + ), + TicketUpdateEvent::class + )->shouldNotBeCalled(); + + $handler = $this->buildHandler($entityManager->reveal(), $eventDispatcher->reveal()); $handler->handle($ticket, $command); @@ -78,7 +98,17 @@ final class SetPersonsCommandHandlerTest extends TestCase $entityManager = $this->prophesize(EntityManagerInterface::class); $entityManager->persist(Argument::that(fn ($arg) => $arg instanceof PersonHistory && $arg->getPerson() === $person2))->shouldBeCalled(); - $handler = $this->buildHandler($entityManager->reveal()); + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch( + Argument::that( + fn ($arg) => $arg instanceof PersonsUpdateEvent + && in_array($person, $arg->personsRemoved, true) && 1 === count($arg->personsRemoved) + && in_array($person2, $arg->personsAdded, true) && 1 === count($arg->personsAdded) + ), + TicketUpdateEvent::class + )->shouldBeCalled(); + + $handler = $this->buildHandler($entityManager->reveal(), $eventDispatcher->reveal()); $handler->handle($ticket, $command); @@ -95,18 +125,28 @@ final class SetPersonsCommandHandlerTest extends TestCase $entityManager = $this->prophesize(EntityManagerInterface::class); $entityManager->persist(Argument::that(fn ($arg) => $arg instanceof PersonHistory && $arg->getPerson() === $person))->shouldBeCalledOnce(); - $handler = $this->buildHandler($entityManager->reveal()); + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch( + Argument::that( + fn ($arg) => $arg instanceof PersonsUpdateEvent + && in_array($person, $arg->personsAdded, true) && 1 === count($arg->personsAdded) + && [] === $arg->personsRemoved + ), + TicketUpdateEvent::class + )->shouldBeCalled(); + + $handler = $this->buildHandler($entityManager->reveal(), $eventDispatcher->reveal()); $handler->handle($ticket, $command); self::assertCount(1, $ticket->getPersons()); } - private function buildHandler(EntityManagerInterface $entityManager): SetPersonsCommandHandler + private function buildHandler(EntityManagerInterface $entityManager, EventDispatcherInterface $eventDispatcher): SetPersonsCommandHandler { $security = $this->prophesize(Security::class); $security->getUser()->willReturn(new User()); - return new SetPersonsCommandHandler(new MockClock(), $entityManager, $security->reveal()); + return new SetPersonsCommandHandler(new MockClock(), $entityManager, $security->reveal(), $eventDispatcher); } } diff --git a/src/Bundle/ChillTicketBundle/tests/Event/EventSubscriber/GeneratePostUpdateTicketEventSubscriberTest.php b/src/Bundle/ChillTicketBundle/tests/Event/EventSubscriber/GeneratePostUpdateTicketEventSubscriberTest.php new file mode 100644 index 000000000..e6a982906 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Event/EventSubscriber/GeneratePostUpdateTicketEventSubscriberTest.php @@ -0,0 +1,58 @@ +getProperty('id'); + $idProperty->setValue($ticket, 1); + $event = new class (TicketUpdateKindEnum::UPDATE_MOTIVE, $ticket) extends TicketUpdateEvent {}; + + $messageBus = $this->prophesize(MessageBusInterface::class); + $messageBus->dispatch(Argument::that(fn ($arg) => $arg instanceof PostTicketUpdateMessage && TicketUpdateKindEnum::UPDATE_MOTIVE === $arg->updateKind && 1 === $arg->ticketId)) + ->will(fn ($args) => new Envelope($args[0])) + ->shouldBeCalled(); + + $eventSubscriber = new GeneratePostUpdateTicketEventSubscriber($messageBus->reveal()); + $eventSubscriber->onTicketUpdate($event); + + $kernel = $this->prophesize(KernelInterface::class); + $terminate = new TerminateEvent($kernel->reveal(), new Request(), new Response()); + $eventSubscriber->onKernelTerminate($terminate); + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Messenger/Handler/PostTicketUpdateMessageHandlerTest.php b/src/Bundle/ChillTicketBundle/tests/Messenger/Handler/PostTicketUpdateMessageHandlerTest.php new file mode 100644 index 000000000..7d5f33e5d --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Messenger/Handler/PostTicketUpdateMessageHandlerTest.php @@ -0,0 +1,92 @@ +getProperty('id'); + $idProperty->setValue($ticket, 123); + + $message = new PostTicketUpdateMessage($ticket, TicketUpdateKindEnum::UPDATE_MOTIVE); + + // Mock repository to return the Ticket when searching by id + $ticketRepository = $this->prophesize(TicketRepositoryInterface::class); + $ticketRepository->find(123)->willReturn($ticket)->shouldBeCalledOnce(); + + // Expect the dispatcher to dispatch a PostTicketUpdateEvent with correct data + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher + ->dispatch(Argument::that(fn ($event) => $event instanceof PostTicketUpdateEvent + && TicketUpdateKindEnum::UPDATE_MOTIVE === $event->updateKind + && $event->ticket === $ticket)) + ->will(fn ($args) => $args[0]) + ->shouldBeCalledOnce(); + + $handler = new PostTicketUpdateMessageHandler($eventDispatcher->reveal(), $ticketRepository->reveal()); + + // Act + $handler($message); + + // Assert: expectations asserted by Prophecy + self::assertTrue(true); + } + + public function testThrowsWhenTicketNotFound(): void + { + // Arrange: a Ticket with an ID for the message, but repository will return null + $ticket = new Ticket(); + $reflection = new \ReflectionClass(Ticket::class); + $idProperty = $reflection->getProperty('id'); + $idProperty->setValue($ticket, 999); + + $message = new PostTicketUpdateMessage($ticket, TicketUpdateKindEnum::UPDATE_MOTIVE); + + $ticketRepository = $this->prophesize(TicketRepositoryInterface::class); + $ticketRepository->find(999)->willReturn(null)->shouldBeCalledOnce(); + + $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $eventDispatcher->dispatch(Argument::any())->shouldNotBeCalled(); + + $handler = new PostTicketUpdateMessageHandler($eventDispatcher->reveal(), $ticketRepository->reveal()); + + // Assert: exception is thrown + $this->expectException(UnrecoverableMessageHandlingException::class); + $this->expectExceptionMessage('Ticket not found'); + + // Act + $handler($message); + } +}