*/ private array $tempFiles = []; protected function setUp(): void { $this->uploadBaseDir = sys_get_temp_dir().'/erp154-uploads-'.bin2hex(random_bytes(4)); } protected function tearDown(): void { // Nettoyage des fichiers sources et de l'arborescence de destination. foreach ($this->tempFiles as $path) { if (is_file($path)) { @unlink($path); } } $this->removeDirectory($this->uploadBaseDir); } public function testRejectsMimeTypeOutsideWhitelist(): void { $uploader = $this->createUploader(); $file = $this->makeUploadedFile('hello world plain text', 'note.txt'); $this->expectException(UnsupportedMimeTypeException::class); $uploader->upload($file); } public function testRejectsFileLargerThanMaxSize(): void { $uploader = $this->createUploader(); // Contenu PDF valide mais artificiellement gonfle au-dela de la borne. $content = "%PDF-1.4\n".str_repeat('A', FileUploader::MAX_SIZE_BYTES + 1); $file = $this->makeUploadedFile($content, 'huge.pdf'); $this->expectException(FileTooLargeException::class); $uploader->upload($file); } public function testStoresPdfAndComputesSha256Checksum(): void { $content = $this->minimalPdf(); $clock = new MockClock(new DateTimeImmutable('2026-06-15 10:00:00')); $uploader = $this->createUploader($clock); $file = $this->makeUploadedFile($content, 'facture.pdf'); $document = $uploader->upload($file); self::assertSame('facture.pdf', $document->getOriginalFilename()); self::assertSame('application/pdf', $document->getMimeType()); self::assertSame(\strlen($content), $document->getSizeBytes()); self::assertSame(hash('sha256', $content), $document->getChecksum()); self::assertSame(64, \strlen($document->getChecksum())); // Chemin relatif date selon l'horloge injectee (2026/06). self::assertStringStartsWith('2026/06/', $document->getStoredPath()); self::assertFileExists($this->uploadBaseDir.'/'.$document->getStoredPath()); // Le fichier ecrit a bien le contenu d'origine (checksum coherent). self::assertSame( $document->getChecksum(), hash_file('sha256', $this->uploadBaseDir.'/'.$document->getStoredPath()), ); } public function testRemoveDeletesStoredFile(): void { $uploader = $this->createUploader(); $file = $this->makeUploadedFile($this->minimalPdf(), 'facture.pdf'); $document = $uploader->upload($file); $storedPath = $this->uploadBaseDir.'/'.$document->getStoredPath(); self::assertFileExists($storedPath); // Compensation : remove() efface le fichier physique... $uploader->remove($document); self::assertFileDoesNotExist($storedPath); // ...et reste silencieux si on le rappelle alors que le fichier a disparu. $uploader->remove($document); self::assertFileDoesNotExist($storedPath); } private function createUploader(?MockClock $clock = null): FileUploader { return new FileUploader($this->uploadBaseDir, $clock ?? new MockClock()); } /** * Cree un UploadedFile en mode test (move() autorise hors contexte HTTP). */ private function makeUploadedFile(string $content, string $clientName): UploadedFile { $path = sys_get_temp_dir().'/erp154-src-'.bin2hex(random_bytes(4)); file_put_contents($path, $content); $this->tempFiles[] = $path; // Le 5e argument `test: true` court-circuite move_uploaded_file(). return new UploadedFile($path, $clientName, null, null, true); } /** * Contenu PDF minimal valide — l'entete `%PDF-1.4` suffit a faire detecter * `application/pdf` par finfo (getMimeType server-side). */ private function minimalPdf(): string { return "%PDF-1.4\n" ."1 0 obj<>endobj\n" ."2 0 obj<>endobj\n" ."3 0 obj<>endobj\n" ."trailer<>\n" ."%%EOF\n"; } private function removeDirectory(string $dir): void { if (!is_dir($dir)) { return; } $items = scandir($dir) ?: []; foreach ($items as $item) { if ('.' === $item || '..' === $item) { continue; } $path = $dir.'/'.$item; is_dir($path) ? $this->removeDirectory($path) : @unlink($path); } @rmdir($dir); } }