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