From 0d5783ba9c772a1444bd16d1d31b0040d543c02b Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 14 Jun 2026 23:05:45 +0300 Subject: [PATCH 1/4] test element file routing --- backend/tests/Feature/DemoSeedDataTest.php | 15 +- .../tests/Feature/ElementsEndpointTest.php | 457 ++++++++++++------ backend/tests/Feature/SetsEndpointTest.php | 3 +- .../Controllers/ElementControllerTest.php | 118 +++-- backend/tests/Unit/Element/ElementTest.php | 24 +- .../Element/UseCases/CreateElementTest.php | 68 ++- .../Unit/Element/UseCases/GetElementTest.php | 5 +- .../UseCases/UpdateElementFieldsTest.php | 57 ++- .../Element/UseCases/UpdateElementTest.php | 302 +++++++++--- .../Element/UseCases/UpdateIconImageTest.php | 43 +- .../Element/UseCases/UpdateLongPdfTest.php | 95 ++++ ...datePdfTest.php => UpdateShortPdfTest.php} | 78 +-- .../cypress/e2e/admin-element.cy.ts | 59 ++- frontend/rabbi_gerzi/cypress/e2e/media.cy.ts | 15 +- 14 files changed, 951 insertions(+), 388 deletions(-) create mode 100644 backend/tests/Unit/Element/UseCases/UpdateLongPdfTest.php rename backend/tests/Unit/Element/UseCases/{UpdatePdfTest.php => UpdateShortPdfTest.php} (65%) diff --git a/backend/tests/Feature/DemoSeedDataTest.php b/backend/tests/Feature/DemoSeedDataTest.php index b802b2b..4e96193 100644 --- a/backend/tests/Feature/DemoSeedDataTest.php +++ b/backend/tests/Feature/DemoSeedDataTest.php @@ -43,7 +43,8 @@ class DemoSeedDataTest extends TestCase Storage::disk('public')->assertExists( 'element-icons/baderech-haavodah-icon.png', ); - $this->assertNull($rootElement->getPdfPath()); + $this->assertNull($rootElement->getShortPdfPath()); + $this->assertNull($rootElement->getLongPdfPath()); $this->assertNull($rootElement->getYoutubeUrl()); $this->assertCount(6, $childElements); $this->assertSame( @@ -96,10 +97,13 @@ class DemoSeedDataTest extends TestCase $childElements[0]->getRichText(), ); $this->assertSame( - 'element-pdfs/baderech.pdf', - $childElements[0]->getPdfPath(), + 'element-pdfs/short/baderech.pdf', + $childElements[0]->getShortPdfPath(), + ); + $this->assertNull($childElements[0]->getLongPdfPath()); + Storage::disk('public')->assertExists( + 'element-pdfs/short/baderech.pdf', ); - Storage::disk('public')->assertExists('element-pdfs/baderech.pdf'); $introductionChildElements = $elementRepository->findByParentElement( $childElements[0], @@ -114,7 +118,8 @@ class DemoSeedDataTest extends TestCase $this->assertSame('', $introductionAudioElement->getDescription()); $this->assertNull($introductionAudioElement->getIconImageUrl()); $this->assertSame('', $introductionAudioElement->getRichText()); - $this->assertNull($introductionAudioElement->getPdfPath()); + $this->assertNull($introductionAudioElement->getShortPdfPath()); + $this->assertNull($introductionAudioElement->getLongPdfPath()); $this->assertSame( 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', $introductionAudioElement->getYoutubeUrl(), diff --git a/backend/tests/Feature/ElementsEndpointTest.php b/backend/tests/Feature/ElementsEndpointTest.php index 48292ce..2c11199 100644 --- a/backend/tests/Feature/ElementsEndpointTest.php +++ b/backend/tests/Feature/ElementsEndpointTest.php @@ -5,8 +5,10 @@ namespace Tests\Feature; use App\Auth\CreateSessionDto; use App\Auth\SessionRepository; use App\Element\CreateElementDto; +use App\Element\Element; use App\Element\ElementRepository; use App\Set\CreateSetDto; +use App\Set\Set; use App\Set\SetRepository; use App\Shared\ValueObject\EmailAddress; use App\User\CreateUserDto; @@ -28,41 +30,43 @@ class ElementsEndpointTest extends TestCase . 'yHx-r4p6hHU&t=1s'; $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: '/assets/baderech-haavodah-icon.png', - richText: '

A structured path for growth

', - pdfPath: '/assets/pdfs/baderech.pdf', - youtubeUrl: $sampleYoutubeUrl, - parentElement: null, - )); - $firstChildElement = $elementRepository->create(new CreateElementDto( - set: $set, - title: 'Avodah Foundations', - description: 'Foundations for steady avodah', - iconImageUrl: null, - richText: '

Foundations rich text

', - pdfPath: '/assets/pdfs/foundations.pdf', - youtubeUrl: null, - parentElement: $element, - )); - $secondChildElement = $elementRepository->create(new CreateElementDto( - set: $set, - title: 'Daily Practice', - description: 'Daily practices for growth', - iconImageUrl: null, - richText: '

Daily practice rich text

', - pdfPath: null, - youtubeUrl: null, - parentElement: $element, - )); + $set = $this->createSet($setRepository); + $element = $this->createElement( + $elementRepository, + $set, + 'Baderech HaAvodah', + 'A structured path for growth', + '/assets/baderech-haavodah-icon.png', + '

A structured path for growth

', + '/assets/pdfs/baderech.pdf', + '/assets/pdfs/baderech-long.pdf', + $sampleYoutubeUrl, + null, + ); + $firstChildElement = $this->createElement( + $elementRepository, + $set, + 'Avodah Foundations', + 'Foundations for steady avodah', + null, + '

Foundations rich text

', + '/assets/pdfs/foundations.pdf', + null, + null, + $element, + ); + $secondChildElement = $this->createElement( + $elementRepository, + $set, + 'Daily Practice', + 'Daily practices for growth', + null, + '

Daily practice rich text

', + null, + null, + null, + $element, + ); $response = $this->getJson("/api/elements/{$element->getId()}"); @@ -86,6 +90,8 @@ class ElementsEndpointTest extends TestCase 'description' => 'A structured path for growth', 'iconImageUrl' => '/assets/baderech-haavodah-icon.png', 'richText' => '

A structured path for growth

', + 'shortPdfPath' => '/assets/pdfs/baderech.pdf', + 'longPdfPath' => '/assets/pdfs/baderech-long.pdf', 'pdfPath' => '/assets/pdfs/baderech.pdf', 'youtubeUrl' => $sampleYoutubeUrl, ], @@ -106,21 +112,19 @@ class ElementsEndpointTest extends TestCase { $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: '/assets/baderech-haavodah-icon.png', - richText: '

A structured path for growth

', - pdfPath: '/assets/pdfs/baderech.pdf', - youtubeUrl: null, - parentElement: null, - )); + $set = $this->createSet($setRepository); + $element = $this->createElement( + $elementRepository, + $set, + 'Baderech HaAvodah', + 'A structured path for growth', + '/assets/baderech-haavodah-icon.png', + '

A structured path for growth

', + '/assets/pdfs/baderech.pdf', + null, + null, + null, + ); $response = $this->postJson('/api/element/update', [ 'elementId' => $element->getId(), @@ -128,7 +132,8 @@ class ElementsEndpointTest extends TestCase 'description' => 'Updated description', 'iconImageUrl' => '/assets/updated-icon.png', 'richText' => '

Updated rich text

', - 'pdfPath' => '/assets/pdfs/updated.pdf', + 'shortPdfPath' => '/assets/pdfs/updated.pdf', + 'longPdfPath' => '/assets/pdfs/updated-long.pdf', 'youtubeUrl' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', ]); @@ -144,21 +149,19 @@ class ElementsEndpointTest extends TestCase . 'dQw4w9WgXcQ'; $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: '/assets/baderech-haavodah-icon.png', - richText: '

A structured path for growth

', - pdfPath: '/assets/pdfs/baderech.pdf', - youtubeUrl: null, - parentElement: null, - )); + $set = $this->createSet($setRepository); + $element = $this->createElement( + $elementRepository, + $set, + 'Baderech HaAvodah', + 'A structured path for growth', + '/assets/baderech-haavodah-icon.png', + '

A structured path for growth

', + '/assets/pdfs/baderech.pdf', + null, + null, + null, + ); $this->createSession('valid-token'); $response = $this->withCredentials() @@ -169,7 +172,8 @@ class ElementsEndpointTest extends TestCase 'description' => 'Updated description', 'iconImageUrl' => '/assets/updated-icon.png', 'richText' => '

Updated rich text

', - 'pdfPath' => '/assets/pdfs/updated.pdf', + 'shortPdfPath' => '/assets/pdfs/updated.pdf', + 'longPdfPath' => '/assets/pdfs/updated-long.pdf', 'youtubeUrl' => $sampleYoutubeUrl, ]); @@ -181,6 +185,8 @@ class ElementsEndpointTest extends TestCase 'description' => 'Updated description', 'iconImageUrl' => '/assets/updated-icon.png', 'richText' => '

Updated rich text

', + 'shortPdfPath' => '/assets/pdfs/updated.pdf', + 'longPdfPath' => '/assets/pdfs/updated-long.pdf', 'pdfPath' => '/assets/pdfs/updated.pdf', 'youtubeUrl' => $sampleYoutubeUrl, ], @@ -191,6 +197,52 @@ class ElementsEndpointTest extends TestCase '

Updated rich text

', $updatedElement->getRichText(), ); + $this->assertSame( + '/assets/pdfs/updated-long.pdf', + $updatedElement->getLongPdfPath(), + ); + } + + public function testAuthenticatedUpdateAcceptsLegacyPdfPath(): void + { + $setRepository = app(SetRepository::class); + $elementRepository = app(ElementRepository::class); + $set = $this->createSet($setRepository); + $element = $this->createElement( + $elementRepository, + $set, + 'Baderech HaAvodah', + 'A structured path for growth', + null, + '

A structured path for growth

', + '/assets/pdfs/baderech.pdf', + '/assets/pdfs/baderech-long.pdf', + null, + null, + ); + $this->createSession('valid-token'); + + $response = $this->withCredentials() + ->withUnencryptedCookie('auth_token', 'valid-token') + ->postJson('/api/element/update', [ + 'elementId' => $element->getId(), + 'pdfPath' => '/assets/pdfs/legacy-updated.pdf', + ]); + + $response->assertOk(); + $body = $response->json(); + $this->assertSame( + '/assets/pdfs/legacy-updated.pdf', + $body['element']['shortPdfPath'], + ); + $this->assertSame( + '/assets/pdfs/legacy-updated.pdf', + $body['element']['pdfPath'], + ); + $this->assertSame( + '/assets/pdfs/baderech-long.pdf', + $body['element']['longPdfPath'], + ); } public function testAuthenticatedUpdateClearsUploadedMedia(): void @@ -201,26 +253,28 @@ class ElementsEndpointTest extends TestCase 'icon bytes', ); Storage::disk('public')->put( - 'element-pdfs/original.pdf', - 'pdf bytes', + 'element-pdfs/short/original.pdf', + 'short pdf bytes', + ); + Storage::disk('public')->put( + 'element-pdfs/long/original-long.pdf', + 'long 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, - )); + $set = $this->createSet($setRepository); + $element = $this->createElement( + $elementRepository, + $set, + 'Baderech HaAvodah', + 'A structured path for growth', + 'element-icons/original-icon.png', + '

A structured path for growth

', + 'element-pdfs/short/original.pdf', + 'element-pdfs/long/original-long.pdf', + null, + null, + ); $this->createSession('valid-token'); $response = $this->withCredentials() @@ -229,21 +283,28 @@ class ElementsEndpointTest extends TestCase 'elementId' => $element->getId(), 'iconImageUrl' => '', 'pdfPath' => '', + 'longPdfPath' => '', ]); $response->assertOk(); $body = $response->json(); $this->assertNull($body['element']['iconImageUrl']); + $this->assertNull($body['element']['shortPdfPath']); + $this->assertNull($body['element']['longPdfPath']); $this->assertNull($body['element']['pdfPath']); $updatedElement = $elementRepository->find($element->getId()); $this->assertNotNull($updatedElement); $this->assertNull($updatedElement->getIconImageUrl()); - $this->assertNull($updatedElement->getPdfPath()); + $this->assertNull($updatedElement->getShortPdfPath()); + $this->assertNull($updatedElement->getLongPdfPath()); Storage::disk('public')->assertMissing( 'element-icons/original-icon.png', ); Storage::disk('public')->assertMissing( - 'element-pdfs/original.pdf', + 'element-pdfs/short/original.pdf', + ); + Storage::disk('public')->assertMissing( + 'element-pdfs/long/original-long.pdf', ); } @@ -251,27 +312,26 @@ class ElementsEndpointTest extends TestCase { $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: null, - richText: '

A structured path for growth

', - pdfPath: null, - youtubeUrl: null, - parentElement: null, - )); + $set = $this->createSet($setRepository); + $element = $this->createElement( + $elementRepository, + $set, + 'Baderech HaAvodah', + 'A structured path for growth', + null, + '

A structured path for growth

', + null, + null, + null, + null, + ); $response = $this->post( '/api/element/update', [ 'elementId' => (string) $element->getId(), - 'iconImage' => $this->iconImageUpload(), + 'fileType' => 'iconImage', + 'file' => $this->iconImageUpload(), ], ); @@ -286,21 +346,19 @@ class ElementsEndpointTest extends TestCase Storage::fake('public'); $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: null, - richText: '

A structured path for growth

', - pdfPath: null, - youtubeUrl: null, - parentElement: null, - )); + $set = $this->createSet($setRepository); + $element = $this->createElement( + $elementRepository, + $set, + 'Baderech HaAvodah', + 'A structured path for growth', + null, + '

A structured path for growth

', + null, + null, + null, + null, + ); $this->createSession('valid-token'); $response = $this->withCredentials() @@ -309,7 +367,8 @@ class ElementsEndpointTest extends TestCase '/api/element/update', [ 'elementId' => (string) $element->getId(), - 'iconImage' => $this->iconImageUpload(), + 'fileType' => 'iconImage', + 'file' => $this->iconImageUpload(), ], ); @@ -331,31 +390,30 @@ class ElementsEndpointTest extends TestCase Storage::disk('public')->assertExists($updatedIconImageUrl); } - public function testPdfUploadRequiresAuthentication(): void + public function testShortPdfUploadRequiresAuthentication(): void { $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: null, - richText: '

A structured path for growth

', - pdfPath: null, - youtubeUrl: null, - parentElement: null, - )); + $set = $this->createSet($setRepository); + $element = $this->createElement( + $elementRepository, + $set, + 'Baderech HaAvodah', + 'A structured path for growth', + null, + '

A structured path for growth

', + null, + null, + null, + null, + ); $response = $this->post( '/api/element/update', [ 'elementId' => (string) $element->getId(), - 'pdf' => $this->pdfUpload(), + 'fileType' => 'shortPdf', + 'file' => $this->pdfUpload(), ], ); @@ -365,26 +423,24 @@ class ElementsEndpointTest extends TestCase ]); } - public function testAuthenticatedPdfUploadReturnsElementPayload(): void + public function testAuthenticatedShortPdfUploadReturnsElementPayload(): void { Storage::fake('public'); $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: null, - richText: '

A structured path for growth

', - pdfPath: null, - youtubeUrl: null, - parentElement: null, - )); + $set = $this->createSet($setRepository); + $element = $this->createElement( + $elementRepository, + $set, + 'Baderech HaAvodah', + 'A structured path for growth', + null, + '

A structured path for growth

', + null, + null, + null, + null, + ); $this->createSession('valid-token'); $response = $this->withCredentials() @@ -393,7 +449,8 @@ class ElementsEndpointTest extends TestCase '/api/element/update', [ 'elementId' => (string) $element->getId(), - 'pdf' => $this->pdfUpload(), + 'fileType' => 'shortPdf', + 'file' => $this->pdfUpload(), ], ); @@ -401,17 +458,107 @@ class ElementsEndpointTest extends TestCase $body = $response->json(); $this->assertSame($element->getId(), $body['element']['id']); $this->assertStringContainsString( - '/storage/element-pdfs/', + '/storage/element-pdfs/short/', + $body['element']['shortPdfPath'], + ); + $this->assertSame( + $body['element']['shortPdfPath'], $body['element']['pdfPath'], ); $updatedElement = $elementRepository->find($element->getId()); $this->assertNotNull($updatedElement); - $updatedPdfPath = $updatedElement->getPdfPath(); + $updatedPdfPath = $updatedElement->getShortPdfPath(); $this->assertIsString($updatedPdfPath); - $this->assertStringStartsWith('element-pdfs/', $updatedPdfPath); + $this->assertStringStartsWith( + 'element-pdfs/short/', + $updatedPdfPath, + ); Storage::disk('public')->assertExists($updatedPdfPath); } + public function testAuthenticatedLongPdfUploadReturnsElementPayload(): void + { + Storage::fake('public'); + $setRepository = app(SetRepository::class); + $elementRepository = app(ElementRepository::class); + $set = $this->createSet($setRepository); + $element = $this->createElement( + $elementRepository, + $set, + 'Baderech HaAvodah', + 'A structured path for growth', + null, + '

A structured path for growth

', + null, + null, + null, + null, + ); + $this->createSession('valid-token'); + + $response = $this->withCredentials() + ->withUnencryptedCookie('auth_token', 'valid-token') + ->post( + '/api/element/update', + [ + 'elementId' => (string) $element->getId(), + 'fileType' => 'longPdf', + 'file' => $this->pdfUpload(), + ], + ); + + $response->assertOk(); + $body = $response->json(); + $this->assertSame($element->getId(), $body['element']['id']); + $this->assertStringContainsString( + '/storage/element-pdfs/long/', + $body['element']['longPdfPath'], + ); + $updatedElement = $elementRepository->find($element->getId()); + $this->assertNotNull($updatedElement); + $updatedPdfPath = $updatedElement->getLongPdfPath(); + $this->assertIsString($updatedPdfPath); + $this->assertStringStartsWith( + 'element-pdfs/long/', + $updatedPdfPath, + ); + Storage::disk('public')->assertExists($updatedPdfPath); + } + + private function createSet(SetRepository $setRepository): Set + { + return $setRepository->create(new CreateSetDto( + name: 'Baderech HaAvodah', + description: 'A structured path for growth', + iconImageUrl: '/assets/baderech-haavodah-icon.png', + )); + } + + private function createElement( + ElementRepository $elementRepository, + Set $set, + string $title, + string $description, + ?string $iconImageUrl, + string $richText, + ?string $shortPdfPath, + ?string $longPdfPath, + ?string $youtubeUrl, + ?Element $parentElement, + ): Element { + return $elementRepository->create(new CreateElementDto( + set: $set, + title: $title, + description: $description, + iconImageUrl: $iconImageUrl, + richText: $richText, + shortPdfPath: $shortPdfPath, + longPdfPath: $longPdfPath, + youtubeUrl: $youtubeUrl, + parentElement: $parentElement, + )); + } + private function createSession(string $token): void { $userRepository = app(UserRepository::class); diff --git a/backend/tests/Feature/SetsEndpointTest.php b/backend/tests/Feature/SetsEndpointTest.php index c7c67c4..2f8264b 100644 --- a/backend/tests/Feature/SetsEndpointTest.php +++ b/backend/tests/Feature/SetsEndpointTest.php @@ -34,7 +34,8 @@ class SetsEndpointTest extends TestCase description: $baderechSet->getDescription(), iconImageUrl: null, richText: '', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: null, parentElement: null, ) diff --git a/backend/tests/Unit/Controllers/ElementControllerTest.php b/backend/tests/Unit/Controllers/ElementControllerTest.php index 5416d92..69ec667 100644 --- a/backend/tests/Unit/Controllers/ElementControllerTest.php +++ b/backend/tests/Unit/Controllers/ElementControllerTest.php @@ -10,9 +10,11 @@ use App\Element\UseCases\UpdateElement\UpdateDescription; use App\Element\UseCases\UpdateElement\UpdateElement; use App\Element\UseCases\UpdateElement\UpdateIconImage; use App\Element\UseCases\UpdateElement\UpdateIconImageUrl; -use App\Element\UseCases\UpdateElement\UpdatePdf; -use App\Element\UseCases\UpdateElement\UpdatePdfPath; +use App\Element\UseCases\UpdateElement\UpdateLongPdf; +use App\Element\UseCases\UpdateElement\UpdateLongPdfPath; use App\Element\UseCases\UpdateElement\UpdateRichText; +use App\Element\UseCases\UpdateElement\UpdateShortPdf; +use App\Element\UseCases\UpdateElement\UpdateShortPdfPath; use App\Element\UseCases\UpdateElement\UpdateTitle; use App\Element\UseCases\UpdateElement\UpdateYoutubeUrl; use App\Set\Set as DomainSet; @@ -40,13 +42,18 @@ class ElementControllerTest extends TestCase new UpdateDescription($this->elementRepo), new UpdateIconImageUrl($this->elementRepo, $this->fileUploader), new UpdateRichText($this->elementRepo), - new UpdatePdfPath($this->elementRepo, $this->fileUploader), + new UpdateShortPdfPath($this->elementRepo, $this->fileUploader), + new UpdateLongPdfPath($this->elementRepo, $this->fileUploader), new UpdateYoutubeUrl($this->elementRepo), new UpdateIconImage( $this->elementRepo, $this->fileUploader, ), - new UpdatePdf( + new UpdateShortPdf( + $this->elementRepo, + $this->fileUploader, + ), + new UpdateLongPdf( $this->elementRepo, $this->fileUploader, ), @@ -69,6 +76,7 @@ class ElementControllerTest extends TestCase '/assets/baderech-icon.png', '

A structured path for growth

', '/assets/pdfs/baderech.pdf', + '/assets/pdfs/baderech-long.pdf', 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', null, ); @@ -80,6 +88,7 @@ class ElementControllerTest extends TestCase '

Foundations rich text

', '/assets/pdfs/foundations.pdf', null, + null, $element, ); $secondChildElement = $this->createElement( @@ -90,6 +99,7 @@ class ElementControllerTest extends TestCase '

Daily practice rich text

', null, null, + null, $element, ); @@ -111,6 +121,14 @@ class ElementControllerTest extends TestCase '/assets/baderech-icon.png', $body['element']['iconImageUrl'], ); + $this->assertSame( + '/assets/pdfs/baderech.pdf', + $body['element']['shortPdfPath'], + ); + $this->assertSame( + '/assets/pdfs/baderech-long.pdf', + $body['element']['longPdfPath'], + ); $this->assertSame( '/assets/pdfs/baderech.pdf', $body['element']['pdfPath'], @@ -158,21 +176,13 @@ class ElementControllerTest extends TestCase public function testUpdateWithIconImageReturnsElementPayload(): void { $set = $this->createSet(1, 'Baderech'); - $element = $this->createElement( - $set, - 'Baderech HaAvodah', - 'A structured path for growth', - null, - '

A structured path for growth

', - null, - null, - null, - ); + $element = $this->createEmptyElement($set); $fixturePath = __DIR__ . '/../../fixtures/icon.png'; $request = new Request([], [ 'elementId' => (string) $element->getId(), + 'fileType' => 'iconImage', ], [], [], [ - 'iconImage' => new UploadedFile( + 'file' => new UploadedFile( $fixturePath, 'icon.png', 'image/png', @@ -196,24 +206,16 @@ class ElementControllerTest extends TestCase ); } - public function testUpdateWithPdfReturnsElementPayload(): void + public function testUpdateWithShortPdfReturnsElementPayload(): void { $set = $this->createSet(1, 'Baderech'); - $element = $this->createElement( - $set, - 'Baderech HaAvodah', - 'A structured path for growth', - null, - '

A structured path for growth

', - null, - null, - null, - ); + $element = $this->createEmptyElement($set); $fixturePath = __DIR__ . '/../../fixtures/baderech.pdf'; $request = new Request([], [ 'elementId' => (string) $element->getId(), + 'fileType' => 'shortPdf', ], [], [], [ - 'pdf' => new UploadedFile( + 'file' => new UploadedFile( $fixturePath, 'baderech.pdf', 'application/pdf', @@ -228,11 +230,48 @@ class ElementControllerTest extends TestCase $body = json_decode($response->getContent(), true); $this->assertSame($element->getId(), $body['element']['id']); $this->assertSame( - 'https://test.local/storage/element-pdfs/fake-baderech.pdf', + 'https://test.local/storage/element-pdfs/short/fake-baderech.pdf', + $body['element']['shortPdfPath'], + ); + $this->assertSame( + $body['element']['shortPdfPath'], $body['element']['pdfPath'], ); $this->assertSame( - 'element-pdfs', + 'element-pdfs/short', + $this->fileUploader->uploads[0]['folder'], + ); + } + + public function testUpdateWithLongPdfReturnsElementPayload(): void + { + $set = $this->createSet(1, 'Baderech'); + $element = $this->createEmptyElement($set); + $fixturePath = __DIR__ . '/../../fixtures/baderech.pdf'; + $request = new Request([], [ + 'elementId' => (string) $element->getId(), + 'fileType' => 'longPdf', + ], [], [], [ + 'file' => new UploadedFile( + $fixturePath, + 'baderech.pdf', + 'application/pdf', + null, + true, + ), + ], ['REQUEST_METHOD' => 'POST']); + + $response = $this->controller->update($request); + + $this->assertEquals(200, $response->getStatusCode()); + $body = json_decode($response->getContent(), true); + $this->assertSame($element->getId(), $body['element']['id']); + $this->assertSame( + 'https://test.local/storage/element-pdfs/long/fake-baderech.pdf', + $body['element']['longPdfPath'], + ); + $this->assertSame( + 'element-pdfs/long', $this->fileUploader->uploads[0]['folder'], ); } @@ -247,13 +286,29 @@ class ElementControllerTest extends TestCase ); } + private function createEmptyElement(DomainSet $set): Element + { + return $this->createElement( + $set, + 'Baderech HaAvodah', + 'A structured path for growth', + null, + '

A structured path for growth

', + null, + null, + null, + null, + ); + } + private function createElement( DomainSet $set, string $title, string $description, ?string $iconImageUrl, string $richText, - ?string $pdfPath, + ?string $shortPdfPath, + ?string $longPdfPath, ?string $youtubeUrl, ?Element $parentElement, ): Element { @@ -263,7 +318,8 @@ class ElementControllerTest extends TestCase description: $description, iconImageUrl: $iconImageUrl, richText: $richText, - pdfPath: $pdfPath, + shortPdfPath: $shortPdfPath, + longPdfPath: $longPdfPath, youtubeUrl: $youtubeUrl, parentElement: $parentElement, )); diff --git a/backend/tests/Unit/Element/ElementTest.php b/backend/tests/Unit/Element/ElementTest.php index 89e9c84..b57eaa5 100644 --- a/backend/tests/Unit/Element/ElementTest.php +++ b/backend/tests/Unit/Element/ElementTest.php @@ -22,7 +22,8 @@ class ElementTest extends TestCase description: 'Root description', iconImageUrl: null, richText: '

Root rich text

', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: null, set: $set, parentElement: null, @@ -33,7 +34,8 @@ class ElementTest extends TestCase description: 'Child description', iconImageUrl: '/assets/child-icon.svg', richText: '

Child rich text

', - pdfPath: '/assets/pdfs/child.pdf', + shortPdfPath: '/assets/pdfs/child.pdf', + longPdfPath: '/assets/pdfs/child-long.pdf', youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', set: $set, parentElement: $rootElement, @@ -55,7 +57,11 @@ class ElementTest extends TestCase ); $this->assertSame( '/assets/pdfs/child.pdf', - $childElement->getPdfPath(), + $childElement->getShortPdfPath(), + ); + $this->assertSame( + '/assets/pdfs/child-long.pdf', + $childElement->getLongPdfPath(), ); $this->assertSame( 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', @@ -64,7 +70,8 @@ class ElementTest extends TestCase $this->assertSame($set, $childElement->getSet()); $this->assertSame($rootElement, $childElement->getParentElement()); $this->assertNull($rootElement->getIconImageUrl()); - $this->assertNull($rootElement->getPdfPath()); + $this->assertNull($rootElement->getShortPdfPath()); + $this->assertNull($rootElement->getLongPdfPath()); $this->assertNull($rootElement->getYoutubeUrl()); $this->assertNull($rootElement->getParentElement()); } @@ -83,7 +90,8 @@ class ElementTest extends TestCase description: 'Original description', iconImageUrl: '/assets/original.svg', richText: '

Original

', - pdfPath: '/assets/pdfs/original.pdf', + shortPdfPath: '/assets/pdfs/original.pdf', + longPdfPath: '/assets/pdfs/original-long.pdf', youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU', set: $set, parentElement: null, @@ -93,14 +101,16 @@ class ElementTest extends TestCase $element->setDescription('Updated description'); $element->setIconImageUrl(null); $element->setRichText('

Updated

'); - $element->setPdfPath(null); + $element->setShortPdfPath(null); + $element->setLongPdfPath(null); $element->setYoutubeUrl(null); $this->assertSame('Updated', $element->getTitle()); $this->assertSame('Updated description', $element->getDescription()); $this->assertNull($element->getIconImageUrl()); $this->assertSame('

Updated

', $element->getRichText()); - $this->assertNull($element->getPdfPath()); + $this->assertNull($element->getShortPdfPath()); + $this->assertNull($element->getLongPdfPath()); $this->assertNull($element->getYoutubeUrl()); } } diff --git a/backend/tests/Unit/Element/UseCases/CreateElementTest.php b/backend/tests/Unit/Element/UseCases/CreateElementTest.php index 09ad0d8..d13234b 100644 --- a/backend/tests/Unit/Element/UseCases/CreateElementTest.php +++ b/backend/tests/Unit/Element/UseCases/CreateElementTest.php @@ -50,7 +50,8 @@ class CreateElementTest extends TestCase description: 'Root description', iconImageUrl: '/assets/root-icon.svg', richText: '

Root rich text

', - pdfPath: '/assets/pdfs/root.pdf', + shortPdfPath: '/assets/pdfs/root.pdf', + longPdfPath: '/assets/pdfs/root-long.pdf', youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', parentElementId: null, )); @@ -63,7 +64,14 @@ class CreateElementTest extends TestCase $element->getIconImageUrl(), ); $this->assertSame('

Root rich text

', $element->getRichText()); - $this->assertSame('/assets/pdfs/root.pdf', $element->getPdfPath()); + $this->assertSame( + '/assets/pdfs/root.pdf', + $element->getShortPdfPath(), + ); + $this->assertSame( + '/assets/pdfs/root-long.pdf', + $element->getLongPdfPath(), + ); $this->assertSame( 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', $element->getYoutubeUrl(), @@ -82,7 +90,8 @@ class CreateElementTest extends TestCase description: 'Root description', iconImageUrl: null, richText: '

Root rich text

', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: null, parentElementId: null, ) @@ -95,7 +104,8 @@ class CreateElementTest extends TestCase description: 'Child description', iconImageUrl: '/assets/child-icon.svg', richText: '

Child rich text

', - pdfPath: '/assets/pdfs/child.pdf', + shortPdfPath: '/assets/pdfs/child.pdf', + longPdfPath: '/assets/pdfs/child-long.pdf', youtubeUrl: 'https://youtu.be/yHx-r4p6hHU', parentElementId: $rootElement->getId(), ) @@ -116,7 +126,11 @@ class CreateElementTest extends TestCase ); $this->assertSame( '/assets/pdfs/child.pdf', - $childElement->getPdfPath(), + $childElement->getShortPdfPath(), + ); + $this->assertSame( + '/assets/pdfs/child-long.pdf', + $childElement->getLongPdfPath(), ); $this->assertSame( 'https://youtu.be/yHx-r4p6hHU', @@ -138,7 +152,8 @@ class CreateElementTest extends TestCase description: null, iconImageUrl: null, richText: null, - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: null, parentElementId: null, )); @@ -146,7 +161,8 @@ class CreateElementTest extends TestCase $this->assertSame('', $element->getDescription()); $this->assertNull($element->getIconImageUrl()); $this->assertSame('', $element->getRichText()); - $this->assertNull($element->getPdfPath()); + $this->assertNull($element->getShortPdfPath()); + $this->assertNull($element->getLongPdfPath()); $this->assertNull($element->getYoutubeUrl()); } @@ -160,7 +176,8 @@ class CreateElementTest extends TestCase description: 'Root description', iconImageUrl: '', richText: '

Root rich text

', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: null, parentElementId: null, )); @@ -168,7 +185,7 @@ class CreateElementTest extends TestCase $this->assertNull($element->getIconImageUrl()); } - public function testCreatesElementWithNullPdfPathWhenBlank(): void + public function testCreatesElementWithNullPdfPathsWhenBlank(): void { $set = $this->createSet('Daily learning'); @@ -178,12 +195,14 @@ class CreateElementTest extends TestCase description: 'Root description', iconImageUrl: null, richText: '

Root rich text

', - pdfPath: '', + shortPdfPath: '', + longPdfPath: '', youtubeUrl: null, parentElementId: null, )); - $this->assertNull($element->getPdfPath()); + $this->assertNull($element->getShortPdfPath()); + $this->assertNull($element->getLongPdfPath()); } public function testCreatesElementWithNullYoutubeUrlWhenBlank(): void @@ -196,7 +215,8 @@ class CreateElementTest extends TestCase description: 'Root description', iconImageUrl: null, richText: '

Root rich text

', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: '', parentElementId: null, )); @@ -215,7 +235,8 @@ class CreateElementTest extends TestCase description: 'Root description', iconImageUrl: null, richText: '

Root rich text

', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: null, parentElementId: null, )); @@ -232,7 +253,8 @@ class CreateElementTest extends TestCase description: 'Root description', iconImageUrl: null, richText: '

Root rich text

', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: null, parentElementId: null, )); @@ -249,7 +271,8 @@ class CreateElementTest extends TestCase description: 'Root description', iconImageUrl: null, richText: '

Root rich text

', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: null, parentElementId: null, )); @@ -270,7 +293,8 @@ class CreateElementTest extends TestCase description: 'Child description', iconImageUrl: null, richText: '

Child rich text

', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: null, parentElementId: 99, )); @@ -285,7 +309,8 @@ class CreateElementTest extends TestCase description: 'Root description', iconImageUrl: null, richText: '

Root rich text

', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: null, parentElementId: null, )); @@ -301,7 +326,8 @@ class CreateElementTest extends TestCase description: 'Another root description', iconImageUrl: null, richText: '

Another root rich text

', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: null, parentElementId: null, )); @@ -318,7 +344,8 @@ class CreateElementTest extends TestCase description: 'Parent root description', iconImageUrl: null, richText: '

Parent root rich text

', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: null, parentElementId: null, ) @@ -335,7 +362,8 @@ class CreateElementTest extends TestCase description: 'Invalid child description', iconImageUrl: null, richText: '

Invalid child rich text

', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: null, parentElementId: $parentElement->getId(), )); diff --git a/backend/tests/Unit/Element/UseCases/GetElementTest.php b/backend/tests/Unit/Element/UseCases/GetElementTest.php index ff066de..ad2e0d9 100644 --- a/backend/tests/Unit/Element/UseCases/GetElementTest.php +++ b/backend/tests/Unit/Element/UseCases/GetElementTest.php @@ -60,7 +60,7 @@ class GetElementTest extends TestCase ); $this->assertSame( '/assets/pdfs/baderech.pdf', - $foundElement->getPdfPath(), + $foundElement->getShortPdfPath(), ); $this->assertSame( 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', @@ -201,7 +201,8 @@ class GetElementTest extends TestCase description: $description, iconImageUrl: $iconImageUrl, richText: $richText, - pdfPath: $pdfPath, + shortPdfPath: $pdfPath, + longPdfPath: null, youtubeUrl: $youtubeUrl, parentElement: $parentElement, )); diff --git a/backend/tests/Unit/Element/UseCases/UpdateElementFieldsTest.php b/backend/tests/Unit/Element/UseCases/UpdateElementFieldsTest.php index ca39700..1d83906 100644 --- a/backend/tests/Unit/Element/UseCases/UpdateElementFieldsTest.php +++ b/backend/tests/Unit/Element/UseCases/UpdateElementFieldsTest.php @@ -8,8 +8,10 @@ use App\Element\UseCases\UpdateElement\UpdateDescription; use App\Element\UseCases\UpdateElement\UpdateDescriptionRequest; use App\Element\UseCases\UpdateElement\UpdateIconImageUrl; use App\Element\UseCases\UpdateElement\UpdateIconImageUrlRequest; -use App\Element\UseCases\UpdateElement\UpdatePdfPath; -use App\Element\UseCases\UpdateElement\UpdatePdfPathRequest; +use App\Element\UseCases\UpdateElement\UpdateLongPdfPath; +use App\Element\UseCases\UpdateElement\UpdateLongPdfPathRequest; +use App\Element\UseCases\UpdateElement\UpdateShortPdfPath; +use App\Element\UseCases\UpdateElement\UpdateShortPdfPathRequest; use App\Element\UseCases\UpdateElement\UpdateRichText; use App\Element\UseCases\UpdateElement\UpdateRichTextRequest; use App\Element\UseCases\UpdateElement\UpdateTitle; @@ -134,19 +136,26 @@ class UpdateElementFieldsTest extends TestCase ); } - public function testUpdatePdfPathClearsEmptyString(): void + public function testUpdateShortPdfPathClearsEmptyString(): void { $element = $this->createFilledElement(); - $updatePdfPath = new UpdatePdfPath( + $updateShortPdfPath = new UpdateShortPdfPath( $this->elementRepo, $this->fileUploader, ); - $updatedElement = $updatePdfPath->execute( - new UpdatePdfPathRequest(id: $element->getId(), pdfPath: '') + $updatedElement = $updateShortPdfPath->execute( + new UpdateShortPdfPathRequest( + id: $element->getId(), + shortPdfPath: '', + ) ); - $this->assertNull($updatedElement->getPdfPath()); + $this->assertNull($updatedElement->getShortPdfPath()); + $this->assertSame( + '/assets/pdfs/original-long.pdf', + $updatedElement->getLongPdfPath(), + ); $this->assertSame($element->getTitle(), $updatedElement->getTitle()); $this->assertSame( ['/assets/pdfs/original.pdf'], @@ -154,6 +163,33 @@ class UpdateElementFieldsTest extends TestCase ); } + public function testUpdateLongPdfPathClearsEmptyString(): void + { + $element = $this->createFilledElement(); + $updateLongPdfPath = new UpdateLongPdfPath( + $this->elementRepo, + $this->fileUploader, + ); + + $updatedElement = $updateLongPdfPath->execute( + new UpdateLongPdfPathRequest( + id: $element->getId(), + longPdfPath: '', + ) + ); + + $this->assertSame( + '/assets/pdfs/original.pdf', + $updatedElement->getShortPdfPath(), + ); + $this->assertNull($updatedElement->getLongPdfPath()); + $this->assertSame($element->getTitle(), $updatedElement->getTitle()); + $this->assertSame( + ['/assets/pdfs/original-long.pdf'], + $this->fileUploader->deletedPaths, + ); + } + public function testUpdateYoutubeUrlClearsEmptyString(): void { $element = $this->createFilledElement(); @@ -175,12 +211,14 @@ class UpdateElementFieldsTest extends TestCase return $this->createElementWithMedia( '/assets/original-icon.png', '/assets/pdfs/original.pdf', + '/assets/pdfs/original-long.pdf', ); } private function createElementWithMedia( ?string $iconImageUrl, - ?string $pdfPath, + ?string $shortPdfPath, + ?string $longPdfPath, ): Element { $set = new DomainSet( id: 1, @@ -195,7 +233,8 @@ class UpdateElementFieldsTest extends TestCase description: 'Original description', iconImageUrl: $iconImageUrl, richText: '

Original rich text

', - pdfPath: $pdfPath, + shortPdfPath: $shortPdfPath, + longPdfPath: $longPdfPath, youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU', parentElement: null, )); diff --git a/backend/tests/Unit/Element/UseCases/UpdateElementTest.php b/backend/tests/Unit/Element/UseCases/UpdateElementTest.php index 8d823e3..cea63d1 100644 --- a/backend/tests/Unit/Element/UseCases/UpdateElementTest.php +++ b/backend/tests/Unit/Element/UseCases/UpdateElementTest.php @@ -9,9 +9,11 @@ use App\Element\UseCases\UpdateElement\UpdateElement; use App\Element\UseCases\UpdateElement\UpdateElementRequest; use App\Element\UseCases\UpdateElement\UpdateIconImage; use App\Element\UseCases\UpdateElement\UpdateIconImageUrl; -use App\Element\UseCases\UpdateElement\UpdatePdfPath; -use App\Element\UseCases\UpdateElement\UpdatePdf; +use App\Element\UseCases\UpdateElement\UpdateLongPdf; +use App\Element\UseCases\UpdateElement\UpdateLongPdfPath; use App\Element\UseCases\UpdateElement\UpdateRichText; +use App\Element\UseCases\UpdateElement\UpdateShortPdf; +use App\Element\UseCases\UpdateElement\UpdateShortPdfPath; use App\Element\UseCases\UpdateElement\UpdateTitle; use App\Element\UseCases\UpdateElement\UpdateYoutubeUrl; use App\Exceptions\BadRequestException; @@ -38,13 +40,18 @@ class UpdateElementTest extends TestCase new UpdateDescription($this->elementRepo), new UpdateIconImageUrl($this->elementRepo, $this->fileUploader), new UpdateRichText($this->elementRepo), - new UpdatePdfPath($this->elementRepo, $this->fileUploader), + new UpdateShortPdfPath($this->elementRepo, $this->fileUploader), + new UpdateLongPdfPath($this->elementRepo, $this->fileUploader), new UpdateYoutubeUrl($this->elementRepo), new UpdateIconImage( $this->elementRepo, $this->fileUploader, ), - new UpdatePdf( + new UpdateShortPdf( + $this->elementRepo, + $this->fileUploader, + ), + new UpdateLongPdf( $this->elementRepo, $this->fileUploader, ), @@ -62,6 +69,7 @@ class UpdateElementTest extends TestCase '/assets/original-icon.png', '

Original rich text

', '/assets/pdfs/original.pdf', + '/assets/pdfs/original-long.pdf', 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', null, ); @@ -73,16 +81,15 @@ class UpdateElementTest extends TestCase description: 'Updated description', iconImageUrl: '/assets/updated-icon.png', richText: '

Updated rich text

', - pdfPath: '/assets/pdfs/updated.pdf', + shortPdfPath: '/assets/pdfs/updated.pdf', + longPdfPath: '/assets/pdfs/updated-long.pdf', + pdfPath: null, youtubeUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', - iconImageContents: null, - iconImageOriginalName: null, - iconImageMimeType: null, - iconImageSizeBytes: null, - pdfContents: null, - pdfOriginalName: null, - pdfMimeType: null, - pdfSizeBytes: null, + fileType: null, + fileContents: null, + fileOriginalName: null, + fileMimeType: null, + fileSizeBytes: null, ) ); @@ -102,7 +109,11 @@ class UpdateElementTest extends TestCase ); $this->assertSame( '/assets/pdfs/updated.pdf', - $updatedElement->getPdfPath(), + $updatedElement->getShortPdfPath(), + ); + $this->assertSame( + '/assets/pdfs/updated-long.pdf', + $updatedElement->getLongPdfPath(), ); $this->assertSame( 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', @@ -114,6 +125,50 @@ class UpdateElementTest extends TestCase $this->assertSame('Updated title', $persistedElement->getTitle()); } + public function testLegacyPdfPathUpdatesShortPdfPath(): void + { + $set = $this->createSet(1, 'Baderech'); + $element = $this->createElement( + $set, + 'Original title', + 'Original description', + null, + '

Original rich text

', + '/assets/pdfs/original.pdf', + '/assets/pdfs/original-long.pdf', + null, + null, + ); + + $updatedElement = $this->updateElement->execute( + new UpdateElementRequest( + id: $element->getId(), + title: null, + description: null, + iconImageUrl: null, + richText: null, + shortPdfPath: null, + longPdfPath: null, + pdfPath: '/assets/pdfs/legacy-updated.pdf', + youtubeUrl: null, + fileType: null, + fileContents: null, + fileOriginalName: null, + fileMimeType: null, + fileSizeBytes: null, + ) + ); + + $this->assertSame( + '/assets/pdfs/legacy-updated.pdf', + $updatedElement->getShortPdfPath(), + ); + $this->assertSame( + '/assets/pdfs/original-long.pdf', + $updatedElement->getLongPdfPath(), + ); + } + public function testConvertsEmptyNullableFieldsToNull(): void { $set = $this->createSet(1, 'Baderech'); @@ -124,6 +179,7 @@ class UpdateElementTest extends TestCase '/assets/original-icon.png', '

Original rich text

', '/assets/pdfs/original.pdf', + '/assets/pdfs/original-long.pdf', 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', null, ); @@ -135,21 +191,21 @@ class UpdateElementTest extends TestCase description: 'Updated description', iconImageUrl: '', richText: '

Updated rich text

', - pdfPath: '', + shortPdfPath: '', + longPdfPath: '', + pdfPath: null, youtubeUrl: '', - iconImageContents: null, - iconImageOriginalName: null, - iconImageMimeType: null, - iconImageSizeBytes: null, - pdfContents: null, - pdfOriginalName: null, - pdfMimeType: null, - pdfSizeBytes: null, + fileType: null, + fileContents: null, + fileOriginalName: null, + fileMimeType: null, + fileSizeBytes: null, ) ); $this->assertNull($updatedElement->getIconImageUrl()); - $this->assertNull($updatedElement->getPdfPath()); + $this->assertNull($updatedElement->getShortPdfPath()); + $this->assertNull($updatedElement->getLongPdfPath()); $this->assertNull($updatedElement->getYoutubeUrl()); } @@ -163,6 +219,7 @@ class UpdateElementTest extends TestCase '/assets/original-icon.png', '

Original rich text

', '/assets/pdfs/original.pdf', + '/assets/pdfs/original-long.pdf', 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', null, ); @@ -174,16 +231,15 @@ class UpdateElementTest extends TestCase description: null, iconImageUrl: null, richText: null, + shortPdfPath: null, + longPdfPath: null, pdfPath: null, youtubeUrl: null, - iconImageContents: null, - iconImageOriginalName: null, - iconImageMimeType: null, - iconImageSizeBytes: null, - pdfContents: null, - pdfOriginalName: null, - pdfMimeType: null, - pdfSizeBytes: null, + fileType: null, + fileContents: null, + fileOriginalName: null, + fileMimeType: null, + fileSizeBytes: null, ) ); @@ -202,7 +258,11 @@ class UpdateElementTest extends TestCase ); $this->assertSame( '/assets/pdfs/original.pdf', - $updatedElement->getPdfPath(), + $updatedElement->getShortPdfPath(), + ); + $this->assertSame( + '/assets/pdfs/original-long.pdf', + $updatedElement->getLongPdfPath(), ); $this->assertSame( 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', @@ -210,7 +270,7 @@ class UpdateElementTest extends TestCase ); } - public function testUpdatesUploadedFiles(): void + public function testRoutesUploadedFilesByFileType(): void { $set = $this->createSet(1, 'Baderech'); $element = $this->createElement( @@ -222,8 +282,41 @@ class UpdateElementTest extends TestCase null, null, null, + null, ); + $this->updateElement->execute(new UpdateElementRequest( + id: $element->getId(), + title: null, + description: null, + iconImageUrl: null, + richText: null, + shortPdfPath: null, + longPdfPath: null, + pdfPath: null, + youtubeUrl: null, + fileType: 'iconImage', + fileContents: 'binary-image-bytes', + fileOriginalName: 'icon.png', + fileMimeType: 'image/png', + fileSizeBytes: 1024, + )); + $this->updateElement->execute(new UpdateElementRequest( + id: $element->getId(), + title: null, + description: null, + iconImageUrl: null, + richText: null, + shortPdfPath: null, + longPdfPath: null, + pdfPath: null, + youtubeUrl: null, + fileType: 'shortPdf', + fileContents: 'binary-short-pdf-bytes', + fileOriginalName: 'baderech.pdf', + fileMimeType: 'application/pdf', + fileSizeBytes: 1024, + )); $updatedElement = $this->updateElement->execute( new UpdateElementRequest( id: $element->getId(), @@ -231,16 +324,15 @@ class UpdateElementTest extends TestCase description: null, iconImageUrl: null, richText: null, + shortPdfPath: null, + longPdfPath: null, pdfPath: null, youtubeUrl: null, - iconImageContents: 'binary-image-bytes', - iconImageOriginalName: 'icon.png', - iconImageMimeType: 'image/png', - iconImageSizeBytes: 1024, - pdfContents: 'binary-pdf-bytes', - pdfOriginalName: 'baderech.pdf', - pdfMimeType: 'application/pdf', - pdfSizeBytes: 1024, + fileType: 'longPdf', + fileContents: 'binary-long-pdf-bytes', + fileOriginalName: 'baderech-long.pdf', + fileMimeType: 'application/pdf', + fileSizeBytes: 1024, ) ); @@ -249,17 +341,71 @@ class UpdateElementTest extends TestCase $updatedElement->getIconImageUrl(), ); $this->assertSame( - 'element-pdfs/fake-baderech.pdf', - $updatedElement->getPdfPath(), + 'element-pdfs/short/fake-baderech.pdf', + $updatedElement->getShortPdfPath(), + ); + $this->assertSame( + 'element-pdfs/long/fake-baderech-long.pdf', + $updatedElement->getLongPdfPath(), ); $this->assertSame( 'element-icons', $this->fileUploader->uploads[0]['folder'], ); $this->assertSame( - 'element-pdfs', + 'element-pdfs/short', $this->fileUploader->uploads[1]['folder'], ); + $this->assertSame( + 'element-pdfs/long', + $this->fileUploader->uploads[2]['folder'], + ); + } + + public function testThrowsWhenFileTypeMissingForUpload(): void + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('fileType is required'); + + $this->updateElement->execute(new UpdateElementRequest( + id: 1, + title: null, + description: null, + iconImageUrl: null, + richText: null, + shortPdfPath: null, + longPdfPath: null, + pdfPath: null, + youtubeUrl: null, + fileType: null, + fileContents: 'binary-image-bytes', + fileOriginalName: 'icon.png', + fileMimeType: 'image/png', + fileSizeBytes: 1024, + )); + } + + public function testThrowsWhenFileTypeInvalid(): void + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('fileType is invalid'); + + $this->updateElement->execute(new UpdateElementRequest( + id: 1, + title: null, + description: null, + iconImageUrl: null, + richText: null, + shortPdfPath: null, + longPdfPath: null, + pdfPath: null, + youtubeUrl: null, + fileType: 'audio', + fileContents: 'binary-audio-bytes', + fileOriginalName: 'shiur.mp3', + fileMimeType: 'audio/mpeg', + fileSizeBytes: 1024, + )); } public function testThrowsWhenIdMissing(): void @@ -273,16 +419,15 @@ class UpdateElementTest extends TestCase description: 'Updated description', iconImageUrl: null, richText: '

Updated rich text

', + shortPdfPath: null, + longPdfPath: null, pdfPath: null, youtubeUrl: null, - iconImageContents: null, - iconImageOriginalName: null, - iconImageMimeType: null, - iconImageSizeBytes: null, - pdfContents: null, - pdfOriginalName: null, - pdfMimeType: null, - pdfSizeBytes: null, + fileType: null, + fileContents: null, + fileOriginalName: null, + fileMimeType: null, + fileSizeBytes: null, )); } @@ -297,16 +442,15 @@ class UpdateElementTest extends TestCase description: null, iconImageUrl: null, richText: null, + shortPdfPath: null, + longPdfPath: null, pdfPath: null, youtubeUrl: null, - iconImageContents: null, - iconImageOriginalName: null, - iconImageMimeType: null, - iconImageSizeBytes: null, - pdfContents: null, - pdfOriginalName: null, - pdfMimeType: null, - pdfSizeBytes: null, + fileType: null, + fileContents: null, + fileOriginalName: null, + fileMimeType: null, + fileSizeBytes: null, )); } @@ -321,16 +465,15 @@ class UpdateElementTest extends TestCase description: 'Updated description', iconImageUrl: null, richText: '

Updated rich text

', + shortPdfPath: null, + longPdfPath: null, pdfPath: null, youtubeUrl: null, - iconImageContents: null, - iconImageOriginalName: null, - iconImageMimeType: null, - iconImageSizeBytes: null, - pdfContents: null, - pdfOriginalName: null, - pdfMimeType: null, - pdfSizeBytes: null, + fileType: null, + fileContents: null, + fileOriginalName: null, + fileMimeType: null, + fileSizeBytes: null, )); } @@ -345,16 +488,15 @@ class UpdateElementTest extends TestCase description: 'Updated description', iconImageUrl: null, richText: '

Updated rich text

', + shortPdfPath: null, + longPdfPath: null, pdfPath: null, youtubeUrl: null, - iconImageContents: null, - iconImageOriginalName: null, - iconImageMimeType: null, - iconImageSizeBytes: null, - pdfContents: null, - pdfOriginalName: null, - pdfMimeType: null, - pdfSizeBytes: null, + fileType: null, + fileContents: null, + fileOriginalName: null, + fileMimeType: null, + fileSizeBytes: null, )); } @@ -374,7 +516,8 @@ class UpdateElementTest extends TestCase string $description, ?string $iconImageUrl, string $richText, - ?string $pdfPath, + ?string $shortPdfPath, + ?string $longPdfPath, ?string $youtubeUrl, ?Element $parentElement, ): Element { @@ -384,7 +527,8 @@ class UpdateElementTest extends TestCase description: $description, iconImageUrl: $iconImageUrl, richText: $richText, - pdfPath: $pdfPath, + shortPdfPath: $shortPdfPath, + longPdfPath: $longPdfPath, youtubeUrl: $youtubeUrl, parentElement: $parentElement, )); diff --git a/backend/tests/Unit/Element/UseCases/UpdateIconImageTest.php b/backend/tests/Unit/Element/UseCases/UpdateIconImageTest.php index af8ae7f..2aef04a 100644 --- a/backend/tests/Unit/Element/UseCases/UpdateIconImageTest.php +++ b/backend/tests/Unit/Element/UseCases/UpdateIconImageTest.php @@ -33,19 +33,19 @@ class UpdateIconImageTest extends TestCase /** * @return array{ - * iconImageContents: string, - * iconImageOriginalName: string, - * iconImageMimeType: string, - * iconImageSizeBytes: int + * fileContents: string, + * fileOriginalName: string, + * fileMimeType: string, + * fileSizeBytes: int * } */ private function imageArgs(): array { return [ - 'iconImageContents' => 'binary-image-bytes', - 'iconImageOriginalName' => 'icon.png', - 'iconImageMimeType' => 'image/png', - 'iconImageSizeBytes' => 1024, + 'fileContents' => 'binary-image-bytes', + 'fileOriginalName' => 'icon.png', + 'fileMimeType' => 'image/png', + 'fileSizeBytes' => 1024, ]; } @@ -97,10 +97,10 @@ class UpdateIconImageTest extends TestCase $this->useCase->execute( new UpdateIconImageRequest( id: 1, - iconImageContents: null, - iconImageOriginalName: null, - iconImageMimeType: null, - iconImageSizeBytes: null, + fileContents: null, + fileOriginalName: null, + fileMimeType: null, + fileSizeBytes: null, ) ); } @@ -115,10 +115,10 @@ class UpdateIconImageTest extends TestCase $this->useCase->execute( new UpdateIconImageRequest( id: 1, - iconImageContents: 'not-an-image', - iconImageOriginalName: 'icon.pdf', - iconImageMimeType: 'application/pdf', - iconImageSizeBytes: 1024, + fileContents: 'not-an-image', + fileOriginalName: 'icon.pdf', + fileMimeType: 'application/pdf', + fileSizeBytes: 1024, ) ); } @@ -131,10 +131,10 @@ class UpdateIconImageTest extends TestCase $this->useCase->execute( new UpdateIconImageRequest( id: 1, - iconImageContents: 'big', - iconImageOriginalName: 'icon.png', - iconImageMimeType: 'image/png', - iconImageSizeBytes: 6 * 1024 * 1024, + fileContents: 'big', + fileOriginalName: 'icon.png', + fileMimeType: 'image/png', + fileSizeBytes: 6 * 1024 * 1024, ) ); } @@ -167,7 +167,8 @@ class UpdateIconImageTest extends TestCase description: 'Original description', iconImageUrl: null, richText: '', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: null, parentElement: null, )); diff --git a/backend/tests/Unit/Element/UseCases/UpdateLongPdfTest.php b/backend/tests/Unit/Element/UseCases/UpdateLongPdfTest.php new file mode 100644 index 0000000..ba34617 --- /dev/null +++ b/backend/tests/Unit/Element/UseCases/UpdateLongPdfTest.php @@ -0,0 +1,95 @@ +elementRepository = new FakeElementRepository(); + $this->fileUploader = new FakeFileUploader(); + $this->useCase = new UpdateLongPdf( + $this->elementRepository, + $this->fileUploader, + ); + } + + public function testUploadsLongPdfAndStoresPath(): void + { + $element = $this->createElement(); + + $updatedElement = $this->useCase->execute( + new UpdateLongPdfRequest( + id: $element->getId(), + fileContents: 'binary-pdf-bytes', + fileOriginalName: 'baderech-long.pdf', + fileMimeType: 'application/pdf', + fileSizeBytes: 1024, + ) + ); + + $this->assertNull($updatedElement->getShortPdfPath()); + $this->assertSame( + 'element-pdfs/long/fake-baderech-long.pdf', + $updatedElement->getLongPdfPath(), + ); + $this->assertSame( + 'element-pdfs/long', + $this->fileUploader->uploads[0]['folder'], + ); + } + + public function testDisallowedMimeThrowsBadRequest(): void + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('pdf must be a pdf file'); + + $this->useCase->execute( + new UpdateLongPdfRequest( + id: 1, + fileContents: 'not-a-pdf', + fileOriginalName: 'icon.png', + fileMimeType: 'image/png', + fileSizeBytes: 1024, + ) + ); + } + + private function createElement(): Element + { + $set = new DomainSet( + id: 1, + name: 'Baderech', + description: 'Baderech description', + iconImageUrl: '/assets/baderech-icon.png', + ); + + return $this->elementRepository->create(new CreateElementDto( + set: $set, + title: 'Original title', + description: 'Original description', + iconImageUrl: null, + richText: '', + shortPdfPath: null, + longPdfPath: null, + youtubeUrl: null, + parentElement: null, + )); + } +} diff --git a/backend/tests/Unit/Element/UseCases/UpdatePdfTest.php b/backend/tests/Unit/Element/UseCases/UpdateShortPdfTest.php similarity index 65% rename from backend/tests/Unit/Element/UseCases/UpdatePdfTest.php rename to backend/tests/Unit/Element/UseCases/UpdateShortPdfTest.php index 9a3b30b..9cea03a 100644 --- a/backend/tests/Unit/Element/UseCases/UpdatePdfTest.php +++ b/backend/tests/Unit/Element/UseCases/UpdateShortPdfTest.php @@ -4,8 +4,8 @@ namespace Tests\Unit\Element\UseCases; use App\Element\CreateElementDto; use App\Element\Element; -use App\Element\UseCases\UpdateElement\UpdatePdf; -use App\Element\UseCases\UpdateElement\UpdatePdfRequest; +use App\Element\UseCases\UpdateElement\UpdateShortPdf; +use App\Element\UseCases\UpdateElement\UpdateShortPdfRequest; use App\Exceptions\BadRequestException; use App\Exceptions\NotFoundException; use App\Set\Set as DomainSet; @@ -13,19 +13,19 @@ use Tests\Fakes\FakeElementRepository; use Tests\Fakes\FakeFileUploader; use Tests\TestCase; -class UpdatePdfTest extends TestCase +class UpdateShortPdfTest extends TestCase { private FakeElementRepository $elementRepository; private FakeFileUploader $fileUploader; - private UpdatePdf $useCase; + private UpdateShortPdf $useCase; protected function setUp(): void { $this->elementRepository = new FakeElementRepository(); $this->fileUploader = new FakeFileUploader(); - $this->useCase = new UpdatePdf( + $this->useCase = new UpdateShortPdf( $this->elementRepository, $this->fileUploader, ); @@ -33,45 +33,46 @@ class UpdatePdfTest extends TestCase /** * @return array{ - * pdfContents: string, - * pdfOriginalName: string, - * pdfMimeType: string, - * pdfSizeBytes: int + * fileContents: string, + * fileOriginalName: string, + * fileMimeType: string, + * fileSizeBytes: int * } */ private function pdfArgs(): array { return [ - 'pdfContents' => 'binary-pdf-bytes', - 'pdfOriginalName' => 'baderech.pdf', - 'pdfMimeType' => 'application/pdf', - 'pdfSizeBytes' => 1024, + 'fileContents' => 'binary-pdf-bytes', + 'fileOriginalName' => 'baderech.pdf', + 'fileMimeType' => 'application/pdf', + 'fileSizeBytes' => 1024, ]; } - public function testUploadsPdfAndStoresPath(): void + public function testUploadsShortPdfAndStoresPath(): void { $element = $this->createElement(); $updatedElement = $this->useCase->execute( - new UpdatePdfRequest( + new UpdateShortPdfRequest( ...$this->pdfArgs(), id: $element->getId(), ) ); $this->assertSame( - 'element-pdfs/fake-baderech.pdf', - $updatedElement->getPdfPath(), + 'element-pdfs/short/fake-baderech.pdf', + $updatedElement->getShortPdfPath(), ); + $this->assertNull($updatedElement->getLongPdfPath()); $storedElement = $this->elementRepository->find($element->getId()); $this->assertNotNull($storedElement); $this->assertSame( - 'element-pdfs/fake-baderech.pdf', - $storedElement->getPdfPath(), + 'element-pdfs/short/fake-baderech.pdf', + $storedElement->getShortPdfPath(), ); $this->assertSame( - 'element-pdfs', + 'element-pdfs/short', $this->fileUploader->uploads[0]['folder'], ); } @@ -82,7 +83,7 @@ class UpdatePdfTest extends TestCase $this->expectExceptionMessage('id is required'); $this->useCase->execute( - new UpdatePdfRequest( + new UpdateShortPdfRequest( ...$this->pdfArgs(), id: null, ) @@ -95,12 +96,12 @@ class UpdatePdfTest extends TestCase $this->expectExceptionMessage('pdf is required'); $this->useCase->execute( - new UpdatePdfRequest( + new UpdateShortPdfRequest( id: 1, - pdfContents: null, - pdfOriginalName: null, - pdfMimeType: null, - pdfSizeBytes: null, + fileContents: null, + fileOriginalName: null, + fileMimeType: null, + fileSizeBytes: null, ) ); } @@ -111,12 +112,12 @@ class UpdatePdfTest extends TestCase $this->expectExceptionMessage('pdf must be a pdf file'); $this->useCase->execute( - new UpdatePdfRequest( + new UpdateShortPdfRequest( id: 1, - pdfContents: 'not-a-pdf', - pdfOriginalName: 'icon.png', - pdfMimeType: 'image/png', - pdfSizeBytes: 1024, + fileContents: 'not-a-pdf', + fileOriginalName: 'icon.png', + fileMimeType: 'image/png', + fileSizeBytes: 1024, ) ); } @@ -127,12 +128,12 @@ class UpdatePdfTest extends TestCase $this->expectExceptionMessage('pdf must be 10MB or smaller'); $this->useCase->execute( - new UpdatePdfRequest( + new UpdateShortPdfRequest( id: 1, - pdfContents: 'big', - pdfOriginalName: 'baderech.pdf', - pdfMimeType: 'application/pdf', - pdfSizeBytes: 11 * 1024 * 1024, + fileContents: 'big', + fileOriginalName: 'baderech.pdf', + fileMimeType: 'application/pdf', + fileSizeBytes: 11 * 1024 * 1024, ) ); } @@ -143,7 +144,7 @@ class UpdatePdfTest extends TestCase $this->expectExceptionMessage('Element not found'); $this->useCase->execute( - new UpdatePdfRequest( + new UpdateShortPdfRequest( ...$this->pdfArgs(), id: 999, ) @@ -165,7 +166,8 @@ class UpdatePdfTest extends TestCase description: 'Original description', iconImageUrl: null, richText: '', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: null, 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 38dc7a1..c6ab389 100644 --- a/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts +++ b/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts @@ -126,28 +126,45 @@ describe('admin element editing', () => { .and('have.attr', 'src') .and('include', '/storage/element-icons/') - cy.get('[data-cy="admin-element-pdf-input"]').selectFile( + cy.get('[data-cy="admin-element-short-pdf-input"]').selectFile( 'public/assets/pdfs/baderech.pdf', { force: true }, ) cy.wait('@updateElement') - cy.get('[data-cy="admin-element-pdf-status"]') + cy.get('[data-cy="admin-element-short-pdf-status"]') .should('be.visible') - .and('contain.text', 'PDF updated') - cy.get('[data-cy="admin-element-current-pdf"]') + .and('contain.text', 'Short PDF updated') + cy.get('[data-cy="admin-element-current-short-pdf"]') .should('be.visible') .and('have.attr', 'href') - .and('include', '/storage/element-pdfs/') + .and('include', '/storage/element-pdfs/short/') + + cy.get('[data-cy="admin-element-long-pdf-input"]').selectFile( + 'public/assets/pdfs/baderech.pdf', + { force: true }, + ) + cy.wait('@updateElement') + cy.get('[data-cy="admin-element-long-pdf-status"]') + .should('be.visible') + .and('contain.text', 'Long PDF updated') + cy.get('[data-cy="admin-element-current-long-pdf"]') + .should('be.visible') + .and('have.attr', 'href') + .and('include', '/storage/element-pdfs/long/') cy.contains('header.site-header a', 'View Element').click() cy.get('[data-cy="element-icon"]') .should('be.visible') .and('have.attr', 'src') .and('include', '/storage/element-icons/') - cy.get('[data-cy="element-pdf-link"]') + cy.get('[data-cy="element-short-pdf-link"]') .should('be.visible') .and('have.attr', 'href') - .and('include', '/storage/element-pdfs/') + .and('include', '/storage/element-pdfs/short/') + cy.get('[data-cy="element-long-pdf-link"]') + .should('be.visible') + .and('have.attr', 'href') + .and('include', '/storage/element-pdfs/long/') cy.resetDb() }) @@ -163,7 +180,12 @@ describe('admin element editing', () => { { force: true }, ) cy.wait('@updateElement') - cy.get('[data-cy="admin-element-pdf-input"]').selectFile( + cy.get('[data-cy="admin-element-short-pdf-input"]').selectFile( + 'public/assets/pdfs/baderech.pdf', + { force: true }, + ) + cy.wait('@updateElement') + cy.get('[data-cy="admin-element-long-pdf-input"]').selectFile( 'public/assets/pdfs/baderech.pdf', { force: true }, ) @@ -177,17 +199,26 @@ describe('admin element editing', () => { 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.contains('button', 'Remove short PDF').click() cy.wait('@updateElement') - cy.get('[data-cy="admin-element-pdf-status"]') + cy.get('[data-cy="admin-element-short-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') + .and('contain.text', 'Short PDF removed') + cy.get('[data-cy="admin-element-current-short-pdf"]').should('not.exist') + cy.contains('No short PDF').should('be.visible') + + cy.contains('button', 'Remove long PDF').click() + cy.wait('@updateElement') + cy.get('[data-cy="admin-element-long-pdf-status"]') + .should('be.visible') + .and('contain.text', 'Long PDF removed') + cy.get('[data-cy="admin-element-current-long-pdf"]').should('not.exist') + cy.contains('No long 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.get('[data-cy="element-short-pdf-link"]').should('not.exist') + cy.get('[data-cy="element-long-pdf-link"]').should('not.exist') cy.resetDb() }) diff --git a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts index e0f361a..805be28 100644 --- a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts +++ b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts @@ -51,7 +51,8 @@ describe('media page sets', () => { cy.get('[data-cy="element-rich-text"]').should('not.exist') cy.get('[data-cy="element-youtube-embed"]') .should('not.exist') - cy.get('[data-cy="element-pdf-link"]').should('not.exist') + cy.get('[data-cy="element-short-pdf-link"]').should('not.exist') + cy.get('[data-cy="element-long-pdf-link"]').should('not.exist') cy.get('[data-cy="child-element-list"]').should('be.visible') cy.get('[data-cy="child-element-list"]') .should( @@ -116,13 +117,14 @@ describe('media page sets', () => { ) .should('have.css', 'text-align', 'center') cy.get('[data-cy="element-youtube-embed"]').should('not.exist') - cy.contains('[data-cy="element-pdf-link"]', 'View PDF') - .as('pdfLink') + cy.contains('[data-cy="element-short-pdf-link"]', 'View Short PDF') + .as('shortPdfLink') .should('be.visible') .and('have.attr', 'target', '_blank') - cy.get('@pdfLink') + cy.get('@shortPdfLink') .and('have.attr', 'href') - .and('include', '/storage/element-pdfs/baderech.pdf') + .and('include', '/storage/element-pdfs/short/baderech.pdf') + cy.get('[data-cy="element-long-pdf-link"]').should('not.exist') cy.contains('[data-cy="child-element-link"]', introductionAudioTitle) .as('introductionAudioLink') .should('have.attr', 'href', '/element/8') @@ -132,7 +134,8 @@ describe('media page sets', () => { cy.contains('h1', introductionAudioTitle).should('be.visible') cy.get('[data-cy="element-icon"]').should('not.exist') cy.get('[data-cy="element-rich-text"]').should('not.exist') - cy.get('[data-cy="element-pdf-link"]').should('not.exist') + cy.get('[data-cy="element-short-pdf-link"]').should('not.exist') + cy.get('[data-cy="element-long-pdf-link"]').should('not.exist') cy.get('[data-cy="child-element-list"]').should('not.exist') cy.get('[data-cy="element-youtube-embed"]') .should('be.visible') From 9bbabc7fa6656e428131d084ea55af75c2ebcb4a Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 14 Jun 2026 23:17:56 +0300 Subject: [PATCH 2/4] route element file uploads Add short and long PDF paths to elements, keep pdfPath as a short PDF compatibility alias, and route generic file uploads by fileType. --- backend/app/Controllers/ElementController.php | 30 ++- backend/app/Element/CreateElementDto.php | 3 +- backend/app/Element/Element.php | 21 +- backend/app/Element/ElementModel.php | 9 +- .../app/Element/EloquentElementRepository.php | 12 +- .../UseCases/CreateElement/CreateElement.php | 13 +- .../CreateElement/CreateElementRequest.php | 3 +- .../UseCases/UpdateElement/UpdateElement.php | 125 ++++++++--- .../UpdateElement/UpdateElementRequest.php | 15 +- .../UpdateElement/UpdateIconImage.php | 20 +- .../UpdateElement/UpdateIconImageRequest.php | 8 +- .../{UpdatePdf.php => UpdateLongPdf.php} | 28 +-- ...pdatePdfPath.php => UpdateLongPdfPath.php} | 16 +- ...quest.php => UpdateLongPdfPathRequest.php} | 4 +- .../UpdateElement/UpdateLongPdfRequest.php | 15 ++ .../UpdateElement/UpdatePdfRequest.php | 15 -- .../UseCases/UpdateElement/UpdateShortPdf.php | 75 +++++++ .../UpdateElement/UpdateShortPdfPath.php | 65 ++++++ .../UpdateShortPdfPathRequest.php | 12 ++ .../UpdateElement/UpdateShortPdfRequest.php | 15 ++ .../2026_05_24_000001_elements_table.php | 3 +- backend/database/seeders/ElementSeeder.php | 23 +- .../element-pdfs/{ => short}/baderech.pdf | Bin backend/tests/Fakes/FakeElementRepository.php | 6 +- .../tests/Feature/ElementsEndpointTest.php | 2 +- frontend/rabbi_gerzi/src/stores/elements.ts | 134 ++++++++---- .../src/views/AdminElementPage.vue | 204 ++++++++++++++---- .../rabbi_gerzi/src/views/ElementPage.vue | 24 ++- 28 files changed, 686 insertions(+), 214 deletions(-) rename backend/app/Element/UseCases/UpdateElement/{UpdatePdf.php => UpdateLongPdf.php} (67%) rename backend/app/Element/UseCases/UpdateElement/{UpdatePdfPath.php => UpdateLongPdfPath.php} (73%) rename backend/app/Element/UseCases/UpdateElement/{UpdatePdfPathRequest.php => UpdateLongPdfPathRequest.php} (65%) create mode 100644 backend/app/Element/UseCases/UpdateElement/UpdateLongPdfRequest.php delete mode 100644 backend/app/Element/UseCases/UpdateElement/UpdatePdfRequest.php create mode 100644 backend/app/Element/UseCases/UpdateElement/UpdateShortPdf.php create mode 100644 backend/app/Element/UseCases/UpdateElement/UpdateShortPdfPath.php create mode 100644 backend/app/Element/UseCases/UpdateElement/UpdateShortPdfPathRequest.php create mode 100644 backend/app/Element/UseCases/UpdateElement/UpdateShortPdfRequest.php rename backend/database/seeders/assets/element-pdfs/{ => short}/baderech.pdf (100%) diff --git a/backend/app/Controllers/ElementController.php b/backend/app/Controllers/ElementController.php index 1cfc205..1d6853f 100644 --- a/backend/app/Controllers/ElementController.php +++ b/backend/app/Controllers/ElementController.php @@ -57,8 +57,7 @@ class ElementController public function update(Request $request): JsonResponse { - $iconImage = $this->uploadedFileInput($request, 'iconImage'); - $pdf = $this->uploadedFileInput($request, 'pdf'); + $file = $this->uploadedFileInput($request, 'file'); try { $element = $this->updateElement->execute( @@ -71,16 +70,15 @@ class ElementController 'iconImageUrl', ), richText: $this->stringInput($request, 'richText'), + shortPdfPath: $this->stringInput($request, 'shortPdfPath'), + longPdfPath: $this->stringInput($request, 'longPdfPath'), pdfPath: $this->stringInput($request, 'pdfPath'), youtubeUrl: $this->stringInput($request, 'youtubeUrl'), - iconImageContents: $iconImage['contents'], - iconImageOriginalName: $iconImage['originalName'], - iconImageMimeType: $iconImage['mimeType'], - iconImageSizeBytes: $iconImage['sizeBytes'], - pdfContents: $pdf['contents'], - pdfOriginalName: $pdf['originalName'], - pdfMimeType: $pdf['mimeType'], - pdfSizeBytes: $pdf['sizeBytes'], + fileType: $this->stringInput($request, 'fileType'), + fileContents: $file['contents'], + fileOriginalName: $file['originalName'], + fileMimeType: $file['mimeType'], + fileSizeBytes: $file['sizeBytes'], ) ); } catch (BadRequestException $exception) { @@ -176,6 +174,8 @@ class ElementController * description: string, * iconImageUrl: string|null, * richText: string, + * shortPdfPath: string|null, + * longPdfPath: string|null, * pdfPath: string|null, * youtubeUrl: string|null, * } @@ -191,8 +191,16 @@ class ElementController 'element-icons/', ), 'richText' => $element->getRichText(), + 'shortPdfPath' => $this->storageUrl( + $element->getShortPdfPath(), + 'element-pdfs/', + ), + 'longPdfPath' => $this->storageUrl( + $element->getLongPdfPath(), + 'element-pdfs/', + ), 'pdfPath' => $this->storageUrl( - $element->getPdfPath(), + $element->getShortPdfPath(), 'element-pdfs/', ), 'youtubeUrl' => $element->getYoutubeUrl(), diff --git a/backend/app/Element/CreateElementDto.php b/backend/app/Element/CreateElementDto.php index b5a9b7f..3d41e23 100644 --- a/backend/app/Element/CreateElementDto.php +++ b/backend/app/Element/CreateElementDto.php @@ -12,7 +12,8 @@ class CreateElementDto public string $description, public ?string $iconImageUrl, public string $richText, - public ?string $pdfPath, + public ?string $shortPdfPath, + public ?string $longPdfPath, public ?string $youtubeUrl, public ?Element $parentElement, ) { diff --git a/backend/app/Element/Element.php b/backend/app/Element/Element.php index 37be364..7b07975 100644 --- a/backend/app/Element/Element.php +++ b/backend/app/Element/Element.php @@ -12,7 +12,8 @@ class Element private string $description, private ?string $iconImageUrl, private string $richText, - private ?string $pdfPath, + private ?string $shortPdfPath, + private ?string $longPdfPath, private ?string $youtubeUrl, private Set $set, private ?Element $parentElement, @@ -64,14 +65,24 @@ class Element $this->richText = $richText; } - public function getPdfPath(): ?string + public function getShortPdfPath(): ?string { - return $this->pdfPath; + return $this->shortPdfPath; } - public function setPdfPath(?string $pdfPath): void + public function setShortPdfPath(?string $shortPdfPath): void { - $this->pdfPath = $pdfPath; + $this->shortPdfPath = $shortPdfPath; + } + + public function getLongPdfPath(): ?string + { + return $this->longPdfPath; + } + + public function setLongPdfPath(?string $longPdfPath): void + { + $this->longPdfPath = $longPdfPath; } public function getYoutubeUrl(): ?string diff --git a/backend/app/Element/ElementModel.php b/backend/app/Element/ElementModel.php index a71eb37..3a8a435 100644 --- a/backend/app/Element/ElementModel.php +++ b/backend/app/Element/ElementModel.php @@ -12,7 +12,8 @@ use Illuminate\Database\Eloquent\Model; * @property string $description * @property string|null $icon_image_url * @property string $rich_text - * @property string|null $pdf_path + * @property string|null $short_pdf_path + * @property string|null $long_pdf_path * @property string|null $youtube_url * @property int|null $parent_element_id * @@ -26,7 +27,8 @@ use Illuminate\Database\Eloquent\Model; * @method static Builder|ElementModel whereDescription($value) * @method static Builder|ElementModel whereIconImageUrl($value) * @method static Builder|ElementModel whereRichText($value) - * @method static Builder|ElementModel wherePdfPath($value) + * @method static Builder|ElementModel whereShortPdfPath($value) + * @method static Builder|ElementModel whereLongPdfPath($value) * @method static Builder|ElementModel whereYoutubeUrl($value) * * @mixin \Eloquent @@ -43,7 +45,8 @@ class ElementModel extends Model 'description', 'icon_image_url', 'rich_text', - 'pdf_path', + 'short_pdf_path', + 'long_pdf_path', 'youtube_url', 'parent_element_id', ]; diff --git a/backend/app/Element/EloquentElementRepository.php b/backend/app/Element/EloquentElementRepository.php index 379abe0..771bc4c 100644 --- a/backend/app/Element/EloquentElementRepository.php +++ b/backend/app/Element/EloquentElementRepository.php @@ -20,7 +20,8 @@ class EloquentElementRepository implements ElementRepository 'description' => $dto->description, 'icon_image_url' => $dto->iconImageUrl, 'rich_text' => $dto->richText, - 'pdf_path' => $dto->pdfPath, + 'short_pdf_path' => $dto->shortPdfPath, + 'long_pdf_path' => $dto->longPdfPath, 'youtube_url' => $dto->youtubeUrl, 'parent_element_id' => $dto->parentElement?->getId(), ]); @@ -31,7 +32,8 @@ class EloquentElementRepository implements ElementRepository description: $dto->description, iconImageUrl: $dto->iconImageUrl, richText: $dto->richText, - pdfPath: $dto->pdfPath, + shortPdfPath: $dto->shortPdfPath, + longPdfPath: $dto->longPdfPath, youtubeUrl: $dto->youtubeUrl, set: $dto->set, parentElement: $dto->parentElement, @@ -52,7 +54,8 @@ class EloquentElementRepository implements ElementRepository $model->description = $element->getDescription(); $model->icon_image_url = $element->getIconImageUrl(); $model->rich_text = $element->getRichText(); - $model->pdf_path = $element->getPdfPath(); + $model->short_pdf_path = $element->getShortPdfPath(); + $model->long_pdf_path = $element->getLongPdfPath(); $model->youtube_url = $element->getYoutubeUrl(); $model->parent_element_id = $element->getParentElement()?->getId(); $model->save(); @@ -137,7 +140,8 @@ class EloquentElementRepository implements ElementRepository description: $model->description, iconImageUrl: $model->icon_image_url, richText: $model->rich_text, - pdfPath: $model->pdf_path, + shortPdfPath: $model->short_pdf_path, + longPdfPath: $model->long_pdf_path, youtubeUrl: $model->youtube_url, set: $set, parentElement: $parentElement, diff --git a/backend/app/Element/UseCases/CreateElement/CreateElement.php b/backend/app/Element/UseCases/CreateElement/CreateElement.php index e91a0ac..3929bce 100644 --- a/backend/app/Element/UseCases/CreateElement/CreateElement.php +++ b/backend/app/Element/UseCases/CreateElement/CreateElement.php @@ -35,7 +35,12 @@ class CreateElement ? null : $request->iconImageUrl; $richText = $request->richText ?? ''; - $pdfPath = $request->pdfPath === '' ? null : $request->pdfPath; + $shortPdfPath = $request->shortPdfPath === '' + ? null + : $request->shortPdfPath; + $longPdfPath = $request->longPdfPath === '' + ? null + : $request->longPdfPath; $youtubeUrl = $request->youtubeUrl === '' ? null : $request->youtubeUrl; @@ -56,7 +61,8 @@ class CreateElement description: $description, iconImageUrl: $iconImageUrl, richText: $richText, - pdfPath: $pdfPath, + shortPdfPath: $shortPdfPath, + longPdfPath: $longPdfPath, youtubeUrl: $youtubeUrl, parentElement: null, )); @@ -82,7 +88,8 @@ class CreateElement description: $description, iconImageUrl: $iconImageUrl, richText: $richText, - pdfPath: $pdfPath, + shortPdfPath: $shortPdfPath, + longPdfPath: $longPdfPath, youtubeUrl: $youtubeUrl, parentElement: $parentElement, )); diff --git a/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php b/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php index ef689cc..3f6d53f 100644 --- a/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php +++ b/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php @@ -10,7 +10,8 @@ class CreateElementRequest public ?string $description, public ?string $iconImageUrl, public ?string $richText, - public ?string $pdfPath, + public ?string $shortPdfPath, + public ?string $longPdfPath, public ?string $youtubeUrl, public ?int $parentElementId, ) { diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateElement.php b/backend/app/Element/UseCases/UpdateElement/UpdateElement.php index 0533b16..aab9cb8 100644 --- a/backend/app/Element/UseCases/UpdateElement/UpdateElement.php +++ b/backend/app/Element/UseCases/UpdateElement/UpdateElement.php @@ -9,15 +9,23 @@ use App\Exceptions\NotFoundException; class UpdateElement { + private const FILE_TYPE_ICON_IMAGE = 'iconImage'; + + private const FILE_TYPE_SHORT_PDF = 'shortPdf'; + + private const FILE_TYPE_LONG_PDF = 'longPdf'; + public function __construct( private UpdateTitle $updateTitle, private UpdateDescription $updateDescription, private UpdateIconImageUrl $updateIconImageUrl, private UpdateRichText $updateRichText, - private UpdatePdfPath $updatePdfPath, + private UpdateShortPdfPath $updateShortPdfPath, + private UpdateLongPdfPath $updateLongPdfPath, private UpdateYoutubeUrl $updateYoutubeUrl, private UpdateIconImage $updateIconImage, - private UpdatePdf $updatePdf, + private UpdateShortPdf $updateShortPdf, + private UpdateLongPdf $updateLongPdf, private ElementRepository $elementRepository, ) { } @@ -36,10 +44,15 @@ class UpdateElement && $request->description === null && $request->iconImageUrl === null && $request->richText === null + && $request->shortPdfPath === null + && $request->longPdfPath === null && $request->pdfPath === null && $request->youtubeUrl === null - && $request->iconImageContents === null - && $request->pdfContents === null; + && $request->fileType === null + && $request->fileContents === null + && $request->fileOriginalName === null + && $request->fileMimeType === null + && $request->fileSizeBytes === null; if ($hasNoFields) { throw new BadRequestException('nothing to update'); } @@ -70,10 +83,21 @@ class UpdateElement richText: $request->richText, )); } - if ($request->pdfPath !== null) { - $this->updatePdfPath->execute(new UpdatePdfPathRequest( + if ($request->shortPdfPath !== null) { + $this->updateShortPdfPath->execute(new UpdateShortPdfPathRequest( id: $request->id, - pdfPath: $request->pdfPath, + shortPdfPath: $request->shortPdfPath, + )); + } elseif ($request->pdfPath !== null) { + $this->updateShortPdfPath->execute(new UpdateShortPdfPathRequest( + id: $request->id, + shortPdfPath: $request->pdfPath, + )); + } + if ($request->longPdfPath !== null) { + $this->updateLongPdfPath->execute(new UpdateLongPdfPathRequest( + id: $request->id, + longPdfPath: $request->longPdfPath, )); } if ($request->youtubeUrl !== null) { @@ -82,27 +106,8 @@ class UpdateElement youtubeUrl: $request->youtubeUrl, )); } - if ($request->iconImageContents !== null) { - $this->updateIconImage->execute( - new UpdateIconImageRequest( - id: $request->id, - iconImageContents: $request->iconImageContents, - iconImageOriginalName: $request->iconImageOriginalName, - iconImageMimeType: $request->iconImageMimeType, - iconImageSizeBytes: $request->iconImageSizeBytes, - ) - ); - } - if ($request->pdfContents !== null) { - $this->updatePdf->execute( - new UpdatePdfRequest( - id: $request->id, - pdfContents: $request->pdfContents, - pdfOriginalName: $request->pdfOriginalName, - pdfMimeType: $request->pdfMimeType, - pdfSizeBytes: $request->pdfSizeBytes, - ) - ); + if ($this->hasFileInput($request)) { + $this->updateFile($request); } $element = $this->elementRepository->find($request->id); @@ -112,4 +117,68 @@ class UpdateElement return $element; } + + /** + * @throws BadRequestException + * @throws NotFoundException + */ + private function updateFile(UpdateElementRequest $request): void + { + if ($request->fileType === null) { + throw new BadRequestException('fileType is required'); + } + + if ($request->fileType === self::FILE_TYPE_ICON_IMAGE) { + $this->updateIconImage->execute( + new UpdateIconImageRequest( + id: $request->id, + fileContents: $request->fileContents, + fileOriginalName: $request->fileOriginalName, + fileMimeType: $request->fileMimeType, + fileSizeBytes: $request->fileSizeBytes, + ) + ); + + return; + } + + if ($request->fileType === self::FILE_TYPE_SHORT_PDF) { + $this->updateShortPdf->execute( + new UpdateShortPdfRequest( + id: $request->id, + fileContents: $request->fileContents, + fileOriginalName: $request->fileOriginalName, + fileMimeType: $request->fileMimeType, + fileSizeBytes: $request->fileSizeBytes, + ) + ); + + return; + } + + if ($request->fileType === self::FILE_TYPE_LONG_PDF) { + $this->updateLongPdf->execute( + new UpdateLongPdfRequest( + id: $request->id, + fileContents: $request->fileContents, + fileOriginalName: $request->fileOriginalName, + fileMimeType: $request->fileMimeType, + fileSizeBytes: $request->fileSizeBytes, + ) + ); + + return; + } + + throw new BadRequestException('fileType is invalid'); + } + + private function hasFileInput(UpdateElementRequest $request): bool + { + return $request->fileType !== null + || $request->fileContents !== null + || $request->fileOriginalName !== null + || $request->fileMimeType !== null + || $request->fileSizeBytes !== null; + } } diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php index aa1aa06..efa1519 100644 --- a/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php +++ b/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php @@ -10,16 +10,15 @@ class UpdateElementRequest public ?string $description, public ?string $iconImageUrl, public ?string $richText, + public ?string $shortPdfPath, + public ?string $longPdfPath, public ?string $pdfPath, public ?string $youtubeUrl, - public ?string $iconImageContents, - public ?string $iconImageOriginalName, - public ?string $iconImageMimeType, - public ?int $iconImageSizeBytes, - public ?string $pdfContents, - public ?string $pdfOriginalName, - public ?string $pdfMimeType, - public ?int $pdfSizeBytes, + public ?string $fileType, + public ?string $fileContents, + public ?string $fileOriginalName, + public ?string $fileMimeType, + public ?int $fileSizeBytes, ) { } } diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateIconImage.php b/backend/app/Element/UseCases/UpdateElement/UpdateIconImage.php index 45d274f..59dcaad 100644 --- a/backend/app/Element/UseCases/UpdateElement/UpdateIconImage.php +++ b/backend/app/Element/UseCases/UpdateElement/UpdateIconImage.php @@ -36,17 +36,17 @@ class UpdateIconImage } if ( - $request->iconImageContents === null - || $request->iconImageOriginalName === null - || $request->iconImageMimeType === null - || $request->iconImageSizeBytes === null + $request->fileContents === null + || $request->fileOriginalName === null + || $request->fileMimeType === null + || $request->fileSizeBytes === null ) { throw new BadRequestException('icon image is required'); } if ( ! in_array( - $request->iconImageMimeType, + $request->fileMimeType, self::ALLOWED_MIME_TYPES, true, ) @@ -56,7 +56,7 @@ class UpdateIconImage ); } - if ($request->iconImageSizeBytes > self::MAX_SIZE_BYTES) { + if ($request->fileSizeBytes > self::MAX_SIZE_BYTES) { throw new BadRequestException( 'icon image must be 5MB or smaller', ); @@ -68,10 +68,10 @@ class UpdateIconImage } $iconImage = new FileToUpload( - contents: $request->iconImageContents, - originalName: $request->iconImageOriginalName, - mimeType: $request->iconImageMimeType, - sizeBytes: $request->iconImageSizeBytes, + contents: $request->fileContents, + originalName: $request->fileOriginalName, + mimeType: $request->fileMimeType, + sizeBytes: $request->fileSizeBytes, ); $path = $this->fileUploader->upload($iconImage, 'element-icons'); $element->setIconImageUrl($path); diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateIconImageRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateIconImageRequest.php index a119053..ccf973d 100644 --- a/backend/app/Element/UseCases/UpdateElement/UpdateIconImageRequest.php +++ b/backend/app/Element/UseCases/UpdateElement/UpdateIconImageRequest.php @@ -6,10 +6,10 @@ class UpdateIconImageRequest { public function __construct( public ?int $id, - public ?string $iconImageContents, - public ?string $iconImageOriginalName, - public ?string $iconImageMimeType, - public ?int $iconImageSizeBytes, + public ?string $fileContents, + public ?string $fileOriginalName, + public ?string $fileMimeType, + public ?int $fileSizeBytes, ) { } } diff --git a/backend/app/Element/UseCases/UpdateElement/UpdatePdf.php b/backend/app/Element/UseCases/UpdateElement/UpdateLongPdf.php similarity index 67% rename from backend/app/Element/UseCases/UpdateElement/UpdatePdf.php rename to backend/app/Element/UseCases/UpdateElement/UpdateLongPdf.php index 0a13f09..014ada9 100644 --- a/backend/app/Element/UseCases/UpdateElement/UpdatePdf.php +++ b/backend/app/Element/UseCases/UpdateElement/UpdateLongPdf.php @@ -9,7 +9,7 @@ use App\Exceptions\NotFoundException; use App\Shared\Files\FileToUpload; use App\Shared\Files\FileUploader; -class UpdatePdf +class UpdateLongPdf { private const ALLOWED_MIME_TYPES = [ 'application/pdf', @@ -27,24 +27,24 @@ class UpdatePdf * @throws BadRequestException * @throws NotFoundException */ - public function execute(UpdatePdfRequest $request): Element + public function execute(UpdateLongPdfRequest $request): Element { if ($request->id === null) { throw new BadRequestException('id is required'); } if ( - $request->pdfContents === null - || $request->pdfOriginalName === null - || $request->pdfMimeType === null - || $request->pdfSizeBytes === null + $request->fileContents === null + || $request->fileOriginalName === null + || $request->fileMimeType === null + || $request->fileSizeBytes === null ) { throw new BadRequestException('pdf is required'); } if ( ! in_array( - $request->pdfMimeType, + $request->fileMimeType, self::ALLOWED_MIME_TYPES, true, ) @@ -52,7 +52,7 @@ class UpdatePdf throw new BadRequestException('pdf must be a pdf file'); } - if ($request->pdfSizeBytes > self::MAX_SIZE_BYTES) { + if ($request->fileSizeBytes > self::MAX_SIZE_BYTES) { throw new BadRequestException('pdf must be 10MB or smaller'); } @@ -62,13 +62,13 @@ class UpdatePdf } $pdf = new FileToUpload( - contents: $request->pdfContents, - originalName: $request->pdfOriginalName, - mimeType: $request->pdfMimeType, - sizeBytes: $request->pdfSizeBytes, + contents: $request->fileContents, + originalName: $request->fileOriginalName, + mimeType: $request->fileMimeType, + sizeBytes: $request->fileSizeBytes, ); - $path = $this->fileUploader->upload($pdf, 'element-pdfs'); - $element->setPdfPath($path); + $path = $this->fileUploader->upload($pdf, 'element-pdfs/long'); + $element->setLongPdfPath($path); return $this->elementRepository->update($element); } diff --git a/backend/app/Element/UseCases/UpdateElement/UpdatePdfPath.php b/backend/app/Element/UseCases/UpdateElement/UpdateLongPdfPath.php similarity index 73% rename from backend/app/Element/UseCases/UpdateElement/UpdatePdfPath.php rename to backend/app/Element/UseCases/UpdateElement/UpdateLongPdfPath.php index 404c78b..ae04422 100644 --- a/backend/app/Element/UseCases/UpdateElement/UpdatePdfPath.php +++ b/backend/app/Element/UseCases/UpdateElement/UpdateLongPdfPath.php @@ -8,7 +8,7 @@ use App\Exceptions\BadRequestException; use App\Exceptions\NotFoundException; use App\Shared\Files\FileUploader; -class UpdatePdfPath +class UpdateLongPdfPath { public function __construct( private ElementRepository $elementRepository, @@ -20,14 +20,14 @@ class UpdatePdfPath * @throws BadRequestException * @throws NotFoundException */ - public function execute(UpdatePdfPathRequest $request): Element + public function execute(UpdateLongPdfPathRequest $request): Element { if ($request->id === null) { throw new BadRequestException('id is required'); } - if ($request->pdfPath === null) { - throw new BadRequestException('pdfPath is required'); + if ($request->longPdfPath === null) { + throw new BadRequestException('longPdfPath is required'); } $element = $this->elementRepository->find($request->id); @@ -35,12 +35,12 @@ class UpdatePdfPath throw new NotFoundException('Element not found'); } - $pdfPath = $this->nullableString($request->pdfPath); - if ($pdfPath === null) { - $this->deleteManagedFile($element->getPdfPath()); + $longPdfPath = $this->nullableString($request->longPdfPath); + if ($longPdfPath === null) { + $this->deleteManagedFile($element->getLongPdfPath()); } - $element->setPdfPath($pdfPath); + $element->setLongPdfPath($longPdfPath); return $this->elementRepository->update($element); } diff --git a/backend/app/Element/UseCases/UpdateElement/UpdatePdfPathRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateLongPdfPathRequest.php similarity index 65% rename from backend/app/Element/UseCases/UpdateElement/UpdatePdfPathRequest.php rename to backend/app/Element/UseCases/UpdateElement/UpdateLongPdfPathRequest.php index c48c47d..4f4a064 100644 --- a/backend/app/Element/UseCases/UpdateElement/UpdatePdfPathRequest.php +++ b/backend/app/Element/UseCases/UpdateElement/UpdateLongPdfPathRequest.php @@ -2,11 +2,11 @@ namespace App\Element\UseCases\UpdateElement; -class UpdatePdfPathRequest +class UpdateLongPdfPathRequest { public function __construct( public ?int $id, - public ?string $pdfPath, + public ?string $longPdfPath, ) { } } diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateLongPdfRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateLongPdfRequest.php new file mode 100644 index 0000000..eb891a6 --- /dev/null +++ b/backend/app/Element/UseCases/UpdateElement/UpdateLongPdfRequest.php @@ -0,0 +1,15 @@ +id === null) { + throw new BadRequestException('id is required'); + } + + if ( + $request->fileContents === null + || $request->fileOriginalName === null + || $request->fileMimeType === null + || $request->fileSizeBytes === null + ) { + throw new BadRequestException('pdf is required'); + } + + if ( + ! in_array( + $request->fileMimeType, + self::ALLOWED_MIME_TYPES, + true, + ) + ) { + throw new BadRequestException('pdf must be a pdf file'); + } + + if ($request->fileSizeBytes > self::MAX_SIZE_BYTES) { + throw new BadRequestException('pdf must be 10MB or smaller'); + } + + $element = $this->elementRepository->find($request->id); + if ($element === null) { + throw new NotFoundException('Element not found'); + } + + $pdf = new FileToUpload( + contents: $request->fileContents, + originalName: $request->fileOriginalName, + mimeType: $request->fileMimeType, + sizeBytes: $request->fileSizeBytes, + ); + $path = $this->fileUploader->upload($pdf, 'element-pdfs/short'); + $element->setShortPdfPath($path); + + return $this->elementRepository->update($element); + } +} diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateShortPdfPath.php b/backend/app/Element/UseCases/UpdateElement/UpdateShortPdfPath.php new file mode 100644 index 0000000..ae4b4e3 --- /dev/null +++ b/backend/app/Element/UseCases/UpdateElement/UpdateShortPdfPath.php @@ -0,0 +1,65 @@ +id === null) { + throw new BadRequestException('id is required'); + } + + if ($request->shortPdfPath === null) { + throw new BadRequestException('shortPdfPath is required'); + } + + $element = $this->elementRepository->find($request->id); + if ($element === null) { + throw new NotFoundException('Element not found'); + } + + $shortPdfPath = $this->nullableString($request->shortPdfPath); + if ($shortPdfPath === null) { + $this->deleteManagedFile($element->getShortPdfPath()); + } + + $element->setShortPdfPath($shortPdfPath); + + return $this->elementRepository->update($element); + } + + private function nullableString(string $value): ?string + { + if ($value === '') { + return null; + } + + return $value; + } + + private function deleteManagedFile(?string $path): void + { + if ($path === null) { + return; + } + + $this->fileUploader->delete($path); + } +} diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateShortPdfPathRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateShortPdfPathRequest.php new file mode 100644 index 0000000..a10555f --- /dev/null +++ b/backend/app/Element/UseCases/UpdateElement/UpdateShortPdfPathRequest.php @@ -0,0 +1,12 @@ +text('description')->default(''); $table->string('icon_image_url')->nullable(); $table->text('rich_text')->default(''); - $table->string('pdf_path')->nullable(); + $table->string('short_pdf_path')->nullable(); + $table->string('long_pdf_path')->nullable(); $table->string('youtube_url')->nullable(); $table->foreignId('parent_element_id') ->nullable() diff --git a/backend/database/seeders/ElementSeeder.php b/backend/database/seeders/ElementSeeder.php index c7df012..ba5aded 100644 --- a/backend/database/seeders/ElementSeeder.php +++ b/backend/database/seeders/ElementSeeder.php @@ -20,7 +20,7 @@ class ElementSeeder extends Seeder 'element-icons/baderech-haavodah-icon.png', ); $introductionPdfPath = $this->seedStorageFile( - 'element-pdfs/baderech.pdf', + 'element-pdfs/short/baderech.pdf', ); $rootElement = $elementRepository->create(new CreateElementDto( @@ -29,7 +29,8 @@ class ElementSeeder extends Seeder description: $baderechSet->getDescription(), iconImageUrl: $rootIconImageUrl, richText: '', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: null, parentElement: null, )); @@ -44,7 +45,7 @@ class ElementSeeder extends Seeder . 'unlock the ability to live with strength, confidence, ' . 'and hope.', 'richText' => $this->introductionRichText(), - 'pdfPath' => $introductionPdfPath, + 'shortPdfPath' => $introductionPdfPath, ], [ 'title' => '2. Foundations', @@ -54,7 +55,7 @@ class ElementSeeder extends Seeder . 'ability to uplift the physical world with spiritual ' . 'intention.', 'richText' => '', - 'pdfPath' => null, + 'shortPdfPath' => null, ], [ 'title' => '3. Divine Plan', @@ -62,14 +63,14 @@ class ElementSeeder extends Seeder . 'generation and the call to remain hopeful, ' . 'optimistic, and resilient.', 'richText' => '', - 'pdfPath' => null, + 'shortPdfPath' => null, ], [ 'title' => '4. Architecture of the Soul', 'description' => "Becoming a Baal Da'at - A Master of " . 'Inner Awareness', 'richText' => '', - 'pdfPath' => null, + 'shortPdfPath' => null, ], [ 'title' => '5. Arba Yesodot', @@ -77,7 +78,7 @@ class ElementSeeder extends Seeder . 'Physical & Financial Health, Relationships, Wisdom ' . 'and Guidance.', 'richText' => '', - 'pdfPath' => null, + 'shortPdfPath' => null, ], [ 'title' => '6. Fluid Integration', @@ -85,7 +86,7 @@ class ElementSeeder extends Seeder . 'engender a shift in mindset, embed healthy routines, ' . 'positive life practices.', 'richText' => '', - 'pdfPath' => null, + 'shortPdfPath' => null, ], ]; @@ -97,7 +98,8 @@ class ElementSeeder extends Seeder description: $baderechChildElement['description'], iconImageUrl: null, richText: $baderechChildElement['richText'], - pdfPath: $baderechChildElement['pdfPath'], + shortPdfPath: $baderechChildElement['shortPdfPath'], + longPdfPath: null, youtubeUrl: null, parentElement: $rootElement, )); @@ -117,7 +119,8 @@ class ElementSeeder extends Seeder description: '', iconImageUrl: null, richText: '', - pdfPath: null, + shortPdfPath: null, + longPdfPath: null, youtubeUrl: 'https://www.youtube.com/watch?v=' . 'yHx-r4p6hHU&t=1s', parentElement: $introductionElement, diff --git a/backend/database/seeders/assets/element-pdfs/baderech.pdf b/backend/database/seeders/assets/element-pdfs/short/baderech.pdf similarity index 100% rename from backend/database/seeders/assets/element-pdfs/baderech.pdf rename to backend/database/seeders/assets/element-pdfs/short/baderech.pdf diff --git a/backend/tests/Fakes/FakeElementRepository.php b/backend/tests/Fakes/FakeElementRepository.php index 9c08e1c..18f1a6b 100644 --- a/backend/tests/Fakes/FakeElementRepository.php +++ b/backend/tests/Fakes/FakeElementRepository.php @@ -23,7 +23,8 @@ class FakeElementRepository implements ElementRepository description: $dto->description, iconImageUrl: $dto->iconImageUrl, richText: $dto->richText, - pdfPath: $dto->pdfPath, + shortPdfPath: $dto->shortPdfPath, + longPdfPath: $dto->longPdfPath, youtubeUrl: $dto->youtubeUrl, set: $dto->set, parentElement: $dto->parentElement, @@ -111,7 +112,8 @@ class FakeElementRepository implements ElementRepository description: $element->getDescription(), iconImageUrl: $element->getIconImageUrl(), richText: $element->getRichText(), - pdfPath: $element->getPdfPath(), + shortPdfPath: $element->getShortPdfPath(), + longPdfPath: $element->getLongPdfPath(), youtubeUrl: $element->getYoutubeUrl(), set: $element->getSet(), parentElement: $parentElement, diff --git a/backend/tests/Feature/ElementsEndpointTest.php b/backend/tests/Feature/ElementsEndpointTest.php index 2c11199..345396c 100644 --- a/backend/tests/Feature/ElementsEndpointTest.php +++ b/backend/tests/Feature/ElementsEndpointTest.php @@ -341,7 +341,7 @@ class ElementsEndpointTest extends TestCase ]); } - public function testAuthenticatedIconImageUploadReturnsElementPayload(): void + public function testAuthenticatedIconImageUploadReturnsPayload(): void { Storage::fake('public'); $setRepository = app(SetRepository::class); diff --git a/frontend/rabbi_gerzi/src/stores/elements.ts b/frontend/rabbi_gerzi/src/stores/elements.ts index 3155637..25d8b5c 100644 --- a/frontend/rabbi_gerzi/src/stores/elements.ts +++ b/frontend/rabbi_gerzi/src/stores/elements.ts @@ -1,4 +1,4 @@ -import { ref } from 'vue' +import { ref, type Ref } from 'vue' import { defineStore } from 'pinia' export interface ChildElement { @@ -10,10 +10,14 @@ export interface ChildElement { export interface Element extends ChildElement { iconImageUrl: string | null richText: string + shortPdfPath: string | null + longPdfPath: string | null pdfPath: string | null youtubeUrl: string | null } +type ElementFileType = 'iconImage' | 'shortPdf' | 'longPdf' + interface ElementResponse { element: Element childElements: ChildElement[] @@ -35,6 +39,8 @@ interface ElementPatchInput { description?: string iconImageUrl?: string richText?: string + shortPdfPath?: string + longPdfPath?: string pdfPath?: string youtubeUrl?: string } @@ -53,7 +59,8 @@ export const useElementsStore = defineStore('elements', () => { const error = ref(null) const isSaving = ref(false) const isUploadingIconImage = ref(false) - const isUploadingPdf = ref(false) + const isUploadingShortPdf = ref(false) + const isUploadingLongPdf = ref(false) const saveError = ref(null) async function fetchElement(elementId: string): Promise { @@ -89,16 +96,35 @@ export const useElementsStore = defineStore('elements', () => { } } - async function updateElement(elementId: string, input: UpdateElementInput): Promise { + async function updateElement( + elementId: string, + input: UpdateElementInput, + ): Promise { return await saveElementPatch(elementId, input, 'Could not save element') } async function clearElementIconImage(elementId: string): Promise { - return await saveElementPatch(elementId, { iconImageUrl: '' }, 'Could not remove icon image') + return await saveElementPatch( + elementId, + { iconImageUrl: '' }, + 'Could not remove icon image', + ) } - async function clearElementPdf(elementId: string): Promise { - return await saveElementPatch(elementId, { pdfPath: '' }, 'Could not remove PDF') + async function clearElementShortPdf(elementId: string): Promise { + return await saveElementPatch( + elementId, + { shortPdfPath: '' }, + 'Could not remove short PDF', + ) + } + + async function clearElementLongPdf(elementId: string): Promise { + return await saveElementPatch( + elementId, + { longPdfPath: '' }, + 'Could not remove long PDF', + ) } async function saveElementPatch( @@ -126,38 +152,64 @@ export const useElementsStore = defineStore('elements', () => { } } - async function uploadElementIconImage(elementId: string, file: File): Promise { - saveError.value = null - isUploadingIconImage.value = true - - try { - const formData = new FormData() - formData.append('elementId', elementId) - formData.append('iconImage', file) - const response = await fetch(ELEMENT_UPDATE_URL, { - method: 'POST', - headers: { Accept: 'application/json' }, - credentials: 'include', - body: formData, - }) - - return await handleElementResponse(response, 'Could not upload icon image') - } catch { - saveError.value = 'Network error - could not upload icon image' - return false - } finally { - isUploadingIconImage.value = false - } + async function uploadElementIconImage( + elementId: string, + file: File, + ): Promise { + return await uploadElementFile( + elementId, + file, + 'iconImage', + isUploadingIconImage, + 'Could not upload icon image', + 'Network error - could not upload icon image', + ) } - async function uploadElementPdf(elementId: string, file: File): Promise { + async function uploadElementShortPdf( + elementId: string, + file: File, + ): Promise { + return await uploadElementFile( + elementId, + file, + 'shortPdf', + isUploadingShortPdf, + 'Could not upload short PDF', + 'Network error - could not upload short PDF', + ) + } + + async function uploadElementLongPdf( + elementId: string, + file: File, + ): Promise { + return await uploadElementFile( + elementId, + file, + 'longPdf', + isUploadingLongPdf, + 'Could not upload long PDF', + 'Network error - could not upload long PDF', + ) + } + + async function uploadElementFile( + elementId: string, + file: File, + fileType: ElementFileType, + isUploadingFile: Ref, + failureMessage: string, + networkErrorMessage: string, + ): Promise { saveError.value = null - isUploadingPdf.value = true + isUploadingFile.value = true try { const formData = new FormData() formData.append('elementId', elementId) - formData.append('pdf', file) + formData.append('fileType', fileType) + formData.append('file', file) const response = await fetch(ELEMENT_UPDATE_URL, { method: 'POST', headers: { Accept: 'application/json' }, @@ -165,12 +217,12 @@ export const useElementsStore = defineStore('elements', () => { body: formData, }) - return await handleElementResponse(response, 'Could not upload PDF') + return await handleElementResponse(response, failureMessage) } catch { - saveError.value = 'Network error - could not upload PDF' + saveError.value = networkErrorMessage return false } finally { - isUploadingPdf.value = false + isUploadingFile.value = false } } @@ -203,7 +255,10 @@ export const useElementsStore = defineStore('elements', () => { return true } - async function errorMessage(response: Response, fallbackMessage: string): Promise { + async function errorMessage( + response: Response, + fallbackMessage: string, + ): Promise { try { const data: ErrorResponse = await response.json() return data.error ?? fallbackMessage @@ -219,13 +274,16 @@ export const useElementsStore = defineStore('elements', () => { error, isSaving, isUploadingIconImage, - isUploadingPdf, + isUploadingShortPdf, + isUploadingLongPdf, saveError, fetchElement, updateElement, uploadElementIconImage, - uploadElementPdf, + uploadElementShortPdf, + uploadElementLongPdf, clearElementIconImage, - clearElementPdf, + clearElementShortPdf, + clearElementLongPdf, } }) diff --git a/frontend/rabbi_gerzi/src/views/AdminElementPage.vue b/frontend/rabbi_gerzi/src/views/AdminElementPage.vue index 37f03ff..261bac3 100644 --- a/frontend/rabbi_gerzi/src/views/AdminElementPage.vue +++ b/frontend/rabbi_gerzi/src/views/AdminElementPage.vue @@ -14,13 +14,23 @@ interface ElementForm { const route = useRoute() const elementsStore = useElementsStore() -const { element, isLoading, error, isSaving, isUploadingIconImage, isUploadingPdf, saveError } = - storeToRefs(elementsStore) +const { + element, + isLoading, + error, + isSaving, + isUploadingIconImage, + isUploadingShortPdf, + isUploadingLongPdf, + saveError, +} = storeToRefs(elementsStore) const savedMessage = ref(null) const iconImageStatus = ref(null) -const pdfStatus = ref(null) +const shortPdfStatus = ref(null) +const longPdfStatus = ref(null) const iconImageInput = ref(null) -const pdfInput = ref(null) +const shortPdfInput = ref(null) +const longPdfInput = ref(null) const form = reactive({ title: '', @@ -56,7 +66,8 @@ watch( savedMessage.value = null iconImageStatus.value = null - pdfStatus.value = null + shortPdfStatus.value = null + longPdfStatus.value = null void elementsStore.fetchElement(currentElementId) }, { immediate: true }, @@ -95,8 +106,12 @@ function chooseIconImage(): void { iconImageInput.value?.click() } -function choosePdf(): void { - pdfInput.value?.click() +function chooseShortPdf(): void { + shortPdfInput.value?.click() +} + +function chooseLongPdf(): void { + longPdfInput.value?.click() } async function handleIconImageChange(changeEvent: Event): Promise { @@ -107,7 +122,10 @@ async function handleIconImageChange(changeEvent: Event): Promise { savedMessage.value = null iconImageStatus.value = null - const uploaded = await elementsStore.uploadElementIconImage(elementId.value, selectedFile) + const uploaded = await elementsStore.uploadElementIconImage( + elementId.value, + selectedFile, + ) resetInput(changeEvent) if (uploaded) { @@ -115,19 +133,41 @@ async function handleIconImageChange(changeEvent: Event): Promise { } } -async function handlePdfChange(changeEvent: Event): Promise { +async function handleShortPdfChange(changeEvent: Event): Promise { const selectedFile = selectedInputFile(changeEvent) if (selectedFile === null || elementId.value === '') { return } savedMessage.value = null - pdfStatus.value = null - const uploaded = await elementsStore.uploadElementPdf(elementId.value, selectedFile) + shortPdfStatus.value = null + const uploaded = await elementsStore.uploadElementShortPdf( + elementId.value, + selectedFile, + ) resetInput(changeEvent) if (uploaded) { - pdfStatus.value = 'PDF updated' + shortPdfStatus.value = 'Short PDF updated' + } +} + +async function handleLongPdfChange(changeEvent: Event): Promise { + const selectedFile = selectedInputFile(changeEvent) + if (selectedFile === null || elementId.value === '') { + return + } + + savedMessage.value = null + longPdfStatus.value = null + const uploaded = await elementsStore.uploadElementLongPdf( + elementId.value, + selectedFile, + ) + resetInput(changeEvent) + + if (uploaded) { + longPdfStatus.value = 'Long PDF updated' } } @@ -144,16 +184,29 @@ async function handleRemoveIconImage(): Promise { } } -async function handleRemovePdf(): Promise { +async function handleRemoveShortPdf(): Promise { if (elementId.value === '') { return } savedMessage.value = null - pdfStatus.value = null - const removed = await elementsStore.clearElementPdf(elementId.value) + shortPdfStatus.value = null + const removed = await elementsStore.clearElementShortPdf(elementId.value) if (removed) { - pdfStatus.value = 'PDF removed' + shortPdfStatus.value = 'Short PDF removed' + } +} + +async function handleRemoveLongPdf(): Promise { + if (elementId.value === '') { + return + } + + savedMessage.value = null + longPdfStatus.value = null + const removed = await elementsStore.clearElementLongPdf(elementId.value) + if (removed) { + longPdfStatus.value = 'Long PDF removed' } } @@ -186,7 +239,9 @@ function resetInput(changeEvent: Event): void {

Edit Element

-

Loading element...

+

+ Loading element... +

{{ error }}

-
+