Refactor workflow guard logic and add internal methods

Removed guard logic from EntityWorkflowTransitionEventSubscriber and created a new EntityWorkflowGuardTransition class for separation of concerns. Marked several setter methods in EntityWorkflowStepSignature as internal to guide proper usage. Added comprehensive tests to ensure the new guard logic functions correctly.
This commit is contained in:
2024-09-11 18:29:44 +02:00
parent f0d581b7f8
commit 70671dadac
4 changed files with 291 additions and 44 deletions

View File

@@ -0,0 +1,168 @@
<?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\EventSubscriber;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Templating\Entity\UserRender;
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
use Chill\MainBundle\Workflow\EventSubscriber\EntityWorkflowGuardTransition;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\DefinitionBuilder;
use Symfony\Component\Workflow\Exception\NotEnabledTransitionException;
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 EntityWorkflowGuardTransitionTest extends TestCase
{
use ProphecyTrait;
public static function buildRegistry(?EventSubscriberInterface $eventSubscriber): Registry
{
$builder = new DefinitionBuilder();
$builder
->setInitialPlaces(['initial'])
->addPlaces(['initial', 'intermediate', 'step1', 'step2', 'step3'])
->addTransition(new Transition('intermediate', 'initial', 'intermediate'))
->addTransition($transition1 = new Transition('transition1', 'intermediate', 'step1'))
->addTransition($transition2 = new Transition('transition2', 'intermediate', 'step2'))
->addTransition($transition3 = new Transition('transition3', 'intermediate', 'step3'))
;
$transitionMetadata = new \SplObjectStorage();
$transitionMetadata->attach($transition1, ['transitionGuard' => 'only-dest']);
$transitionMetadata->attach($transition2, ['transitionGuard' => 'only-dest+system']);
$transitionMetadata->attach($transition3, ['transitionGuard' => 'system']);
$builder->setMetadataStore(new InMemoryMetadataStore(transitionsMetadata: $transitionMetadata));
if (null !== $eventSubscriber) {
$eventDispatcher = new EventDispatcher();
$eventDispatcher->addSubscriber($eventSubscriber);
}
$workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), $eventDispatcher ?? null, 'dummy');
$registry = new Registry();
$registry->addWorkflow(
$workflow,
new class () implements WorkflowSupportStrategyInterface {
public function supports(WorkflowInterface $workflow, object $subject): bool
{
return true;
}
}
);
return $registry;
}
/**
* @dataProvider provideBlockingTransition
*/
public function testTransitionGuardBlocked(EntityWorkflow $entityWorkflow, string $transition, ?User $user, string $uuid): void
{
$userRender = $this->prophesize(UserRender::class);
$userRender->renderString(Argument::type(User::class), [])->willReturn('user-as-string');
$security = $this->prophesize(Security::class);
$security->getUser()->willReturn($user);
$transitionGuard = new EntityWorkflowGuardTransition($userRender->reveal(), $security->reveal());
$registry = self::buildRegistry($transitionGuard);
$workflow = $registry->get($entityWorkflow, 'dummy');
$context = new WorkflowTransitionContextDTO($entityWorkflow);
self::expectException(NotEnabledTransitionException::class);
try {
$workflow->apply($entityWorkflow, $transition, ['context' => $context, 'byUser' => $user, 'transition' => $transition, 'transitionAt' => new \DateTimeImmutable('now')]);
} catch (NotEnabledTransitionException $e) {
$list = $e->getTransitionBlockerList();
self::assertEquals(1, $list->count());
$list = iterator_to_array($list->getIterator());
self::assertEquals($uuid, $list[0]->getCode());
throw $e;
}
}
/**
* @dataProvider provideValidTransition
*/
public function testTransitionGuardValid(EntityWorkflow $entityWorkflow, string $transition, ?User $user, string $newStep): void
{
$userRender = $this->prophesize(UserRender::class);
$userRender->renderString(Argument::type(User::class), [])->willReturn('user-as-string');
$security = $this->prophesize(Security::class);
$security->getUser()->willReturn($user);
$transitionGuard = new EntityWorkflowGuardTransition($userRender->reveal(), $security->reveal());
$registry = self::buildRegistry($transitionGuard);
$workflow = $registry->get($entityWorkflow, 'dummy');
$context = new WorkflowTransitionContextDTO($entityWorkflow);
$workflow->apply($entityWorkflow, $transition, ['context' => $context, 'byUser' => $user, 'transition' => $transition, 'transitionAt' => new \DateTimeImmutable('now')]);
self::assertEquals($newStep, $entityWorkflow->getStep());
}
public static function provideBlockingTransition(): iterable
{
yield [self::buildEntityWorkflow([new User()]), 'transition1', new User(), 'f3eeb57c-7532-11ec-9495-e7942a2ac7bc'];
yield [self::buildEntityWorkflow([]), 'transition1', null, 'd9e39a18-704c-11ef-b235-8fe0619caee7'];
yield [self::buildEntityWorkflow([new User()]), 'transition1', null, 'd9e39a18-704c-11ef-b235-8fe0619caee7'];
yield [self::buildEntityWorkflow([$user = new User()]), 'transition3', $user, '5b6b95e0-704d-11ef-a5a9-4b6fc11a8eeb'];
}
public static function provideValidTransition(): iterable
{
yield [self::buildEntityWorkflow([$u = new User()]), 'transition1', $u, 'step1'];
yield [self::buildEntityWorkflow([$u = new User()]), 'transition2', $u, 'step2'];
yield [self::buildEntityWorkflow([new User()]), 'transition2', null, 'step2'];
yield [self::buildEntityWorkflow([]), 'transition2', null, 'step2'];
yield [self::buildEntityWorkflow([new User()]), 'transition3', null, 'step3'];
yield [self::buildEntityWorkflow([]), 'transition3', null, 'step3'];
}
public static function buildEntityWorkflow(array $futureDestUsers): EntityWorkflow
{
$registry = self::buildRegistry(null);
$baseContext = ['transition' => 'intermediate', 'transitionAt' => new \DateTimeImmutable()];
// test a user not is destination is blocked
$entityWorkflow = new EntityWorkflow();
$workflow = $registry->get($entityWorkflow, 'dummy');
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers = $futureDestUsers;
$workflow->apply($entityWorkflow, 'intermediate', ['context' => $dto, ...$baseContext]);
return $entityWorkflow;
}
}