Add message handling for public view creation

Introduce `PostPublicViewMessage` and `PostPublicViewMessageHandler` to handle external user views on public links by applying workflow transitions. Integrate with `WorkflowViewSendPublicController` and add relevant tests.
This commit is contained in:
Julien Fastré 2024-10-09 21:33:09 +02:00
parent 40b8fae8ba
commit 82e2b9a0f6
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
6 changed files with 334 additions and 4 deletions

View File

@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSendView; use Chill\MainBundle\Entity\Workflow\EntityWorkflowSendView;
use Chill\MainBundle\Workflow\EntityWorkflowManager; use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\Exception\HandlerWithPublicViewNotFoundException; use Chill\MainBundle\Workflow\Exception\HandlerWithPublicViewNotFoundException;
use Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage;
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO; use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -22,6 +23,7 @@ use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Twig\Environment; use Twig\Environment;
@ -35,6 +37,7 @@ final readonly class WorkflowViewSendPublicController
private EntityWorkflowManager $entityWorkflowManager, private EntityWorkflowManager $entityWorkflowManager,
private ClockInterface $clock, private ClockInterface $clock,
private Environment $environment, private Environment $environment,
private MessageBusInterface $messageBus,
) {} ) {}
#[Route('/public/main/workflow/send/{uuid}/view/{verificationKey}', name: 'chill_main_workflow_send_view_public', methods: ['GET'])] #[Route('/public/main/workflow/send/{uuid}/view/{verificationKey}', name: 'chill_main_workflow_send_view_public', methods: ['GET'])]
@ -75,6 +78,7 @@ final readonly class WorkflowViewSendPublicController
$view = new EntityWorkflowSendView($workflowSend, $this->clock->now(), $request->getClientIp()); $view = new EntityWorkflowSendView($workflowSend, $this->clock->now(), $request->getClientIp());
$this->entityManager->persist($view); $this->entityManager->persist($view);
$this->messageBus->dispatch(new PostPublicViewMessage($view->getId()));
$this->entityManager->flush(); $this->entityManager->flush();
return $response; return $response;

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSendView;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\ObjectRepository;
/**
* @template-implements ObjectRepository<EntityWorkflowSendView>
*/
class EntityWorkflowSendViewRepository implements ObjectRepository
{
private readonly ObjectRepository $repository;
public function __construct(ManagerRegistry $registry)
{
$this->repository = $registry->getRepository($this->getClassName());
}
public function find($id): ?EntityWorkflowSendView
{
return $this->repository->find($id);
}
public function findAll()
{
return $this->repository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria): ?EntityWorkflowSendView
{
return $this->repository->findOneBy($criteria);
}
public function getClassName()
{
return EntityWorkflowSendView::class;
}
}

View File

@ -18,6 +18,8 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflowSendView;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\MainBundle\Workflow\EntityWorkflowManager; use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\EntityWorkflowWithPublicViewInterface; use Chill\MainBundle\Workflow\EntityWorkflowWithPublicViewInterface;
use Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage;
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
use Chill\ThirdPartyBundle\Entity\ThirdParty; use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -27,6 +29,8 @@ use Psr\Log\NullLogger;
use Symfony\Component\Clock\MockClock; use Symfony\Component\Clock\MockClock;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Workflow\Registry; use Symfony\Component\Workflow\Registry;
use Twig\Environment; use Twig\Environment;
@ -44,8 +48,10 @@ class WorkflowViewSendPublicControllerTest extends TestCase
$environment = $this->prophesize(Environment::class); $environment = $this->prophesize(Environment::class);
$entityManager = $this->prophesize(EntityManagerInterface::class); $entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->flush()->shouldNotBeCalled(); $entityManager->flush()->shouldNotBeCalled();
$messageBus = $this->prophesize(MessageBusInterface::class);
$messageBus->dispatch(Argument::type(PostPublicViewMessage::class))->shouldNotBeCalled();
$controller = new WorkflowViewSendPublicController($entityManager->reveal(), new NullLogger(), new EntityWorkflowManager([], new Registry()), new MockClock(), $environment->reveal()); $controller = new WorkflowViewSendPublicController($entityManager->reveal(), new NullLogger(), new EntityWorkflowManager([], new Registry()), new MockClock(), $environment->reveal(), $messageBus->reveal());
self::expectException(AccessDeniedHttpException::class); self::expectException(AccessDeniedHttpException::class);
@ -63,8 +69,10 @@ class WorkflowViewSendPublicControllerTest extends TestCase
$environment = $this->prophesize(Environment::class); $environment = $this->prophesize(Environment::class);
$entityManager = $this->prophesize(EntityManagerInterface::class); $entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->flush()->shouldBeCalled(); $entityManager->flush()->shouldBeCalled();
$messageBus = $this->prophesize(MessageBusInterface::class);
$messageBus->dispatch(Argument::type(PostPublicViewMessage::class))->shouldNotBeCalled();
$controller = new WorkflowViewSendPublicController($entityManager->reveal(), new NullLogger(), new EntityWorkflowManager([], new Registry()), new MockClock(), $environment->reveal()); $controller = new WorkflowViewSendPublicController($entityManager->reveal(), new NullLogger(), new EntityWorkflowManager([], new Registry()), new MockClock(), $environment->reveal(), $messageBus->reveal());
self::expectException(AccessDeniedHttpException::class); self::expectException(AccessDeniedHttpException::class);
@ -86,8 +94,10 @@ class WorkflowViewSendPublicControllerTest extends TestCase
$entityManager = $this->prophesize(EntityManagerInterface::class); $entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->flush()->shouldNotBeCalled(); $entityManager->flush()->shouldNotBeCalled();
$messageBus = $this->prophesize(MessageBusInterface::class);
$messageBus->dispatch(Argument::type(PostPublicViewMessage::class))->shouldNotBeCalled();
$controller = new WorkflowViewSendPublicController($entityManager->reveal(), new NullLogger(), new EntityWorkflowManager([], new Registry()), new MockClock('next year'), $environment->reveal()); $controller = new WorkflowViewSendPublicController($entityManager->reveal(), new NullLogger(), new EntityWorkflowManager([], new Registry()), new MockClock('next year'), $environment->reveal(), $messageBus->reveal());
$send = $this->buildEntityWorkflowSend(); $send = $this->buildEntityWorkflowSend();
@ -102,6 +112,8 @@ class WorkflowViewSendPublicControllerTest extends TestCase
$environment = $this->prophesize(Environment::class); $environment = $this->prophesize(Environment::class);
$entityManager = $this->prophesize(EntityManagerInterface::class); $entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->flush()->shouldNotBeCalled(); $entityManager->flush()->shouldNotBeCalled();
$messageBus = $this->prophesize(MessageBusInterface::class);
$messageBus->dispatch(Argument::type(PostPublicViewMessage::class))->shouldNotBeCalled();
$controller = new WorkflowViewSendPublicController( $controller = new WorkflowViewSendPublicController(
$entityManager->reveal(), $entityManager->reveal(),
@ -109,6 +121,7 @@ class WorkflowViewSendPublicControllerTest extends TestCase
new EntityWorkflowManager([], new Registry()), new EntityWorkflowManager([], new Registry()),
new MockClock(), new MockClock(),
$environment->reveal(), $environment->reveal(),
$messageBus->reveal(),
); );
self::expectException(\RuntimeException::class); self::expectException(\RuntimeException::class);
@ -123,9 +136,18 @@ class WorkflowViewSendPublicControllerTest extends TestCase
$environment = $this->prophesize(Environment::class); $environment = $this->prophesize(Environment::class);
$entityManager = $this->prophesize(EntityManagerInterface::class); $entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->persist(Argument::that(function (EntityWorkflowSendView $view) use ($send) { $entityManager->persist(Argument::that(function (EntityWorkflowSendView $view) use ($send) {
$reflection = new \ReflectionClass($view);
$idProperty = $reflection->getProperty('id');
$idProperty->setAccessible(true);
$idProperty->setValue($view, 5);
return $send === $view->getSend(); return $send === $view->getSend();
}))->shouldBeCalled(); }))->shouldBeCalled();
$entityManager->flush()->shouldBeCalled(); $entityManager->flush()->shouldBeCalled();
$messageBus = $this->prophesize(MessageBusInterface::class);
$messageBus->dispatch(Argument::type(PostPublicViewMessage::class))->shouldBeCalled()
->will(fn ($args) => new Envelope($args[0]));
$controller = new WorkflowViewSendPublicController( $controller = new WorkflowViewSendPublicController(
$entityManager->reveal(), $entityManager->reveal(),
@ -135,6 +157,7 @@ class WorkflowViewSendPublicControllerTest extends TestCase
], new Registry()), ], new Registry()),
new MockClock(), new MockClock(),
$environment->reveal(), $environment->reveal(),
$messageBus->reveal(),
); );
$response = $controller($send, $send->getPrivateToken(), $this->buildRequest()); $response = $controller($send, $send->getPrivateToken(), $this->buildRequest());
@ -211,7 +234,7 @@ class WorkflowViewSendPublicControllerTest extends TestCase
throw new \BadMethodCallException('not implemented'); throw new \BadMethodCallException('not implemented');
} }
public function renderPublicView(EntityWorkflowSend $entityWorkflowSend): string public function renderPublicView(EntityWorkflowSend $entityWorkflowSend, EntityWorkflowViewMetadataDTO $metadata): string
{ {
return 'content'; return 'content';
} }

View File

@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Tests\Workflow\Messenger;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSendView;
use Chill\MainBundle\Repository\EntityWorkflowSendViewRepository;
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
use Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage;
use Chill\MainBundle\Workflow\Messenger\PostPublicViewMessageHandler;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Workflow\DefinitionBuilder;
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\Workflow;
use Symfony\Component\Workflow\WorkflowInterface;
/**
* @internal
*
* @coversNothing
*/
class PostPublicViewMessageHandlerTest extends TestCase
{
use ProphecyTrait;
private function buildRegistry(): Registry
{
$builder = new DefinitionBuilder();
$builder
->setInitialPlaces(['initial'])
->addPlaces(['initial', 'waiting_for_views', 'waiting_for_views_transition_unavailable', 'post_view'])
->addTransitions([
new Transition('post_view', 'waiting_for_views', 'post_view'),
])
->setMetadataStore(
new InMemoryMetadataStore(
placesMetadata: [
'waiting_for_views' => [
'isSentExternal' => true,
'onExternalView' => 'post_view',
],
'waiting_for_views_transition_unavailable' => [
'isSentExternal' => true,
'onExternalView' => 'post_view',
],
]
)
);
$workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), name: 'dummy');
$registry = new Registry();
$registry
->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
public function supports(WorkflowInterface $workflow, object $subject): bool
{
return true;
}
});
return $registry;
}
public function testHandleTransitionToPostViewSuccessful(): void
{
$entityWorkflow = new EntityWorkflow();
$entityWorkflow->setWorkflowName('dummy');
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestineeThirdParties = [new ThirdParty()];
$entityWorkflow->setStep('waiting_for_views', $dto, 'to_waiting_for_views', new \DateTimeImmutable(), new User());
$send = $entityWorkflow->getCurrentStep()->getSends()->first();
$view = new EntityWorkflowSendView($send, new \DateTimeImmutable(), '127.0.0.1');
$repository = $this->prophesize(EntityWorkflowSendViewRepository::class);
$repository->find(6)->willReturn($view);
$handler = new PostPublicViewMessageHandler($repository->reveal(), $this->buildRegistry(), new NullLogger());
$handler(new PostPublicViewMessage(6));
self::assertEquals('post_view', $entityWorkflow->getStep());
}
public function testHandleTransitionToPostViewAlreadyMoved(): void
{
$entityWorkflow = new EntityWorkflow();
$entityWorkflow->setWorkflowName('dummy');
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestineeThirdParties = [new ThirdParty()];
$entityWorkflow->setStep('waiting_for_views', $dto, 'to_waiting_for_views', new \DateTimeImmutable(), new User());
$send = $entityWorkflow->getCurrentStep()->getSends()->first();
$view = new EntityWorkflowSendView($send, new \DateTimeImmutable(), '127.0.0.1');
// move again
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$entityWorkflow->setStep('post_view', $dto, 'post_view', new \DateTimeImmutable(), new User());
$lastStep = $entityWorkflow->getCurrentStep();
$repository = $this->prophesize(EntityWorkflowSendViewRepository::class);
$repository->find(6)->willReturn($view);
$handler = new PostPublicViewMessageHandler($repository->reveal(), $this->buildRegistry(), new NullLogger());
$handler(new PostPublicViewMessage(6));
self::assertEquals('post_view', $entityWorkflow->getStep());
self::assertSame($lastStep, $entityWorkflow->getCurrentStep());
}
public function testHandleTransitionToPostViewBlocked(): void
{
$entityWorkflow = new EntityWorkflow();
$entityWorkflow->setWorkflowName('dummy');
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestineeThirdParties = [new ThirdParty()];
$entityWorkflow->setStep('waiting_for_views_transition_unavailable', $dto, 'to_waiting_for_views', new \DateTimeImmutable(), new User());
$send = $entityWorkflow->getCurrentStep()->getSends()->first();
$view = new EntityWorkflowSendView($send, new \DateTimeImmutable(), '127.0.0.1');
$repository = $this->prophesize(EntityWorkflowSendViewRepository::class);
$repository->find(6)->willReturn($view);
$handler = new PostPublicViewMessageHandler($repository->reveal(), $this->buildRegistry(), new NullLogger());
$handler(new PostPublicViewMessage(6));
self::assertEquals('waiting_for_views_transition_unavailable', $entityWorkflow->getStep());
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Workflow\Messenger;
/**
* Message sent after a EntityWorkflowSendView was created, which means that
* an external user has seen a link for a public view.
*/
class PostPublicViewMessage
{
public function __construct(
public int $entityWorkflowSendViewId,
) {}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Workflow\Messenger;
use Chill\MainBundle\Repository\EntityWorkflowSendViewRepository;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Symfony\Component\Workflow\Registry;
/**
* Handle the behaviour after a EntityWorkflowSentView was created.
*
* This handler apply a transition if the workflow's configuration defines one.
*/
final readonly class PostPublicViewMessageHandler implements MessageHandlerInterface
{
private const LOG_PREFIX = '[PostPublicViewMessageHandler] ';
private const TRANSITION_ON_VIEW = 'onExternalView';
public function __construct(
private EntityWorkflowSendViewRepository $sendViewRepository,
private Registry $registry,
private LoggerInterface $logger,
) {}
public function __invoke(PostPublicViewMessage $message): void
{
$view = $this->sendViewRepository->find($message->entityWorkflowSendViewId);
if (null === $view) {
throw new \RuntimeException("EntityworkflowSendViewId {$message->entityWorkflowSendViewId} not found");
}
$step = $view->getSend()->getEntityWorkflowStep();
$entityWorkflow = $step->getEntityWorkflow();
if ($step !== $entityWorkflow->getCurrentStep()) {
$this->logger->info(self::LOG_PREFIX."Do not handle view, as the current's step for the associated EntityWorkflow has already moved", [
'id' => $message->entityWorkflowSendViewId,
'entityWorkflow' => $entityWorkflow->getId(),
]);
return;
}
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$metadata = $workflow->getMetadataStore();
foreach ($workflow->getMarking($entityWorkflow)->getPlaces() as $place => $key) {
$placeMetadata = $metadata->getPlaceMetadata($place);
if (array_key_exists(self::TRANSITION_ON_VIEW, $placeMetadata)) {
if ($workflow->can($entityWorkflow, $placeMetadata[self::TRANSITION_ON_VIEW])) {
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->transition = $workflow->getEnabledTransition($entityWorkflow, $placeMetadata[self::TRANSITION_ON_VIEW]);
$workflow->apply($entityWorkflow, $placeMetadata[self::TRANSITION_ON_VIEW], [
'context' => $dto,
'transitionAt' => $view->getViewAt(),
'transition' => $placeMetadata[self::TRANSITION_ON_VIEW],
]);
return;
}
$this->logger->info(self::LOG_PREFIX.'Not able to apply this transition', ['transition' => $placeMetadata[self::TRANSITION_ON_VIEW],
'entityWorkflowId' => $entityWorkflow->getId(), 'viewId' => $view->getId()]);
}
}
}
}