diff --git a/backend/tests/Fakes/FakeFileUploader.php b/backend/tests/Fakes/FakeFileUploader.php index 61ab381..9273887 100644 --- a/backend/tests/Fakes/FakeFileUploader.php +++ b/backend/tests/Fakes/FakeFileUploader.php @@ -12,6 +12,11 @@ class FakeFileUploader implements FileUploader */ public array $uploads = []; + /** + * @var string[] + */ + public array $deletedPaths = []; + public function upload(FileToUpload $file, string $folder): string { $this->uploads[] = ['file' => $file, 'folder' => $folder]; @@ -23,4 +28,9 @@ class FakeFileUploader implements FileUploader { return 'https://test.local/storage/' . $path; } + + public function delete(string $path): void + { + $this->deletedPaths[] = $path; + } } diff --git a/backend/tests/Feature/ElementsEndpointTest.php b/backend/tests/Feature/ElementsEndpointTest.php index 47b9f37..48292ce 100644 --- a/backend/tests/Feature/ElementsEndpointTest.php +++ b/backend/tests/Feature/ElementsEndpointTest.php @@ -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: '
A structured path for growth
', + 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 { $setRepository = app(SetRepository::class); diff --git a/backend/tests/Unit/Element/UseCases/UpdateElementFieldsTest.php b/backend/tests/Unit/Element/UseCases/UpdateElementFieldsTest.php index 7c27c9c..14f8a34 100644 --- a/backend/tests/Unit/Element/UseCases/UpdateElementFieldsTest.php +++ b/backend/tests/Unit/Element/UseCases/UpdateElementFieldsTest.php @@ -19,15 +19,19 @@ use App\Element\UseCases\UpdateElement\UpdateYoutubeUrlRequest; use App\Exceptions\BadRequestException; use App\Set\Set as DomainSet; use Tests\Fakes\FakeElementRepository; +use Tests\Fakes\FakeFileUploader; use Tests\TestCase; class UpdateElementFieldsTest extends TestCase { private FakeElementRepository $elementRepo; + private FakeFileUploader $fileUploader; + protected function setUp(): void { $this->elementRepo = new FakeElementRepository(); + $this->fileUploader = new FakeFileUploader(); } public function testUpdateTitleUpdatesOnlyTitle(): void @@ -88,7 +92,10 @@ class UpdateElementFieldsTest extends TestCase public function testUpdateIconImageUrlClearsEmptyString(): void { $element = $this->createFilledElement(); - $updateIconImageUrl = new UpdateIconImageUrl($this->elementRepo); + $updateIconImageUrl = new UpdateIconImageUrl( + $this->elementRepo, + $this->fileUploader, + ); $updatedElement = $updateIconImageUrl->execute( new UpdateIconImageUrlRequest( @@ -99,6 +106,32 @@ class UpdateElementFieldsTest extends TestCase $this->assertNull($updatedElement->getIconImageUrl()); $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 @@ -126,7 +159,10 @@ class UpdateElementFieldsTest extends TestCase public function testUpdatePdfPathClearsEmptyString(): void { $element = $this->createFilledElement(); - $updatePdfPath = new UpdatePdfPath($this->elementRepo); + $updatePdfPath = new UpdatePdfPath( + $this->elementRepo, + $this->fileUploader, + ); $updatedElement = $updatePdfPath->execute( new UpdatePdfPathRequest(id: $element->getId(), pdfPath: '') @@ -134,6 +170,29 @@ class UpdateElementFieldsTest extends TestCase $this->assertNull($updatedElement->getPdfPath()); $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 @@ -154,6 +213,16 @@ class UpdateElementFieldsTest extends TestCase 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( id: 1, name: 'Baderech', @@ -165,9 +234,9 @@ class UpdateElementFieldsTest extends TestCase set: $set, title: 'Original title', description: 'Original description', - iconImageUrl: '/assets/original-icon.png', + iconImageUrl: $iconImageUrl, richText: 'Original rich text
', - pdfPath: '/assets/pdfs/original.pdf', + pdfPath: $pdfPath, youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU', parentElement: null, )); diff --git a/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts index 35b5745..38dc7a1 100644 --- a/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts +++ b/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts @@ -151,4 +151,44 @@ describe('admin element editing', () => { 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() + }) })