chore: stage all in-progress work before repo split
CI / test (push) Has been cancelled

Web app: new entities (Image, RenderedAsset, SharedImage, Token,
DeviceImageHistory), enums, repositories, controllers, message handlers,
migrations, tests, frontend upload/library/sticker UI, Vue components.

Firmware: EPD background screen binaries + gen scripts, setup_bg header.

Infra: ddev config, test bundle, gitignore coverage dir.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 12:11:31 -04:00
parent 062c52eec7
commit 12245759ac
149 changed files with 14846 additions and 92 deletions
@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace App\Tests\Integration\MessageHandler;
use App\Entity\Device;
use App\Entity\Image;
use App\Message\RunImageCleanupMessage;
use App\MessageHandler\RunImageCleanupMessageHandler;
use App\Repository\ImageRepository;
use App\Tests\AppKernelTestCase;
use Doctrine\ORM\EntityManagerInterface;
class RunImageCleanupMessageHandlerTest extends AppKernelTestCase
{
private string $projectDir;
private array $createdDirs = [];
protected function setUp(): void
{
parent::setUp();
$this->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');
}
}