Selaa lähdekoodia

complete unit tests

Olivier Massot 10 kuukautta sitten
vanhempi
commit
514f9c7b3b

+ 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 - 5
src/Service/Cron/Job/CleanTempFiles.php

@@ -108,7 +108,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 +115,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 +140,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 - 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;

+ 4 - 4
src/Service/File/Storage/LocalStorage.php

@@ -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;
         }

+ 4 - 1
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;
@@ -82,7 +83,9 @@ class Typo3Service
      */
     public function hardDeleteSite(int $organizationId): ResponseInterface
     {
-        $headers = ['Confirmation-Token' => "DEL-$organizationId-".date('Ymd')];
+        $date = DatesUtils::new()->format('Ymd');
+
+        $headers = ['Confirmation-Token' => "DEL-$organizationId-".$date];
 
         return $this->sendCommand(
             '/otadmin/site/delete',

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

@@ -0,0 +1,290 @@
+<?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));
+    }
+}

+ 167 - 7
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;
@@ -171,7 +175,7 @@ class CleanTempFilesTest extends TestCase
 
         $files = [$file1, $file2, $file3];
 
-        $this->connection->expects($this->once())->method('beginTransaction');
+        $this->connection->expects($this->exactly(3))->method('beginTransaction');
         $this->connection->expects($this->once())->method('setAutoCommit')->with(false);
 
         $queryBuilder = $this->getMockBuilder(QueryBuilder::class)->disableOriginalConstructor()->getMock();
@@ -182,11 +186,6 @@ class CleanTempFilesTest extends TestCase
             ->method('delete')
             ->willReturnSelf();
 
-        $queryBuilder
-            ->expects(self::exactly(3))
-            ->method('delete')
-            ->willReturnSelf();
-
         $queryBuilder
             ->expects(self::exactly(3))
             ->method('where')
@@ -203,7 +202,8 @@ class CleanTempFilesTest extends TestCase
             ->method('hardDelete')
             ->withConsecutive([$file1], [$file1], [$file3]);
 
-        $this->connection->expects($this->once())->method('commit');
+        $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'],
@@ -213,4 +213,164 @@ class CleanTempFilesTest extends TestCase
 
         $cleanTempFiles->deleteFiles($files);
     }
+
+    public function testDeleteFilesWithNonBlockingErrors(): void {
+        $cleanTempFiles = $this->getMockFor('deleteFiles');
+
+        $file1 = $this->getMockBuilder(File::class)->getMock();
+        $file1->method('getId')->willReturn(1);
+
+        $file2 = $this->getMockBuilder(File::class)->getMock();
+        $file2->method('getId')->willReturn(2);
+
+        $files = [$file1, $file2];
+
+        $this->connection->expects($this->exactly(2))->method('beginTransaction');
+        $this->connection->expects($this->once())->method('setAutoCommit')->with(false);
+
+        $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->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 testDeleteFilesWithBlockingError(): void {
+        $cleanTempFiles = $this->getMockFor('deleteFiles');
+
+        $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);
+
+        $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->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->deleteFiles($files);
+    }
+
+    public function testGetQueryConditions(): void {
+        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();
+
+        $expr = $this->getMockBuilder(Expr::class)->disableOriginalConstructor()->getMock();
+        $queryBuilder->method('expr')->willReturn($expr);
+
+        $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();
+
+        $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);
-    }
-}

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

@@ -0,0 +1,207 @@
+<?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 | null $value): void
+    {
+        $this->previousTimeConstraintState = $value;
+    }
+
+    public function getPreviousTimeConstraintState(): bool | null
+    {
+        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
+        );
+    }
+}

+ 67 - 30
tests/Unit/Service/File/FileManagerTest.php

@@ -55,36 +55,19 @@ class FileManagerTest extends TestCase
             ->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
     {
@@ -103,6 +86,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');
@@ -182,4 +185,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);
+    }
 }

+ 98 - 19
tests/Unit/Service/File/Storage/ApiLegacyStorageTest.php

@@ -3,25 +3,40 @@
 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
-    {
-        $apiLegacyRequestService = $this->getMockBuilder(ApiLegacyRequestService::class)
+    private ApiLegacyRequestService | MockObject $apiLegacyRequestService;
+    private ApiLegacyRequestService | MockObject $dataManager;
+    private ApiLegacyRequestService | MockObject $urlBuilder;
+
+    public function setUp(): void {
+        $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 +48,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 +63,79 @@ 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);
+    }
 }

+ 258 - 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,15 @@ 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
@@ -151,6 +162,210 @@ 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()
      */
@@ -525,6 +740,49 @@ 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()
      */

+ 42 - 0
tests/Unit/Service/Organization/OrganizationFactoryTest.php

@@ -2137,6 +2137,48 @@ class OrganizationFactoryTest extends TestCase
         );
     }
 
+    public function testGetFutureOrphanPersons(): void
+    {
+        $organizationFactory = $this->getOrganizationFactoryMockFor('getFutureOrphanPersons');
+
+        $organization = $this->getMockBuilder(Organization::class)->getMock();
+
+        $access1 = $this->getMockBuilder(Access::class)->getMock();
+        $person1 = $this->getMockBuilder(Person::class)->getMock();
+        $access1->method('getPerson')->willReturn($person1);
+
+        $access2 = $this->getMockBuilder(Access::class)->getMock();
+        $person2 = $this->getMockBuilder(Person::class)->getMock();
+        $access2->method('getPerson')->willReturn($person2);
+
+        $access3 = $this->getMockBuilder(Access::class)->getMock();
+        $person3 = $this->getMockBuilder(Person::class)->getMock();
+        $access3->method('getPerson')->willReturn($person3);
+
+        $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]));
+
+        $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');

+ 14 - 0
tests/Unit/Service/Typo3/SubdomainServiceTest.php

@@ -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()
      */

+ 27 - 0
tests/Unit/Service/Typo3/Typo3ServiceTest.php

@@ -5,6 +5,7 @@
 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;
@@ -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()
      */

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

@@ -5,10 +5,21 @@
 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 +95,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')
+        );
+    }
 }