test media removal

This commit is contained in:
Yisroel Baum 2026-06-02 16:58:00 +03:00
parent d0810ba4b3
commit 4b432d9b99
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
4 changed files with 177 additions and 4 deletions

View file

@ -12,6 +12,11 @@ class FakeFileUploader implements FileUploader
*/ */
public array $uploads = []; public array $uploads = [];
/**
* @var string[]
*/
public array $deletedPaths = [];
public function upload(FileToUpload $file, string $folder): string public function upload(FileToUpload $file, string $folder): string
{ {
$this->uploads[] = ['file' => $file, 'folder' => $folder]; $this->uploads[] = ['file' => $file, 'folder' => $folder];
@ -23,4 +28,9 @@ class FakeFileUploader implements FileUploader
{ {
return 'https://test.local/storage/' . $path; return 'https://test.local/storage/' . $path;
} }
public function delete(string $path): void
{
$this->deletedPaths[] = $path;
}
} }

View file

@ -193,6 +193,60 @@ class ElementsEndpointTest extends TestCase
); );
} }
public function testAuthenticatedUpdateClearsUploadedMedia(): void
{
Storage::fake('public');
Storage::disk('public')->put(
'element-icons/original-icon.png',
'icon bytes',
);
Storage::disk('public')->put(
'element-pdfs/original.pdf',
'pdf bytes',
);
$setRepository = app(SetRepository::class);
$elementRepository = app(ElementRepository::class);
$set = $setRepository->create(new CreateSetDto(
name: 'Baderech HaAvodah',
description: 'A structured path for growth',
iconImageUrl: '/assets/baderech-haavodah-icon.png',
));
$element = $elementRepository->create(new CreateElementDto(
set: $set,
title: 'Baderech HaAvodah',
description: 'A structured path for growth',
iconImageUrl: 'element-icons/original-icon.png',
richText: '<p>A structured path for growth</p>',
pdfPath: 'element-pdfs/original.pdf',
youtubeUrl: null,
parentElement: null,
));
$this->createSession('valid-token');
$response = $this->withCredentials()
->withUnencryptedCookie('auth_token', 'valid-token')
->postJson('/api/element/update', [
'elementId' => $element->getId(),
'iconImageUrl' => '',
'pdfPath' => '',
]);
$response->assertOk();
$body = $response->json();
$this->assertNull($body['element']['iconImageUrl']);
$this->assertNull($body['element']['pdfPath']);
$updatedElement = $elementRepository->find($element->getId());
$this->assertNotNull($updatedElement);
$this->assertNull($updatedElement->getIconImageUrl());
$this->assertNull($updatedElement->getPdfPath());
Storage::disk('public')->assertMissing(
'element-icons/original-icon.png',
);
Storage::disk('public')->assertMissing(
'element-pdfs/original.pdf',
);
}
public function testIconImageUploadRequiresAuthentication(): void public function testIconImageUploadRequiresAuthentication(): void
{ {
$setRepository = app(SetRepository::class); $setRepository = app(SetRepository::class);

View file

@ -19,15 +19,19 @@ use App\Element\UseCases\UpdateElement\UpdateYoutubeUrlRequest;
use App\Exceptions\BadRequestException; use App\Exceptions\BadRequestException;
use App\Set\Set as DomainSet; use App\Set\Set as DomainSet;
use Tests\Fakes\FakeElementRepository; use Tests\Fakes\FakeElementRepository;
use Tests\Fakes\FakeFileUploader;
use Tests\TestCase; use Tests\TestCase;
class UpdateElementFieldsTest extends TestCase class UpdateElementFieldsTest extends TestCase
{ {
private FakeElementRepository $elementRepo; private FakeElementRepository $elementRepo;
private FakeFileUploader $fileUploader;
protected function setUp(): void protected function setUp(): void
{ {
$this->elementRepo = new FakeElementRepository(); $this->elementRepo = new FakeElementRepository();
$this->fileUploader = new FakeFileUploader();
} }
public function testUpdateTitleUpdatesOnlyTitle(): void public function testUpdateTitleUpdatesOnlyTitle(): void
@ -88,7 +92,10 @@ class UpdateElementFieldsTest extends TestCase
public function testUpdateIconImageUrlClearsEmptyString(): void public function testUpdateIconImageUrlClearsEmptyString(): void
{ {
$element = $this->createFilledElement(); $element = $this->createFilledElement();
$updateIconImageUrl = new UpdateIconImageUrl($this->elementRepo); $updateIconImageUrl = new UpdateIconImageUrl(
$this->elementRepo,
$this->fileUploader,
);
$updatedElement = $updateIconImageUrl->execute( $updatedElement = $updateIconImageUrl->execute(
new UpdateIconImageUrlRequest( new UpdateIconImageUrlRequest(
@ -99,6 +106,32 @@ class UpdateElementFieldsTest extends TestCase
$this->assertNull($updatedElement->getIconImageUrl()); $this->assertNull($updatedElement->getIconImageUrl());
$this->assertSame($element->getTitle(), $updatedElement->getTitle()); $this->assertSame($element->getTitle(), $updatedElement->getTitle());
$this->assertSame([], $this->fileUploader->deletedPaths);
}
public function testUpdateIconImageUrlDeletesManagedFileWhenCleared(): void
{
$element = $this->createElementWithMedia(
'element-icons/original-icon.png',
'/assets/pdfs/original.pdf',
);
$updateIconImageUrl = new UpdateIconImageUrl(
$this->elementRepo,
$this->fileUploader,
);
$updatedElement = $updateIconImageUrl->execute(
new UpdateIconImageUrlRequest(
id: $element->getId(),
iconImageUrl: '',
)
);
$this->assertNull($updatedElement->getIconImageUrl());
$this->assertSame(
['element-icons/original-icon.png'],
$this->fileUploader->deletedPaths,
);
} }
public function testUpdateRichTextUpdatesOnlyRichText(): void public function testUpdateRichTextUpdatesOnlyRichText(): void
@ -126,7 +159,10 @@ class UpdateElementFieldsTest extends TestCase
public function testUpdatePdfPathClearsEmptyString(): void public function testUpdatePdfPathClearsEmptyString(): void
{ {
$element = $this->createFilledElement(); $element = $this->createFilledElement();
$updatePdfPath = new UpdatePdfPath($this->elementRepo); $updatePdfPath = new UpdatePdfPath(
$this->elementRepo,
$this->fileUploader,
);
$updatedElement = $updatePdfPath->execute( $updatedElement = $updatePdfPath->execute(
new UpdatePdfPathRequest(id: $element->getId(), pdfPath: '') new UpdatePdfPathRequest(id: $element->getId(), pdfPath: '')
@ -134,6 +170,29 @@ class UpdateElementFieldsTest extends TestCase
$this->assertNull($updatedElement->getPdfPath()); $this->assertNull($updatedElement->getPdfPath());
$this->assertSame($element->getTitle(), $updatedElement->getTitle()); $this->assertSame($element->getTitle(), $updatedElement->getTitle());
$this->assertSame([], $this->fileUploader->deletedPaths);
}
public function testUpdatePdfPathDeletesManagedFileWhenCleared(): void
{
$element = $this->createElementWithMedia(
'/assets/original-icon.png',
'element-pdfs/original.pdf',
);
$updatePdfPath = new UpdatePdfPath(
$this->elementRepo,
$this->fileUploader,
);
$updatedElement = $updatePdfPath->execute(
new UpdatePdfPathRequest(id: $element->getId(), pdfPath: '')
);
$this->assertNull($updatedElement->getPdfPath());
$this->assertSame(
['element-pdfs/original.pdf'],
$this->fileUploader->deletedPaths,
);
} }
public function testUpdateYoutubeUrlClearsEmptyString(): void public function testUpdateYoutubeUrlClearsEmptyString(): void
@ -154,6 +213,16 @@ class UpdateElementFieldsTest extends TestCase
private function createFilledElement(): Element private function createFilledElement(): Element
{ {
return $this->createElementWithMedia(
'/assets/original-icon.png',
'/assets/pdfs/original.pdf',
);
}
private function createElementWithMedia(
?string $iconImageUrl,
?string $pdfPath,
): Element {
$set = new DomainSet( $set = new DomainSet(
id: 1, id: 1,
name: 'Baderech', name: 'Baderech',
@ -165,9 +234,9 @@ class UpdateElementFieldsTest extends TestCase
set: $set, set: $set,
title: 'Original title', title: 'Original title',
description: 'Original description', description: 'Original description',
iconImageUrl: '/assets/original-icon.png', iconImageUrl: $iconImageUrl,
richText: '<p>Original rich text</p>', richText: '<p>Original rich text</p>',
pdfPath: '/assets/pdfs/original.pdf', pdfPath: $pdfPath,
youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU', youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU',
parentElement: null, parentElement: null,
)); ));

View file

@ -151,4 +151,44 @@ describe('admin element editing', () => {
cy.resetDb() cy.resetDb()
}) })
it('removes uploaded icon and pdf files immediately through the UI', () => {
cy.resetDb()
loginAsAdmin()
cy.visit('/admin/element/1')
cy.intercept('POST', /\/api\/element\/update$/).as('updateElement')
cy.get('[data-cy="admin-element-icon-image-input"]').selectFile(
'public/assets/baderech-haavodah-icon.png',
{ force: true },
)
cy.wait('@updateElement')
cy.get('[data-cy="admin-element-pdf-input"]').selectFile(
'public/assets/pdfs/baderech.pdf',
{ force: true },
)
cy.wait('@updateElement')
cy.contains('button', 'Remove icon image').click()
cy.wait('@updateElement')
cy.get('[data-cy="admin-element-icon-image-status"]')
.should('be.visible')
.and('contain.text', 'Icon image removed')
cy.get('[data-cy="admin-element-current-icon"]').should('not.exist')
cy.contains('No icon image').should('be.visible')
cy.contains('button', 'Remove PDF').click()
cy.wait('@updateElement')
cy.get('[data-cy="admin-element-pdf-status"]')
.should('be.visible')
.and('contain.text', 'PDF removed')
cy.get('[data-cy="admin-element-current-pdf"]').should('not.exist')
cy.contains('No PDF').should('be.visible')
cy.contains('header.site-header a', 'View Element').click()
cy.get('[data-cy="element-icon"]').should('not.exist')
cy.get('[data-cy="element-pdf-link"]').should('not.exist')
cy.resetDb()
})
}) })