first commit
This commit is contained in:
256
tests/Media/MediaServiceTest.php
Normal file
256
tests/Media/MediaServiceTest.php
Normal file
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Media;
|
||||
|
||||
use App\Media\Exception\FileTooLargeException;
|
||||
use App\Media\Exception\InvalidMimeTypeException;
|
||||
use App\Media\Media;
|
||||
use App\Media\MediaRepositoryInterface;
|
||||
use App\Media\MediaService;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour MediaService.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
final class MediaServiceTest extends TestCase
|
||||
{
|
||||
/** @var MediaRepositoryInterface&MockObject */
|
||||
private MediaRepositoryInterface $repository;
|
||||
|
||||
private string $uploadDir;
|
||||
|
||||
private MediaService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repository = $this->createMock(MediaRepositoryInterface::class);
|
||||
$this->uploadDir = sys_get_temp_dir() . '/slim_media_test_' . uniqid();
|
||||
@mkdir($this->uploadDir, 0755, true);
|
||||
|
||||
$this->service = new MediaService(
|
||||
mediaRepository: $this->repository,
|
||||
uploadDir: $this->uploadDir,
|
||||
uploadUrl: '/media',
|
||||
maxSize: 5 * 1024 * 1024,
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
$hash = hash_file('sha256', $tmpFile);
|
||||
|
||||
$existing = new Media(7, 'existing.jpg', '/media/existing.jpg', $hash, 1);
|
||||
$this->repository->method('findByHash')->willReturn($existing);
|
||||
$this->repository->expects($this->never())->method('create');
|
||||
|
||||
$file = $this->makeUploadedFileFromPath($tmpFile, filesize($tmpFile));
|
||||
$url = $this->service->store($file, 1);
|
||||
|
||||
$this->assertSame('/media/existing.jpg', $url);
|
||||
|
||||
@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('findByHash')->willReturn(null);
|
||||
$this->repository->expects($this->once())->method('create');
|
||||
|
||||
$file = $this->makeUploadedFileFromPath($tmpFile, filesize($tmpFile));
|
||||
$url = $this->service->store($file, 1);
|
||||
|
||||
$this->assertStringStartsWith('/media/', $url);
|
||||
|
||||
// Le fichier doit exister sur le disque
|
||||
$filename = basename($url);
|
||||
$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);
|
||||
|
||||
$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);
|
||||
|
||||
// Ne doit pas lever d'exception
|
||||
$this->service->delete($media);
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
|
||||
// ── 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->method('findById')->with(3)->willReturn($media);
|
||||
|
||||
$this->assertSame($media, $this->service->findById(3));
|
||||
}
|
||||
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Crée un mock d'UploadedFile avec une taille donnée et un tmpPath bidon.
|
||||
*/
|
||||
private function makeUploadedFile(int $size): UploadedFileInterface
|
||||
{
|
||||
$stream = $this->createMock(StreamInterface::class);
|
||||
$stream->method('getMetadata')->with('uri')->willReturn('/nonexistent/path');
|
||||
|
||||
$file = $this->createMock(UploadedFileInterface::class);
|
||||
$file->method('getSize')->willReturn($size);
|
||||
$file->method('getError')->willReturn(UPLOAD_ERR_OK);
|
||||
$file->method('getStream')->willReturn($stream);
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un mock d'UploadedFile pointant vers un fichier réel.
|
||||
*/
|
||||
private function makeUploadedFileFromPath(string $path, int $size): UploadedFileInterface
|
||||
{
|
||||
$stream = $this->createMock(StreamInterface::class);
|
||||
$stream->method('getMetadata')->with('uri')->willReturn($path);
|
||||
|
||||
$file = $this->createMock(UploadedFileInterface::class);
|
||||
$file->method('getSize')->willReturn($size);
|
||||
$file->method('getError')->willReturn(UPLOAD_ERR_OK);
|
||||
$file->method('getStream')->willReturn($stream);
|
||||
$file->method('moveTo')->willReturnCallback(function (string $dest) use ($path) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user