first commit

This commit is contained in:
julien
2026-03-20 22:13:41 +01:00
commit 41f8b3afb4
323 changed files with 27222 additions and 0 deletions

View File

@@ -0,0 +1,348 @@
<?php
declare(strict_types=1);
namespace Tests\Media;
use Netig\Netslim\Media\Application\MediaApplicationService;
use Netig\Netslim\Media\Application\UseCase\DeleteMedia;
use Netig\Netslim\Media\Application\UseCase\StoreMedia;
use Netig\Netslim\Media\Contracts\MediaUsageReaderInterface;
use Netig\Netslim\Media\Contracts\MediaUsageReference;
use Netig\Netslim\Media\Domain\Entity\Media;
use Netig\Netslim\Media\Domain\Exception\FileTooLargeException;
use Netig\Netslim\Media\Domain\Exception\InvalidMimeTypeException;
use Netig\Netslim\Media\Domain\Repository\MediaRepositoryInterface;
use Netig\Netslim\Media\Domain\Service\UploadedMediaInterface;
use Netig\Netslim\Media\Infrastructure\LocalMediaStorage;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
/**
* Tests unitaires pour MediaApplicationService.
*
* Stratégie : les opérations sur le système de fichiers réel (finfo, GD,
* copy, moveTo) sont exercées via de vrais fichiers JPEG temporaires ;
* le repository reste un mock pour isoler la logique de persistance.
*
* Les cas couverts :
* - rejet si taille > maxSize
* - rejet si type MIME non autorisé
* - déduplication : retour de l'URL existante si hash déjà connu
* - stockage : fichier écrit sur disque, media créé en base
* - suppression : fichier supprimé du disque et entrée retirée de la base
*/
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
final class MediaServiceTest extends TestCase
{
/** @var MediaRepositoryInterface&MockObject */
private MediaRepositoryInterface $repository;
private MediaUsageReaderInterface $mediaUsageReader;
private string $uploadDir;
private MediaApplicationService $service;
protected function setUp(): void
{
$this->repository = $this->createMock(MediaRepositoryInterface::class);
$this->mediaUsageReader = $this->createMock(MediaUsageReaderInterface::class);
$this->uploadDir = sys_get_temp_dir() . '/slim_media_test_' . uniqid();
@mkdir($this->uploadDir, 0755, true);
$storage = new LocalMediaStorage($this->uploadDir);
$this->service = new MediaApplicationService(
mediaRepository: $this->repository,
mediaUsageReader: $this->mediaUsageReader,
storeMedia: new StoreMedia($this->repository, $storage, '/media', 5 * 1024 * 1024),
deleteMedia: new DeleteMedia($this->repository, $storage),
);
}
protected function tearDown(): void
{
// Nettoyage du répertoire temporaire
foreach (glob($this->uploadDir . '/*') ?: [] as $file) {
@unlink($file);
}
@rmdir($this->uploadDir);
}
// ── store — rejets ─────────────────────────────────────────────
/**
* store() doit lever FileTooLargeException si la taille dépasse le maximum.
*/
public function testStoreThrowsFileTooLargeWhenOversized(): void
{
$file = $this->makeUploadedFile(size: 6 * 1024 * 1024);
$this->expectException(FileTooLargeException::class);
$this->service->store($file, 1);
}
/**
* store() doit lever InvalidMimeTypeException pour un type MIME non autorisé.
*/
public function testStoreThrowsInvalidMimeType(): void
{
// Créer un vrai fichier texte — finfo retournera text/plain
$tmpFile = tempnam(sys_get_temp_dir(), 'slim_test_');
file_put_contents($tmpFile, 'ceci est un fichier texte');
$file = $this->makeUploadedFileFromPath($tmpFile, filesize($tmpFile));
$this->expectException(InvalidMimeTypeException::class);
try {
$this->service->store($file, 1);
} finally {
@unlink($tmpFile);
}
}
// ── store — déduplication ──────────────────────────────────────
/**
* store() doit retourner l'URL existante sans créer de doublon si le hash est connu.
*/
public function testStoreReturnsDuplicateUrl(): void
{
$tmpFile = $this->createMinimalJpeg();
$existing = new Media(7, 'existing.jpg', '/media/existing.jpg', 'existing-hash', 1);
$this->repository
->expects($this->once())
->method('findByHashForUser')
->with($this->callback(static fn (mixed $value): bool => is_string($value) && $value !== ''), 1)
->willReturn($existing);
$this->repository->expects($this->never())->method('create');
$file = $this->makeUploadedFileFromPath($tmpFile, filesize($tmpFile));
$media = $this->service->store($file, 1);
$this->assertSame('/media/existing.jpg', $media->getUrl());
$this->assertSame(7, $media->getId());
@unlink($tmpFile);
}
// ── store — stockage nominal ───────────────────────────────────
/**
* store() doit créer le fichier sur disque et appeler repository::create().
*/
public function testStoreWritesFileAndCreatesRecord(): void
{
$tmpFile = $this->createMinimalJpeg();
$this->repository->method('findByHashForUser')->willReturn(null);
$this->repository->expects($this->once())->method('create');
$file = $this->makeUploadedFileFromPath($tmpFile, filesize($tmpFile));
$media = $this->service->store($file, 1);
$this->assertStringStartsWith('/media/', $media->getUrl());
// Le fichier doit exister sur le disque
$filename = basename($media->getUrl());
$this->assertFileExists($this->uploadDir . DIRECTORY_SEPARATOR . $filename);
@unlink($tmpFile);
}
// ── delete ─────────────────────────────────────────────────────
/**
* delete() doit supprimer le fichier physique et appeler repository::delete().
*/
public function testDeleteRemovesFileAndRecord(): void
{
$filename = 'test_media.jpg';
$filePath = $this->uploadDir . DIRECTORY_SEPARATOR . $filename;
file_put_contents($filePath, 'fake image data');
$media = new Media(42, $filename, '/media/' . $filename, 'fakehash', 1);
$this->repository->expects($this->once())->method('delete')->with(42)->willReturn(1);
$this->service->delete($media);
$this->assertFileDoesNotExist($filePath);
}
/**
* delete() ne doit pas lever d'exception si le fichier physique n'existe plus.
*/
public function testDeleteIsSilentWhenFileMissing(): void
{
$media = new Media(99, 'inexistant.jpg', '/media/inexistant.jpg', 'hash', 1);
$this->repository->expects($this->once())->method('delete')->with(99)->willReturn(1);
// Ne doit pas lever d'exception
$this->service->delete($media);
$this->assertTrue(true);
}
/**
* delete() doit conserver le fichier si la suppression en base echoue.
*/
public function testDeleteKeepsFileWhenRepositoryDeleteFails(): void
{
$filename = 'test_media_keep.jpg';
$filePath = $this->uploadDir . DIRECTORY_SEPARATOR . $filename;
file_put_contents($filePath, 'fake image data');
$media = new Media(43, $filename, '/media/' . $filename, 'fakehash', 1);
$this->repository->expects($this->once())
->method('delete')
->with(43)
->willThrowException(new \RuntimeException('db delete failed'));
$this->expectException(\RuntimeException::class);
try {
$this->service->delete($media);
} finally {
$this->assertFileExists($filePath);
}
}
/**
* delete() ne doit pas supprimer le fichier si la ligne base n'existe plus.
*/
public function testDeleteKeepsFileWhenRecordWasAlreadyMissing(): void
{
$filename = 'test_media_orphan.jpg';
$filePath = $this->uploadDir . DIRECTORY_SEPARATOR . $filename;
file_put_contents($filePath, 'fake image data');
$media = new Media(44, $filename, '/media/' . $filename, 'fakehash', 1);
$this->repository->expects($this->once())
->method('delete')
->with(44)
->willReturn(0);
$this->service->delete($media);
$this->assertFileExists($filePath);
}
// ── Lectures déléguées ─────────────────────────────────────────
/**
* findById() doit déléguer au repository et retourner null si absent.
*/
public function testFindByIdReturnsNullWhenMissing(): void
{
$this->repository->method('findById')->willReturn(null);
$this->assertNull($this->service->findById(999));
}
/**
* findById() doit retourner le média trouvé.
*/
public function testFindByIdReturnsMedia(): void
{
$media = new Media(3, 'photo.jpg', '/media/photo.jpg', 'abc123', 1);
$this->repository->expects($this->once())->method('findById')->with(3)->willReturn($media);
$this->assertSame($media, $this->service->findById(3));
}
/**
* getUsageSummary() doit déléguer au port de lecture des usages média.
*/
public function testGetUsageSummaryDelegatesToMediaUsageReader(): void
{
$media = new Media(3, 'photo.jpg', '/media/photo.jpg', 'abc123', 1);
$reference = new MediaUsageReference(12, 'Contenu lié', '/admin/content/edit/12');
$this->mediaUsageReader->expects($this->once())->method('countUsagesByMediaIds')->with([3])->willReturn([3 => 2]);
$this->mediaUsageReader->expects($this->once())->method('findUsagesByMediaIds')->with([3], 3)->willReturn([3 => [$reference]]);
$summary = $this->service->getUsageSummary($media, 3);
$this->assertSame(2, $summary['count']);
$this->assertSame([$reference], $summary['references']);
}
public function testGetUsageSummariesDelegatesToMediaUsageReaderInBatch(): void
{
$mediaA = new Media(3, 'photo.jpg', '/media/photo.jpg', 'abc123', 1);
$mediaB = new Media(8, 'banner.jpg', '/media/banner.jpg', 'def456', 1);
$reference = new MediaUsageReference(12, 'Contenu lié', '/admin/content/edit/12');
$this->mediaUsageReader->expects($this->once())->method('countUsagesByMediaIds')->with([3, 8])->willReturn([
3 => 2,
8 => 0,
]);
$this->mediaUsageReader->expects($this->once())->method('findUsagesByMediaIds')->with([3, 8], 4)->willReturn([
3 => [$reference],
]);
$summaries = $this->service->getUsageSummaries([$mediaA, $mediaB], 4);
$this->assertSame(2, $summaries[3]['count']);
$this->assertSame([$reference], $summaries[3]['references']);
$this->assertSame(0, $summaries[8]['count']);
$this->assertSame([], $summaries[8]['references']);
}
// ── Helpers ────────────────────────────────────────────────────
/**
* Crée un mock d'UploadedFile avec une taille donnée et un tmpPath bidon.
*/
private function makeUploadedFile(int $size): UploadedMediaInterface
{
$file = $this->createMock(UploadedMediaInterface::class);
$file->method('getSize')->willReturn($size);
$file->method('getTemporaryPath')->willReturn('/nonexistent/path');
return $file;
}
/**
* Crée un mock d'UploadedFile pointant vers un fichier réel.
*/
private function makeUploadedFileFromPath(string $path, int $size): UploadedMediaInterface
{
$file = $this->createMock(UploadedMediaInterface::class);
$file->method('getSize')->willReturn($size);
$file->method('getTemporaryPath')->willReturn($path);
$file->method('moveTo')->willReturnCallback(function (string $dest) use ($path): void {
copy($path, $dest);
});
return $file;
}
/**
* Crée un fichier JPEG valide via GD (1×1 pixel).
* Retourne le chemin du fichier temporaire.
*/
private function createMinimalJpeg(): string
{
$tmpFile = tempnam(sys_get_temp_dir(), 'slim_jpeg_') . '.jpg';
$img = imagecreatetruecolor(1, 1);
imagejpeg($img, $tmpFile);
imagedestroy($img);
return $tmpFile;
}
}