diff --git a/CHANGELOG.md b/CHANGELOG.md index d598cce..0329b12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Latest ------ ## Fixes +* [#228](https://github.com/cleverage/ui-process-bundle/issues/228) `ProcessExecuteHandler`, `ProcessExecuteController` (`cleverage_ui_process_http_execute`): only the files written by the launch form or the HTTP API for the execution are deleted (#209), flagged by the new `deleteInput` argument of `ProcessExecuteMessage` (`false` by default): a file of the upload directory given by its path (HTTP API, text input of the launch form, schedule, default input) is no longer deleted. A message failing in a Messenger worker keeps its file while it will be retried or when it is sent to a failure transport (`messenger:failed:retry`), new internal `UploadedFileSubscriber`. The file uploaded with a request rejected by the HTTP API (422) is deleted. The messages queued by 3.4 have no flag: their file is no longer deleted by the worker, it may need a manual cleanup of the upload directory (`cleverage_ui_process.upload_directory`). Update documentation, add tests. * [#229](https://github.com/cleverage/ui-process-bundle/issues/229) Login throttling of the `main` firewall (#207, #222): the requests of the HTTP API (`HttpProcessExecutionAuthenticator`) are no longer counted, on every firewall with a login throttling (`LoginThrottlingPass` decorates its limiter, the firewall settings still apply to the login form): failed logins blocked a valid API token, and invalid tokens blocked the login form of the IP address. `login_throttling: false` in the application `main` firewall disables it (it was enabled again by the bundle). With `framework.rate_limiter: false`, an explicit error tells how to fix the configuration instead of "Parent definition "limiter" does not exist". `HttpProcessExecutionAuthenticator::onAuthenticationFailure()` answers the message key of the exception, instead of an empty message when there were too many attempts. Update documentation, add tests. v3.4 diff --git a/config/services/event_subscriber.yaml b/config/services/event_subscriber.yaml index 6ba6f92..2e8aa38 100644 --- a/config/services/event_subscriber.yaml +++ b/config/services/event_subscriber.yaml @@ -9,3 +9,12 @@ services: - '@cleverage_ui_process.monolog_handler.doctrine_process' - '@cleverage_ui_process.manager.process_execution' - '@event_dispatcher' + + cleverage_ui_process.event_subscriber.uploaded_file: + class: CleverAge\UiProcessBundle\EventSubscriber\UploadedFileSubscriber + public: false + tags: + - { name: 'kernel.event_subscriber' } + arguments: + - '@cleverage_ui_process.manager.uploaded_file' + # Failure transports: set by the UploadedFileSubscriberPass diff --git a/config/services/message.yaml b/config/services/message.yaml index 124cb3b..6eedbcd 100644 --- a/config/services/message.yaml +++ b/config/services/message.yaml @@ -17,5 +17,6 @@ services: - '@cleverage_process.manager.process' - '@cleverage_ui_process.monolog_handler.process' - '@cleverage_ui_process.manager.uploaded_file' + - '@cleverage_ui_process.event_subscriber.uploaded_file' tags: [messenger.message_handler] diff --git a/docs/reference/01-bundle_configuration.md b/docs/reference/01-bundle_configuration.md index f1072d8..00a71ec 100644 --- a/docs/reference/01-bundle_configuration.md +++ b/docs/reference/01-bundle_configuration.md @@ -97,7 +97,7 @@ Container parameters | Parameter | Default | Description | |-----------------------------------------|-------------------------------------------|-------------| | `cleverage_ui_process.upload_directory` | `%kernel.project_dir%/var/storage/uploads` | Directory where files uploaded from the launch form or the [HTTP API](06-http_api.md) are stored before being passed as process input. | -| `cleverage_ui_process.delete_uploaded_files` | `true` | Delete the uploaded file once the process has been executed (successfully or not), by the Messenger handler or by the HTTP API when the process is not queued. Only the files of `cleverage_ui_process.upload_directory` are deleted. Set it to `false` to keep them, e.g. when a process stores the path of its input. | +| `cleverage_ui_process.delete_uploaded_files` | `true` | Delete the uploaded file once the process has been executed (successfully or not), by the Messenger handler or by the HTTP API when the process is not queued; the file of a request rejected by the HTTP API is also deleted. Only the files written by the bundle for the execution are deleted, never a file given by its path, even in `cleverage_ui_process.upload_directory`. A message failing in a Messenger worker keeps its file while it will be retried, or when it is sent to a failure transport (see [messenger](08-messenger.md#uploaded-files)). Set it to `false` to keep them, e.g. when a process stores the path of its input. | | `upload_directory` | `%cleverage_ui_process.upload_directory%` | Deprecated since 3.3, removed in 4.0: use `cleverage_ui_process.upload_directory`. When your application still sets it, its value is used, with a deprecation. If your application has its own `upload_directory` parameter (e.g. for another bundle), also set `cleverage_ui_process.upload_directory`: the bundle then leaves `upload_directory` alone, without deprecation. | It can be overridden in your application: diff --git a/docs/reference/02-process_ui_options.md b/docs/reference/02-process_ui_options.md index d4b7b7c..095a7f1 100644 --- a/docs/reference/02-process_ui_options.md +++ b/docs/reference/02-process_ui_options.md @@ -86,7 +86,8 @@ The launch form (`/process/launch?process=`) contains: When `entrypoint_type` is `file`, the uploaded file is saved to the `cleverage_ui_process.upload_directory` directory (see [bundle configuration](01-bundle_configuration.md#container-parameters)) with a random name keeping the original extension, and its absolute path is passed as process input. The file is deleted once the process has been executed, -unless `cleverage_ui_process.delete_uploaded_files` is `false`. +unless `cleverage_ui_process.delete_uploaded_files` is `false`. When `entrypoint_type` is `text`, a file path given as +input is never deleted. Default values -------------- diff --git a/docs/reference/06-http_api.md b/docs/reference/06-http_api.md index 35331cd..7e10a48 100644 --- a/docs/reference/06-http_api.md +++ b/docs/reference/06-http_api.md @@ -34,7 +34,7 @@ Parameters can be sent either as a JSON body, or as form data (`application/x-ww | Parameter | Type | Required | Default | Description | |-----------|---------------------------|:--------:|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `code` | `string` | **X** | | Code of the process. It must exist and be public. | -| `input` | `string` or file | | `null` | Process input. With form data, it can be an uploaded file: the file is saved in the `cleverage_ui_process.upload_directory` directory (see [bundle configuration](01-bundle_configuration.md#container-parameters)) and its path is passed as input; the file is deleted once the process has been executed, unless `cleverage_ui_process.delete_uploaded_files` is `false`. | +| `input` | `string` or file | | `null` | Process input. With form data, it can be an uploaded file: the file is saved in the `cleverage_ui_process.upload_directory` directory (see [bundle configuration](01-bundle_configuration.md#container-parameters)) and its path is passed as input; the file is deleted once the process has been executed, or when the request is rejected (422), unless `cleverage_ui_process.delete_uploaded_files` is `false`. A file given by its path is never deleted. | | `context` | `object` or JSON `string` | | `{}` | Process context, as key/value pairs. A JSON string must decode to an object or an array (`422` otherwise). With form data, send one field per value: `context[key]=value`. | | `queue` | `bool` | | `true` | `true`: the process is queued to the `execute_process` transport (see [messenger](08-messenger.md)). `false`: the process is executed during the HTTP request. | diff --git a/docs/reference/08-messenger.md b/docs/reference/08-messenger.md index 00acd2d..87992d4 100644 --- a/docs/reference/08-messenger.md +++ b/docs/reference/08-messenger.md @@ -10,7 +10,7 @@ Messages | Message | Transport | Handler | |--------------------------------------------------------|-------------------|-----------------------------------------------------------------------------------------------------------------| -| `CleverAge\UiProcessBundle\Message\ProcessExecuteMessage` (`code`, `input`, `context`) | `execute_process` | Executes the process with the process manager, like `cleverage:process:execute`. | +| `CleverAge\UiProcessBundle\Message\ProcessExecuteMessage` (`code`, `input`, `context`, `deleteInput`) | `execute_process` | Executes the process with the process manager, like `cleverage:process:execute`. | | `CleverAge\UiProcessBundle\Message\CronProcessMessage` (`processSchedule`) | `scheduler_cron` | Reads the schedule again, and dispatches a `ProcessExecuteMessage` with its current input and context, unless it has been disabled or deleted. | You can dispatch a `ProcessExecuteMessage` yourself to queue a process from your code: @@ -27,6 +27,22 @@ public function importProducts(string $filePath): void } ``` +Uploaded files +-------------- + +The launch form and the HTTP API set `deleteInput` (`false` by default) when the input is the file they have just +written in the upload directory: the file is deleted once the process has been executed, successfully or not (see the +`cleverage_ui_process.delete_uploaded_files` [parameter](01-bundle_configuration.md#container-parameters)). In a +worker, the file of a failed message is kept while the message will be retried (retry strategy of the transport), or +when it is sent to a failure transport, so that `messenger:failed:retry` executes it with its file. It is deleted once +the message no longer fails, or when it fails again from the failure transport and Symfony drops it (not sent again to +the failure transport it has been received from). A file of a message removed from the failure transport +(`messenger:failed:remove`) is kept. With Symfony 6.4.0 to 6.4.4, which sends a message failing again from the failure +transport back to it, its file is deleted anyway. + +The messages queued by the version 3.4 of the bundle have no `deleteInput`: their file is not deleted, you may have to +remove it from the upload directory. + Transports ---------- diff --git a/src/CleverAgeUiProcessBundle.php b/src/CleverAgeUiProcessBundle.php index ae3680e..c037153 100644 --- a/src/CleverAgeUiProcessBundle.php +++ b/src/CleverAgeUiProcessBundle.php @@ -16,6 +16,7 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -38,5 +39,6 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new CheckDoctrineLazyGhostObjectsPass()); $container->addCompilerPass(new UploadDirectoryParameterPass()); $container->addCompilerPass(new LoginThrottlingPass()); + $container->addCompilerPass(new UploadedFileSubscriberPass()); } } diff --git a/src/Controller/Admin/Process/LaunchAction.php b/src/Controller/Admin/Process/LaunchAction.php index 910aca6..491f831 100644 --- a/src/Controller/Admin/Process/LaunchAction.php +++ b/src/Controller/Admin/Process/LaunchAction.php @@ -113,16 +113,19 @@ public function __invoke( $form->handleRequest($this->requestStack->getMainRequest()); if ($form->isSubmitted() && $form->isValid()) { $input = $form->get('input')->getData(); + // Only the file written here is deleted once executed, never a file given by its path (text input) + $deleteInput = false; if ($input instanceof UploadedFile) { $filename = \sprintf('%s/%s.%s', $uploadDirectory, Uuid::v4(), $input->getClientOriginalExtension()); (new Filesystem())->dumpFile($filename, $input->getContent()); $input = $filename; + $deleteInput = true; } /** @var string $processCode */ $processCode = $form->getConfig()->getOption('process_code'); /** @var array $context */ $context = $form->get('context')->getData(); - $this->dispatch($processCode, $input, $context); + $this->dispatch($processCode, $input, $context, $deleteInput); $this->addFlash( 'success', 'Process has been added to queue. It will start as soon as possible' @@ -143,7 +146,7 @@ public function __invoke( /** * @param array $context */ - protected function dispatch(string $processCode, mixed $input = null, array $context = []): void + protected function dispatch(string $processCode, mixed $input = null, array $context = [], bool $deleteInput = false): void { $message = new ProcessExecuteMessage( $processCode, @@ -151,7 +154,8 @@ protected function dispatch(string $processCode, mixed $input = null, array $con array_merge( ['execution_user' => $this->getUser()?->getEmail()], $context - ) + ), + $deleteInput ); $this->messageBus->dispatch($message); } diff --git a/src/Controller/ProcessExecuteController.php b/src/Controller/ProcessExecuteController.php index ecbd71c..72f1366 100644 --- a/src/Controller/ProcessExecuteController.php +++ b/src/Controller/ProcessExecuteController.php @@ -16,11 +16,13 @@ use CleverAge\ProcessBundle\Manager\ProcessManagerInterface; use CleverAge\UiProcessBundle\Entity\ProcessExecution; use CleverAge\UiProcessBundle\Http\Model\HttpProcessExecution; +use CleverAge\UiProcessBundle\Http\ValueResolver\HttpProcessExecuteValueResolver; use CleverAge\UiProcessBundle\Manager\ProcessExecutionManager; use CleverAge\UiProcessBundle\Manager\UploadedFileManager; use CleverAge\UiProcessBundle\Message\ProcessExecuteMessage; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\ValueResolver; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; @@ -47,13 +49,18 @@ public function __construct( public function __invoke( #[ValueResolver('http_process_execution')] HttpProcessExecution $httpProcessExecution, + Request $request, ): JsonResponse { + // Only the file written by the value resolver is deleted, never a file given by its path + $uploadedInput = $request->attributes->get(HttpProcessExecuteValueResolver::UPLOADED_INPUT_ATTRIBUTE); + $deleteInput = \is_string($uploadedInput) && $uploadedInput === $httpProcessExecution->input; $violations = $this->validator->validate($httpProcessExecution); if ($violations->count() > 0) { $violationsMessages = []; foreach ($violations as $violation) { $violationsMessages[] = $violation->getMessage(); } + $this->uploadedFileManager?->deleteInput($uploadedInput); throw new UnprocessableEntityHttpException(implode('. ', $violationsMessages)); } /** @var array $context */ @@ -65,7 +72,8 @@ public function __invoke( new ProcessExecuteMessage( $httpProcessExecution->code ?? '', $httpProcessExecution->input, - $context + $context, + $deleteInput ) ); @@ -87,7 +95,9 @@ public function __invoke( return new JsonResponse($message, Response::HTTP_INTERNAL_SERVER_ERROR); } finally { - $this->uploadedFileManager?->deleteInput($httpProcessExecution->input); + if ($deleteInput) { + $this->uploadedFileManager?->deleteInput($httpProcessExecution->input); + } } return new JsonResponse('Process has been proceed well.'); diff --git a/src/DependencyInjection/Compiler/UploadedFileSubscriberPass.php b/src/DependencyInjection/Compiler/UploadedFileSubscriberPass.php new file mode 100644 index 0000000..61b220f --- /dev/null +++ b/src/DependencyInjection/Compiler/UploadedFileSubscriberPass.php @@ -0,0 +1,44 @@ +hasDefinition(self::SUBSCRIBER) || !$container->hasDefinition(self::FAILURE_LISTENER)) { + return; + } + + $failureListenerArguments = $container->getDefinition(self::FAILURE_LISTENER)->getArguments(); + $subscriber = $container->getDefinition(self::SUBSCRIBER)->setArgument(1, $failureListenerArguments[0] ?? null); + // Failure transport names by transport name: since Symfony 6.4.34 / 7.4.6 / 8.0.6 + if (\array_key_exists(2, $failureListenerArguments)) { + $subscriber->setArgument(2, $failureListenerArguments[2]); + } + } +} diff --git a/src/EventSubscriber/UploadedFileSubscriber.php b/src/EventSubscriber/UploadedFileSubscriber.php new file mode 100644 index 0000000..e18f904 --- /dev/null +++ b/src/EventSubscriber/UploadedFileSubscriber.php @@ -0,0 +1,120 @@ +|null $failureTransportsByName names of the failure transports by transport name, + * null before Symfony 6.4.34 / 7.4.6 / 8.0.6 (no such + * argument: a message already sent to a failure + * transport is not sent again) + */ + public function __construct( + private readonly UploadedFileManager $uploadedFileManager, + private readonly ?ContainerInterface $failureSenders = null, + private readonly ?array $failureTransportsByName = null, + ) { + } + + public static function getSubscribedEvents(): array + { + return [ + WorkerMessageReceivedEvent::class => 'onMessageReceived', + WorkerMessageHandledEvent::class => 'onMessageHandled', + WorkerMessageFailedEvent::class => 'onMessageFailed', + ]; + } + + /** + * Whether the message is handled by a worker: if it fails, its input is deleted by this subscriber. + */ + public function isHandledByWorker(ProcessExecuteMessage $message): bool + { + return $message === $this->workerMessage; + } + + public function onMessageReceived(WorkerMessageReceivedEvent $event): void + { + $this->workerMessage = $event->getEnvelope()->getMessage(); + } + + public function onMessageHandled(WorkerMessageHandledEvent $event): void + { + $this->workerMessage = null; + } + + public function onMessageFailed(WorkerMessageFailedEvent $event): void + { + $this->workerMessage = null; + $message = $event->getEnvelope()->getMessage(); + // deleteInput is uninitialized in a message queued by a previous version of the bundle + // @phpstan-ignore nullCoalesce.property + if (!$message instanceof ProcessExecuteMessage || !($message->deleteInput ?? false) || $event->willRetry() + || $this->isSentToFailureTransport($event) + ) { + return; + } + + $this->uploadedFileManager->deleteInput($message->input); + } + + /** + * Whether the Symfony SendFailedMessageToFailureTransportListener sends the message to a failure transport, from + * which it can be retried (messenger:failed:retry). + */ + private function isSentToFailureTransport(WorkerMessageFailedEvent $event): bool + { + $receiverName = $event->getReceiverName(); + if (!$this->failureSenders?->has($receiverName)) { + return false; + } + $stamp = $event->getEnvelope()->last(SentToFailureTransportStamp::class); + if (!$stamp instanceof SentToFailureTransportStamp) { + return true; + } + + // Already sent to a failure transport (e.g. received from it by messenger:failed:retry): sent again only to + // another failure transport, otherwise the message is dropped + return null !== $this->failureTransportsByName + && ($this->failureTransportsByName[$receiverName] ?? null) !== $receiverName + && $stamp->getOriginalReceiverName() !== $receiverName; + } +} diff --git a/src/Http/ValueResolver/HttpProcessExecuteValueResolver.php b/src/Http/ValueResolver/HttpProcessExecuteValueResolver.php index fa2a4ea..d8ee4dc 100644 --- a/src/Http/ValueResolver/HttpProcessExecuteValueResolver.php +++ b/src/Http/ValueResolver/HttpProcessExecuteValueResolver.php @@ -28,6 +28,12 @@ #[AsTargetedValueResolver('http_process_execution')] readonly class HttpProcessExecuteValueResolver implements ValueResolverInterface { + /** + * Request attribute: path of the uploaded input file written by the resolver, to be deleted once the process has + * been executed or when the request is rejected (an input given by its path is never deleted). + */ + public const UPLOADED_INPUT_ATTRIBUTE = '_cleverage_ui_process_uploaded_input'; + public function __construct(private string $storageDir, private SerializerInterface $serializer) { } @@ -61,6 +67,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable if ($input instanceof UploadedFile) { $uploadFileName = $this->storageDir.\DIRECTORY_SEPARATOR.date('YmdHis').'_'.uniqid().'_'.$input->getClientOriginalName(); (new Filesystem())->dumpFile($uploadFileName, $input->getContent()); + $request->attributes->set(self::UPLOADED_INPUT_ATTRIBUTE, $uploadFileName); $input = $uploadFileName; } diff --git a/src/Manager/UploadedFileManager.php b/src/Manager/UploadedFileManager.php index 598483f..6b159d1 100644 --- a/src/Manager/UploadedFileManager.php +++ b/src/Manager/UploadedFileManager.php @@ -31,6 +31,10 @@ public function __construct( /** * Deletes the input if it is a file of the upload directory. + * + * Only call it for a file the bundle has just written for this execution (launch form, HTTP API): a file of the + * upload directory can also be given by its path (HTTP API, text input of the launch form, schedule, default + * input), it must then be kept. */ public function deleteInput(mixed $input): void { @@ -38,7 +42,7 @@ public function deleteInput(mixed $input): void return; } $uploadDirectory = realpath($this->uploadDirectory); - // Only the files written by the bundle, never a file given by its path + // Defense in depth: never a file outside of the upload directory if (false === $uploadDirectory || realpath(\dirname($input)) !== $uploadDirectory) { return; } diff --git a/src/Message/ProcessExecuteHandler.php b/src/Message/ProcessExecuteHandler.php index 3fed142..52728a2 100644 --- a/src/Message/ProcessExecuteHandler.php +++ b/src/Message/ProcessExecuteHandler.php @@ -14,6 +14,7 @@ namespace CleverAge\UiProcessBundle\Message; use CleverAge\ProcessBundle\Manager\ProcessManagerInterface; +use CleverAge\UiProcessBundle\EventSubscriber\UploadedFileSubscriber; use CleverAge\UiProcessBundle\Manager\UploadedFileManager; use CleverAge\UiProcessBundle\Monolog\Handler\ProcessHandler; use Symfony\Component\Messenger\Attribute\AsMessageHandler; @@ -28,6 +29,7 @@ public function __construct( private ProcessManagerInterface $manager, private ProcessHandler $processHandler, private ?UploadedFileManager $uploadedFileManager = null, + private ?UploadedFileSubscriber $uploadedFileSubscriber = null, ) { } @@ -36,7 +38,21 @@ public function __invoke(ProcessExecuteMessage $message): void $this->processHandler->close(); try { $this->manager->execute($message->code, $message->input, $message->context); - } finally { + } catch (\Throwable $e) { + // In a worker, the message may be retried: the UploadedFileSubscriber deletes the input when it will not + if (!$this->uploadedFileSubscriber?->isHandledByWorker($message)) { + $this->deleteInput($message); + } + + throw $e; + } + $this->deleteInput($message); + } + + private function deleteInput(ProcessExecuteMessage $message): void + { + // Uninitialized in a message queued by a previous version of the bundle + if ($message->deleteInput ?? false) { // @phpstan-ignore nullCoalesce.property $this->uploadedFileManager?->deleteInput($message->input); } } diff --git a/src/Message/ProcessExecuteMessage.php b/src/Message/ProcessExecuteMessage.php index fd4c891..28e4626 100644 --- a/src/Message/ProcessExecuteMessage.php +++ b/src/Message/ProcessExecuteMessage.php @@ -20,8 +20,15 @@ { /** * @param array $context + * @param bool $deleteInput whether the input is a file written in the upload directory for this + * execution, deleted once the process has been executed (uninitialized + * in a message serialized by a previous version: read it with ?? false) */ - public function __construct(public string $code, public mixed $input, public array $context = []) - { + public function __construct( + public string $code, + public mixed $input, + public array $context = [], + public bool $deleteInput = false, + ) { } } diff --git a/tests/CleverAgeUiProcessBundleTest.php b/tests/CleverAgeUiProcessBundleTest.php index efa9ba4..b097286 100644 --- a/tests/CleverAgeUiProcessBundleTest.php +++ b/tests/CleverAgeUiProcessBundleTest.php @@ -18,6 +18,7 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use CleverAge\UiProcessBundle\Routing\DeprecatedEasyAdminRouteAliasLoader; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; @@ -30,6 +31,7 @@ #[UsesClass(CheckDoctrineLazyGhostObjectsPass::class)] #[UsesClass(UploadDirectoryParameterPass::class)] #[UsesClass(LoginThrottlingPass::class)] +#[UsesClass(UploadedFileSubscriberPass::class)] class CleverAgeUiProcessBundleTest extends TestCase { public function testPathIsTheBundleRootDirectory(): void @@ -84,4 +86,16 @@ public function testLoginThrottlingPassIsRegistered(): void ); self::assertCount(1, $passes); } + + public function testUploadedFileSubscriberPassIsRegistered(): void + { + $container = new ContainerBuilder(); + (new CleverAgeUiProcessBundle())->build($container); + + $passes = array_filter( + $container->getCompilerPassConfig()->getPasses(), + static fn (object $pass): bool => $pass instanceof UploadedFileSubscriberPass, + ); + self::assertCount(1, $passes); + } } diff --git a/tests/DependencyInjection/Compiler/UploadedFileSubscriberPassTest.php b/tests/DependencyInjection/Compiler/UploadedFileSubscriberPassTest.php new file mode 100644 index 0000000..e7f1d03 --- /dev/null +++ b/tests/DependencyInjection/Compiler/UploadedFileSubscriberPassTest.php @@ -0,0 +1,82 @@ +setDefinition(UploadedFileSubscriberPass::SUBSCRIBER, new Definition(UploadedFileSubscriber::class, [new Reference('manager')])); + $failureSenders = new Reference('failure_senders_locator'); + $failureTransportsByName = ['execute_process' => 'failed', 'failed' => 'failed']; + $container->setDefinition(UploadedFileSubscriberPass::FAILURE_LISTENER, new Definition(SendFailedMessageToFailureTransportListener::class, [$failureSenders, new Reference('logger'), $failureTransportsByName])); + + (new UploadedFileSubscriberPass())->process($container); + + $arguments = $container->getDefinition(UploadedFileSubscriberPass::SUBSCRIBER)->getArguments(); + self::assertSame($failureSenders, $arguments[1]); + self::assertSame($failureTransportsByName, $arguments[2]); + } + + /** + * Before Symfony 6.4.34 / 7.4.6 / 8.0.6, the listener has no "failure transports by name" argument. + */ + public function testWithoutFailureTransportsByName(): void + { + $container = new ContainerBuilder(); + $container->setDefinition(UploadedFileSubscriberPass::SUBSCRIBER, new Definition(UploadedFileSubscriber::class, [new Reference('manager')])); + $failureSenders = new Reference('failure_senders_locator'); + $container->setDefinition(UploadedFileSubscriberPass::FAILURE_LISTENER, new Definition(SendFailedMessageToFailureTransportListener::class, [$failureSenders, new Reference('logger')])); + + (new UploadedFileSubscriberPass())->process($container); + + $arguments = $container->getDefinition(UploadedFileSubscriberPass::SUBSCRIBER)->getArguments(); + self::assertSame($failureSenders, $arguments[1]); + self::assertArrayNotHasKey(2, $arguments); + } + + /** + * Symfony removes the listener when no failure transport is configured. + */ + public function testWithoutFailureTransport(): void + { + $container = new ContainerBuilder(); + $container->setDefinition(UploadedFileSubscriberPass::SUBSCRIBER, new Definition(UploadedFileSubscriber::class, [new Reference('manager')])); + + (new UploadedFileSubscriberPass())->process($container); + + self::assertEquals([new Reference('manager')], $container->getDefinition(UploadedFileSubscriberPass::SUBSCRIBER)->getArguments()); + } + + #[DoesNotPerformAssertions] + public function testWithoutSubscriber(): void + { + $container = new ContainerBuilder(); + $container->setDefinition(UploadedFileSubscriberPass::FAILURE_LISTENER, new Definition(SendFailedMessageToFailureTransportListener::class, [new Reference('failure_senders_locator')])); + + (new UploadedFileSubscriberPass())->process($container); + } +} diff --git a/tests/EventSubscriber/UploadedFileSubscriberTest.php b/tests/EventSubscriber/UploadedFileSubscriberTest.php new file mode 100644 index 0000000..3d3d0ce --- /dev/null +++ b/tests/EventSubscriber/UploadedFileSubscriberTest.php @@ -0,0 +1,248 @@ + 'failed', 'failed' => 'failed']; + + /** + * The failure transport has a failure transport of its own. + */ + private const CHAINED_FAILURE_TRANSPORTS = ['execute_process' => 'failed', 'failed' => 'failed2']; + + private string $directory; + private string $input; + + protected function setUp(): void + { + $this->directory = sys_get_temp_dir().\DIRECTORY_SEPARATOR.uniqid('uploaded_file_subscriber_test_', true); + $this->input = $this->directory.'/input.csv'; + (new Filesystem())->dumpFile($this->input, 'data'); + } + + protected function tearDown(): void + { + (new Filesystem())->remove($this->directory); + } + + public function testSubscribedEvents(): void + { + self::assertSame( + [ + WorkerMessageReceivedEvent::class => 'onMessageReceived', + WorkerMessageHandledEvent::class => 'onMessageHandled', + WorkerMessageFailedEvent::class => 'onMessageFailed', + ], + UploadedFileSubscriber::getSubscribedEvents() + ); + } + + public function testMessageHandledByWorker(): void + { + $subscriber = new UploadedFileSubscriber(new UploadedFileManager($this->directory)); + $message = new ProcessExecuteMessage('test.process', $this->input, [], true); + $envelope = new Envelope($message); + self::assertFalse($subscriber->isHandledByWorker($message)); + + $subscriber->onMessageReceived(new WorkerMessageReceivedEvent($envelope, 'execute_process')); + self::assertTrue($subscriber->isHandledByWorker($message)); + self::assertFalse($subscriber->isHandledByWorker(new ProcessExecuteMessage('test.process', $this->input, [], true))); + + $subscriber->onMessageHandled(new WorkerMessageHandledEvent($envelope, 'execute_process')); + self::assertFalse($subscriber->isHandledByWorker($message)); + // Deleted by the handler after a successful execution + self::assertFileExists($this->input); + + $subscriber->onMessageReceived(new WorkerMessageReceivedEvent($envelope, 'execute_process')); + $subscriber->onMessageFailed(new WorkerMessageFailedEvent($envelope, 'execute_process', new \RuntimeException())); + self::assertFalse($subscriber->isHandledByWorker($message)); + } + + /** + * @param array|null $failureTransportsByName + */ + #[DataProvider('provideFailedMessage')] + public function testFailedMessage(bool $deleteInput, bool $willRetry, string $receiverName, ?string $sentToFailureTransportFrom, ?array $failureTransportsByName, bool $deleted): void + { + $subscriber = new UploadedFileSubscriber( + new UploadedFileManager($this->directory), + $this->getFailureSenders($failureTransportsByName ?? self::FAILURE_TRANSPORTS), + $failureTransportsByName, + ); + + $subscriber->onMessageFailed($this->createFailedEvent($deleteInput, $willRetry, $receiverName, $sentToFailureTransportFrom)); + + self::assertSame(!$deleted, file_exists($this->input)); + } + + /** + * @return iterable|null, bool}> + */ + public static function provideFailedMessage(): iterable + { + yield 'not retried, without failure transport' => [true, false, 'other', null, self::FAILURE_TRANSPORTS, true]; + yield 'retried' => [true, true, 'execute_process', null, self::FAILURE_TRANSPORTS, false]; + // Kept for messenger:failed:retry + yield 'sent to the failure transport' => [true, false, 'execute_process', null, self::FAILURE_TRANSPORTS, false]; + yield 'sent to the failure transport, Symfony < 6.4.34' => [true, false, 'execute_process', null, null, false]; + // Failing again when retried from the failure transport: dropped by Symfony + yield 'received from the failure transport' => [true, false, 'failed', 'execute_process', self::FAILURE_TRANSPORTS, true]; + yield 'received from the failure transport, Symfony < 6.4.34' => [true, false, 'failed', 'execute_process', null, true]; + yield 'received from the failure transport, retried' => [true, true, 'failed', 'execute_process', self::FAILURE_TRANSPORTS, false]; + // The failure transport has a failure transport of its own + yield 'received from the failure transport, sent to another one' => [true, false, 'failed', 'execute_process', self::CHAINED_FAILURE_TRANSPORTS, false]; + // Opted out of the failure transport + yield 'already sent to the failure transport of its transport' => [true, false, 'execute_process', 'execute_process', self::FAILURE_TRANSPORTS, true]; + yield 'given by its path' => [false, false, 'other', null, self::FAILURE_TRANSPORTS, false]; + yield 'given by its path, sent to the failure transport' => [false, false, 'execute_process', null, self::FAILURE_TRANSPORTS, false]; + } + + /** + * The file is kept exactly when the Symfony listener sends the message to a failure transport (installed version). + * + * @param array|null $failureTransportsByName + */ + #[DataProvider('provideFailedMessage')] + public function testFailedMessageAsTheSymfonyFailureTransportListener(bool $deleteInput, bool $willRetry, string $receiverName, ?string $sentToFailureTransportFrom, ?array $failureTransportsByName, bool $deleted): void + { + $version = InstalledVersions::getVersion('symfony/messenger') ?? ''; + if (version_compare($version, '6.4.5', '<')) { + self::markTestSkipped('Before Symfony 6.4.5, a message already sent to a failure transport is sent again.'); + } + // As the UploadedFileSubscriberPass: the failure transport names only when the listener has this argument + $listenerHasFailureTransportsByName = (new \ReflectionMethod(SendFailedMessageToFailureTransportListener::class, '__construct'))->getNumberOfParameters() > 2; + if (!$listenerHasFailureTransportsByName && null !== $failureTransportsByName) { + $failureTransportsByName = null; + } + $failureTransports = $failureTransportsByName ?? self::FAILURE_TRANSPORTS; + $transports = []; + foreach (array_unique(array_values($failureTransports)) as $name) { + $transports[$name] = new InMemoryTransport(); + } + $failureSenders = new ServiceLocator(array_map( + static fn (string $failureTransport): \Closure => static fn (): InMemoryTransport => $transports[$failureTransport], + $failureTransports + )); + $eventDispatcher = new EventDispatcher(); + $eventDispatcher->addSubscriber(new UploadedFileSubscriber(new UploadedFileManager($this->directory), $failureSenders, $failureTransportsByName)); + $eventDispatcher->addSubscriber(new SendFailedMessageToFailureTransportListener($failureSenders, null, $failureTransports)); + + $eventDispatcher->dispatch($this->createFailedEvent($deleteInput, $willRetry, $receiverName, $sentToFailureTransportFrom)); + + $sent = array_sum(array_map(static fn (InMemoryTransport $transport): int => \count($transport->getSent()), $transports)); + self::assertSame($deleteInput && !$willRetry && 0 === $sent, !file_exists($this->input)); + if ($listenerHasFailureTransportsByName) { + self::assertSame($deleted, !file_exists($this->input)); + } + } + + /** + * @param array $failureTransportsByName + * + * @return ServiceLocator + */ + private function getFailureSenders(array $failureTransportsByName): ServiceLocator + { + return new ServiceLocator(array_map( + static fn (): \Closure => static fn (): InMemoryTransport => new InMemoryTransport(), + $failureTransportsByName + )); + } + + private function createFailedEvent(bool $deleteInput, bool $willRetry, string $receiverName, ?string $sentToFailureTransportFrom): WorkerMessageFailedEvent + { + $envelope = new Envelope(new ProcessExecuteMessage('test.process', $this->input, [], $deleteInput)); + if (null !== $sentToFailureTransportFrom) { + $envelope = $envelope->with(new SentToFailureTransportStamp($sentToFailureTransportFrom)); + } + $event = new WorkerMessageFailedEvent($envelope, $receiverName, new \RuntimeException('Process failed')); + if ($willRetry) { + $event->setForRetry(); + } + + return $event; + } + + public function testWithoutFailureTransports(): void + { + $subscriber = new UploadedFileSubscriber(new UploadedFileManager($this->directory)); + + $subscriber->onMessageFailed(new WorkerMessageFailedEvent( + new Envelope(new ProcessExecuteMessage('test.process', $this->input, [], true)), + 'execute_process', + new \RuntimeException('Process failed') + )); + + self::assertFileDoesNotExist($this->input); + } + + /** + * A message queued by a previous version of the bundle, without the "deleteInput" property: its input is kept. + */ + public function testMessageOfAPreviousVersion(): void + { + $subscriber = new UploadedFileSubscriber(new UploadedFileManager($this->directory)); + $message = unserialize(\sprintf( + 'O:%d:"%s":3:{s:4:"code";s:12:"test.process";s:5:"input";s:%d:"%s";s:7:"context";a:0:{}}', + \strlen(ProcessExecuteMessage::class), + ProcessExecuteMessage::class, + \strlen($this->input), + $this->input + )); + self::assertInstanceOf(ProcessExecuteMessage::class, $message); + + $subscriber->onMessageFailed(new WorkerMessageFailedEvent(new Envelope($message), 'execute_process', new \RuntimeException('Failed'))); + + self::assertFileExists($this->input); + } + + public function testOtherMessagesAreIgnored(): void + { + $subscriber = new UploadedFileSubscriber(new UploadedFileManager($this->directory)); + + $subscriber->onMessageFailed(new WorkerMessageFailedEvent( + new Envelope(new \stdClass()), + 'execute_process', + new \RuntimeException('Failed') + )); + + self::assertFileExists($this->input); + } +} diff --git a/tests/Functional/HttpProcessExecuteTest.php b/tests/Functional/HttpProcessExecuteTest.php index 71ff1cd..626059d 100644 --- a/tests/Functional/HttpProcessExecuteTest.php +++ b/tests/Functional/HttpProcessExecuteTest.php @@ -24,11 +24,13 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use CleverAge\UiProcessBundle\DependencyInjection\Configuration; use CleverAge\UiProcessBundle\Entity\ProcessExecution; use CleverAge\UiProcessBundle\Entity\User; use CleverAge\UiProcessBundle\Event\ProcessExecutionEndedEvent; use CleverAge\UiProcessBundle\EventSubscriber\ProcessEventSubscriber; +use CleverAge\UiProcessBundle\EventSubscriber\UploadedFileSubscriber; use CleverAge\UiProcessBundle\Http\Model\HttpProcessExecution; use CleverAge\UiProcessBundle\Http\ValueResolver\HttpProcessExecuteValueResolver; use CleverAge\UiProcessBundle\Manager\ProcessConfigurationsManager; @@ -53,6 +55,8 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\UsesClass; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher; /** @@ -60,6 +64,8 @@ */ #[CoversClass(ProcessExecuteController::class)] #[UsesClass(UploadedFileManager::class)] +#[UsesClass(UploadedFileSubscriber::class)] +#[UsesClass(UploadedFileSubscriberPass::class)] #[CoversClass(HttpProcessExecutionAuthenticator::class)] #[UsesClass(CleverAgeUiProcessBundle::class)] #[UsesClass(CheckDoctrineLazyGhostObjectsPass::class)] @@ -257,6 +263,133 @@ public function testSynchronousExecutionWithJsonContext(): void self::assertSame('"Process has been proceed well."', $this->client->getResponse()->getContent()); } + public function testUploadedFileQueued(): void + { + $this->upload(['code' => 'test.process']); + + self::assertResponseIsSuccessful(); + $messages = $this->getDispatchedMessages(); + self::assertCount(1, $messages); + self::assertIsString($messages[0]->input); + self::assertStringStartsWith($this->getUploadDirectory().'/', $messages[0]->input); + // Deleted once executed by the Messenger handler + self::assertTrue($messages[0]->deleteInput); + self::assertFileExists($messages[0]->input); + } + + public function testUploadedFileSynchronous(): void + { + $this->upload(['code' => 'test.process', 'queue' => '0']); + + self::assertResponseIsSuccessful(); + self::assertSame([], $this->getUploadedFiles()); + } + + public function testUploadedFileFailingSynchronous(): void + { + $this->upload(['code' => 'test.failing', 'queue' => '0']); + + self::assertResponseStatusCodeSame(500); + self::assertSame([], $this->getUploadedFiles()); + } + + /** + * The file written for a rejected request is deleted (it was kept forever). + */ + #[DataProvider('provideQueue')] + public function testUploadedFileRejected(bool $queue): void + { + $this->upload(['code' => 'unknown', 'queue' => $queue ? '1' : '0']); + + self::assertResponseStatusCodeSame(422); + self::assertSame([], $this->getDispatchedMessages()); + self::assertSame([], $this->getUploadedFiles()); + } + + /** + * A file of the upload directory given by its path (e.g. a file uploaded by another user, still waiting in the + * queue) is never deleted. + */ + #[DataProvider('provideQueue')] + public function testFileOfTheUploadDirectoryGivenByItsPathIsNotDeleted(bool $queue): void + { + $file = $this->getUploadDirectory().'/other_user_file.csv'; + (new Filesystem())->dumpFile($file, 'data'); + + $this->execute(['code' => 'test.process', 'input' => $file, 'queue' => $queue ? '1' : '0']); + + self::assertResponseIsSuccessful(); + self::assertFileExists($file); + if ($queue) { + $messages = $this->getDispatchedMessages(); + self::assertCount(1, $messages); + self::assertSame($file, $messages[0]->input); + self::assertFalse($messages[0]->deleteInput); + } + (new Filesystem())->remove($file); + } + + public function testFileOfTheUploadDirectoryGivenByItsPathInAJsonBodyIsNotDeleted(): void + { + $file = $this->getUploadDirectory().'/other_user_file.csv'; + (new Filesystem())->dumpFile($file, 'data'); + + $this->client->request( + 'POST', + '/http/process/execute', + server: ['HTTP_AUTHORIZATION' => 'Bearer '.self::TOKEN, 'CONTENT_TYPE' => 'application/json'], + content: json_encode(['code' => 'test.process', 'input' => $file, 'queue' => false, 'deleteInput' => true], \JSON_THROW_ON_ERROR) + ); + + self::assertResponseIsSuccessful(); + self::assertFileExists($file); + (new Filesystem())->remove($file); + } + + /** + * @return iterable + */ + public static function provideQueue(): iterable + { + yield 'queued' => [true]; + yield 'synchronous' => [false]; + } + + /** + * @param array $parameters + */ + private function upload(array $parameters): void + { + (new Filesystem())->remove($this->getUploadDirectory()); + $file = tempnam(sys_get_temp_dir(), 'upload'); + file_put_contents((string) $file, 'line1'); + + $this->client->request( + 'POST', + '/http/process/execute', + $parameters, + ['input' => new UploadedFile((string) $file, 'data.csv', 'text/csv', null, true)], + ['HTTP_AUTHORIZATION' => 'Bearer '.self::TOKEN] + ); + unlink((string) $file); + } + + private function getUploadDirectory(): string + { + $uploadDirectory = static::getContainer()->getParameter('cleverage_ui_process.upload_directory'); + self::assertIsString($uploadDirectory); + + return $uploadDirectory; + } + + /** + * @return list + */ + private function getUploadedFiles(): array + { + return array_map(basename(...), glob($this->getUploadDirectory().'/*') ?: []); + } + /** * @param array $parameters */ diff --git a/tests/Functional/LaunchActionTest.php b/tests/Functional/LaunchActionTest.php index 6926a29..351ae5a 100644 --- a/tests/Functional/LaunchActionTest.php +++ b/tests/Functional/LaunchActionTest.php @@ -25,6 +25,7 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use CleverAge\UiProcessBundle\DependencyInjection\Configuration; use CleverAge\UiProcessBundle\Entity\User; use CleverAge\UiProcessBundle\EventSubscriber\ProcessEventSubscriber; @@ -67,6 +68,7 @@ #[UsesClass(CheckDoctrineLazyGhostObjectsPass::class)] #[UsesClass(UploadDirectoryParameterPass::class)] #[UsesClass(LoginThrottlingPass::class)] +#[UsesClass(UploadedFileSubscriberPass::class)] #[UsesClass(LogRecordCrudController::class)] #[UsesClass(ProcessExecutionCrudController::class)] #[UsesClass(ProcessScheduleCrudController::class)] @@ -185,6 +187,8 @@ public function testLaunchWithForm(): void self::assertCount(1, $messages); self::assertSame('test.form', $messages[0]->code); self::assertSame('my input', $messages[0]->input); + // A text input is never deleted, even when it is a file of the upload directory + self::assertFalse($messages[0]->deleteInput); self::assertSame(['execution_user' => 'user@example.com', 'key1' => 'value1', 'key2' => 'value2'], $messages[0]->context); } @@ -218,6 +222,8 @@ public function testLaunchWithFileUpload(): void self::assertIsString($uploadDirectory); self::assertStringStartsWith($uploadDirectory.'/', $messages[0]->input); self::assertSame('line1', file_get_contents($messages[0]->input)); + // Deleted once executed by the Messenger handler + self::assertTrue($messages[0]->deleteInput); unlink((string) $file); } diff --git a/tests/Functional/LogRecordCrudTest.php b/tests/Functional/LogRecordCrudTest.php index ff87287..547709c 100644 --- a/tests/Functional/LogRecordCrudTest.php +++ b/tests/Functional/LogRecordCrudTest.php @@ -26,6 +26,7 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use CleverAge\UiProcessBundle\DependencyInjection\Configuration; use CleverAge\UiProcessBundle\Entity\LogRecord; use CleverAge\UiProcessBundle\Entity\ProcessExecution; @@ -67,6 +68,7 @@ #[UsesClass(CheckDoctrineLazyGhostObjectsPass::class)] #[UsesClass(UploadDirectoryParameterPass::class)] #[UsesClass(LoginThrottlingPass::class)] +#[UsesClass(UploadedFileSubscriberPass::class)] #[UsesClass(ProcessExecutionCrudController::class)] #[UsesClass(ProcessScheduleCrudController::class)] #[UsesClass(UserCrudController::class)] diff --git a/tests/Functional/LoginThrottlingTest.php b/tests/Functional/LoginThrottlingTest.php index 3d36ad8..9dd2dfe 100644 --- a/tests/Functional/LoginThrottlingTest.php +++ b/tests/Functional/LoginThrottlingTest.php @@ -29,11 +29,13 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use CleverAge\UiProcessBundle\DependencyInjection\Configuration; use CleverAge\UiProcessBundle\Entity\ProcessExecution; use CleverAge\UiProcessBundle\Entity\User; use CleverAge\UiProcessBundle\Event\ProcessExecutionEndedEvent; use CleverAge\UiProcessBundle\EventSubscriber\ProcessEventSubscriber; +use CleverAge\UiProcessBundle\EventSubscriber\UploadedFileSubscriber; use CleverAge\UiProcessBundle\Http\Model\HttpProcessExecution; use CleverAge\UiProcessBundle\Http\ValueResolver\HttpProcessExecuteValueResolver; use CleverAge\UiProcessBundle\Manager\ProcessConfigurationsManager; @@ -78,6 +80,8 @@ #[UsesClass(CleverAgeUiProcessBundle::class)] #[UsesClass(CheckDoctrineLazyGhostObjectsPass::class)] #[UsesClass(UploadDirectoryParameterPass::class)] +#[UsesClass(UploadedFileSubscriberPass::class)] +#[UsesClass(UploadedFileSubscriber::class)] #[UsesClass(ContextField::class)] #[UsesClass(EnumField::class)] #[UsesClass(LogProcessFilter::class)] diff --git a/tests/Functional/ProcessExecuteWorkerTest.php b/tests/Functional/ProcessExecuteWorkerTest.php new file mode 100644 index 0000000..744f10a --- /dev/null +++ b/tests/Functional/ProcessExecuteWorkerTest.php @@ -0,0 +1,240 @@ +getParameter('cleverage_ui_process.upload_directory'); + self::assertIsString($uploadDirectory); + $input = $uploadDirectory.'/worker_input.csv'; + (new Filesystem())->dumpFile($input, 'data'); + /** @var InMemoryTransport $transport */ + $transport = static::getContainer()->get('messenger.transport.execute_process'); + $transport->send(new Envelope(new ProcessExecuteMessage($code, $input, [], $deleteInput))); + + $eventDispatcher = new EventDispatcher(); + /** @var EventSubscriberInterface $subscriber */ + $subscriber = static::getContainer()->get('cleverage_ui_process.event_subscriber.uploaded_file'); + $eventDispatcher->addSubscriber($subscriber); + $eventDispatcher->addSubscriber(new StopWorkerOnMessageLimitListener(1)); + if ($retry) { + // As the retry strategy of a transport with retries + $eventDispatcher->addListener(WorkerMessageFailedEvent::class, static function (WorkerMessageFailedEvent $event): void { + $event->setForRetry(); + }, 100); + } + /** @var MessageBusInterface $bus */ + $bus = static::getContainer()->get('messenger.default_bus'); + (new Worker(['execute_process' => $transport], $bus, $eventDispatcher))->run(['sleep' => 0]); + + // The message has been consumed (acknowledged or rejected) + self::assertCount(1, [...$transport->getAcknowledged(), ...$transport->getRejected()]); + $exists = file_exists($input); + (new Filesystem())->remove($input); + self::assertSame(!$deleted, $exists); + } + + /** + * @return iterable + */ + public static function provideConsume(): iterable + { + yield 'uploaded, success' => ['test.process', true, false, true]; + yield 'uploaded, failure without retry' => ['test.failing', true, false, true]; + yield 'uploaded, failure with retry' => ['test.failing', true, true, false]; + yield 'given by its path, success' => ['test.process', false, false, false]; + yield 'given by its path, failure' => ['test.failing', false, false, false]; + } + + /** + * With a failure transport (Symfony recipe): the file of a message sent to the failure transport is kept for + * messenger:failed:retry, and deleted when the message fails again there (dropped by Symfony). + */ + public function testFailureTransport(): void + { + $this->createFailureTransportClient(); + $input = $this->createInput(); + $transport = $this->getTransport('execute_process'); + $failureTransport = $this->getTransport('failed'); + $transport->send(new Envelope(new ProcessExecuteMessage('test.failing', $input, [], true))); + + $this->consume('execute_process'); + + self::assertCount(1, $failureTransport->getSent()); + self::assertFileExists($input); + + // messenger:failed:retry, failing again + $this->consume('failed'); + + self::assertCount(1, $failureTransport->getSent(), 'Not sent again to the failure transport'); + self::assertFileDoesNotExist($input); + } + + public function testFailureTransportRetrySucceeds(): void + { + $this->createFailureTransportClient(); + $input = $this->createInput(); + $this->getTransport('failed')->send( + (new Envelope(new ProcessExecuteMessage('test.process', $input, [], true))) + ->with(new SentToFailureTransportStamp('execute_process')) + ); + + $this->consume('failed'); + + self::assertFileDoesNotExist($input); + } + + private function createFailureTransportClient(): void + { + $version = InstalledVersions::getVersion('symfony/messenger') ?? ''; + if (version_compare($version, '6.4.5', '<')) { + self::markTestSkipped('Before Symfony 6.4.5, a message already sent to a failure transport is sent again.'); + } + // Messenger failure transport, as configured by the Symfony recipe + $this->createTestClient(['framework' => ['messenger' => [ + 'failure_transport' => 'failed', + 'transports' => ['failed' => ['dsn' => 'in-memory://', 'retry_strategy' => ['max_retries' => 0]]], + ]]]); + } + + private function createInput(): string + { + $uploadDirectory = static::getContainer()->getParameter('cleverage_ui_process.upload_directory'); + self::assertIsString($uploadDirectory); + $input = $uploadDirectory.'/worker_input.csv'; + (new Filesystem())->dumpFile($input, 'data'); + + return $input; + } + + private function getTransport(string $name): InMemoryTransport + { + $transport = static::getContainer()->get('messenger.transport.'.$name); + self::assertInstanceOf(InMemoryTransport::class, $transport); + + return $transport; + } + + /** + * Consumes one message with the event dispatcher of the application, as messenger:consume. + */ + private function consume(string $transportName): void + { + /** @var EventDispatcherInterface $eventDispatcher */ + $eventDispatcher = static::getContainer()->get('event_dispatcher'); + $stopListener = new StopWorkerOnMessageLimitListener(1); + $eventDispatcher->addSubscriber($stopListener); + /** @var MessageBusInterface $bus */ + $bus = static::getContainer()->get('messenger.default_bus'); + try { + (new Worker([$transportName => $this->getTransport($transportName)], $bus, $eventDispatcher))->run(['sleep' => 0]); + } finally { + $eventDispatcher->removeSubscriber($stopListener); + } + } +} diff --git a/tests/Functional/ProcessExecutionCrudTest.php b/tests/Functional/ProcessExecutionCrudTest.php index d407fef..889f37a 100644 --- a/tests/Functional/ProcessExecutionCrudTest.php +++ b/tests/Functional/ProcessExecutionCrudTest.php @@ -26,6 +26,7 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use CleverAge\UiProcessBundle\DependencyInjection\Configuration; use CleverAge\UiProcessBundle\Entity\Enum\ProcessExecutionStatus; use CleverAge\UiProcessBundle\Entity\LogRecord; @@ -71,6 +72,7 @@ #[UsesClass(CheckDoctrineLazyGhostObjectsPass::class)] #[UsesClass(UploadDirectoryParameterPass::class)] #[UsesClass(LoginThrottlingPass::class)] +#[UsesClass(UploadedFileSubscriberPass::class)] #[UsesClass(LogRecordCrudController::class)] #[UsesClass(ProcessScheduleCrudController::class)] #[UsesClass(UserCrudController::class)] diff --git a/tests/Functional/ProcessExecutionNotificationTest.php b/tests/Functional/ProcessExecutionNotificationTest.php index 7ba14f2..a630d1a 100644 --- a/tests/Functional/ProcessExecutionNotificationTest.php +++ b/tests/Functional/ProcessExecutionNotificationTest.php @@ -19,6 +19,7 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use CleverAge\UiProcessBundle\DependencyInjection\Configuration; use CleverAge\UiProcessBundle\Entity\ProcessExecution; use CleverAge\UiProcessBundle\Event\ProcessExecutionEndedEvent; @@ -45,6 +46,7 @@ #[UsesClass(CheckDoctrineLazyGhostObjectsPass::class)] #[UsesClass(UploadDirectoryParameterPass::class)] #[UsesClass(LoginThrottlingPass::class)] +#[UsesClass(UploadedFileSubscriberPass::class)] #[UsesClass(CleverAgeUiProcessExtension::class)] #[UsesClass(DeprecatedEasyAdminRouteAliasLoader::class)] #[UsesClass(Configuration::class)] diff --git a/tests/Functional/ProcessListTest.php b/tests/Functional/ProcessListTest.php index bd67e06..8444d0f 100644 --- a/tests/Functional/ProcessListTest.php +++ b/tests/Functional/ProcessListTest.php @@ -24,6 +24,7 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use CleverAge\UiProcessBundle\DependencyInjection\Configuration; use CleverAge\UiProcessBundle\Entity\User; use CleverAge\UiProcessBundle\EventSubscriber\ProcessEventSubscriber; @@ -60,6 +61,7 @@ #[UsesClass(CheckDoctrineLazyGhostObjectsPass::class)] #[UsesClass(UploadDirectoryParameterPass::class)] #[UsesClass(LoginThrottlingPass::class)] +#[UsesClass(UploadedFileSubscriberPass::class)] #[UsesClass(LogRecordCrudController::class)] #[UsesClass(ProcessExecutionCrudController::class)] #[UsesClass(ProcessScheduleCrudController::class)] diff --git a/tests/Functional/ProcessScheduleCrudTest.php b/tests/Functional/ProcessScheduleCrudTest.php index 0571397..95ddc03 100644 --- a/tests/Functional/ProcessScheduleCrudTest.php +++ b/tests/Functional/ProcessScheduleCrudTest.php @@ -24,6 +24,7 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use CleverAge\UiProcessBundle\DependencyInjection\Configuration; use CleverAge\UiProcessBundle\Entity\Enum\ProcessScheduleType; use CleverAge\UiProcessBundle\Entity\ProcessSchedule; @@ -68,6 +69,7 @@ #[UsesClass(CheckDoctrineLazyGhostObjectsPass::class)] #[UsesClass(UploadDirectoryParameterPass::class)] #[UsesClass(LoginThrottlingPass::class)] +#[UsesClass(UploadedFileSubscriberPass::class)] #[UsesClass(LogRecordCrudController::class)] #[UsesClass(ProcessExecutionCrudController::class)] #[UsesClass(UserCrudController::class)] diff --git a/tests/Functional/PurgeLogsCommandTest.php b/tests/Functional/PurgeLogsCommandTest.php index 312c117..f969961 100644 --- a/tests/Functional/PurgeLogsCommandTest.php +++ b/tests/Functional/PurgeLogsCommandTest.php @@ -20,6 +20,7 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use CleverAge\UiProcessBundle\DependencyInjection\Configuration; use CleverAge\UiProcessBundle\Entity\LogRecord; use CleverAge\UiProcessBundle\Entity\ProcessExecution; @@ -39,6 +40,7 @@ #[UsesClass(CheckDoctrineLazyGhostObjectsPass::class)] #[UsesClass(UploadDirectoryParameterPass::class)] #[UsesClass(LoginThrottlingPass::class)] +#[UsesClass(UploadedFileSubscriberPass::class)] #[UsesClass(CleverAgeUiProcessExtension::class)] #[UsesClass(DeprecatedEasyAdminRouteAliasLoader::class)] #[UsesClass(Configuration::class)] diff --git a/tests/Functional/SecurityTest.php b/tests/Functional/SecurityTest.php index ba8bc2b..2bf83e0 100644 --- a/tests/Functional/SecurityTest.php +++ b/tests/Functional/SecurityTest.php @@ -31,6 +31,7 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use CleverAge\UiProcessBundle\DependencyInjection\Configuration; use CleverAge\UiProcessBundle\Entity\User; use CleverAge\UiProcessBundle\EventSubscriber\ProcessEventSubscriber; @@ -80,6 +81,7 @@ #[UsesClass(LoginThrottlingPass::class)] #[UsesClass(HttpProcessExecutionRequestRateLimiter::class)] #[UsesClass(PeekableHttpProcessExecutionRequestRateLimiter::class)] +#[UsesClass(UploadedFileSubscriberPass::class)] #[UsesClass(CleverAgeUiProcessExtension::class)] #[UsesClass(DeprecatedEasyAdminRouteAliasLoader::class)] #[UsesClass(Configuration::class)] diff --git a/tests/Functional/UserCrudTest.php b/tests/Functional/UserCrudTest.php index 2b653a0..908f527 100644 --- a/tests/Functional/UserCrudTest.php +++ b/tests/Functional/UserCrudTest.php @@ -23,6 +23,7 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use CleverAge\UiProcessBundle\DependencyInjection\Configuration; use CleverAge\UiProcessBundle\Entity\User; use CleverAge\UiProcessBundle\EventSubscriber\ProcessEventSubscriber; @@ -52,6 +53,7 @@ #[UsesClass(CheckDoctrineLazyGhostObjectsPass::class)] #[UsesClass(UploadDirectoryParameterPass::class)] #[UsesClass(LoginThrottlingPass::class)] +#[UsesClass(UploadedFileSubscriberPass::class)] #[UsesClass(LogRecordCrudController::class)] #[UsesClass(ProcessExecutionCrudController::class)] #[UsesClass(ProcessScheduleCrudController::class)] diff --git a/tests/Manager/ProcessExecutionManagerEntityManagerTest.php b/tests/Manager/ProcessExecutionManagerEntityManagerTest.php index 9dce700..43bcdf8 100644 --- a/tests/Manager/ProcessExecutionManagerEntityManagerTest.php +++ b/tests/Manager/ProcessExecutionManagerEntityManagerTest.php @@ -18,6 +18,7 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use CleverAge\UiProcessBundle\DependencyInjection\Configuration; use CleverAge\UiProcessBundle\Entity\Enum\ProcessExecutionStatus; use CleverAge\UiProcessBundle\Entity\LogRecord; @@ -48,6 +49,7 @@ #[UsesClass(CheckDoctrineLazyGhostObjectsPass::class)] #[UsesClass(UploadDirectoryParameterPass::class)] #[UsesClass(LoginThrottlingPass::class)] +#[UsesClass(UploadedFileSubscriberPass::class)] #[UsesClass(CleverAgeUiProcessExtension::class)] #[UsesClass(DeprecatedEasyAdminRouteAliasLoader::class)] #[UsesClass(Configuration::class)] diff --git a/tests/Message/ProcessExecuteHandlerTest.php b/tests/Message/ProcessExecuteHandlerTest.php index cd4937c..aa37c6d 100644 --- a/tests/Message/ProcessExecuteHandlerTest.php +++ b/tests/Message/ProcessExecuteHandlerTest.php @@ -14,20 +14,25 @@ namespace CleverAge\UiProcessBundle\Tests\Message; use CleverAge\ProcessBundle\Manager\ProcessManagerInterface; +use CleverAge\UiProcessBundle\EventSubscriber\UploadedFileSubscriber; use CleverAge\UiProcessBundle\Manager\ProcessExecutionManager; use CleverAge\UiProcessBundle\Manager\UploadedFileManager; use CleverAge\UiProcessBundle\Message\ProcessExecuteHandler; use CleverAge\UiProcessBundle\Message\ProcessExecuteMessage; use CleverAge\UiProcessBundle\Monolog\Handler\ProcessHandler; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent; #[CoversClass(ProcessExecuteHandler::class)] #[UsesClass(ProcessExecuteMessage::class)] #[UsesClass(ProcessHandler::class)] #[UsesClass(UploadedFileManager::class)] +#[UsesClass(UploadedFileSubscriber::class)] class ProcessExecuteHandlerTest extends TestCase { public function testExecuteTheProcessWithANewLogFile(): void @@ -52,7 +57,92 @@ public function testExecuteTheProcessWithANewLogFile(): void ); } - public function testTheUploadedInputIsDeletedEvenWhenTheProcessFails(): void + /** + * The input is deleted only when the message is flagged (file written by the launch form or the HTTP API), after a + * successful or a failed execution outside of a worker; never a file of the upload directory given by its path. + */ + #[DataProvider('provideDeleteInput')] + public function testDeleteInput(bool $deleteInput, bool $fails, bool $handledByWorker, bool $deleted): void + { + $directory = sys_get_temp_dir().\DIRECTORY_SEPARATOR.uniqid('process_execute_handler_test_', true); + $input = $directory.'/input.csv'; + (new Filesystem())->dumpFile($input, 'data'); + $message = new ProcessExecuteMessage('test.process', $input, [], $deleteInput); + + $manager = $this->createStub(ProcessManagerInterface::class); + if ($fails) { + $manager->method('execute')->willThrowException(new \RuntimeException('Process failed')); + } + $uploadedFileManager = new UploadedFileManager($directory); + $subscriber = new UploadedFileSubscriber($uploadedFileManager); + if ($handledByWorker) { + $subscriber->onMessageReceived(new WorkerMessageReceivedEvent(new Envelope($message), 'execute_process')); + } + $handler = new ProcessExecuteHandler( + $manager, + new ProcessHandler(sys_get_temp_dir(), $this->createStub(ProcessExecutionManager::class)), + $uploadedFileManager, + $subscriber, + ); + + try { + $handler($message); + self::assertFalse($fails, 'The process should have failed'); + } catch (\RuntimeException $e) { + self::assertSame('Process failed', $e->getMessage()); + } finally { + $exists = file_exists($input); + (new Filesystem())->remove($directory); + } + + self::assertSame(!$deleted, $exists); + } + + /** + * @return iterable + */ + public static function provideDeleteInput(): iterable + { + yield 'uploaded, success' => [true, false, false, true]; + yield 'uploaded, failure' => [true, true, false, true]; + yield 'uploaded, success in a worker' => [true, false, true, true]; + // The UploadedFileSubscriber deletes it unless the message will be retried + yield 'uploaded, failure in a worker' => [true, true, true, false]; + yield 'given by its path, success' => [false, false, false, false]; + yield 'given by its path, failure' => [false, true, false, false]; + yield 'given by its path, success in a worker' => [false, false, true, false]; + } + + /** + * A message queued by a previous version of the bundle, without the "deleteInput" property: its input is kept. + */ + public function testMessageOfAPreviousVersion(): void + { + $directory = sys_get_temp_dir().\DIRECTORY_SEPARATOR.uniqid('process_execute_handler_test_', true); + $input = $directory.'/input.csv'; + (new Filesystem())->dumpFile($input, 'data'); + $message = unserialize(\sprintf( + 'O:%d:"%s":3:{s:4:"code";s:12:"test.process";s:5:"input";s:%d:"%s";s:7:"context";a:0:{}}', + \strlen(ProcessExecuteMessage::class), + ProcessExecuteMessage::class, + \strlen($input), + $input + )); + self::assertInstanceOf(ProcessExecuteMessage::class, $message); + $handler = new ProcessExecuteHandler( + $this->createStub(ProcessManagerInterface::class), + new ProcessHandler(sys_get_temp_dir(), $this->createStub(ProcessExecutionManager::class)), + new UploadedFileManager($directory), + ); + + $handler($message); + + $exists = file_exists($input); + (new Filesystem())->remove($directory); + self::assertTrue($exists); + } + + public function testWithoutUploadedFileSubscriberTheUploadedInputIsDeletedWhenTheProcessFails(): void { $directory = sys_get_temp_dir().\DIRECTORY_SEPARATOR.uniqid('process_execute_handler_test_', true); $input = $directory.'/input.csv'; @@ -67,7 +157,7 @@ public function testTheUploadedInputIsDeletedEvenWhenTheProcessFails(): void ); try { - $handler(new ProcessExecuteMessage('test.process', $input)); + $handler(new ProcessExecuteMessage('test.process', $input, [], true)); self::fail('The process should have failed'); } catch (\RuntimeException) { } finally { diff --git a/tests/Message/ProcessExecuteMessageTest.php b/tests/Message/ProcessExecuteMessageTest.php index fd77697..fbebe8a 100644 --- a/tests/Message/ProcessExecuteMessageTest.php +++ b/tests/Message/ProcessExecuteMessageTest.php @@ -36,4 +36,57 @@ public function testContextDefaultsToEmptyArray(): void self::assertNull($message->input); self::assertSame([], $message->context); } + + public function testInputIsNotDeletedByDefault(): void + { + self::assertFalse((new ProcessExecuteMessage('demo.process', '/uploads/file.csv'))->deleteInput); + self::assertTrue((new ProcessExecuteMessage('demo.process', '/uploads/file.csv', [], true))->deleteInput); + } + + public function testSerialization(): void + { + $message = unserialize(serialize(new ProcessExecuteMessage('demo.process', '/uploads/file.csv', [], true))); + + self::assertInstanceOf(ProcessExecuteMessage::class, $message); + self::assertTrue($message->deleteInput); + } + + /** + * A message queued by a previous version of the bundle, without the "deleteInput" property (uninitialized). + */ + public function testUnserializeMessageOfAPreviousVersion(): void + { + $message = unserialize(\sprintf( + 'O:%d:"%s":3:{s:4:"code";s:12:"demo.process";s:5:"input";s:17:"/uploads/file.csv";s:7:"context";a:0:{}}', + \strlen(ProcessExecuteMessage::class), + ProcessExecuteMessage::class + )); + + self::assertInstanceOf(ProcessExecuteMessage::class, $message); + self::assertSame('/uploads/file.csv', $message->input); + self::assertFalse($message->deleteInput ?? false); // @phpstan-ignore nullCoalesce.property + } + + /** + * A subclass with properties of its own (the class is final since 3.3 only). + */ + public function testSerializeSubclass(): void + { + $message = unserialize(serialize(new ProcessExecuteMessageSubclass('demo.process', 'input', 'extra'))); + + self::assertInstanceOf(ProcessExecuteMessageSubclass::class, $message); + self::assertSame('extra', $message->extra); + self::assertFalse($message->deleteInput); + } +} + +/** + * @internal + */ +readonly class ProcessExecuteMessageSubclass extends ProcessExecuteMessage // @phpstan-ignore class.extendsFinalByPhpDoc +{ + public function __construct(string $code, mixed $input, public string $extra) + { + parent::__construct($code, $input); + } } diff --git a/tests/Monolog/Handler/DoctrineProcessHandlerEntityManagerTest.php b/tests/Monolog/Handler/DoctrineProcessHandlerEntityManagerTest.php index 167d5a8..a8fb8b9 100644 --- a/tests/Monolog/Handler/DoctrineProcessHandlerEntityManagerTest.php +++ b/tests/Monolog/Handler/DoctrineProcessHandlerEntityManagerTest.php @@ -18,6 +18,7 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use CleverAge\UiProcessBundle\DependencyInjection\Configuration; use CleverAge\UiProcessBundle\Entity\Enum\ProcessExecutionStatus; use CleverAge\UiProcessBundle\Entity\LogRecord; @@ -43,6 +44,7 @@ #[UsesClass(CheckDoctrineLazyGhostObjectsPass::class)] #[UsesClass(UploadDirectoryParameterPass::class)] #[UsesClass(LoginThrottlingPass::class)] +#[UsesClass(UploadedFileSubscriberPass::class)] #[UsesClass(CleverAgeUiProcessExtension::class)] #[UsesClass(DeprecatedEasyAdminRouteAliasLoader::class)] #[UsesClass(Configuration::class)] diff --git a/tests/Repository/ProcessExecutionRepositoryTest.php b/tests/Repository/ProcessExecutionRepositoryTest.php index 17bcd24..4102dd1 100644 --- a/tests/Repository/ProcessExecutionRepositoryTest.php +++ b/tests/Repository/ProcessExecutionRepositoryTest.php @@ -18,6 +18,7 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use CleverAge\UiProcessBundle\DependencyInjection\Configuration; use CleverAge\UiProcessBundle\Entity\LogRecord; use CleverAge\UiProcessBundle\Entity\ProcessExecution; @@ -38,6 +39,7 @@ #[UsesClass(CheckDoctrineLazyGhostObjectsPass::class)] #[UsesClass(UploadDirectoryParameterPass::class)] #[UsesClass(LoginThrottlingPass::class)] +#[UsesClass(UploadedFileSubscriberPass::class)] #[UsesClass(CleverAgeUiProcessExtension::class)] #[UsesClass(DeprecatedEasyAdminRouteAliasLoader::class)] #[UsesClass(Configuration::class)] diff --git a/tests/Repository/ProcessScheduleRepositoryTest.php b/tests/Repository/ProcessScheduleRepositoryTest.php index 673d63d..a87beb0 100644 --- a/tests/Repository/ProcessScheduleRepositoryTest.php +++ b/tests/Repository/ProcessScheduleRepositoryTest.php @@ -18,6 +18,7 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use CleverAge\UiProcessBundle\DependencyInjection\Configuration; use CleverAge\UiProcessBundle\Entity\Enum\ProcessScheduleType; use CleverAge\UiProcessBundle\Entity\ProcessSchedule; @@ -36,6 +37,7 @@ #[UsesClass(CheckDoctrineLazyGhostObjectsPass::class)] #[UsesClass(UploadDirectoryParameterPass::class)] #[UsesClass(LoginThrottlingPass::class)] +#[UsesClass(UploadedFileSubscriberPass::class)] #[UsesClass(CleverAgeUiProcessExtension::class)] #[UsesClass(DeprecatedEasyAdminRouteAliasLoader::class)] #[UsesClass(Configuration::class)] diff --git a/tests/StopChecker/ProcessExecutionStopCheckerTest.php b/tests/StopChecker/ProcessExecutionStopCheckerTest.php index 2101df3..2a9d72b 100644 --- a/tests/StopChecker/ProcessExecutionStopCheckerTest.php +++ b/tests/StopChecker/ProcessExecutionStopCheckerTest.php @@ -21,6 +21,7 @@ use CleverAge\UiProcessBundle\DependencyInjection\Compiler\CheckDoctrineLazyGhostObjectsPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\LoginThrottlingPass; use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadDirectoryParameterPass; +use CleverAge\UiProcessBundle\DependencyInjection\Compiler\UploadedFileSubscriberPass; use CleverAge\UiProcessBundle\DependencyInjection\Configuration; use CleverAge\UiProcessBundle\Entity\ProcessExecution; use CleverAge\UiProcessBundle\EventSubscriber\ProcessEventSubscriber; @@ -41,6 +42,7 @@ #[UsesClass(CheckDoctrineLazyGhostObjectsPass::class)] #[UsesClass(UploadDirectoryParameterPass::class)] #[UsesClass(LoginThrottlingPass::class)] +#[UsesClass(UploadedFileSubscriberPass::class)] #[UsesClass(CleverAgeUiProcessExtension::class)] #[UsesClass(DeprecatedEasyAdminRouteAliasLoader::class)] #[UsesClass(Configuration::class)]