diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml index ff43cf1ed..39eab3875 100644 --- a/config/packages/messenger.yaml +++ b/config/packages/messenger.yaml @@ -63,6 +63,7 @@ framework: 'Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage': async 'Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage': async 'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority + 'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async # end of routes added by chill-bundles recipes # Route your messages to the transports # 'App\Message\YourMessage': async diff --git a/src/Bundle/ChillMainBundle/Export/Cronjob/RemoveExpiredExportGenerationCronJob.php b/src/Bundle/ChillMainBundle/Export/Cronjob/RemoveExpiredExportGenerationCronJob.php new file mode 100644 index 000000000..2f15bf095 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/Cronjob/RemoveExpiredExportGenerationCronJob.php @@ -0,0 +1,52 @@ +getLastStart()->getTimestamp() < $this->clock->now()->sub(new \DateInterval('PT24H'))->getTimestamp(); + } + + public function getKey(): string + { + return self::KEY; + } + + public function run(array $lastExecutionData): ?array + { + $now = $this->clock->now(); + + foreach ($this->exportGenerationRepository->findExpiredExportGeneration($now) as $exportGeneration) { + $this->messageBus->dispatch(new Envelope(new RemoveExportGenerationMessage($exportGeneration))); + } + + return ['last-deletion' => $now->getTimestamp()]; + } +} diff --git a/src/Bundle/ChillMainBundle/Export/Messenger/RemoveExportGenerationMessage.php b/src/Bundle/ChillMainBundle/Export/Messenger/RemoveExportGenerationMessage.php new file mode 100644 index 000000000..31e68a82d --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/Messenger/RemoveExportGenerationMessage.php @@ -0,0 +1,24 @@ +exportGenerationId = $exportGeneration->getId()->toString(); + } +} diff --git a/src/Bundle/ChillMainBundle/Export/Messenger/RemoveExportGenerationMessageHandler.php b/src/Bundle/ChillMainBundle/Export/Messenger/RemoveExportGenerationMessageHandler.php new file mode 100644 index 000000000..e3e3063ca --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/Messenger/RemoveExportGenerationMessageHandler.php @@ -0,0 +1,49 @@ +exportGenerationRepository->find($message->exportGenerationId); + + if (null === $exportGeneration) { + $this->logger->error(self::LOG_PREFIX.'ExportGeneration not found'); + throw new UnrecoverableMessageHandlingException(self::LOG_PREFIX.'ExportGeneration not found'); + } + + $storedObject = $exportGeneration->getStoredObject(); + $storedObject->setDeleteAt($this->clock->now()); + + $this->entityManager->remove($exportGeneration); + $this->entityManager->flush(); + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/ExportGenerationRepository.php b/src/Bundle/ChillMainBundle/Repository/ExportGenerationRepository.php index 7b6cbf13c..bb70a22ba 100644 --- a/src/Bundle/ChillMainBundle/Repository/ExportGenerationRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/ExportGenerationRepository.php @@ -73,4 +73,13 @@ class ExportGenerationRepository extends ServiceEntityRepository implements Asso ->getQuery() ->getResult(); } + + public function findExpiredExportGeneration(\DateTimeImmutable $atDate): iterable + { + return $this->createQueryBuilder('e') + ->where('e.deleteAt < :atDate') + ->setParameter('atDate', $atDate) + ->getQuery() + ->toIterable(); + } } diff --git a/src/Bundle/ChillMainBundle/Tests/Export/Cronjob/RemoveExpiredExportGenerationCronJobTest.php b/src/Bundle/ChillMainBundle/Tests/Export/Cronjob/RemoveExpiredExportGenerationCronJobTest.php new file mode 100644 index 000000000..21c47aaf0 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Export/Cronjob/RemoveExpiredExportGenerationCronJobTest.php @@ -0,0 +1,118 @@ +prophesize(ExportGenerationRepository::class); + $bus = $this->prophesize(MessageBusInterface::class); + + $cronJob = new RemoveExpiredExportGenerationCronJob( + $clock, + $repo->reveal(), + $bus->reveal() + ); + + $this->assertTrue($cronJob->canRun(null)); + } + + public function testCanRunReturnsTrueWhenLastStartIsOlderThan24Hours() + { + $clock = new MockClock(new \DateTimeImmutable('2024-06-25 10:00:00')); + $repo = $this->prophesize(ExportGenerationRepository::class); + $bus = $this->prophesize(MessageBusInterface::class); + + $cronJob = new RemoveExpiredExportGenerationCronJob( + $clock, + $repo->reveal(), + $bus->reveal() + ); + + $execution = new CronJobExecution('remove-expired-export-generation'); + $execution->setLastStart(new \DateTimeImmutable('2024-06-24 09:59:59')); + + $this->assertTrue($cronJob->canRun($execution)); + } + + public function testCanRunReturnsFalseWhenLastStartIsWithin24Hours() + { + $clock = new MockClock(new \DateTimeImmutable('2024-06-25 10:00:00')); + $repo = $this->prophesize(ExportGenerationRepository::class); + $bus = $this->prophesize(MessageBusInterface::class); + + $cronJob = new RemoveExpiredExportGenerationCronJob( + $clock, + $repo->reveal(), + $bus->reveal() + ); + + $execution = new CronJobExecution('remove-expired-export-generation'); + $execution->setLastStart(new \DateTimeImmutable('2024-06-24 10:01:00')); + + $this->assertFalse($cronJob->canRun($execution)); + } + + public function testRunDispatchesMessagesForExpiredExportsAndReturnsLastDeletion() + { + $clock = new MockClock(new \DateTimeImmutable('2024-06-25 11:21:00')); + $repo = $this->prophesize(ExportGenerationRepository::class); + $bus = $this->prophesize(MessageBusInterface::class); + + $expiredExports = [ + new ExportGeneration('dummy', []), + ]; + + $repo->findExpiredExportGeneration(Argument::that(function ($dateTime) use ($clock) { + // Ensure the repository is called with the current clock time + return $dateTime instanceof \DateTimeImmutable + && $dateTime->getTimestamp() === $clock->now()->getTimestamp(); + }))->willReturn($expiredExports); + + // Expect one RemoveExportGenerationMessage for each expired export + $bus->dispatch(Argument::that(fn (Envelope $envelope) => $envelope->getMessage() instanceof RemoveExportGenerationMessage)) + ->shouldBeCalledTimes(1) + ->will(fn ($args) => $args[0]); + + $cronJob = new RemoveExpiredExportGenerationCronJob( + $clock, + $repo->reveal(), + $bus->reveal() + ); + + $result = $cronJob->run([]); + + $this->assertIsArray($result); + $this->assertEquals(['last-deletion' => $clock->now()->getTimestamp()], $result); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Export/Messenger/RemoveExportGenerationMessageHandlerTest.php b/src/Bundle/ChillMainBundle/Tests/Export/Messenger/RemoveExportGenerationMessageHandlerTest.php new file mode 100644 index 000000000..70f6f9f75 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Export/Messenger/RemoveExportGenerationMessageHandlerTest.php @@ -0,0 +1,76 @@ + 'bar']); + $storedObject = $exportGeneration->getStoredObject(); + + // 3. Mock ExportGenerationRepository to return the ExportGeneration + $exportGenerationRepository = $this->prophesize(ExportGenerationRepository::class); + $exportGenerationRepository + ->find($exportGeneration->getId()) + ->willReturn($exportGeneration); + + // 4. Mock EntityManagerInterface and set expectations + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->remove($exportGeneration)->shouldBeCalled(); + $entityManager->flush()->shouldBeCalled(); + + // 6. Create message + $message = new RemoveExportGenerationMessage($exportGeneration); + + // 7. Handler instantiation + $handler = new RemoveExportGenerationMessageHandler( + $exportGenerationRepository->reveal(), + $entityManager->reveal(), + new NullLogger(), + $clock + ); + + // Pre-condition: deleteAt not set. + $this->assertNull($storedObject->getDeleteAt()); + + // Act + $handler->__invoke($message); + + // Assert + $this->assertEquals($now, $storedObject->getDeleteAt(), 'deleteAt of stored object was updated'); + } +} diff --git a/src/Bundle/ChillMainBundle/config/services/export.yaml b/src/Bundle/ChillMainBundle/config/services/export.yaml index 3027532e1..d6a15b6d2 100644 --- a/src/Bundle/ChillMainBundle/config/services/export.yaml +++ b/src/Bundle/ChillMainBundle/config/services/export.yaml @@ -6,8 +6,13 @@ services: Chill\MainBundle\Export\Helper\: resource: '../../Export/Helper' + Chill\MainBundle\Export\Cronjob\: + resource: '../../Export/Cronjob' + Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessageHandler: ~ + Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessageHandler: ~ + Chill\MainBundle\Export\Messenger\OnExportGenerationFails: ~ Chill\MainBundle\Export\ExportFormHelper: ~