Explorar o código

Merge branch 'release/2.4.3' into develop

Olivier Massot hai 10 meses
pai
achega
bd0d6c535a
Modificáronse 46 ficheiros con 1564 adicións e 504 borrados
  1. 5 5
      config/packages/messenger.yaml
  2. 1 0
      doc/internal_requests.md
  3. 8 6
      src/ApiResources/Organization/OrganizationDeletionRequest.php
  4. 4 2
      src/ApiResources/Profile/AccessProfile.php
  5. 1 0
      src/Entity/Access/Access.php
  6. 3 3
      src/Entity/Organization/Organization.php
  7. 5 2
      src/Entity/Person/Person.php
  8. 2 2
      src/Message/Handler/MailerHandler.php
  9. 37 28
      src/Message/Handler/OrganizationDeletionHandler.php
  10. 1 1
      src/Message/Message/Mailer.php
  11. 2 1
      src/Message/Message/OrganizationDeletion.php
  12. 13 13
      src/Service/Cron/Job/CleanDb.php
  13. 8 6
      src/Service/Cron/Job/CleanTempFiles.php
  14. 2 1
      src/Service/Doctrine/FiltersConfigurationService.php
  15. 2 8
      src/Service/File/FileManager.php
  16. 2 8
      src/Service/File/Storage/ApiLegacyStorage.php
  17. 2 0
      src/Service/File/Storage/FileStorageInterface.php
  18. 22 31
      src/Service/File/Storage/LocalStorage.php
  19. 26 35
      src/Service/Organization/OrganizationFactory.php
  20. 1 3
      src/Service/Organization/Utils.php
  21. 8 12
      src/Service/Rest/ApiRequestInterface.php
  22. 13 9
      src/Service/Rest/ApiRequestService.php
  23. 2 1
      src/Service/Security/InternalRequestsService.php
  24. 3 0
      src/Service/ServiceIterator/StorageIterator.php
  25. 2 2
      src/Service/Typo3/SubdomainService.php
  26. 23 2
      src/Service/Typo3/Typo3Service.php
  27. 1 0
      src/Service/Utils/UrlBuilder.php
  28. 1 1
      src/State/Processor/Export/LicenceCmf/ExportRequestProcessor.php
  29. 3 3
      src/State/Processor/Organization/OrganizationCreationRequestProcessor.php
  30. 5 6
      src/State/Processor/Organization/OrganizationDeletionRequestProcessor.php
  31. 1 0
      tests/Fixture/Factory/Organization/OrganizationFactory.php
  32. 301 0
      tests/Unit/Service/Cron/Job/CleanDbTest.php
  33. 190 81
      tests/Unit/Service/Cron/Job/CleanTempFilesTest.php
  34. 0 90
      tests/Unit/Service/Doctrine/FiltersConfigurationService.php
  35. 213 0
      tests/Unit/Service/Doctrine/FiltersConfigurationServiceTest.php
  36. 1 1
      tests/Unit/Service/Dolibarr/DolibarrAccountCreatorTest.php
  37. 80 34
      tests/Unit/Service/File/FileManagerTest.php
  38. 99 18
      tests/Unit/Service/File/Storage/ApiLegacyStorageTest.php
  39. 262 0
      tests/Unit/Service/File/Storage/LocalStorageTest.php
  40. 122 73
      tests/Unit/Service/Organization/OrganizationFactoryTest.php
  41. 0 1
      tests/Unit/Service/Organization/UtilsTest.php
  42. 2 4
      tests/Unit/Service/Rest/ApiRequestServiceTest.php
  43. 3 4
      tests/Unit/Service/Security/InternalRequestsServiceTest.php
  44. 19 5
      tests/Unit/Service/Typo3/SubdomainServiceTest.php
  45. 29 2
      tests/Unit/Service/Typo3/Typo3ServiceTest.php
  46. 34 0
      tests/Unit/Service/Utils/UrlBuilderTest.php

+ 5 - 5
config/packages/messenger.yaml

@@ -11,8 +11,8 @@ framework:
 
         routing:
             # Route your messages to the transports
-            'App\Message\Command\MailerCommand': async
-            'App\Message\Command\Export': async
-            'App\Message\Command\Typo3\Typo3UpdateCommand': async
-            'App\Message\Command\Typo3\Typo3DeleteCommand': async
-            'App\Message\Command\Typo3\Typo3UndeleteCommand': async
+            'App\Message\Message\Mailer': async
+            'App\Message\Message\Export': async
+            'App\Message\Message\Typo3\Typo3Update': async
+            'App\Message\Message\Typo3\Typo3Delete': async
+            'App\Message\Message\Typo3\Typo3Undelete': async

+ 1 - 0
doc/internal_requests.md

@@ -21,6 +21,7 @@ Ainsi, si l'on prend l'exemple d'une requête `/internal/download/123` envoyée
 * Un utilisateur hors VPN, même s'il connaissait le token, recevra une erreur 403, car n'ayant pas une ip autorisée
 * Une requête issue de la V1 avec le bon token et provenant d'une ip interne sera autorisée sans authentification
 * Une requête d'un utilisateur connecté en tant que super admin et à l'intérieur du VPN pourra aussi aboutir.
+
 ### Ip internes 
 
 Les ips considérées comme interne sont :

+ 8 - 6
src/ApiResources/Organization/OrganizationDeletionRequest.php

@@ -9,7 +9,7 @@ use App\State\Processor\Organization\OrganizationDeletionRequestProcessor;
 use Symfony\Component\Validator\Constraints as Assert;
 
 /**
- * Requête de création d'une nouvelle organisation
+ * Requête de création d'une nouvelle organisation.
  */
 #[ApiResource(
     operations: [
@@ -36,20 +36,17 @@ class OrganizationDeletionRequest
 
     /**
      * A quelle adresse email notifier la création de l'organisation, ou d'éventuelles erreurs ?
-     * @var string|null
      */
     #[Assert\Email(message: 'The email {{ value }} is not a valid email.')]
     private ?string $sendConfirmationEmailAt = null;
 
     /**
-     * Statut de l'opération
-     * @var string
+     * Statut de l'opération.
      */
     private string $status = self::STATUS_PENDING;
 
     /**
-     * For testing purposes only
-     * @var bool
+     * For testing purposes only.
      */
     private bool $async = true;
 
@@ -61,6 +58,7 @@ class OrganizationDeletionRequest
     public function setId(int $id): self
     {
         $this->id = $id;
+
         return $this;
     }
 
@@ -72,6 +70,7 @@ class OrganizationDeletionRequest
     public function setOrganizationId(int $organizationId): self
     {
         $this->organizationId = $organizationId;
+
         return $this;
     }
 
@@ -83,6 +82,7 @@ class OrganizationDeletionRequest
     public function setSendConfirmationEmailAt(?string $sendConfirmationEmailAt): self
     {
         $this->sendConfirmationEmailAt = $sendConfirmationEmailAt;
+
         return $this;
     }
 
@@ -94,6 +94,7 @@ class OrganizationDeletionRequest
     public function setStatus(string $status): self
     {
         $this->status = $status;
+
         return $this;
     }
 
@@ -105,6 +106,7 @@ class OrganizationDeletionRequest
     public function setAsync(bool $async): self
     {
         $this->async = $async;
+
         return $this;
     }
 }

+ 4 - 2
src/ApiResources/Profile/AccessProfile.php

@@ -302,7 +302,8 @@ class AccessProfile implements ApiResourcesInterface
     }
 
     /**
-     * return required for PHP Stan
+     * return required for PHP Stan.
+     *
      * @return bool[]
      */
     public function getHistorical(): array
@@ -311,7 +312,8 @@ class AccessProfile implements ApiResourcesInterface
     }
 
     /**
-     * param require for PHP Stan
+     * param require for PHP Stan.
+     *
      * @param bool[] $historical
      *
      * @return $this

+ 1 - 0
src/Entity/Access/Access.php

@@ -507,6 +507,7 @@ class Access implements UserInterface, PasswordAuthenticatedUserInterface
     public function setLoginEnabled(bool $loginEnabled): self
     {
         $this->loginEnabled = $loginEnabled;
+
         return $this;
     }
 

+ 3 - 3
src/Entity/Organization/Organization.php

@@ -282,13 +282,13 @@ class Organization
     #[ORM\OneToMany(mappedBy: 'recipientOrganization', targetEntity: Notification::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $notifications;
 
-    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Email::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Email::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $emails;
 
-    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Mail::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Mail::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $mails;
 
-    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Sms::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Sms::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $sms;
 
     #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Activity::class, cascade: ['persist', 'remove'], orphanRemoval: true)]

+ 5 - 2
src/Entity/Person/Person.php

@@ -48,7 +48,7 @@ class Person implements UserInterface, PasswordAuthenticatedUserInterface
     /**
      * @var array<mixed>|null
      */
-    #[ORM\Column(type: "array", nullable: true)]
+    #[ORM\Column(type: 'array', nullable: true)]
     private ?array $roles = [];
 
     #[ORM\Column(nullable: true)]
@@ -113,7 +113,7 @@ class Person implements UserInterface, PasswordAuthenticatedUserInterface
     private Collection $documentWishes;
 
     /** @var array<string, string> */
-    #[ORM\Column(type: "json", nullable: true)]
+    #[ORM\Column(type: 'json', nullable: true)]
     private array $confidentiality = [];
 
     #[Pure]
@@ -162,6 +162,7 @@ class Person implements UserInterface, PasswordAuthenticatedUserInterface
     public function setEnabled(bool $enabled): self
     {
         $this->enabled = $enabled;
+
         return $this;
     }
 
@@ -623,11 +624,13 @@ class Person implements UserInterface, PasswordAuthenticatedUserInterface
 
     /**
      * @param array<string, string> $confidentiality
+     *
      * @return $this
      */
     public function setConfidentiality(array $confidentiality): self
     {
         $this->confidentiality = $confidentiality;
+
         return $this;
     }
 }

+ 2 - 2
src/Message/Handler/MailerHandler.php

@@ -5,7 +5,7 @@ declare(strict_types=1);
 namespace App\Message\Handler;
 
 use App\Entity\Message\Email;
-use App\Message\Message\MailerCommand;
+use App\Message\Message\Mailer as MailerMessage;
 use App\Repository\Access\AccessRepository;
 use App\Service\Mailer\Mailer;
 use App\Service\Notifier;
@@ -21,7 +21,7 @@ class MailerHandler
     ) {
     }
 
-    public function __invoke(MailerCommand $mailerCommand): void
+    public function __invoke(MailerMessage $mailerCommand): void
     {
         $mailerModel = $mailerCommand->getMailerModel();
         $emails = $this->mailer->main($mailerModel);

+ 37 - 28
src/Message/Handler/OrganizationDeletionHandler.php

@@ -4,57 +4,66 @@ declare(strict_types=1);
 
 namespace App\Message\Handler;
 
-use App\Message\Message\OrganizationCreation;
 use App\Message\Message\OrganizationDeletion;
 use App\Service\Organization\OrganizationFactory;
+use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
 use Symfony\Component\Mailer\MailerInterface;
 use Symfony\Component\Messenger\Attribute\AsMessageHandler;
 use Symfony\Component\Mime\Address;
 use Symfony\Component\Mime\Email as SymfonyEmail;
-use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
-use Throwable;
 
 #[AsMessageHandler(priority: 1)]
-class OrganizationDeletionHandler
+readonly class OrganizationDeletionHandler
 {
     public function __construct(
-        private readonly OrganizationFactory $organizationFactory,
-        private readonly MailerInterface $symfonyMailer,
-        private readonly string $opentalentMailReport
-    ) {}
+        private OrganizationFactory $organizationFactory,
+        private MailerInterface     $symfonyMailer,
+        private string              $opentalentMailReport,
+    ) {
+    }
 
     /**
-     * @throws Throwable
+     * @throws \Throwable
      * @throws TransportExceptionInterface
-     * @throws \Symfony\Component\Mailer\Exception\TransportExceptionInterface
      */
     public function __invoke(OrganizationDeletion $organizationDeletionCommand): void
     {
         $organizationCreationRequest = $organizationDeletionCommand->getOrganizationDeletionRequest();
-        $mail = ['subject' => '', 'content' => ''];
 
         try {
             $this->organizationFactory->delete($organizationCreationRequest);
 
-            $mail['subject'] = 'Organization deleted';
-            $mail['content'] = 'The organization n° ' . $organizationCreationRequest->getOrganizationId() . ' has been deleted successfully.';
-
+            $this->sendMail(
+                $organizationCreationRequest->getSendConfirmationEmailAt() ?? $this->opentalentMailReport,
+                'Organization deleted',
+                'The organization n° '.$organizationCreationRequest->getOrganizationId().' has been deleted successfully.',
+            );
         } catch (\Exception $e) {
-            $mail['subject'] = 'Organization deletion : an error occured';
-            $mail['content'] = 'An error occured while deleting the new organization : \n' . $e->getMessage();
+            $this->sendMail(
+                $organizationCreationRequest->getSendConfirmationEmailAt() ?? $this->opentalentMailReport,
+                'Organization deletion : an error occurred',
+                'An error occurred while deleting the new organization : \n'.$e->getMessage()
+            );
             throw $e;
-
-        } finally {
-            if ($organizationCreationRequest->getSendConfirmationEmailAt() !== null) {
-                $symfonyMail = (new SymfonyEmail())
-                    ->from($this->opentalentMailReport)
-                    ->replyTo($this->opentalentMailReport)
-                    ->returnPath(Address::create($this->opentalentMailReport))
-                    ->to($organizationCreationRequest->getSendConfirmationEmailAt())
-                    ->subject($mail['subject'])
-                    ->text($mail['content']);
-                $this->symfonyMailer->send($symfonyMail);
-            }
         }
     }
+
+    /**
+     * @throws TransportExceptionInterface
+     */
+    private function sendMail(
+        string $to,
+        string $subject,
+        string $content,
+    ): void
+    {
+        $symfonyMail = (new SymfonyEmail())
+            ->from($this->opentalentMailReport)
+            ->replyTo($this->opentalentMailReport)
+            ->returnPath(Address::create($this->opentalentMailReport))
+            ->to($to)
+            ->subject($subject)
+            ->text($content);
+        $this->symfonyMailer->send($symfonyMail);
+    }
 }

+ 1 - 1
src/Message/Message/MailerCommand.php → src/Message/Message/Mailer.php

@@ -9,7 +9,7 @@ use App\Service\Mailer\Model\MailerModelInterface;
 /**
  * Classe ... qui ...
  */
-class MailerCommand
+class Mailer
 {
     public function __construct(
         private MailerModelInterface $model,

+ 2 - 1
src/Message/Message/OrganizationDeletion.php

@@ -12,7 +12,7 @@ use App\ApiResources\Organization\OrganizationDeletionRequest;
 class OrganizationDeletion
 {
     public function __construct(
-        private OrganizationDeletionRequest $organizationDeletionRequest
+        private OrganizationDeletionRequest $organizationDeletionRequest,
     ) {
     }
 
@@ -24,6 +24,7 @@ class OrganizationDeletion
     public function setOrganizationDeletionRequest(OrganizationDeletionRequest $organizationDeletionRequest): self
     {
         $this->organizationDeletionRequest = $organizationDeletionRequest;
+
         return $this;
     }
 }

+ 13 - 13
src/Service/Cron/Job/CleanDb.php

@@ -76,7 +76,7 @@ class CleanDb extends BaseCronJob
 
             if ($commit) {
                 $this->connection->commit();
-                $this->ui->print('DB purged - '.$purged.' records permanently deleted');
+                $this->logger->info('DB purged - '.$purged.' records permanently deleted');
 
                 return;
             } else {
@@ -98,7 +98,7 @@ class CleanDb extends BaseCronJob
      */
     protected function purgeAuditTables(\DateTime $maxDate): int
     {
-        $this->ui->print('Purge Audit_* tables from the records created before the '.$maxDate->format('c'));
+        $this->logger->info('Purge Audit_* tables from the records created before the '.$maxDate->format('c'));
 
         $tableNames = $this->connection->getSchemaManager()->listTableNames();
 
@@ -115,7 +115,7 @@ class CleanDb extends BaseCronJob
                 $stmt = $this->connection->prepare($sql);
                 $purged = $stmt->executeStatement(['maxDate' => $maxDate->format('Y-m-d')]);
 
-                $this->ui->print('* '.$tableName.' : '.$purged.' lines to delete');
+                $this->logger->debug('* '.$tableName.' : '.$purged.' lines to delete');
 
                 $total += $purged;
             }
@@ -131,7 +131,7 @@ class CleanDb extends BaseCronJob
      */
     protected function purgeMessages(\DateTime $maxDate): int
     {
-        $this->ui->print('Purge the DB from the messages created before the '.$maxDate->format('c'));
+        $this->logger->info('Purge the DB from the messages created before the '.$maxDate->format('c'));
 
         $sql = 'DELETE r
                 FROM opentalent.Message m
@@ -139,18 +139,18 @@ class CleanDb extends BaseCronJob
                 where (m.dateSent < :maxDate or (m.dateSent is null and m.createDate < :maxDate)) and m.isSystem = true and m.id > 0;';
 
         $stmt = $this->connection->prepare($sql);
-        $purgedReportMessage = $stmt->executeStatement(['maxDate' => $maxDate->format('c')]);
+        $purgedReportMessage = $stmt->executeStatement(['maxDate' => $maxDate->format('Y-m-d')]);
 
-        $this->ui->print('* ReportMessage : '.$purgedReportMessage.' lines to delete');
+        $this->logger->debug('* ReportMessage : '.$purgedReportMessage.' lines to delete');
 
         $sql = 'DELETE
                 FROM opentalent.Message
                 where (dateSent < :maxDate or (dateSent is null and createDate < :maxDate)) and isSystem = true and id > 0;';
 
         $stmt = $this->connection->prepare($sql);
-        $purgedMessage = $stmt->executeStatement(['maxDate' => $maxDate->format('c')]);
+        $purgedMessage = $stmt->executeStatement(['maxDate' => $maxDate->format('Y-m-d')]);
 
-        $this->ui->print('* Message : '.$purgedMessage.' lines to delete');
+        $this->logger->debug('* Message : '.$purgedMessage.' lines to delete');
 
         return $purgedReportMessage + $purgedMessage;
     }
@@ -162,7 +162,7 @@ class CleanDb extends BaseCronJob
      */
     protected function purgeNotifications(\DateTime $maxDate): int
     {
-        $this->ui->print('Purge the DB from the notifications created before the '.$maxDate->format('c'));
+        $this->logger->info('Purge the DB from the notifications created before the '.$maxDate->format('Y-m-d'));
 
         $sql = "DELETE u
                 FROM opentalent.Information i
@@ -170,18 +170,18 @@ class CleanDb extends BaseCronJob
                 where i.createDate < :maxDate and i.discr = 'notification';";
 
         $stmt = $this->connection->prepare($sql);
-        $purgedNotificationUser = $stmt->executeStatement(['maxDate' => $maxDate->format('c')]);
+        $purgedNotificationUser = $stmt->executeStatement(['maxDate' => $maxDate->format('Y-m-d')]);
 
-        $this->ui->print('* NotificationUser : '.$purgedNotificationUser.' lines to delete');
+        $this->logger->debug('* NotificationUser : '.$purgedNotificationUser.' lines to delete');
 
         $sql = "DELETE
                 FROM opentalent.Information
                 where createDate < :maxDate and discr = 'notification';";
 
         $stmt = $this->connection->prepare($sql);
-        $purgedNotification = $stmt->executeStatement(['maxDate' => $maxDate->format('c')]);
+        $purgedNotification = $stmt->executeStatement(['maxDate' => $maxDate->format('Y-m-d')]);
 
-        $this->ui->print('* Information : '.$purgedNotification.' lines to delete');
+        $this->logger->debug('* Information : '.$purgedNotification.' lines to delete');
 
         return $purgedNotificationUser + $purgedNotification;
     }

+ 8 - 6
src/Service/Cron/Job/CleanTempFiles.php

@@ -10,7 +10,6 @@ use App\Service\Cron\BaseCronJob;
 use App\Service\File\Storage\LocalStorage;
 use App\Service\Utils\DatesUtils;
 use Doctrine\DBAL\Connection;
-use Doctrine\DBAL\ConnectionException;
 use Doctrine\ORM\QueryBuilder;
 use JetBrains\PhpStorm\Pure;
 
@@ -108,7 +107,6 @@ class CleanTempFiles extends BaseCronJob
         $total = count($files);
         $this->logger->info($total.' temporary files to be removed');
 
-        $this->connection->beginTransaction();
         $this->connection->setAutoCommit(false);
         $queryBuilder = $this->fileRepository->createQueryBuilder('f');
 
@@ -116,19 +114,24 @@ class CleanTempFiles extends BaseCronJob
         $i = 0;
         $deleted = 0;
         $this->ui->progress(0, $total);
+
         foreach ($files as $file) {
-            try {
-                ++$i;
-                $this->ui->progress($i, $total);
+            $this->connection->beginTransaction();
+            $this->ui->progress($i, $total);
+            ++$i;
 
+            try {
                 // Delete from disk
                 $this->storage->hardDelete($file);
 
                 // Remove from DB
                 $queryBuilder->delete()->where('f.id = :id')->setParameter('id', $file->getId());
 
+                $this->connection->commit();
                 ++$deleted;
             } catch (\RuntimeException|\InvalidArgumentException $e) {
+                // Non blocking errors
+                $this->connection->rollback();
                 $this->logger->error('ERROR : '.$e->getMessage());
             } catch (\Exception $exception) {
                 $this->connection->rollback();
@@ -136,7 +139,6 @@ class CleanTempFiles extends BaseCronJob
             }
         }
 
-        $this->connection->commit();
         $this->logger->info($deleted.' files deleted');
     }
 

+ 2 - 1
src/Service/Doctrine/FiltersConfigurationService.php

@@ -22,7 +22,7 @@ class FiltersConfigurationService
      * Si $previousTimeConstraintState est `true`, les filtres étaient activés, et si c'est `false`, les filtres
      * étaient désactivés. Si les filtres ne sont pas suspendus, $previousTimeConstraintState est null.
      */
-    private ?bool $previousTimeConstraintState = null;
+    protected ?bool $previousTimeConstraintState = null;
 
     public function __construct(
         private EntityManagerInterface $entityManager,
@@ -79,6 +79,7 @@ class FiltersConfigurationService
         $filter = $filters->getFilter('date_time_filter');
 
         $this->previousTimeConstraintState = $filter->isDisabled() === false;
+
         $filter->setDisabled(true);
     }
 

+ 2 - 8
src/Service/File/FileManager.php

@@ -144,10 +144,7 @@ class FileManager
 
     /**
      * Permanently delete the organization's files from each storage, and remove any reference
-     * in the DB
-     *
-     * @param int $organizationId
-     * @return void
+     * in the DB.
      */
     public function deleteOrganizationFiles(int $organizationId): void
     {
@@ -160,10 +157,7 @@ class FileManager
 
     /**
      * Permanently delete the person's files from each storage, and remove any reference
-     * * in the DB
- *
-     * @param int $personId
-     * @return void
+     * * in the DB.
      */
     public function deletePersonFiles(int $personId): void
     {

+ 2 - 8
src/Service/File/Storage/ApiLegacyStorage.php

@@ -58,10 +58,7 @@ class ApiLegacyStorage implements FileStorageInterface
     }
 
     /**
-     * Permanently delete the entire file storage of the given Organization
-     *
-     * @param int $organizationId
-     * @return void
+     * Permanently delete the entire file storage of the given Organization.
      */
     public function deleteOrganizationFiles(int $organizationId): void
     {
@@ -70,10 +67,7 @@ class ApiLegacyStorage implements FileStorageInterface
     }
 
     /**
-     * Permanently delete the entire file storage of the given Person
-     *
-     * @param int $personId
-     * @return void
+     * Permanently delete the entire file storage of the given Person.
      */
     public function deletePersonFiles(int $personId): void
     {

+ 2 - 0
src/Service/File/Storage/FileStorageInterface.php

@@ -5,6 +5,7 @@ declare(strict_types=1);
 namespace App\Service\File\Storage;
 
 use App\Entity\Core\File;
+use App\Enum\Core\FileSizeEnum;
 
 interface FileStorageInterface
 {
@@ -12,6 +13,7 @@ interface FileStorageInterface
 
     public function read(File $file): string;
 
+    // TODO: remplacer `string $size` par `FileSizeEnum $size`
     public function getImageUrl(File $file, string $size, bool $relativePath): string;
 
     public function support(File $file): bool;

+ 22 - 31
src/Service/File/Storage/LocalStorage.php

@@ -47,15 +47,15 @@ class LocalStorage implements FileStorageInterface
     protected FilesystemInterface $filesystem;
 
     public function __construct(
-        protected readonly FilesystemMap          $filesystemMap,
+        protected readonly FilesystemMap $filesystemMap,
         protected readonly EntityManagerInterface $entityManager,
-        protected readonly AccessRepository       $accessRepository,
-        protected readonly DataManager            $dataManager,
-        protected readonly CacheManager           $cacheManager,
-        protected readonly ImageFactory           $imageFactory,
-        protected readonly FileUtils              $fileUtils,
-        protected readonly UrlBuilder             $urlBuilder,
-        protected readonly string                 $fileStorageDir
+        protected readonly AccessRepository $accessRepository,
+        protected readonly DataManager $dataManager,
+        protected readonly CacheManager $cacheManager,
+        protected readonly ImageFactory $imageFactory,
+        protected readonly FileUtils $fileUtils,
+        protected readonly UrlBuilder $urlBuilder,
+        protected readonly string $fileStorageDir,
     ) {
         $this->filesystem = $filesystemMap->get(static::FS_KEY);
     }
@@ -115,17 +115,17 @@ class LocalStorage implements FileStorageInterface
     /**
      * Retourne le filtre Liip correspondant à la taille désirée.
      */
-    private function getFilterFromSizeAndConfig(string $size, bool $configExist): string
+    protected function getFilterFromSizeAndConfig(string $size, bool $configExist): string
     {
         switch ($size) {
-            case FileSizeEnum::SM :
+            case FileSizeEnum::SM->value :
                 $filter = $configExist ? self::CROP_SM : self::SM_FOLDER;
                 break;
-            case FileSizeEnum::MD :
+            case FileSizeEnum::MD->value :
             default:
                 $filter = $configExist ? self::CROP_MD : self::MD_FOLDER;
                 break;
-            case FileSizeEnum::LG :
+            case FileSizeEnum::LG->value :
                 $filter = $configExist ? self::CROP_LG : self::LG_FOLDER;
                 break;
         }
@@ -296,39 +296,30 @@ class LocalStorage implements FileStorageInterface
     }
 
     /**
-     * Permanently delete the entire file storage of the given Organization
-     *
-     * @param int $organizationId
-     * @return void
+     * Permanently delete the entire file storage of the given Organization.
      */
     public function deleteOrganizationFiles(int $organizationId): void
     {
-        $this->rrmDir('organization/' . $organizationId);
-        $this->rrmDir('temp/organization/' . $organizationId);
+        $this->rrmDir('organization/'.$organizationId);
+        $this->rrmDir('temp/organization/'.$organizationId);
     }
 
     /**
-     * Permanently delete the entire file storage of the given Person
-     *
-     * @param int $personId
-     * @return void
+     * Permanently delete the entire file storage of the given Person.
      */
     public function deletePersonFiles(int $personId): void
     {
-        $this->rrmDir('person/' . $personId);
-        $this->rrmDir('temp/person/' . $personId);
+        $this->rrmDir('person/'.$personId);
+        $this->rrmDir('temp/person/'.$personId);
     }
 
     /**
-     * Supprime récursivement un répertoire
+     * Supprime récursivement un répertoire.
      *
-     * (Au moment du développement, Gaufrette ne permet pas la suppression de répertoire, on laissera
-     * le soin à un cron de supprimer les répertoires vides du storage)
-     *
-     * @param string $dirKey
-     * @return void
+     * (Au moment du développement, Gaufrette ne permet pas la suppression de répertoires)
      */
-    protected function rrmDir(string $dirKey): void {
+    protected function rrmDir(string $dirKey): void
+    {
         if (!$this->filesystem->isDirectory($dirKey)) {
             throw new \RuntimeException('Directory `'.$dirKey.'` does not exist');
         }

+ 26 - 35
src/Service/Organization/OrganizationFactory.php

@@ -38,8 +38,8 @@ use App\Service\Typo3\BindFileService;
 use App\Service\Typo3\SubdomainService;
 use App\Service\Typo3\Typo3Service;
 use App\Service\Utils\DatesUtils;
-use App\Service\Utils\UrlBuilder;
 use App\Service\Utils\SecurityUtils;
+use App\Service\Utils\UrlBuilder;
 use Doctrine\ORM\EntityManagerInterface;
 use Elastica\Param;
 use libphonenumber\NumberParseException;
@@ -72,6 +72,7 @@ class OrganizationFactory
         private readonly OrganizationIdentificationRepository $organizationIdentificationRepository,
         private readonly ApiLegacyRequestService $apiLegacyRequestService,
         private readonly FunctionTypeRepository $functionTypeRepository,
+        private readonly FileManager $fileManager,
     ) {
         $this->phoneNumberUtil = PhoneNumberUtil::getInstance();
     }
@@ -178,6 +179,7 @@ class OrganizationFactory
             $this->logger->warning('-- Operation ended with errors, check the logs for more information --');
         } else {
             $organizationCreationRequest->setStatus(OrganizationCreationRequest::STATUS_OK);
+            $this->logger->info("The organization has been created (id=".$organization->getId().").");
         }
 
         return $organization;
@@ -758,7 +760,7 @@ class OrganizationFactory
     protected function updateAdminassosDb(Organization $organization): void
     {
         $response = $this->apiLegacyRequestService->get(
-            UrlBuilder::concatPath('/_internal/request/adminassos/create/organization/', [(string)$organization->getId()])
+            UrlBuilder::concatPath('/_internal/request/adminassos/create/organization/', [(string) $organization->getId()])
         );
 
         if ($response->getStatusCode() !== Response::HTTP_OK) {
@@ -797,7 +799,7 @@ class OrganizationFactory
 
         $organization = $this->organizationRepository->find($organizationDeletionRequest->getOrganizationId());
         if (!$organization) {
-            throw new \RuntimeException("No organization was found for id : " . $organizationDeletionRequest->getOrganizationId());
+            throw new \RuntimeException('No organization was found for id : '.$organizationDeletionRequest->getOrganizationId());
         }
 
         $this->logger->info(
@@ -809,7 +811,7 @@ class OrganizationFactory
         $withError = false;
 
         try {
-            $orphanPersons = $this->getOrphansToBePersons($organization);
+            $orphanPersons = $this->getFutureOrphanPersons($organization);
 
             // On est obligé de supprimer manuellement les paramètres, car c'est l'entité Parameters qui est
             // propriétaire de la relation Organization ↔ Parameters.
@@ -827,6 +829,7 @@ class OrganizationFactory
 
             $this->entityManager->flush();
             $this->entityManager->commit();
+            $this->logger->info("Organization deleted");
         } catch (\Exception $e) {
             $this->logger->critical("An error happened, operation cancelled\n".$e);
             $this->entityManager->rollback();
@@ -835,6 +838,7 @@ class OrganizationFactory
 
         try {
             $this->deleteTypo3Website($organizationDeletionRequest->getOrganizationId());
+            $this->logger->info("Typo3 website deleted");
         } catch (\Exception $e) {
             $this->logger->critical('An error happened while deleting the Typo3 website, please proceed manually.');
             $this->logger->debug($e);
@@ -842,7 +846,8 @@ class OrganizationFactory
         }
 
         try {
-            $this->switchDolibarrSocietyToProspect($organization);
+            $this->switchDolibarrSocietyToProspect($organizationDeletionRequest->getOrganizationId());
+            $this->logger->info("Dolibarr society switched to prospect");
         } catch (\Exception $e) {
             $this->logger->critical('An error happened while updating the Dolibarr society, please proceed manually.');
             $this->logger->debug($e);
@@ -851,6 +856,9 @@ class OrganizationFactory
 
         try {
             $this->fileManager->deleteOrganizationFiles($organizationDeletionRequest->getOrganizationId());
+            $this->logger->info("Organization files deleted");
+        } catch (\RuntimeException $e) {
+            // Nothing to delete
         } catch (\Exception $e) {
             $this->logger->critical("An error happened while deleting the organization's files, please proceed manually.");
             $this->logger->debug($e);
@@ -860,12 +868,15 @@ class OrganizationFactory
         foreach ($deletedPersonIds as $personId) {
             try {
                 $this->fileManager->deletePersonFiles($personId);
+            } catch (\RuntimeException $e) {
+                // Nothing to delete
             } catch (\Exception $e) {
-                $this->logger->critical("An error happened while deleting the person's files, please proceed manually (id=" . $person->getId() . ").");
+                $this->logger->critical("An error happened while deleting the person's files, please proceed manually (id=".$personId.').');
                 $this->logger->debug($e);
                 $withError = true;
             }
         }
+        $this->logger->info("Organization's persons files deleted");
 
         if ($withError) {
             $organizationDeletionRequest->setStatus(OrganizationDeletionRequest::STATUS_OK_WITH_ERRORS);
@@ -874,6 +885,10 @@ class OrganizationFactory
             $organizationDeletionRequest->setStatus(OrganizationDeletionRequest::STATUS_OK);
         }
 
+        $this->logger->info(
+            "The organization has been deleted."
+        );
+
         return $organizationDeletionRequest;
     }
 
@@ -881,10 +896,9 @@ class OrganizationFactory
      * Supprime tous les Access d'une organisation, ainsi que la Person
      * rattachée (si celle-ci n'est pas liée à d'autres Access).
      *
-     * @param Organization $organization
      * @return array<Person>
      */
-    protected function getOrphansToBePersons(Organization $organization): array
+    protected function getFutureOrphanPersons(Organization $organization): array
     {
         $orphans = [];
 
@@ -894,40 +908,17 @@ class OrganizationFactory
                 $orphans[] = $person;
             }
         }
+
         return $orphans;
     }
 
-    // TODO: à revoir, c'est du many to many
-    //    protected function removeTypeOfPractices(Organization $organization): void {
-    //        foreach ($organization->getTypeOfPractices() as $typeOfPractice) {
-    //            $organization->removeTypeOfPractice($typeOfPractice);
-    //        }
-    //    }
-
-    // TODO: à revoir, c'est du many to many
-    //    protected function deleteContactPoints(Organization $organization): void
-    //    {
-    //        foreach ($organization->getContactPoints() as $contactPoint) {
-    //            $this->entityManager->remove($contactPoint);
-    //        }
-    //    }
-
-    // TODO: à revoir, c'est du many to many
-    //    protected function deleteBankAccounts(Organization $organization): void {
-    //        foreach ($organization->getBankAccounts() as $bankAccount) {
-    //            $this->entityManager->remove($bankAccount);
-    //        }
-    //    }
-
-
     protected function deleteTypo3Website(int $organizationId): void
     {
-        // TODO: implement
-        //        $this->typo3Service->deleteSite($organization->getId());
+        $this->typo3Service->hardDeleteSite($organizationId);
     }
 
-    protected function switchDolibarrSocietyToProspect(Organization $organization): void
+    protected function switchDolibarrSocietyToProspect(int $organizationId): void
     {
-        $this->dolibarrApiService->switchSocietyToProspect($organization->getId());
+        $this->dolibarrApiService->switchSocietyToProspect($organizationId);
     }
 }

+ 1 - 3
src/Service/Organization/Utils.php

@@ -293,10 +293,8 @@ class Utils
     }
 
     /**
-     * Retourne le premier NetworkOrganization actif de la structure
+     * Retourne le premier NetworkOrganization actif de la structure.
      *
-     * @param Organization $organization
-     * @return NetworkOrganization|null
      * @throws \Exception
      */
     public function getActiveNetworkOrganization(Organization $organization): ?NetworkOrganization

+ 8 - 12
src/Service/Rest/ApiRequestInterface.php

@@ -47,28 +47,24 @@ interface ApiRequestInterface
     /**
      * Sends a POST request and returns the response.
      *
-     * @param string $path
-     * @param array<mixed>| string $body
-     * @param array<mixed> $parameters
-     * @param array<mixed> $options
+     * @param array<mixed>|string $body
+     * @param array<mixed>        $parameters
+     * @param array<mixed>        $options
      *
-     * @return ResponseInterface
      * @throws HttpException
      */
-    public function post(string $path, array | string $body, array $parameters = [], array $options = []): ResponseInterface;
+    public function post(string $path, array|string $body, array $parameters = [], array $options = []): ResponseInterface;
 
     /**
      * Sends a PUT request and returns the response.
      *
-     * @param string $path
-     * @param array<mixed> | string $body
-     * @param array<mixed> $parameters
-     * @param array<mixed> $options
+     * @param array<mixed>|string $body
+     * @param array<mixed>        $parameters
+     * @param array<mixed>        $options
      *
-     * @return ResponseInterface
      * @throws HttpException
      */
-    public function put(string $path, array | string $body, array $parameters = [], array $options = []): ResponseInterface;
+    public function put(string $path, array|string $body, array $parameters = [], array $options = []): ResponseInterface;
 
     /**
      * Sends a DELETE request and returns the response.

+ 13 - 9
src/Service/Rest/ApiRequestService.php

@@ -72,15 +72,17 @@ class ApiRequestService implements ApiRequestInterface
     }
 
     /**
-     * Complète les options en y ajoutant le body
+     * Complète les options en y ajoutant le body.
      *
-     * @param array<mixed> $options
+     * @param array<mixed>        $options
      * @param array<mixed>|string $body
+     *
      * @return array<mixed>
      */
-    protected function addBodyOption(array $options, array | string $body): array
+    protected function addBodyOption(array $options, array|string $body): array
     {
         $option = is_array($body) ? ['json' => $body] : ['body' => $body];
+
         return array_merge($options, $option);
     }
 
@@ -88,14 +90,15 @@ class ApiRequestService implements ApiRequestInterface
      * Sends a POST request and returns the response.
      *
      * @param array<mixed>|string $body
-     * @param array<mixed> $parameters
-     * @param array<mixed> $options
+     * @param array<mixed>        $parameters
+     * @param array<mixed>        $options
      *
      * @throws HttpException
      */
-    public function post(string $path, array | string $body, array $parameters = [], array $options = []): ResponseInterface
+    public function post(string $path, array|string $body, array $parameters = [], array $options = []): ResponseInterface
     {
         $options = $this->addBodyOption($options, $body);
+
         return $this->request('POST', $path, $parameters, $options);
     }
 
@@ -103,14 +106,15 @@ class ApiRequestService implements ApiRequestInterface
      * Sends a PUT request and returns the response.
      *
      * @param array<mixed>|string $body
-     * @param array<mixed> $parameters
-     * @param array<mixed> $options
+     * @param array<mixed>        $parameters
+     * @param array<mixed>        $options
      *
      * @throws HttpException
      */
-    public function put(string $path, array | string $body, array $parameters = [], array $options = []): ResponseInterface
+    public function put(string $path, array|string $body, array $parameters = [], array $options = []): ResponseInterface
     {
         $options = $this->addBodyOption($options, $body);
+
         return $this->request('PUT', $path, $parameters, $options);
     }
 

+ 2 - 1
src/Service/Security/InternalRequestsService.php

@@ -26,7 +26,7 @@ class InternalRequestsService
 
     public function __construct(
         readonly private string $internalRequestsToken,
-        private Security $security
+        private Security $security,
     ) {
     }
 
@@ -60,6 +60,7 @@ class InternalRequestsService
         if (!$user instanceof UserInterface) {
             return false;
         }
+
         return $user->getSuperAdminAccess();
     }
 

+ 3 - 0
src/Service/ServiceIterator/StorageIterator.php

@@ -22,6 +22,9 @@ class StorageIterator
     ) {
     }
 
+    /**
+     * @return iterable<FileStorageInterface>
+     */
     public function getStorages(): iterable
     {
         return $this->storageServices;

+ 2 - 2
src/Service/Typo3/SubdomainService.php

@@ -4,7 +4,7 @@ namespace App\Service\Typo3;
 
 use App\Entity\Organization\Organization;
 use App\Entity\Organization\Subdomain;
-use App\Message\Message\MailerCommand;
+use App\Message\Message\Mailer;
 use App\Message\Message\Typo3\Typo3Update;
 use App\Repository\Access\AccessRepository;
 use App\Repository\Organization\SubdomainRepository;
@@ -244,7 +244,7 @@ class SubdomainService
 
         // Envoi d'un email
         $this->messageBus->dispatch(
-            new MailerCommand($model)
+            new Mailer($model)
         );
     }
 }

+ 23 - 2
src/Service/Typo3/Typo3Service.php

@@ -2,6 +2,7 @@
 
 namespace App\Service\Typo3;
 
+use App\Service\Utils\DatesUtils;
 use App\Service\Utils\UrlBuilder;
 use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
 use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -21,17 +22,19 @@ class Typo3Service
      * Send a command to the given route of the Typo3 API.
      *
      * @param array<mixed> $parameters
+     * @param array<mixed> $headers
      *
      * @throws TransportExceptionInterface
      */
-    protected function sendCommand(string $route, array $parameters): ResponseInterface
+    protected function sendCommand(string $route, array $parameters, array $headers = []): ResponseInterface
     {
         $url = UrlBuilder::concat(
             '/typo3',
             [$route],
             $parameters
         );
-        return $this->typo3_client->request('GET', $url);
+
+        return $this->typo3_client->request('GET', $url, ['headers' => $headers]);
     }
 
     /**
@@ -74,6 +77,24 @@ class Typo3Service
         return $this->sendCommand('/otadmin/site/delete', ['organization-id' => $organizationId]);
     }
 
+    /**
+     * Permanently delete the organization's website.
+     *
+     * @throws TransportExceptionInterface
+     */
+    public function hardDeleteSite(int $organizationId): ResponseInterface
+    {
+        $date = DatesUtils::new()->format('Ymd');
+
+        $headers = ['Confirmation-Token' => "DEL-$organizationId-".$date];
+
+        return $this->sendCommand(
+            '/otadmin/site/delete',
+            ['organization-id' => $organizationId, 'hard' => 1],
+            $headers
+        );
+    }
+
     /**
      * Restore a website that has been deleted with 'deleteSite'.
      *

+ 1 - 0
src/Service/Utils/UrlBuilder.php

@@ -25,6 +25,7 @@ class UrlBuilder
         foreach ($tails as $tail) {
             $url = rtrim($url, '/').'/'.ltrim(strval($tail), '/');
         }
+
         return $url;
     }
 

+ 1 - 1
src/State/Processor/Export/LicenceCmf/ExportRequestProcessor.php

@@ -9,8 +9,8 @@ use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\ProcessorInterface;
 use App\ApiResources\Export\ExportRequest;
 use App\Entity\Access\Access;
-use App\Entity\Core\File;
 use App\Message\Message\Export;
+use App\Service\Network\Utils as NetworkUtils;
 use App\Service\ServiceIterator\ExporterIterator;
 use Symfony\Bundle\SecurityBundle\Security;
 use Symfony\Component\HttpFoundation\Response;

+ 3 - 3
src/State/Processor/Organization/OrganizationCreationRequestProcessor.php

@@ -4,11 +4,11 @@ declare(strict_types=1);
 
 namespace App\State\Processor\Organization;
 
-use App\Entity\Access\Access;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\Metadata\Post;
 use ApiPlatform\State\ProcessorInterface;
 use App\ApiResources\Organization\OrganizationCreationRequest;
+use App\Entity\Access\Access;
 use App\Message\Message\OrganizationCreation;
 use App\Service\Organization\OrganizationFactory;
 use App\Service\Utils\DatesUtils;
@@ -27,8 +27,8 @@ class OrganizationCreationRequestProcessor implements ProcessorInterface
 
     /**
      * @param OrganizationCreationRequest $data
-     * @param mixed[]       $uriVariables
-     * @param mixed[]       $context
+     * @param mixed[]                     $uriVariables
+     * @param mixed[]                     $context
      *
      * @throws \Exception
      */

+ 5 - 6
src/State/Processor/Organization/OrganizationDeletionRequestProcessor.php

@@ -4,13 +4,10 @@ declare(strict_types=1);
 
 namespace App\State\Processor\Organization;
 
-use ApiPlatform\Metadata\Delete;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\Metadata\Post;
 use ApiPlatform\State\ProcessorInterface;
-use App\ApiResources\Organization\OrganizationCreationRequest;
 use App\ApiResources\Organization\OrganizationDeletionRequest;
-use App\Message\Message\OrganizationCreation;
 use App\Message\Message\OrganizationDeletion;
 use App\Service\Organization\OrganizationFactory;
 use Symfony\Component\HttpFoundation\Response;
@@ -21,12 +18,13 @@ class OrganizationDeletionRequestProcessor implements ProcessorInterface
     public function __construct(
         private readonly MessageBusInterface $messageBus,
         private readonly OrganizationFactory $organizationFactory,
-    ) {}
+    ) {
+    }
 
     /**
      * @param OrganizationDeletionRequest $data
-     * @param mixed[]       $uriVariables
-     * @param mixed[]       $context
+     * @param mixed[]                     $uriVariables
+     * @param mixed[]                     $context
      *
      * @throws \Exception
      */
@@ -50,6 +48,7 @@ class OrganizationDeletionRequestProcessor implements ProcessorInterface
             // For testing purposes only
             $this->organizationFactory->delete($organizationDeletionRequest);
         }
+
         return $organizationDeletionRequest;
     }
 }

+ 1 - 0
tests/Fixture/Factory/Organization/OrganizationFactory.php

@@ -3,6 +3,7 @@
 namespace App\Tests\Fixture\Factory\Organization;
 
 use App\Entity\Organization\Organization;
+use App\Repository\Organization\OrganizationRepository;
 use Zenstruck\Foundry\ModelFactory;
 use Zenstruck\Foundry\Proxy;
 use Zenstruck\Foundry\RepositoryProxy;

+ 301 - 0
tests/Unit/Service/Cron/Job/CleanDbTest.php

@@ -0,0 +1,301 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Tests\Unit\Service\Cron\Job;
+
+use App\Service\Cron\Job\CleanDb;
+use App\Service\Cron\UI\CronUIInterface;
+use App\Service\Utils\DatesUtils;
+use Doctrine\DBAL\Connection;
+use Doctrine\DBAL\Statement;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+
+class TestableCleanDb extends CleanDb
+{
+    public function purgeDb(bool $commit = true): void
+    {
+        parent::purgeDb($commit);
+    }
+
+    public function purgeAuditTables(\DateTime $maxDate): int
+    {
+        return parent::purgeAuditTables($maxDate);
+    }
+
+    public function purgeMessages(\DateTime $maxDate): int
+    {
+        return parent::purgeMessages($maxDate);
+    }
+
+    public function purgeNotifications(\DateTime $maxDate): int
+    {
+        return parent::purgeNotifications($maxDate);
+    }
+}
+
+class CleanDbTest extends TestCase
+{
+    private CronUIInterface|MockObject $ui;
+    private MockObject|LoggerInterface $logger;
+    private Connection|MockObject $connection;
+
+    public function setUp(): void
+    {
+        $this->ui = $this->getMockBuilder(CronUIInterface::class)->disableOriginalConstructor()->getMock();
+        $this->logger = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock();
+
+        $this->connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock();
+    }
+
+    private function getMockFor(string $method): MockObject|TestableCleanDb
+    {
+        $cleanDb = $this->getMockBuilder(TestableCleanDb::class)
+            ->setConstructorArgs([$this->connection])
+            ->setMethodsExcept([$method, 'setUI', 'setLoggerInterface'])
+            ->getMock();
+        $cleanDb->setUI($this->ui);
+        $cleanDb->setLoggerInterface($this->logger);
+
+        return $cleanDb;
+    }
+
+    public function testPreview(): void
+    {
+        $cleanDb = $this->getMockFor('preview');
+
+        $cleanDb->expects(self::once())->method('purgeDb')->with(false);
+
+        $cleanDb->preview();
+    }
+
+    public function testExecute(): void
+    {
+        $cleanDb = $this->getMockFor('execute');
+
+        $cleanDb->expects(self::once())->method('purgeDb');
+
+        $cleanDb->execute();
+    }
+
+    public function testPurgeDb(): void
+    {
+        DatesUtils::setFakeDatetime('2022-01-08 00:00:00');
+
+        $maxDate = DatesUtils::new();
+        $maxDate->sub(new \DateInterval('P60D'));
+
+        $maxDateAudit = DatesUtils::new();
+        $maxDateAudit->sub(new \DateInterval('P180D'));
+
+        $cleanDb = $this->getMockFor('purgeDb');
+
+        $this->connection->expects($this->once())->method('beginTransaction');
+        $this->connection->expects($this->once())->method('setAutoCommit')->with(false);
+        $this->connection->expects($this->once())->method('commit');
+        $this->connection->expects($this->never())->method('rollback');
+
+        $cleanDb->expects(self::once())->method('purgeAuditTables')->with($maxDateAudit)->willReturn(100);
+        $cleanDb->expects(self::once())->method('purgeMessages')->with($maxDate)->willReturn(101);
+        $cleanDb->expects(self::once())->method('purgeNotifications')->with($maxDate)->willReturn(102);
+
+        $this->logger->expects(self::once())->method('info')->with('DB purged - 303 records permanently deleted');
+
+        $cleanDb->purgeDb();
+    }
+
+    public function testPurgeDbNoCommit(): void
+    {
+        DatesUtils::setFakeDatetime('2022-01-08 00:00:00');
+
+        $maxDate = DatesUtils::new();
+        $maxDate->sub(new \DateInterval('P60D'));
+
+        $maxDateAudit = DatesUtils::new();
+        $maxDateAudit->sub(new \DateInterval('P180D'));
+
+        $cleanDb = $this->getMockFor('purgeDb');
+
+        $this->connection->expects($this->once())->method('beginTransaction');
+        $this->connection->expects($this->once())->method('setAutoCommit')->with(false);
+        $this->connection->expects($this->never())->method('commit');
+        $this->connection->expects($this->once())->method('rollback');
+
+        $cleanDb->expects(self::once())->method('purgeAuditTables')->with($maxDateAudit)->willReturn(100);
+        $cleanDb->expects(self::once())->method('purgeMessages')->with($maxDate)->willReturn(101);
+        $cleanDb->expects(self::once())->method('purgeNotifications')->with($maxDate)->willReturn(102);
+
+        $this->ui->expects(self::once())->method('print')->with('DB purged - 303 records would be permanently deleted');
+
+        $cleanDb->purgeDb(false);
+    }
+
+    public function testPurgeDbWithError(): void
+    {
+        DatesUtils::setFakeDatetime('2022-01-08 00:00:00');
+
+        $maxDate = DatesUtils::new();
+        $maxDate->sub(new \DateInterval('P60D'));
+
+        $maxDateAudit = DatesUtils::new();
+        $maxDateAudit->sub(new \DateInterval('P180D'));
+
+        $cleanDb = $this->getMockFor('purgeDb');
+
+        $this->connection->expects($this->once())->method('beginTransaction');
+        $this->connection->expects($this->once())->method('setAutoCommit')->with(false);
+        $this->connection->expects($this->never())->method('commit');
+        $this->connection->expects($this->once())->method('rollback');
+
+        $cleanDb->expects(self::once())->method('purgeAuditTables')->with($maxDateAudit)->willThrowException(new \Exception('Error'));
+
+        $this->expectException(\Exception::class);
+
+        $cleanDb->purgeDb(false);
+    }
+
+    public function testPurgeAuditTables(): void
+    {
+        $maxDateAudit = DatesUtils::new('2022-06-30 00:00:00');
+        $maxDateAudit->sub(new \DateInterval('P180D'));
+
+        $cleanDb = $this->getMockFor('purgeAuditTables');
+
+        $schemaManager = $this->getMockBuilder(\Doctrine\DBAL\Schema\AbstractSchemaManager::class)->disableOriginalConstructor()->getMock();
+        $this->connection->method('getSchemaManager')->willReturn($schemaManager);
+
+        $schemaManager->method('listTableNames')->willReturn([
+            'table1',
+            'Audit_table1',
+            'table2',
+            'Audit_table2',
+        ]);
+
+        $stmt1 = $this->getMockBuilder(Statement::class)->disableOriginalConstructor()->getMock();
+        $stmt2 = $this->getMockBuilder(Statement::class)->disableOriginalConstructor()->getMock();
+
+        $this->connection
+            ->expects(self::exactly(2))
+            ->method('prepare')
+            ->willReturnMap([
+                [
+                    'DELETE a, r 
+                     FROM opentalent.Audit_table1 a
+                     INNER JOIN opentalent.revisions r ON r.id = a.rev
+                     WHERE r.timestamp < :maxDate;',
+                    $stmt1,
+                ],
+                [
+                    'DELETE a, r 
+                     FROM opentalent.Audit_table2 a
+                     INNER JOIN opentalent.revisions r ON r.id = a.rev
+                     WHERE r.timestamp < :maxDate;',
+                    $stmt2,
+                ],
+            ]);
+
+        $stmt1
+            ->expects(self::once())
+            ->method('executeStatement')
+            ->with(['maxDate' => '2022-01-01'])
+            ->willReturn(100);
+
+        $stmt2
+            ->expects(self::once())
+            ->method('executeStatement')
+            ->with(['maxDate' => '2022-01-01'])
+            ->willReturn(100);
+
+        $this->assertEquals(200, $cleanDb->purgeAuditTables($maxDateAudit));
+    }
+
+    public function testPurgeMessages(): void
+    {
+        $maxDate = DatesUtils::new('2022-03-02');
+        $maxDate->sub(new \DateInterval('P60D'));
+
+        $cleanDb = $this->getMockFor('purgeMessages');
+
+        $stmt1 = $this->getMockBuilder(Statement::class)->disableOriginalConstructor()->getMock();
+        $stmt2 = $this->getMockBuilder(Statement::class)->disableOriginalConstructor()->getMock();
+
+        $this->connection
+            ->expects(self::exactly(2))
+            ->method('prepare')
+            ->willReturnMap([
+                [
+                    'DELETE r
+                FROM opentalent.Message m
+                inner join opentalent.ReportMessage r on r.message_id = m.id
+                where (m.dateSent < :maxDate or (m.dateSent is null and m.createDate < :maxDate)) and m.isSystem = true and m.id > 0;',
+                    $stmt1,
+                ],
+                [
+                    'DELETE
+                FROM opentalent.Message
+                where (dateSent < :maxDate or (dateSent is null and createDate < :maxDate)) and isSystem = true and id > 0;',
+                    $stmt2,
+                ],
+            ]);
+
+        $stmt1
+            ->expects(self::once())
+            ->method('executeStatement')
+            ->with(['maxDate' => '2022-01-01'])
+            ->willReturn(100);
+
+        $stmt2
+            ->expects(self::once())
+            ->method('executeStatement')
+            ->with(['maxDate' => '2022-01-01'])
+            ->willReturn(100);
+
+        $this->assertEquals(200, $cleanDb->purgeMessages($maxDate));
+    }
+
+    public function testPurgeNotifications(): void
+    {
+        $maxDate = DatesUtils::new('2022-03-02');
+        $maxDate->sub(new \DateInterval('P60D'));
+
+        $cleanDb = $this->getMockFor('purgeNotifications');
+
+        $stmt1 = $this->getMockBuilder(Statement::class)->disableOriginalConstructor()->getMock();
+        $stmt2 = $this->getMockBuilder(Statement::class)->disableOriginalConstructor()->getMock();
+
+        $this->connection
+            ->expects(self::exactly(2))
+            ->method('prepare')
+            ->willReturnMap([
+                [
+                    "DELETE u
+                FROM opentalent.Information i
+                inner join opentalent.NotificationUser u on u.notification_id = i.id
+                where i.createDate < :maxDate and i.discr = 'notification';",
+                    $stmt1,
+                ],
+                [
+                    "DELETE
+                FROM opentalent.Information
+                where createDate < :maxDate and discr = 'notification';",
+                    $stmt2,
+                ],
+            ]);
+
+        $stmt1
+            ->expects(self::once())
+            ->method('executeStatement')
+            ->with(['maxDate' => '2022-01-01'])
+            ->willReturn(100);
+
+        $stmt2
+            ->expects(self::once())
+            ->method('executeStatement')
+            ->with(['maxDate' => '2022-01-01'])
+            ->willReturn(100);
+
+        $this->assertEquals(200, $cleanDb->purgeNotifications($maxDate));
+    }
+}

+ 190 - 81
tests/Unit/Service/Cron/Job/CleanTempFilesTest.php

@@ -3,6 +3,8 @@
 namespace App\Tests\Unit\Service\Cron\Job;
 
 use App\Entity\Core\File;
+use App\Enum\Core\FileHostEnum;
+use App\Enum\Core\FileStatusEnum;
 use App\Repository\Core\FileRepository;
 use App\Service\Cron\Job\CleanTempFiles;
 use App\Service\Cron\UI\CronUIInterface;
@@ -10,6 +12,8 @@ use App\Service\File\Storage\LocalStorage;
 use App\Service\Utils\DatesUtils;
 use Doctrine\DBAL\Connection;
 use Doctrine\ORM\AbstractQuery;
+use Doctrine\ORM\Query\Expr;
+use Doctrine\ORM\Query\Expr\Comparison;
 use Doctrine\ORM\QueryBuilder;
 use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
@@ -27,16 +31,6 @@ class TestableCleanTempFile extends CleanTempFiles
         parent::deleteFiles($files);
     }
 
-    public function purgeDb(\DateTime $maxDate, bool $commit = true): void
-    {
-        parent::purgeDb($maxDate, $commit);
-    }
-
-    public function purgeFiles(\DateTime $maxDate): int
-    {
-        return parent::purgeFiles($maxDate);
-    }
-
     public function getQueryConditions(QueryBuilder $queryBuilder, \DateTime $maxDate): void
     {
         parent::getQueryConditions($queryBuilder, $maxDate);
@@ -61,7 +55,7 @@ class CleanTempFilesTest extends TestCase
         $this->storage = $this->getMockBuilder(LocalStorage::class)->disableOriginalConstructor()->getMock();
     }
 
-    private function getMockFor(string $method)
+    private function getMockFor(string $method): MockObject|TestableCleanTempFile
     {
         $cleanTempFiles = $this->getMockBuilder(TestableCleanTempFile::class)
             ->setConstructorArgs([$this->connection, $this->fileRepository, $this->storage])
@@ -100,8 +94,6 @@ class CleanTempFilesTest extends TestCase
             ['  * /foo/bar']
         );
 
-        $cleanTempFiles->expects(self::once())->method('purgeDb')->with($maxDate, false);
-
         $cleanTempFiles->preview();
     }
 
@@ -119,16 +111,14 @@ class CleanTempFilesTest extends TestCase
             $this->getMockBuilder(File::class)->getMock(),
         ];
 
-        $cleanTempFiles->method('listFilesToDelete')->willReturn($files)->with($maxDate);
+        $cleanTempFiles->method('listFilesToDelete')->with($maxDate)->willReturn($files);
 
         $cleanTempFiles->expects(self::once())->method('deleteFiles')->with($files);
 
-        $cleanTempFiles->expects(self::once())->method('purgeDb')->with($maxDate);
-
         $cleanTempFiles->execute();
     }
 
-    public function testListFilesToDelete()
+    public function testListFilesToDelete(): void
     {
         $cleanTempFiles = $this->getMockFor('listFilesToDelete');
 
@@ -171,101 +161,220 @@ class CleanTempFilesTest extends TestCase
         $this->assertEquals([], $result);
     }
 
-    public function testPurgeDbCommitsTransactionIfCommitIsTrue(): void
+    public function testDeleteFiles(): void
     {
-        $cleanTempFiles = $this->getMockFor('purgeDb');
+        $cleanTempFiles = $this->getMockFor('deleteFiles');
 
-        DatesUtils::setFakeDatetime('2022-01-08 00:00:00');
-        $maxDate = new \DateTime('now');
+        $file1 = $this->getMockBuilder(File::class)->getMock();
+        $file1->method('getId')->willReturn(1);
 
-        $this->connection->expects($this->once())
-            ->method('beginTransaction');
-        $this->connection->expects($this->once())
-            ->method('setAutoCommit')
-            ->with(false);
+        $file2 = $this->getMockBuilder(File::class)->getMock();
+        $file2->method('getId')->willReturn(2);
+
+        $file3 = $this->getMockBuilder(File::class)->getMock();
+        $file3->method('getId')->willReturn(3);
+
+        $files = [$file1, $file2, $file3];
+
+        $this->connection->expects($this->exactly(3))->method('beginTransaction');
+        $this->connection->expects($this->once())->method('setAutoCommit')->with(false);
 
-        $cleanTempFiles->method('purgeFiles')->willReturn(5)->with($maxDate);
+        $queryBuilder = $this->getMockBuilder(QueryBuilder::class)->disableOriginalConstructor()->getMock();
+        $this->fileRepository->expects($this->once())->method('createQueryBuilder')->with('f')->willReturn($queryBuilder);
 
-        $this->connection->expects($this->once())
-            ->method('commit');
+        $queryBuilder
+            ->expects(self::exactly(3))
+            ->method('delete')
+            ->willReturnSelf();
+
+        $queryBuilder
+            ->expects(self::exactly(3))
+            ->method('where')
+            ->with('f.id = :id')
+            ->willReturnSelf();
 
-        $this->ui->expects($this->once())
-            ->method('print')
-            ->with('DB purged - 5 records permanently deleted');
+        $queryBuilder
+            ->expects(self::exactly(3))
+            ->method('setParameter')
+            ->withConsecutive(['id', 1], ['id', 2], ['id', 3]);
 
-        $cleanTempFiles->purgeDb($maxDate);
+        $this->storage
+            ->expects(self::exactly(3))
+            ->method('hardDelete')
+            ->withConsecutive([$file1], [$file1], [$file3]);
+
+        $this->connection->expects($this->exactly(3))->method('commit');
+        $this->connection->expects($this->never())->method('rollback');
+
+        $this->logger->expects(self::atLeastOnce())->method('info')->withConsecutive(
+            ['3 temporary files to be removed'],
+            ['Deleting files...'],
+            ['3 files deleted']
+        );
+
+        $cleanTempFiles->deleteFiles($files);
     }
 
-    public function testPurgeDbRollsbackTransactionIfCommitIsFalse(): void
+    public function testDeleteFilesWithNonBlockingErrors(): void
     {
-        $cleanTempFiles = $this->getMockFor('purgeDb');
+        $cleanTempFiles = $this->getMockFor('deleteFiles');
 
-        DatesUtils::setFakeDatetime('2022-01-08 00:00:00');
-        $maxDate = DatesUtils::new();
+        $file1 = $this->getMockBuilder(File::class)->getMock();
+        $file1->method('getId')->willReturn(1);
+
+        $file2 = $this->getMockBuilder(File::class)->getMock();
+        $file2->method('getId')->willReturn(2);
 
-        $this->connection->expects($this->once())
-            ->method('beginTransaction');
-        $this->connection->expects($this->once())
-            ->method('setAutoCommit')
-            ->with(false);
+        $files = [$file1, $file2];
 
-        $cleanTempFiles->method('purgeFiles')->willReturn(5)->with($maxDate);
+        $this->connection->expects($this->exactly(2))->method('beginTransaction');
+        $this->connection->expects($this->once())->method('setAutoCommit')->with(false);
 
-        $this->connection->expects($this->once())
-            ->method('rollback');
-        $this->ui->expects($this->once())
-            ->method('print')
-            ->with('DB purged - 5 records would be permanently deleted');
+        $queryBuilder = $this->getMockBuilder(QueryBuilder::class)->disableOriginalConstructor()->getMock();
+        $this->fileRepository->expects($this->once())->method('createQueryBuilder')->with('f')->willReturn($queryBuilder);
 
-        $cleanTempFiles->purgeDb($maxDate, false);
+        $queryBuilder
+            ->expects(self::never())
+            ->method('delete')
+            ->willReturnSelf();
+
+        $this->storage
+            ->expects(self::exactly(2))
+            ->method('hardDelete')
+            ->willReturnCallback(function ($file) {
+                switch ($file->getId()) {
+                    case 1:
+                        throw new \RuntimeException('Some error');
+                    case 2:
+                        throw new \InvalidArgumentException('Some other error');
+                }
+            });
+
+        $this->connection->expects($this->never())->method('commit');
+        $this->connection->expects($this->exactly(2))->method('rollback');
+
+        $this->logger->expects(self::atLeastOnce())->method('info')->withConsecutive(
+            ['2 temporary files to be removed'],
+            ['Deleting files...'],
+            ['0 files deleted']
+        );
+
+        $this->logger
+            ->expects(self::exactly(2))
+            ->method('error')
+            ->withConsecutive(
+                ['ERROR : Some error'],
+                ['ERROR : Some other error'],
+            );
+
+        $cleanTempFiles->deleteFiles($files);
     }
 
-    public function testPurgeDbRollsbackTransactionOnException(): void
+    public function testDeleteFilesWithBlockingError(): void
     {
-        $cleanTempFiles = $this->getMockFor('purgeDb');
+        $cleanTempFiles = $this->getMockFor('deleteFiles');
 
-        DatesUtils::setFakeDatetime('2022-01-08 00:00:00');
-        $maxDate = DatesUtils::new();
+        $file1 = $this->getMockBuilder(File::class)->getMock();
+        $file1->method('getId')->willReturn(1);
+
+        $files = [$file1];
+
+        $this->connection->expects($this->once())->method('beginTransaction');
+        $this->connection->expects($this->once())->method('setAutoCommit')->with(false);
 
-        $cleanTempFiles->method('purgeFiles')->willThrowException(new \Exception('error'))->with($maxDate);
+        $queryBuilder = $this->getMockBuilder(QueryBuilder::class)->disableOriginalConstructor()->getMock();
+        $this->fileRepository->expects($this->once())->method('createQueryBuilder')->with('f')->willReturn($queryBuilder);
+
+        $queryBuilder
+            ->expects(self::never())
+            ->method('delete')
+            ->willReturnSelf();
 
-        $this->connection->expects($this->once())
-            ->method('beginTransaction');
-        $this->connection->expects($this->once())
-            ->method('rollback');
-        $this->ui->expects($this->never())
-            ->method('print');
+        $this->storage
+            ->expects(self::exactly(1))
+            ->method('hardDelete')
+            ->willReturnCallback(function ($file) {
+                switch ($file->getId()) {
+                    case 1:
+                        throw new \Exception('Some unknown error');
+                }
+            });
+
+        $this->connection->expects($this->never())->method('commit');
+        $this->connection->expects($this->once())->method('rollback');
+
+        $this->logger->expects(self::atLeastOnce())->method('info')->withConsecutive(
+            ['1 temporary files to be removed'],
+            ['Deleting files...'],
+        );
 
         $this->expectException(\Exception::class);
-        $cleanTempFiles->purgeDb($maxDate, true);
+
+        $cleanTempFiles->deleteFiles($files);
     }
 
-    public function testPurgeFilesDeletes()
+    public function testGetQueryConditions(): void
     {
-        $cleanTempFiles = $this->getMockFor('purgeFiles');
-
         DatesUtils::setFakeDatetime('2022-01-08 00:00:00');
+
+        $cleanTempFiles = $this->getMockFor('getQueryConditions');
+
         $maxDate = DatesUtils::new();
+        $maxDate->sub(new \DateInterval('P60D'));
 
-        $queryBuilder = $this->getMockBuilder(QueryBuilder::class)
-            ->disableOriginalConstructor()
-            ->getMock();
-        $queryBuilder->expects($this->once())
-            ->method('delete')
-            ->willReturnSelf();
+        $queryBuilder = $this->getMockBuilder(QueryBuilder::class)->disableOriginalConstructor()->getMock();
 
-        $query = $this->getMockBuilder(AbstractQuery::class)
-            ->disableOriginalConstructor()
-            ->getMock();
-        $queryBuilder->expects($this->once())
-            ->method('getQuery')
-            ->willReturn($query);
-        $query->expects($this->once())
-            ->method('execute')
-            ->willReturn(3);
+        $expr = $this->getMockBuilder(Expr::class)->disableOriginalConstructor()->getMock();
+        $queryBuilder->method('expr')->willReturn($expr);
 
-        $this->fileRepository->method('createQueryBuilder')->with('f')->willReturn($queryBuilder);
+        $cmp1 = $this->getMockBuilder(Comparison::class)->disableOriginalConstructor()->getMock();
+        $cmp2 = $this->getMockBuilder(Comparison::class)->disableOriginalConstructor()->getMock();
+        $cmp3 = $this->getMockBuilder(Comparison::class)->disableOriginalConstructor()->getMock();
+        $cmp4 = $this->getMockBuilder(Comparison::class)->disableOriginalConstructor()->getMock();
+
+        $expr->expects(self::exactly(3))->method('eq')->willReturnMap(
+            [
+                ['f.isTemporaryFile', ':temporaryTrue', $cmp1],
+                ['f.status', ':status', $cmp2],
+                ['f.host', ':host', $cmp3],
+            ]
+        );
+
+        $expr->expects(self::once())->method('lt')->with('f.createDate', ':maxDate')->willReturn($cmp4);
+
+        $expr->expects(self::once())->method('isNull')->with('f.createDate')->willReturn('f.createDate is null');
+
+        $orX1 = $this->getMockBuilder(Expr\Orx::class)->disableOriginalConstructor()->getMock();
+        $orX2 = $this->getMockBuilder(Expr\Orx::class)->disableOriginalConstructor()->getMock();
+
+        $expr->expects(self::exactly(2))->method('orX')->willReturnMap(
+            [
+                [$cmp1, $cmp2, $orX1],
+                [$cmp4, 'f.createDate is null', $orX2],
+            ]
+        );
+
+        $queryBuilder
+            ->expects(self::exactly(3))
+            ->method('andWhere')
+            ->withConsecutive(
+                [$orX1],
+                [$cmp3],
+                [$orX2],
+            )
+            ->willReturnSelf();
+
+        $queryBuilder
+            ->expects(self::exactly(4))
+            ->method('setParameter')
+            ->withConsecutive(
+                ['temporaryTrue', true],
+                ['host', FileHostEnum::AP2I],
+                ['status', FileStatusEnum::DELETED],
+                ['maxDate', '2021-11-09'],
+            )
+            ->willReturnSelf();
 
-        $this->assertEquals(3, $cleanTempFiles->purgeFiles($maxDate));
+        $cleanTempFiles->getQueryConditions($queryBuilder, $maxDate);
     }
 }

+ 0 - 90
tests/Unit/Service/Doctrine/FiltersConfigurationService.php

@@ -1,90 +0,0 @@
-<?php
-
-namespace App\Tests\Unit\Service\Doctrine;
-
-use App\Filter\Doctrine\TimeConstraint\ActivityYearFilter;
-use App\Filter\Doctrine\TimeConstraint\DatetimeFilter;
-use App\Service\Constraint\ActivityYearConstraint;
-use App\Service\Constraint\DateTimeConstraint;
-use App\Service\Doctrine\FiltersConfigurationService;
-use Doctrine\ORM\EntityManagerInterface;
-use Doctrine\ORM\Query\FilterCollection;
-use PHPUnit\Framework\MockObject\MockObject;
-use PHPUnit\Framework\TestCase;
-
-class TestableFiltersConfigurationService extends FiltersConfigurationService
-{
-    public function getFilters(): FilterCollection
-    {
-        return parent::getFilters();
-    }
-}
-
-class FiltersConfigurationServiceTest extends TestCase
-{
-    private \PHPUnit\Framework\MockObject\MockObject|EntityManagerInterface $em;
-    private DateTimeConstraint|MockObject $dateTimeConstraint;
-    private ActivityYearConstraint|MockObject $activityYearConstraint;
-
-    public function setUp(): void
-    {
-        $this->em = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
-        $this->dateTimeConstraint = $this->getMockBuilder(DateTimeConstraint::class)->disableOriginalConstructor()->getMock();
-        $this->activityYearConstraint = $this->getMockBuilder(ActivityYearConstraint::class)->disableOriginalConstructor()->getMock();
-    }
-
-    public function getSUTMockForMethod(string $methodName): MockObject|TestableFiltersConfigurationService
-    {
-        return $this
-            ->getMockBuilder(TestableFiltersConfigurationService::class)
-            ->setConstructorArgs([$this->em, $this->dateTimeConstraint, $this->activityYearConstraint])
-            ->setMethodsExcept([$methodName])
-            ->getMock();
-    }
-
-    public function testGetFilters(): void
-    {
-        $filterConfigurationService = $this->getSUTMockForMethod('getFilters');
-
-        $filterCollection = $this->getMockBuilder(FilterCollection::class)->disableOriginalConstructor()->getMock();
-
-        $this->em->expects(self::once())->method('getFilters')->willReturn($filterCollection);
-
-        $this->assertEquals(
-            $filterCollection,
-            $filterConfigurationService->getFilters()
-        );
-    }
-
-    public function testConfigureTimeConstraintFilters(): void
-    {
-        $filterConfigurationService = $this->getSUTMockForMethod('configureTimeConstraintFilters');
-
-        $accessId = 123;
-
-        $filterCollection = $this->getMockBuilder(FilterCollection::class)->disableOriginalConstructor()->getMock();
-
-        $filterConfigurationService
-            ->method('getFilters')
-            ->willReturn($filterCollection);
-
-        $datetimeFilter = $this->getMockBuilder(DatetimeFilter::class)->disableOriginalConstructor()->getMock();
-        $activityYearFilter = $this->getMockBuilder(ActivityYearFilter::class)->disableOriginalConstructor()->getMock();
-
-        $filterCollection
-            ->expects(self::exactly(2))
-            ->method('enable')
-            ->willReturnMap([
-                ['date_time_filter', $datetimeFilter],
-                ['activity_year_filter', $activityYearFilter],
-            ]);
-
-        $datetimeFilter->expects(self::once())->method('setAccessId')->with($accessId);
-        $datetimeFilter->expects(self::once())->method('setTimeConstraint')->with($this->dateTimeConstraint);
-
-        $activityYearFilter->expects(self::once())->method('setAccessId')->with($accessId);
-        $activityYearFilter->expects(self::once())->method('setTimeConstraint')->with($this->activityYearConstraint);
-
-        $filterConfigurationService->configureTimeConstraintFilters($accessId);
-    }
-}

+ 213 - 0
tests/Unit/Service/Doctrine/FiltersConfigurationServiceTest.php

@@ -0,0 +1,213 @@
+<?php
+
+namespace App\Tests\Unit\Service\Doctrine;
+
+use App\Filter\Doctrine\TimeConstraint\ActivityYearFilter;
+use App\Filter\Doctrine\TimeConstraint\DatetimeFilter;
+use App\Service\Constraint\ActivityYearConstraint;
+use App\Service\Constraint\DateTimeConstraint;
+use App\Service\Doctrine\FiltersConfigurationService;
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\Query\FilterCollection;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class TestableFiltersConfigurationService extends FiltersConfigurationService
+{
+    public function getFilters(): FilterCollection
+    {
+        return parent::getFilters();
+    }
+
+    public function setPreviousTimeConstraintState(?bool $value): void
+    {
+        $this->previousTimeConstraintState = $value;
+    }
+
+    public function getPreviousTimeConstraintState(): ?bool
+    {
+        return $this->previousTimeConstraintState;
+    }
+}
+
+class FiltersConfigurationServiceTest extends TestCase
+{
+    private \PHPUnit\Framework\MockObject\MockObject|EntityManagerInterface $em;
+    private DateTimeConstraint|MockObject $dateTimeConstraint;
+    private ActivityYearConstraint|MockObject $activityYearConstraint;
+
+    public function setUp(): void
+    {
+        $this->em = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
+        $this->dateTimeConstraint = $this->getMockBuilder(DateTimeConstraint::class)->disableOriginalConstructor()->getMock();
+        $this->activityYearConstraint = $this->getMockBuilder(ActivityYearConstraint::class)->disableOriginalConstructor()->getMock();
+    }
+
+    public function getSUTMockForMethod(string $methodName): MockObject|TestableFiltersConfigurationService
+    {
+        return $this
+            ->getMockBuilder(TestableFiltersConfigurationService::class)
+            ->setConstructorArgs([$this->em, $this->dateTimeConstraint, $this->activityYearConstraint])
+            ->setMethodsExcept([$methodName, 'getPreviousTimeConstraintState', 'setPreviousTimeConstraintState'])
+            ->getMock();
+    }
+
+    public function testGetFilters(): void
+    {
+        $filterConfigurationService = $this->getSUTMockForMethod('getFilters');
+
+        $filterCollection = $this->getMockBuilder(FilterCollection::class)->disableOriginalConstructor()->getMock();
+
+        $this->em->expects(self::once())->method('getFilters')->willReturn($filterCollection);
+
+        $this->assertEquals(
+            $filterCollection,
+            $filterConfigurationService->getFilters()
+        );
+    }
+
+    public function testConfigureTimeConstraintFilters(): void
+    {
+        $filterConfigurationService = $this->getSUTMockForMethod('configureTimeConstraintFilters');
+
+        $accessId = 123;
+
+        $filterCollection = $this->getMockBuilder(FilterCollection::class)->disableOriginalConstructor()->getMock();
+
+        $filterConfigurationService
+            ->method('getFilters')
+            ->willReturn($filterCollection);
+
+        $datetimeFilter = $this->getMockBuilder(DatetimeFilter::class)->disableOriginalConstructor()->getMock();
+        $activityYearFilter = $this->getMockBuilder(ActivityYearFilter::class)->disableOriginalConstructor()->getMock();
+
+        $filterCollection
+            ->expects(self::exactly(2))
+            ->method('enable')
+            ->willReturnMap([
+                ['date_time_filter', $datetimeFilter],
+                ['activity_year_filter', $activityYearFilter],
+            ]);
+
+        $datetimeFilter->expects(self::once())->method('setAccessId')->with($accessId);
+        $datetimeFilter->expects(self::once())->method('setTimeConstraint')->with($this->dateTimeConstraint);
+
+        $activityYearFilter->expects(self::once())->method('setAccessId')->with($accessId);
+        $activityYearFilter->expects(self::once())->method('setTimeConstraint')->with($this->activityYearConstraint);
+
+        $filterConfigurationService->configureTimeConstraintFilters($accessId);
+    }
+
+    public function testSuspendTimeConstraintFilters(): void
+    {
+        $filterConfigurationService = $this->getSUTMockForMethod('suspendTimeConstraintFilters');
+
+        $filterCollection = $this->getMockBuilder(FilterCollection::class)->disableOriginalConstructor()->getMock();
+
+        $this->em->expects(self::once())->method('getFilters')->willReturn($filterCollection);
+
+        $filterCollection->method('isEnabled')->with('date_time_filter')->willReturn(true);
+
+        $datetimeFilter = $this->getMockBuilder(DatetimeFilter::class)->disableOriginalConstructor()->getMock();
+
+        $filterCollection->method('getFilter')->with('date_time_filter')->willReturn($datetimeFilter);
+
+        $datetimeFilter->method('isDisabled')->willReturn(false);
+        $datetimeFilter->expects(self::once())->method('setDisabled')->with(true);
+
+        $filterConfigurationService->suspendTimeConstraintFilters();
+
+        $this->assertEquals(
+            true,
+            $filterConfigurationService->getPreviousTimeConstraintState()
+        );
+    }
+
+    public function testSuspendTimeConstraintFiltersAlreadySuspended(): void
+    {
+        $filterConfigurationService = $this->getSUTMockForMethod('suspendTimeConstraintFilters');
+
+        $filterConfigurationService->setPreviousTimeConstraintState(true);
+
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionMessage('date_time_filter is already suspended');
+
+        $filterConfigurationService->suspendTimeConstraintFilters();
+    }
+
+    public function testSuspendTimeConstraintFiltersAlreadyDisabled(): void
+    {
+        $filterConfigurationService = $this->getSUTMockForMethod('suspendTimeConstraintFilters');
+
+        $filterCollection = $this->getMockBuilder(FilterCollection::class)->disableOriginalConstructor()->getMock();
+
+        $this->em->expects(self::once())->method('getFilters')->willReturn($filterCollection);
+
+        $filterCollection->method('isEnabled')->with('date_time_filter')->willReturn(false);
+
+        $filterConfigurationService->suspendTimeConstraintFilters();
+
+        $this->assertEquals(
+            false,
+            $filterConfigurationService->getPreviousTimeConstraintState()
+        );
+    }
+
+    public function testRestoreTimeConstraintFilters(): void
+    {
+        $filterConfigurationService = $this->getSUTMockForMethod('restoreTimeConstraintFilters');
+
+        $filterConfigurationService->setPreviousTimeConstraintState(true);
+
+        $filterCollection = $this->getMockBuilder(FilterCollection::class)->disableOriginalConstructor()->getMock();
+
+        $this->em->expects(self::once())->method('getFilters')->willReturn($filterCollection);
+
+        $filterCollection->method('isEnabled')->with('date_time_filter')->willReturn(true);
+
+        $datetimeFilter = $this->getMockBuilder(DatetimeFilter::class)->disableOriginalConstructor()->getMock();
+
+        $filterCollection->method('getFilter')->with('date_time_filter')->willReturn($datetimeFilter);
+
+        $datetimeFilter->expects(self::once())->method('setDisabled')->with(true);
+
+        $filterConfigurationService->restoreTimeConstraintFilters();
+
+        $this->assertEquals(
+            $filterConfigurationService->getPreviousTimeConstraintState(),
+            null
+        );
+    }
+
+    public function testRestoreTimeConstraintFiltersNotSuspended(): void
+    {
+        $filterConfigurationService = $this->getSUTMockForMethod('restoreTimeConstraintFilters');
+
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionMessage('date_time_filter has not been suspended, can not be restored');
+
+        $filterConfigurationService->restoreTimeConstraintFilters();
+    }
+
+    public function testRestoreTimeConstraintFiltersNotEnabled(): void
+    {
+        $filterConfigurationService = $this->getSUTMockForMethod('restoreTimeConstraintFilters');
+
+        $filterConfigurationService->setPreviousTimeConstraintState(true);
+
+        $filterCollection = $this->getMockBuilder(FilterCollection::class)->disableOriginalConstructor()->getMock();
+
+        $this->em->expects(self::once())->method('getFilters')->willReturn($filterCollection);
+
+        $filterCollection->method('isEnabled')->with('date_time_filter')->willReturn(false);
+
+        $filterCollection->expects(self::never())->method('getFilter')->with('date_time_filter');
+
+        $filterConfigurationService->restoreTimeConstraintFilters();
+
+        $this->assertEquals(
+            $filterConfigurationService->getPreviousTimeConstraintState(),
+            null
+        );
+    }
+}

+ 1 - 1
tests/Unit/Service/Dolibarr/DolibarrAccountCreatorTest.php

@@ -118,7 +118,7 @@ class DolibarrAccountCreatorTest extends TestCase
         $accountData = [
             'id' => '2',
             'code_client' => 'C2',
-            'array_options' => ['2iopen_software_opentalent' => 'Opentalent Artist'],
+            'array_options' => ['options_2iopen_software_opentalent' => 'Opentalent Artist'],
         ];
 
         $dolibarrAccount = $dolibarrAccountCreator->createDolibarrAccount($organizationId, $accountData);

+ 80 - 34
tests/Unit/Service/File/FileManagerTest.php

@@ -8,16 +8,15 @@ use ApiPlatform\Metadata\Get;
 use App\Entity\Access\Access;
 use App\Entity\Core\File;
 use App\Entity\Organization\Organization;
-use App\Enum\Core\FileHostEnum;
 use App\Enum\Core\FileTypeEnum;
 use App\Enum\Core\FileVisibilityEnum;
-use App\Service\File\Exception\FileNotFoundException;
+use App\Repository\Core\FileRepository;
 use App\Service\File\Factory\ImageFactory;
 use App\Service\File\FileManager;
-use App\Service\File\Storage\ApiLegacyStorage;
 use App\Service\File\Storage\FileStorageInterface;
 use App\Service\File\Storage\LocalStorage;
 use App\Service\ServiceIterator\StorageIterator;
+use Doctrine\ORM\EntityManagerInterface;
 use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 
@@ -34,46 +33,39 @@ class FileManagerTest extends TestCase
         $this->storageIterator = $this->getMockBuilder(StorageIterator::class)->disableOriginalConstructor()->getMock();
         $this->imageFactory = $this->getMockBuilder(ImageFactory::class)->disableOriginalConstructor()->getMock();
         $this->localStorage = $this->getMockBuilder(LocalStorage::class)->disableOriginalConstructor()->getMock();
+        $this->entityManager = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
+        $this->fileRepository = $this->getMockBuilder(FileRepository::class)->disableOriginalConstructor()->getMock();
     }
 
     public function getFileManagerMockFor(string $methodName): FileManager|MockObject
     {
         return $this->getMockBuilder(FileManager::class)
-            ->setConstructorArgs([$this->iriConverter, $this->storageIterator, $this->imageFactory, $this->localStorage])
+            ->setConstructorArgs([
+                $this->iriConverter,
+                $this->storageIterator,
+                $this->imageFactory,
+                $this->localStorage,
+                $this->entityManager,
+                $this->fileRepository,
+            ])
             ->setMethodsExcept([$methodName])
             ->getMock();
     }
 
-    //    public function testGetStorageFor(): void {
-    //        $fileManager = $this->getFileManagerMockFor('getStorageFor');
-    //
-    //        $file1 = $this->getMockBuilder(File::class)->getMock();
-    //        $file1->method('getHost')->willReturn(FileHostEnum::API1()->getValue());
-    //
-    //        $file2 = $this->getMockBuilder(File::class)->getMock();
-    //        $file2->method('getHost')->willReturn(FileHostEnum::AP2I()->getValue());
-    //
-    //        $this->assertInstanceOf(
-    //            ApiLegacyStorage::class,
-    //            $fileManager->getStorageFor($file1)
-    //        );
-    //
-    //        $this->assertInstanceOf(
-    //            LocalStorage::class,
-    //            $fileManager->getStorageFor($file2)
-    //        );
-    //    }
-    //
-    //    public function testGetStorageForUnknown(): void {
-    //        $fileManager = $this->getFileManagerMockFor('getStorageFor');
-    //
-    //        $file = $this->getMockBuilder(File::class)->getMock();
-    //        $file->method('getHost')->willReturn('unknown');
-    //
-    //        $this->expectException(FileNotFoundException::class);
-    //
-    //        $fileManager->getStorageFor($file);
-    //    }
+    public function testGetStorageFor(): void
+    {
+        $fileManager = $this->getFileManagerMockFor('getStorageFor');
+
+        $file = $this->getMockBuilder(File::class)->getMock();
+        $fileStorage = $this->getMockBuilder(FileStorageInterface::class)->getMock();
+
+        $this->storageIterator->expects(self::once())->method('getStorageFor')->with($file)->willReturn($fileStorage);
+
+        $this->assertEquals(
+            $fileStorage,
+            $fileManager->getStorageFor($file)
+        );
+    }
 
     public function testRead(): void
     {
@@ -92,6 +84,26 @@ class FileManagerTest extends TestCase
         );
     }
 
+    public function testGetImageUrl(): void
+    {
+        $fileManager = $this->getFileManagerMockFor('getImageUrl');
+
+        $file = $this->getMockBuilder(File::class)->getMock();
+        $fileStorage = $this->getMockBuilder(FileStorageInterface::class)->getMock();
+
+        $fileManager->expects(self::once())->method('getStorageFor')->with($file)->willReturn($fileStorage);
+
+        $fileStorage
+            ->method('getImageUrl')
+            ->with($file, 'md', true)
+            ->willReturn('foo');
+
+        $this->assertEquals(
+            'foo',
+            $fileManager->getImageUrl($file, 'md', true)
+        );
+    }
+
     public function testPrepareFile(): void
     {
         $fileManager = $this->getFileManagerMockFor('prepareFile');
@@ -171,4 +183,38 @@ class FileManagerTest extends TestCase
             $fileManager->getDownloadIri($file)
         );
     }
+
+    public function testDeleteOrganizationFiles(): void
+    {
+        $fileManager = $this->getFileManagerMockFor('deleteOrganizationFiles');
+
+        $fileStorage1 = $this->getMockBuilder(FileStorageInterface::class)->getMock();
+        $fileStorage2 = $this->getMockBuilder(FileStorageInterface::class)->getMock();
+
+        $this->storageIterator->method('getStorages')->willReturn([$fileStorage1, $fileStorage2]);
+
+        $fileStorage1->expects(self::once())->method('deleteOrganizationFiles')->with(123);
+        $fileStorage2->expects(self::once())->method('deleteOrganizationFiles')->with(123);
+
+        $this->fileRepository->expects(self::once())->method('deleteByOrganization')->with(123);
+
+        $fileManager->deleteOrganizationFiles(123);
+    }
+
+    public function testDeletePersonFiles(): void
+    {
+        $fileManager = $this->getFileManagerMockFor('deletePersonFiles');
+
+        $fileStorage1 = $this->getMockBuilder(FileStorageInterface::class)->getMock();
+        $fileStorage2 = $this->getMockBuilder(FileStorageInterface::class)->getMock();
+
+        $this->storageIterator->method('getStorages')->willReturn([$fileStorage1, $fileStorage2]);
+
+        $fileStorage1->expects(self::once())->method('deletePersonFiles')->with(123);
+        $fileStorage2->expects(self::once())->method('deletePersonFiles')->with(123);
+
+        $this->fileRepository->expects(self::once())->method('deleteByPerson')->with(123);
+
+        $fileManager->deletePersonFiles(123);
+    }
 }

+ 99 - 18
tests/Unit/Service/File/Storage/ApiLegacyStorageTest.php

@@ -3,25 +3,41 @@
 namespace App\Tests\Unit\Service\File\Storage;
 
 use App\Entity\Core\File;
+use App\Enum\Core\FileHostEnum;
 use App\Service\ApiLegacy\ApiLegacyRequestService;
 use App\Service\File\Storage\ApiLegacyStorage;
 use App\Service\Utils\UrlBuilder;
 use Liip\ImagineBundle\Imagine\Data\DataManager;
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 
 class ApiLegacyStorageTest extends TestCase
 {
-    public function testExists(): void
+    private ApiLegacyRequestService|MockObject $apiLegacyRequestService;
+    private ApiLegacyRequestService|MockObject $dataManager;
+    private ApiLegacyRequestService|MockObject $urlBuilder;
+
+    public function setUp(): void
     {
-        $apiLegacyRequestService = $this->getMockBuilder(ApiLegacyRequestService::class)
+        $this->apiLegacyRequestService = $this->getMockBuilder(ApiLegacyRequestService::class)
             ->disableOriginalConstructor()
             ->getMock();
+        $this->dataManager = $this->getMockBuilder(DataManager::class)->disableOriginalConstructor()->getMock();
+        $this->urlBuilder = $this->getMockBuilder(UrlBuilder::class)->disableOriginalConstructor()->getMock();
+    }
 
-        $apiLegacyStorage = $this
+    public function getApiLegacyStorageMockFor(string $methodName): ApiLegacyStorage|MockObject
+    {
+        return $this
             ->getMockBuilder(ApiLegacyStorage::class)
-            ->disableOriginalConstructor()
-            ->setMethodsExcept(['exists'])
+            ->setConstructorArgs([$this->apiLegacyRequestService, $this->dataManager, $this->urlBuilder, 'url', 'publicUrl'])
+            ->setMethodsExcept([$methodName])
             ->getMock();
+    }
+
+    public function testExists(): void
+    {
+        $apiLegacyStorage = $this->getApiLegacyStorageMockFor('exists');
 
         $this->expectException(\RuntimeException::class);
         $this->expectExceptionMessage('not implemented error');
@@ -33,23 +49,12 @@ class ApiLegacyStorageTest extends TestCase
 
     public function testRead(): void
     {
-        $apiLegacyRequestService = $this->getMockBuilder(ApiLegacyRequestService::class)
-            ->disableOriginalConstructor()
-            ->getMock();
-
-        $dataManager = $this->getMockBuilder(DataManager::class)->disableOriginalConstructor()->getMock();
-        $urlBuilder = $this->getMockBuilder(UrlBuilder::class)->disableOriginalConstructor()->getMock();
-
-        $apiLegacyStorage = $this
-            ->getMockBuilder(ApiLegacyStorage::class)
-            ->setConstructorArgs([$apiLegacyRequestService, $dataManager, $urlBuilder, 'url', 'publicUrl'])
-            ->setMethodsExcept(['read'])
-            ->getMock();
+        $apiLegacyStorage = $this->getApiLegacyStorageMockFor('read');
 
         $file = $this->getMockBuilder(File::class)->getMock();
         $file->method('getId')->willReturn(123);
 
-        $apiLegacyRequestService
+        $this->apiLegacyRequestService
             ->expects(self::once())
             ->method('getContent')
             ->with('_internal/secure/files/123')
@@ -59,4 +64,80 @@ class ApiLegacyStorageTest extends TestCase
 
         $this->assertEquals('xyz', $result);
     }
+
+    public function testGetImageUrl(): void
+    {
+        $apiLegacyStorage = $this->getApiLegacyStorageMockFor('getImageUrl');
+
+        $file = $this->getMockBuilder(File::class)->getMock();
+        $file->method('getId')->willReturn(123);
+
+        $this->apiLegacyRequestService
+            ->expects(self::once())
+            ->method('getContent')
+            ->with('api/files/123/download/md?relativePath=1')
+            ->willReturn('xyz');
+
+        $this->assertEquals(
+            'publicUrl/xyz',
+            $apiLegacyStorage->getImageUrl($file, 'md', false)
+        );
+    }
+
+    public function testGetImageUrlRelativePath(): void
+    {
+        $apiLegacyStorage = $this->getApiLegacyStorageMockFor('getImageUrl');
+
+        $file = $this->getMockBuilder(File::class)->getMock();
+        $file->method('getId')->willReturn(123);
+
+        $this->apiLegacyRequestService
+            ->expects(self::once())
+            ->method('getContent')
+            ->with('api/files/123/download/lg?relativePath=1')
+            ->willReturn('xyz');
+
+        $this->assertEquals(
+            'url/xyz',
+            $apiLegacyStorage->getImageUrl($file, 'lg', true)
+        );
+    }
+
+    public function testSupport(): void
+    {
+        $apiLegacyStorage = $this->getApiLegacyStorageMockFor('support');
+
+        $file1 = $this->getMockBuilder(File::class)->getMock();
+        $file1->method('getHost')->willReturn(FileHostEnum::API1);
+
+        $file2 = $this->getMockBuilder(File::class)->getMock();
+        $file2->method('getHost')->willReturn(FileHostEnum::AP2I);
+
+        $this->assertTrue($apiLegacyStorage->support($file1));
+        $this->assertFalse($apiLegacyStorage->support($file2));
+    }
+
+    public function testDeleteOrganizationFiles(): void
+    {
+        $apiLegacyStorage = $this->getApiLegacyStorageMockFor('deleteOrganizationFiles');
+
+        $this->apiLegacyRequestService
+            ->expects(self::once())
+            ->method('get')
+            ->with('/_internal/request/organization-files/delete/123');
+
+        $apiLegacyStorage->deleteOrganizationFiles(123);
+    }
+
+    public function testDeletePersonFiles(): void
+    {
+        $apiLegacyStorage = $this->getApiLegacyStorageMockFor('deletePersonFiles');
+
+        $this->apiLegacyRequestService
+            ->expects(self::once())
+            ->method('get')
+            ->with('/_internal/request/person-files/delete/123');
+
+        $apiLegacyStorage->deletePersonFiles(123);
+    }
 }

+ 262 - 0
tests/Unit/Service/File/Storage/LocalStorageTest.php

@@ -6,6 +6,8 @@ use App\Entity\Access\Access;
 use App\Entity\Core\File;
 use App\Entity\Organization\Organization;
 use App\Entity\Person\Person;
+use App\Enum\Core\FileHostEnum;
+use App\Enum\Core\FileSizeEnum;
 use App\Enum\Core\FileStatusEnum;
 use App\Enum\Core\FileTypeEnum;
 use App\Enum\Core\FileVisibilityEnum;
@@ -37,6 +39,16 @@ class TestableLocalStorage extends LocalStorage
     {
         return parent::getOrganizationAndPersonFromOwner($owner);
     }
+
+    public function getFilterFromSizeAndConfig(string $size, bool $configExist): string
+    {
+        return parent::getFilterFromSizeAndConfig($size, $configExist);
+    }
+
+    public function rrmDir(string $dirKey): void
+    {
+        parent::rrmDir($dirKey);
+    }
 }
 
 class LocalStorageTest extends TestCase
@@ -78,6 +90,7 @@ class LocalStorageTest extends TestCase
                 $this->imageFactory,
                 $this->fileUtils,
                 $this->urlBuilder,
+                '/file/storage/dir/',
             ])
             ->setMethodsExcept([$methodName])
             ->getMock();
@@ -150,6 +163,211 @@ class LocalStorageTest extends TestCase
         );
     }
 
+    public function testGetImageUrl(): void
+    {
+        $localStorage = $this->getMockForMethod('getImageUrl');
+
+        $localStorage
+            ->method('getFilterFromSizeAndConfig')
+            ->with('sm', true)
+            ->willReturn('crop_sm');
+
+        $file = $this->getMockBuilder(File::class)->getMock();
+        $file->method('getPath')->willReturn('abc');
+        $file->method('getConfig')->willReturn('xyz');
+
+        $this->cacheManager
+            ->method('isStored')
+            ->with('abc', 'crop_sm')
+            ->willReturn(false);
+
+        $this->cacheManager
+            ->method('resolve')
+            ->with('abc', 'crop_sm')
+            ->willReturn('publicUrl/xyz');
+
+        $this->assertEquals(
+            'publicUrl/xyz',
+            $localStorage->getImageUrl($file, 'sm', false)
+        );
+    }
+
+    public function testGetImageUrlRelativePath(): void
+    {
+        $localStorage = $this->getMockForMethod('getImageUrl');
+
+        $localStorage
+            ->method('getFilterFromSizeAndConfig')
+            ->with('sm', true)
+            ->willReturn('crop_sm');
+
+        $file = $this->getMockBuilder(File::class)->getMock();
+        $file->method('getPath')->willReturn('abc');
+        $file->method('getConfig')->willReturn('xyz');
+
+        $this->cacheManager
+            ->method('isStored')
+            ->with('abc', 'crop_sm')
+            ->willReturn(false);
+
+        $this->cacheManager
+            ->method('resolve')
+            ->with('abc', 'crop_sm')
+            ->willReturn('publicUrl/xyz');
+
+        $this->urlBuilder
+            ->method('getRelativeUrl')
+            ->with('publicUrl/xyz')
+            ->willReturn('xyz');
+
+        $this->assertEquals(
+            'xyz',
+            $localStorage->getImageUrl($file, 'sm', true)
+        );
+    }
+
+    public function testGetImageUrlNotCached(): void
+    {
+        $localStorage = $this->getMockForMethod('getImageUrl');
+
+        $localStorage
+            ->method('getFilterFromSizeAndConfig')
+            ->with('sm', true)
+            ->willReturn('crop_sm');
+
+        $file = $this->getMockBuilder(File::class)->getMock();
+        $file->method('getPath')->willReturn('abc');
+        $file->method('getConfig')->willReturn('xyz');
+
+        $this->cacheManager
+            ->method('isStored')
+            ->with('abc', 'crop_sm')
+            ->willReturn(false);
+
+        $this->imageFactory
+            ->expects(self::once())
+            ->method('createImageContent')
+            ->with($file, 'crop_sm');
+
+        $this->cacheManager
+            ->method('resolve')
+            ->with('abc', 'crop_sm')
+            ->willReturn('publicUrl/xyz');
+
+        $this->assertEquals(
+            'publicUrl/xyz',
+            $localStorage->getImageUrl($file, 'sm', false)
+        );
+    }
+
+    public function testGetImageUrlNotCachedMissingFile(): void
+    {
+        $localStorage = $this->getMockForMethod('getImageUrl');
+
+        $localStorage
+            ->method('getFilterFromSizeAndConfig')
+            ->with('sm', true)
+            ->willReturn('crop_sm');
+
+        $file = $this->getMockBuilder(File::class)->getMock();
+        $file->method('getPath')->willReturn('abc');
+        $file->method('getConfig')->willReturn('xyz');
+
+        $this->cacheManager
+            ->method('isStored')
+            ->with('abc', 'crop_sm')
+            ->willReturn(false);
+
+        $this->imageFactory
+            ->expects(self::once())
+            ->method('createImageContent')
+            ->with($file, 'crop_sm')
+            ->willThrowException(new \Exception('File not found'));
+
+        $this->cacheManager
+            ->expects(self::never())
+            ->method('resolve');
+
+        $this->urlBuilder
+            ->method('getAbsoluteUrl')
+            ->with('images/missing-file.png')
+            ->willReturn('publicUrl/images/missing-file.png');
+
+        $this->assertEquals(
+            'publicUrl/images/missing-file.png',
+            $localStorage->getImageUrl($file, 'sm', false)
+        );
+    }
+
+    public function testGetImageUrlNotCachedMissingFileRelativePath(): void
+    {
+        $localStorage = $this->getMockForMethod('getImageUrl');
+
+        $localStorage
+            ->method('getFilterFromSizeAndConfig')
+            ->with('sm', true)
+            ->willReturn('crop_sm');
+
+        $file = $this->getMockBuilder(File::class)->getMock();
+        $file->method('getPath')->willReturn('abc');
+        $file->method('getConfig')->willReturn('xyz');
+
+        $this->cacheManager
+            ->method('isStored')
+            ->with('abc', 'crop_sm')
+            ->willReturn(false);
+
+        $this->imageFactory
+            ->expects(self::once())
+            ->method('createImageContent')
+            ->with($file, 'crop_sm')
+            ->willThrowException(new \Exception('File not found'));
+
+        $this->cacheManager
+            ->expects(self::never())
+            ->method('resolve');
+
+        $this->assertEquals(
+            'images/missing-file.png',
+            $localStorage->getImageUrl($file, 'sm', true)
+        );
+    }
+
+    public function testGetFilterFromSizeAndConfig(): void
+    {
+        $fileStorage = $this->getMockForMethod('getFilterFromSizeAndConfig');
+
+        $this->assertEquals(
+            'crop_sm',
+            $fileStorage->getFilterFromSizeAndConfig(FileSizeEnum::SM->value, true)
+        );
+
+        $this->assertEquals(
+            'sm',
+            $fileStorage->getFilterFromSizeAndConfig(FileSizeEnum::SM->value, false)
+        );
+
+        $this->assertEquals(
+            'crop_md',
+            $fileStorage->getFilterFromSizeAndConfig(FileSizeEnum::MD->value, true)
+        );
+
+        $this->assertEquals(
+            'md',
+            $fileStorage->getFilterFromSizeAndConfig(FileSizeEnum::MD->value, false)
+        );
+
+        $this->assertEquals(
+            'crop_lg',
+            $fileStorage->getFilterFromSizeAndConfig(FileSizeEnum::LG->value, true)
+        );
+
+        $this->assertEquals(
+            'lg',
+            $fileStorage->getFilterFromSizeAndConfig(FileSizeEnum::LG->value, false)
+        );
+    }
+
     /**
      * @see LocalStorage::prepareFile()
      */
@@ -524,6 +742,50 @@ class LocalStorageTest extends TestCase
         $fileStorage->hardDelete($file);
     }
 
+    public function testDeleteOrganizationFiles(): void
+    {
+        $fileStorage = $this->getMockForMethod('deleteOrganizationFiles');
+
+        $fileStorage
+            ->expects(self::exactly(2))
+            ->method('rrmDir')
+            ->withConsecutive(
+                ['organization/123'],
+                ['temp/organization/123'],
+            );
+
+        $fileStorage->deleteOrganizationFiles(123);
+    }
+
+    public function testDeletePersonFiles(): void
+    {
+        $fileStorage = $this->getMockForMethod('deletePersonFiles');
+
+        $fileStorage
+            ->expects(self::exactly(2))
+            ->method('rrmDir')
+            ->withConsecutive(
+                ['person/123'],
+                ['temp/person/123'],
+            );
+
+        $fileStorage->deletePersonFiles(123);
+    }
+
+    public function testSupport(): void
+    {
+        $apiLegacyStorage = $this->getMockForMethod('support');
+
+        $file1 = $this->getMockBuilder(File::class)->getMock();
+        $file1->method('getHost')->willReturn(FileHostEnum::API1);
+
+        $file2 = $this->getMockBuilder(File::class)->getMock();
+        $file2->method('getHost')->willReturn(FileHostEnum::AP2I);
+
+        $this->assertFalse($apiLegacyStorage->support($file1));
+        $this->assertTrue($apiLegacyStorage->support($file2));
+    }
+
     /**
      * @see LocalStorage::getPrefix()
      */

+ 122 - 73
tests/Unit/Service/Organization/OrganizationFactoryTest.php

@@ -38,6 +38,7 @@ use App\Repository\Organization\OrganizationRepository;
 use App\Repository\Person\PersonRepository;
 use App\Service\ApiLegacy\ApiLegacyRequestService;
 use App\Service\Dolibarr\DolibarrApiService;
+use App\Service\File\FileManager;
 use App\Service\Organization\OrganizationFactory;
 use App\Service\Organization\Utils as OrganizationUtils;
 use App\Service\Typo3\BindFileService;
@@ -152,34 +153,19 @@ class TestableOrganizationFactory extends OrganizationFactory
         return parent::normalizeIdentificationField($value);
     }
 
-    public function deleteOrganizationAccesses(Organization $organization): void
+    public function deleteTypo3Website(int $organizationId): void
     {
-        parent::deleteOrganizationAccesses($organization);
+        parent::deleteTypo3Website($organizationId);
     }
 
-    public function deleteTypo3Website(Organization $organization): void
+    public function switchDolibarrSocietyToProspect(int $organizationId): void
     {
-        parent::deleteTypo3Website($organization);
+        parent::switchDolibarrSocietyToProspect($organizationId);
     }
 
-    public function switchDolibarrSocietyToProspect(Organization $organization): void
+    public function getFutureOrphanPersons(Organization $organization): array
     {
-        parent::switchDolibarrSocietyToProspect($organization);
-    }
-
-    public function deleteOrganizationFiles(Organization $organization): void
-    {
-        parent::deleteOrganizationFiles($organization);
-    }
-
-    public function deleteDirectoriesV1(Organization $organization): void
-    {
-        parent::deleteDirectoriesV1($organization);
-    }
-
-    public function deleteDirectories59(Organization $organization): void
-    {
-        parent::deleteDirectories59($organization);
+        return parent::getFutureOrphanPersons($organization);
     }
 }
 
@@ -216,6 +202,7 @@ class OrganizationFactoryTest extends TestCase
         $this->apiLegacyRequestService = $this->getMockBuilder(ApiLegacyRequestService::class)->disableOriginalConstructor()->getMock();
         $this->phoneNumberUtil = $this->getMockBuilder(PhoneNumberUtil::class)->disableOriginalConstructor()->getMock();
         $this->functionTypeRepository = $this->getMockBuilder(FunctionTypeRepository::class)->disableOriginalConstructor()->getMock();
+        $this->fileManager = $this->getMockBuilder(FileManager::class)->disableOriginalConstructor()->getMock();
     }
 
     public function tearDown(): void
@@ -241,6 +228,7 @@ class OrganizationFactoryTest extends TestCase
                     $this->organizationIdentificationRepository,
                     $this->apiLegacyRequestService,
                     $this->functionTypeRepository,
+                    $this->fileManager,
                 ])
             ->setMethodsExcept(['setLoggerInterface', 'setPhoneNumberUtil', $methodName])
             ->getMock();
@@ -1019,7 +1007,7 @@ class OrganizationFactoryTest extends TestCase
         $organizationFactory->makeOrganizationWithRelations($organizationCreationRequest);
     }
 
-    public function testMakeOrganization()
+    public function testMakeOrganization(): void
     {
         $organizationFactory = $this->getOrganizationFactoryMockFor('makeOrganization');
 
@@ -1130,7 +1118,7 @@ class OrganizationFactoryTest extends TestCase
         );
     }
 
-    public function testMakePostalAddressUnexistingCountry(): void
+    public function testMakePostalAddressNonExistingCountry(): void
     {
         $organizationFactory = $this->getOrganizationFactoryMockFor('makePostalAddress');
 
@@ -1596,7 +1584,7 @@ class OrganizationFactoryTest extends TestCase
         );
     }
 
-    public function testMakeAccessPostalAddress()
+    public function testMakeAccessPostalAddress(): void
     {
         $organizationFactory = $this->getOrganizationFactoryMockFor('makePersonPostalAddress');
 
@@ -1893,8 +1881,8 @@ class OrganizationFactoryTest extends TestCase
 
         $this->apiLegacyRequestService
             ->expects(self::once())
-            ->method('post')
-            ->with('/_internal/secure/organization/creation-event', ['organizationId' => 123])
+            ->method('get')
+            ->with('/_internal/request/adminassos/create/organization/123')
             ->willReturn($response);
 
         $organization = $this->getMockBuilder(Organization::class)->getMock();
@@ -1913,8 +1901,8 @@ class OrganizationFactoryTest extends TestCase
 
         $this->apiLegacyRequestService
             ->expects(self::once())
-            ->method('post')
-            ->with('/_internal/secure/organization/creation-event', ['organizationId' => 123])
+            ->method('get')
+            ->with('/_internal/request/adminassos/create/organization/123')
             ->willReturn($response);
 
         $organization = $this->getMockBuilder(Organization::class)->getMock();
@@ -1935,6 +1923,7 @@ class OrganizationFactoryTest extends TestCase
             $organizationFactory->normalizeIdentificationField("C'est une phrase normalisée.")
         );
     }
+
     public function testDelete(): void
     {
         $organizationFactory = $this->getOrganizationFactoryMockFor('delete');
@@ -1959,18 +1948,37 @@ class OrganizationFactoryTest extends TestCase
         $this->entityManager->expects(self::once())->method('commit');
         $this->entityManager->expects(self::never())->method('rollback');
 
-        $organizationFactory->expects(self::once())->method('deleteOrganizationAccesses')->with($organization);
+        $person1 = $this->getMockBuilder(Person::class)->getMock();
+        $person1->method('getId')->willReturn(1);
+
+        $person2 = $this->getMockBuilder(Person::class)->getMock();
+        $person2->method('getId')->willReturn(2);
+
+        $person3 = $this->getMockBuilder(Person::class)->getMock();
+        $person3->method('getId')->willReturn(3);
 
-        $this->entityManager->expects(self::exactly(2))->method('remove')->withConsecutive(
+        $organizationFactory
+            ->method('getFutureOrphanPersons')
+            ->with($organization)
+            ->willReturn([$person1, $person2, $person3]);
+
+        $this->entityManager->expects(self::exactly(5))->method('remove')->withConsecutive(
             [$parameters],
-            [$organization]
+            [$organization],
+            [$person1],
+            [$person2],
+            [$person3],
         );
 
-        $organizationFactory->expects(self::once())->method('deleteTypo3Website')->with($organization);
-        $organizationFactory->expects(self::once())->method('switchDolibarrSocietyToProspect')->with($organization);
-        $organizationFactory->expects(self::once())->method('deleteOrganizationFiles')->with($organization);
-        $organizationFactory->expects(self::once())->method('deleteDirectoriesV1')->with($organization);
-        $organizationFactory->expects(self::once())->method('deleteDirectories59')->with($organization);
+        $organizationFactory->expects(self::once())->method('deleteTypo3Website')->with(123);
+        $organizationFactory->expects(self::once())->method('switchDolibarrSocietyToProspect')->with(123);
+
+        $this->fileManager->expects(self::once())->method('deleteOrganizationFiles')->with(123);
+
+        $this->fileManager
+            ->expects(self::exactly(3))
+            ->method('deletePersonFiles')
+            ->withConsecutive([1], [2], [3]);
 
         $organizationDeletionRequest
             ->expects(self::once())
@@ -1997,6 +2005,20 @@ class OrganizationFactoryTest extends TestCase
         $organization = $this->getMockBuilder(Organization::class)->getMock();
         $organization->method('getParameters')->willReturn($parameters);
 
+        $person1 = $this->getMockBuilder(Person::class)->getMock();
+        $person1->method('getId')->willReturn(1);
+
+        $person2 = $this->getMockBuilder(Person::class)->getMock();
+        $person2->method('getId')->willReturn(2);
+
+        $person3 = $this->getMockBuilder(Person::class)->getMock();
+        $person3->method('getId')->willReturn(3);
+
+        $organizationFactory
+            ->method('getFutureOrphanPersons')
+            ->with($organization)
+            ->willReturn([$person1, $person2, $person3]);
+
         $this->organizationRepository
             ->expects(self::once())
             ->method('find')
@@ -2008,15 +2030,10 @@ class OrganizationFactoryTest extends TestCase
         $this->entityManager->expects(self::never())->method('commit');
         $this->entityManager->expects(self::once())->method('rollback');
 
-        $organizationFactory->expects(self::once())->method('deleteOrganizationAccesses')->with($organization);
-
         $this->entityManager->method('remove')->willThrowException(new \Exception('some error'));
 
         $organizationFactory->expects(self::never())->method('deleteTypo3Website');
         $organizationFactory->expects(self::never())->method('switchDolibarrSocietyToProspect');
-        $organizationFactory->expects(self::never())->method('deleteOrganizationFiles');
-        $organizationFactory->expects(self::never())->method('deleteDirectoriesV1');
-        $organizationFactory->expects(self::never())->method('deleteDirectories59');
 
         $organizationDeletionRequest
             ->expects(self::never())
@@ -2047,6 +2064,20 @@ class OrganizationFactoryTest extends TestCase
         $organization->method('getId')->willReturn(123);
         $organization->method('getParameters')->willReturn($parameters);
 
+        $person1 = $this->getMockBuilder(Person::class)->getMock();
+        $person1->method('getId')->willReturn(1);
+
+        $person2 = $this->getMockBuilder(Person::class)->getMock();
+        $person2->method('getId')->willReturn(2);
+
+        $person3 = $this->getMockBuilder(Person::class)->getMock();
+        $person3->method('getId')->willReturn(3);
+
+        $organizationFactory
+            ->method('getFutureOrphanPersons')
+            ->with($organization)
+            ->willReturn([$person1, $person2, $person3]);
+
         $this->organizationRepository
             ->expects(self::once())
             ->method('find')
@@ -2058,33 +2089,44 @@ class OrganizationFactoryTest extends TestCase
         $this->entityManager->expects(self::once())->method('commit');
         $this->entityManager->expects(self::never())->method('rollback');
 
-        $organizationFactory->expects(self::once())->method('deleteOrganizationAccesses')->with($organization);
-
-        $this->entityManager->expects(self::exactly(2))->method('remove')->withConsecutive(
+        $this->entityManager->expects(self::exactly(5))->method('remove')->withConsecutive(
             [$parameters],
-            [$organization]
+            [$organization],
+            [$person1],
+            [$person2],
+            [$person3],
         );
 
         $organizationFactory->expects(self::once())->method('deleteTypo3Website')->willThrowException(new \Exception('some error'));
         $organizationFactory->expects(self::once())->method('switchDolibarrSocietyToProspect')->willThrowException(new \Exception('some error'));
-        $organizationFactory->expects(self::once())->method('deleteOrganizationFiles')->willThrowException(new \Exception('some error'));
-        $organizationFactory->expects(self::once())->method('deleteDirectoriesV1')->willThrowException(new \Exception('some error'));
-        $organizationFactory->expects(self::once())->method('deleteDirectories59')->willThrowException(new \Exception('some error'));
 
         $organizationDeletionRequest
             ->expects(self::once())
             ->method('setStatus')
             ->with(OrganizationDeletionRequest::STATUS_OK_WITH_ERRORS);
 
+        $this->fileManager
+            ->expects(self::once())
+            ->method('deleteOrganizationFiles')
+            ->with(123)
+            ->willThrowException(new \Exception('some error'));
+
+        $this->fileManager
+            ->expects(self::exactly(3))
+            ->method('deletePersonFiles')
+            ->withConsecutive([1], [2], [3])
+            ->willThrowException(new \Exception('some error'));
+
         $this->logger
-            ->expects(self::exactly(5))
+            ->expects(self::exactly(6))
             ->method('critical')
         ->withConsecutive(
             ['An error happened while deleting the Typo3 website, please proceed manually.'],
             ['An error happened while updating the Dolibarr society, please proceed manually.'],
-            ['An error happened while deleting the local directories, please proceed manually.'],
-            ['An error happened while deleting the V1 directories, please proceed manually.'],
-            ['An error happened while deleting the 5.9 directories, please proceed manually.'],
+            ["An error happened while deleting the organization's files, please proceed manually."],
+            ["An error happened while deleting the person's files, please proceed manually (id=1)."],
+            ["An error happened while deleting the person's files, please proceed manually (id=2)."],
+            ["An error happened while deleting the person's files, please proceed manually (id=3)."],
         );
 
         $result = $organizationFactory->delete($organizationDeletionRequest);
@@ -2095,50 +2137,57 @@ class OrganizationFactoryTest extends TestCase
         );
     }
 
-    public function testDeleteOrganizationAccesses(): void
+    public function testGetFutureOrphanPersons(): void
     {
-        $organizationFactory = $this->getOrganizationFactoryMockFor('deleteOrganizationAccesses');
+        $organizationFactory = $this->getOrganizationFactoryMockFor('getFutureOrphanPersons');
 
         $organization = $this->getMockBuilder(Organization::class)->getMock();
 
         $access1 = $this->getMockBuilder(Access::class)->getMock();
-        $access2 = $this->getMockBuilder(Access::class)->getMock();
-        $access_other = $this->getMockBuilder(Access::class)->getMock();
-
         $person1 = $this->getMockBuilder(Person::class)->getMock();
-        $person1->method('getAccesses')->willReturn(new ArrayCollection([$access1]));
         $access1->method('getPerson')->willReturn($person1);
 
+        $access2 = $this->getMockBuilder(Access::class)->getMock();
         $person2 = $this->getMockBuilder(Person::class)->getMock();
-        $person2->method('getAccesses')->willReturn(new ArrayCollection([$access2, $access_other]));
         $access2->method('getPerson')->willReturn($person2);
 
-        $organization->method('getAccesses')->willReturn(new ArrayCollection([$access1, $access2]));
+        $access3 = $this->getMockBuilder(Access::class)->getMock();
+        $person3 = $this->getMockBuilder(Person::class)->getMock();
+        $access3->method('getPerson')->willReturn($person3);
 
-        $this->entityManager
-            ->expects(self::exactly(3))
-            ->method('remove')
-            ->withConsecutive(
-                [$person1],
-                [$access1],
-                [$access2],
-            );
+        $otherAccess1 = $this->getMockBuilder(Access::class)->getMock();
+        $otherAccess2 = $this->getMockBuilder(Access::class)->getMock();
+
+        $person1->method('getAccesses')->willReturn(new ArrayCollection([$access1, $otherAccess1]));
+        $person2->method('getAccesses')->willReturn(new ArrayCollection([$access2, $otherAccess2]));
+        $person3->method('getAccesses')->willReturn(new ArrayCollection([$access3]));
 
-        $organizationFactory->deleteOrganizationAccesses($organization);
+        $organization->method('getAccesses')->willReturn(new ArrayCollection([$access1, $access2, $access3]));
+
+        $this->assertEquals(
+            [$person3],
+            $organizationFactory->getFutureOrphanPersons($organization)
+        );
+    }
+
+    public function testDeleteTypo3Website(): void
+    {
+        $organizationFactory = $this->getOrganizationFactoryMockFor('deleteTypo3Website');
+
+        $this->typo3Service->expects(self::once())->method('hardDeleteSite')->with(123);
+
+        $organizationFactory->deleteTypo3Website(123);
     }
 
     public function testSwitchDolibarrSocietyToProspect(): void
     {
         $organizationFactory = $this->getOrganizationFactoryMockFor('switchDolibarrSocietyToProspect');
 
-        $organization = $this->getMockBuilder(Organization::class)->getMock();
-        $organization->method('getId')->willReturn(123);
-
         $this->dolibarrApiService
             ->expects(self::once())
             ->method('switchSocietyToProspect')
             ->with(123);
 
-        $organizationFactory->switchDolibarrSocietyToProspect($organization);
+        $organizationFactory->switchDolibarrSocietyToProspect(123);
     }
 }

+ 0 - 1
tests/Unit/Service/Organization/UtilsTest.php

@@ -714,5 +714,4 @@ class UtilsTest extends TestCase
 
         $this->assertEquals(null, $result);
     }
-
 }

+ 2 - 4
tests/Unit/Service/Rest/ApiRequestServiceTest.php

@@ -9,15 +9,14 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
 use Symfony\Contracts\HttpClient\HttpClientInterface;
 use Symfony\Contracts\HttpClient\ResponseInterface;
 
-class TestableApiRequestService extends ApiRequestService {
+class TestableApiRequestService extends ApiRequestService
+{
     public function addBodyOption(array $options, array|string $body): array
     {
         return parent::addBodyOption($options, $body);
     }
 }
 
-
-
 class ApiRequestServiceTest extends TestCase
 {
     private HttpClientInterface $client;
@@ -146,7 +145,6 @@ class ApiRequestServiceTest extends TestCase
         );
     }
 
-
     public function testAddBodyOptionKeyExists()
     {
         $apiRequestService = $this->getMockBuilder(TestableApiRequestService::class)

+ 3 - 4
tests/Unit/Service/Security/InternalRequestsServiceTest.php

@@ -7,16 +7,15 @@ use App\Service\Security\InternalRequestsService;
 use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 use Symfony\Bundle\SecurityBundle\Security;
-use Symfony\Component\Security\Core\User\UserInterface;
 
 class TestableInternalRequestsService extends InternalRequestsService
 {
-    public function isInternalIp(string $ip): bool
+    public function isInternalIp(string $clientIp): bool
     {
-        return parent::isInternalIp($ip);
+        return parent::isInternalIp($clientIp);
     }
 
-    public function tokenIsValid(string|null $token): bool
+    public function tokenIsValid(?string $token): bool
     {
         return parent::tokenIsValid($token);
     }

+ 19 - 5
tests/Unit/Service/Typo3/SubdomainServiceTest.php

@@ -7,7 +7,7 @@ use App\Entity\Organization\Organization;
 use App\Entity\Organization\Parameters;
 use App\Entity\Organization\Subdomain;
 use App\Entity\Person\Person;
-use App\Message\Message\MailerCommand;
+use App\Message\Message\Mailer;
 use App\Message\Message\Typo3\Typo3Update;
 use App\Repository\Access\AccessRepository;
 use App\Repository\Organization\SubdomainRepository;
@@ -87,6 +87,20 @@ class SubdomainServiceTest extends TestCase
             ->getMock();
     }
 
+    public function testGetSubdomain(): void
+    {
+        $subdomainService = $this->makeSubdomainServiceMockFor('getSubdomain');
+
+        $subdomain = $this->getMockBuilder(Subdomain::class)->getMock();
+
+        $this->subdomainRepository->method('findOneBy')->with(['subdomain' => 'abc'])->willReturn($subdomain);
+
+        $this->assertEquals(
+            $subdomain,
+            $subdomainService->getSubdomain('abc')
+        );
+    }
+
     /**
      * @see SubdomainService::canRegisterNewSubdomain()
      */
@@ -388,8 +402,8 @@ class SubdomainServiceTest extends TestCase
         $subdomainService->expects(self::never())->method('updateTypo3Website');
         $subdomainService->expects(self::never())->method('sendConfirmationEmail');
 
-//        $parameters->expects(self::once())->method('setSubDomain')->with('sub');
-//        $parameters->expects(self::once())->method('setOtherWebsite')->with('https://sub.opentalent.fr');
+        //        $parameters->expects(self::once())->method('setSubDomain')->with('sub');
+        //        $parameters->expects(self::once())->method('setOtherWebsite')->with('https://sub.opentalent.fr');
 
         $this->expectException(\RuntimeException::class);
         $this->expectExceptionMessage('Can not activate a non-persisted subdomain');
@@ -523,8 +537,8 @@ class SubdomainServiceTest extends TestCase
         $this->messageBus
             ->expects(self::once())
             ->method('dispatch')
-            ->with(self::isInstanceOf(MailerCommand::class))
-            ->willReturn(new Envelope(new MailerCommand($subdomainChangeModel)));
+            ->with(self::isInstanceOf(Mailer::class))
+            ->willReturn(new Envelope(new Mailer($subdomainChangeModel)));
 
         $subdomainService->sendConfirmationEmail($subdomain);
     }

+ 29 - 2
tests/Unit/Service/Typo3/Typo3ServiceTest.php

@@ -5,15 +5,16 @@
 namespace App\Tests\Unit\Service\Typo3;
 
 use App\Service\Typo3\Typo3Service;
+use App\Service\Utils\DatesUtils;
 use PHPUnit\Framework\TestCase;
 use Symfony\Contracts\HttpClient\HttpClientInterface;
 use Symfony\Contracts\HttpClient\ResponseInterface;
 
 class TestableTypo3Service extends Typo3Service
 {
-    public function sendCommand(string $route, array $parameters): ResponseInterface
+    public function sendCommand(string $route, array $parameters, array $headers = []): ResponseInterface
     {
-        return parent::sendCommand($route, $parameters);
+        return parent::sendCommand($route, $parameters, $headers);
     }
 }
 
@@ -123,6 +124,32 @@ class Typo3ServiceTest extends TestCase
         $typo3Service->deleteSite(1);
     }
 
+    /**
+     * @see Typo3Service::deleteSite()
+     */
+    public function testHardDeleteSite(): void
+    {
+        DatesUtils::setFakeDatetime('2025-01-01 00:00:00');
+
+        $typo3Service = $this->getMockBuilder(TestableTypo3Service::class)
+            ->setMethodsExcept(['hardDeleteSite'])
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $response = $this->getMockBuilder(ResponseInterface::class)->disableOriginalConstructor()->getMock();
+
+        $typo3Service->expects(self::once())
+            ->method('sendCommand')
+            ->with(
+                '/otadmin/site/delete',
+                ['organization-id' => 1, 'hard' => 1],
+                ['Confirmation-Token' => 'DEL-1-20250101']
+            )
+            ->willReturn($response);
+
+        $typo3Service->hardDeleteSite(1);
+    }
+
     /**
      * @see Typo3Service::undeleteSite()
      */

+ 34 - 0
tests/Unit/Service/Utils/UrlBuilderTest.php

@@ -5,10 +5,20 @@
 namespace App\Tests\Unit\Service\Utils;
 
 use App\Service\Utils\UrlBuilder;
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 
 class UrlBuilderTest extends TestCase
 {
+    private function getMockFor(string $methodName): UrlBuilder|MockObject
+    {
+        return $this
+            ->getMockBuilder(UrlBuilder::class)
+            ->setConstructorArgs(['https://mydomain.net'])
+            ->setMethodsExcept([$methodName])
+            ->getMock();
+    }
+
     /**
      * @see UrlBuilder::concatPath()
      */
@@ -84,4 +94,28 @@ class UrlBuilderTest extends TestCase
             UrlBuilder::concat('domain.org', ['abc'], ['a' => 1], true)
         );
     }
+
+    public function testGetRelativeUrl(): void
+    {
+        $urlBuilder = $this->getMockFor('getRelativeUrl');
+
+        $this->assertEquals(
+            '/abc?q=1',
+            $urlBuilder->getRelativeUrl('https://mydomain.net/abc?q=1')
+        );
+        $this->assertEquals(
+            '/abc?q=1',
+            $urlBuilder->getRelativeUrl('/abc?q=1')
+        );
+    }
+
+    public function testGetAbsoluteUrl(): void
+    {
+        $urlBuilder = $this->getMockFor('getAbsoluteUrl');
+
+        $this->assertEquals(
+            'https://mydomain.net/abc?q=1',
+            $urlBuilder->getAbsoluteUrl('/abc?q=1')
+        );
+    }
 }