fix: keep upload files until their deletion is committed

This commit is contained in:
fromme committed 2026-10-15 12:49:48 +02:00
1 parent 7e182e3d51
commit 4aa2d7a5e6
5 files changed
+221 -12

No files matched your search

@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace App\Tests\EventListener;
use App\Entity\Upload;
use App\Service\Upload\UploadHandler;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Filesystem\Filesystem;
/**
* Doctrine dispatches preRemove inside EntityManager::remove(), so a listener deleting the file
* there loses it whenever the request fails between remove() and flush() - the row survives and
* points at nothing. These tests pin that the file only goes once the deletion has committed.
*/
class DeleteUploadListenerTest extends KernelTestCase
{
private EntityManagerInterface $entityManager;
private UploadHandler $uploadHandler;
/**
* @var array<int, string>
*/
private array $createdFiles = [];
protected function setUp(): void
{
self::bootKernel();
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
$this->uploadHandler = self::getContainer()->get(UploadHandler::class);
$this->entityManager->createQuery(sprintf('DELETE FROM %s', Upload::class))->execute();
}
protected function tearDown(): void
{
(new Filesystem())->remove($this->createdFiles);
parent::tearDown();
}
public function testTheFileIsRemovedOnceTheDeletionIsFlushed(): void
{
$upload = $this->storedUpload();
$this->entityManager->remove($upload);
$this->entityManager->flush();
$this->assertFileDoesNotExist($this->filepath($upload));
}
public function testTheFileIsKeptWhileTheDeletionIsNotFlushed(): void
{
$upload = $this->storedUpload();
$this->entityManager->remove($upload);
$this->assertFileExists($this->filepath($upload));
}
public function testTheFileIsKeptWhenTheFlushFails(): void
{
$upload = $this->storedUpload();
$filepath = $this->filepath($upload);
$this->entityManager->remove($upload);
// No filename: violates NOT NULL, so the flush rolls back.
$this->entityManager->persist((new Upload())
->setType(Upload::TYPE_DOCUMENT)
->setOriginalFilename('broken.pdf')
->setMimeType('application/pdf')
->setSize(1024)
);
try {
$this->entityManager->flush();
$this->fail('The flush was expected to fail');
} catch (DBALException) {
}
$this->assertFileExists($filepath);
}
public function testAFailedFlushDoesNotHandItsFilesToTheNextOne(): void
{
$upload = $this->storedUpload();
$filepath = $this->filepath($upload);
$this->entityManager->remove($upload);
$this->entityManager->persist((new Upload())
->setType(Upload::TYPE_DOCUMENT)
->setOriginalFilename('broken.pdf')
->setMimeType('application/pdf')
->setSize(1024)
);
try {
$this->entityManager->flush();
} catch (DBALException) {
}
/** @var EntityManagerInterface $entityManager */
$entityManager = self::getContainer()->get(ManagerRegistry::class)->resetManager();
$entityManager->persist($this->upload('unrelated.pdf'));
$entityManager->flush();
$this->assertFileExists($filepath);
}
private function storedUpload(): Upload
{
$upload = $this->upload(sprintf('test-delete-upload-%s.pdf', bin2hex(random_bytes(8))));
$this->entityManager->persist($upload);
$this->entityManager->flush();
$filepath = $this->filepath($upload);
(new Filesystem())->dumpFile($filepath, '%PDF-1.4');
$this->createdFiles[] = $filepath;
return $upload;
}
private function upload(string $filename): Upload
{
return (new Upload())
->setType(Upload::TYPE_DOCUMENT)
->setFilename($filename)
->setOriginalFilename($filename)
->setMimeType('application/pdf')
->setSize(1024)
;
}
private function filepath(Upload $upload): string
{
return $this->uploadHandler->getUploadFilepath($upload);
}
}