projectDir = static::getContainer()->getParameter('kernel.project_dir'); $this->createdDirs = []; } protected function tearDown(): void { foreach ($this->createdDirs as $dir) { if (is_dir($dir)) { $this->deleteDir($dir); } } parent::tearDown(); } private function deleteDir(string $dir): void { foreach (new \FilesystemIterator($dir) as $item) { $item->isDir() ? $this->deleteDir($item->getPathname()) : unlink($item->getPathname()); } rmdir($dir); } private function invokeHandler(): void { $handler = static::getContainer()->get(RunImageCleanupMessageHandler::class); $handler(new RunImageCleanupMessage()); } private function makeImage(string $email): Image { $user = $this->createUser($email); $image = (new Image())->setUser($user) ->setOriginalFilename('del.jpg') ->setStoragePath('var/storage/images/_cleanup_test.jpg'); $this->em()->persist($image); $this->em()->flush(); return $image; } // CL-01: soft-deleted image with no approvals → hard-deleted from DB and filesystem public function test_cl01_soft_deleted_no_approvals_is_hard_deleted(): void { $image = $this->makeImage('cl01@example.com'); $image->setDeletedAt(new \DateTimeImmutable('-1 day')); $this->em()->flush(); $imageId = $image->getId(); $dir = $this->projectDir . '/var/storage/images/' . $imageId; mkdir($dir, 0755, true); $this->createdDirs[] = $dir; // Add a file and a subdirectory to cover unlink + recursive deleteDirectory branches file_put_contents($dir . '/v1_landscape.bin', 'FAKEBIN'); $subdir = $dir . '/subdir'; mkdir($subdir, 0755, true); file_put_contents($subdir . '/nested.bin', 'NESTEDBIN'); $this->invokeHandler(); $this->em()->clear(); $found = $this->em()->find(Image::class, $imageId); $this->assertNull($found, 'Image should be hard-deleted from the database'); $this->assertFalse(is_dir($dir), 'Image directory should be removed from filesystem'); $this->createdDirs = array_filter($this->createdDirs, fn($d) => $d !== $dir); } // CL-02: soft-deleted image with approved device → skipped, stays in DB public function test_cl02_soft_deleted_with_approval_is_skipped(): void { $image = $this->makeImage('cl02@example.com'); $device = new Device(); $device->setMac('CC:DD:EE:FF:00:01'); $device->setName('Frame'); $device->setUser($image->getUser()); $this->em()->persist($device); $image->approveForDevice($device); $image->setDeletedAt(new \DateTimeImmutable('-1 day')); $this->em()->flush(); $imageId = $image->getId(); $this->invokeHandler(); $this->em()->clear(); $found = $this->em()->find(Image::class, $imageId); $this->assertNotNull($found, 'Image with active approvals should not be hard-deleted'); } // CL-03: non-deleted image → not touched public function test_cl03_non_deleted_image_is_not_touched(): void { $image = $this->makeImage('cl03@example.com'); $imageId = $image->getId(); $this->invokeHandler(); $this->em()->clear(); $found = $this->em()->find(Image::class, $imageId); $this->assertNotNull($found, 'Non-deleted image should not be affected'); } // CL-04: error during cleanup is caught and logged; no exception propagates public function test_cl04_error_during_cleanup_is_caught(): void { $image = $this->makeImage('cl04@example.com'); $image->setDeletedAt(new \DateTimeImmutable('-1 day')); $this->em()->flush(); $mockRepo = $this->createStub(ImageRepository::class); $mockRepo->method('findSoftDeleted')->willReturn([$image]); $mockEm = $this->createStub(EntityManagerInterface::class); $mockEm->method('remove')->willThrowException(new \RuntimeException('simulated error')); $logger = static::getContainer()->get(\Psr\Log\LoggerInterface::class); $handler = new RunImageCleanupMessageHandler( $mockRepo, $mockEm, $logger, $this->projectDir, ); $handler(new RunImageCleanupMessage()); $this->assertTrue(true, 'catch block executed — no exception propagated'); } }