diff --git a/backend/app/Controllers/ElementController.php b/backend/app/Controllers/ElementController.php index 1cfc205..6a63a85 100644 --- a/backend/app/Controllers/ElementController.php +++ b/backend/app/Controllers/ElementController.php @@ -9,17 +9,14 @@ use App\Element\UseCases\UpdateElement\UpdateElement; use App\Element\UseCases\UpdateElement\UpdateElementRequest; use App\Exceptions\BadRequestException; use App\Exceptions\NotFoundException; -use App\Shared\Files\FileUploader; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Http\UploadedFile; class ElementController { public function __construct( private GetElement $getElement, private UpdateElement $updateElement, - private FileUploader $fileUploader, ) { } @@ -55,15 +52,12 @@ class ElementController ], 200); } - public function update(Request $request): JsonResponse + public function update(?int $id, Request $request): JsonResponse { - $iconImage = $this->uploadedFileInput($request, 'iconImage'); - $pdf = $this->uploadedFileInput($request, 'pdf'); - try { $element = $this->updateElement->execute( new UpdateElementRequest( - id: $this->intInput($request, 'elementId'), + id: $id, title: $this->stringInput($request, 'title'), description: $this->stringInput($request, 'description'), iconImageUrl: $this->stringInput( @@ -73,14 +67,6 @@ class ElementController richText: $this->stringInput($request, 'richText'), 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'], ) ); } catch (BadRequestException $exception) { @@ -98,31 +84,9 @@ class ElementController ], 200); } - private function intInput(Request $request, string $key): ?int - { - $value = $request->input($key); - if (is_int($value)) { - return $value; - } - - if (is_string($value) && ctype_digit($value)) { - return (int) $value; - } - - return null; - } - private function stringInput(Request $request, string $key): ?string { - if (! $request->exists($key)) { - return null; - } - $value = $request->input($key); - if ($value === null) { - return ''; - } - if (! is_string($value)) { return null; } @@ -130,45 +94,6 @@ class ElementController return $value; } - /** - * @return array{ - * contents: string|null, - * originalName: string|null, - * mimeType: string|null, - * sizeBytes: int|null - * } - */ - private function uploadedFileInput(Request $request, string $key): array - { - $emptyFileInput = [ - 'contents' => null, - 'originalName' => null, - 'mimeType' => null, - 'sizeBytes' => null, - ]; - - if (! $request->hasFile($key)) { - return $emptyFileInput; - } - - $file = $request->file($key); - if (! $file instanceof UploadedFile) { - return $emptyFileInput; - } - - $realPath = $file->getRealPath(); - if ($realPath === false) { - return $emptyFileInput; - } - - return [ - 'contents' => (string) file_get_contents($realPath), - 'originalName' => $file->getClientOriginalName(), - 'mimeType' => (string) $file->getMimeType(), - 'sizeBytes' => (int) $file->getSize(), - ]; - } - /** * @return array{ * id: int, @@ -186,29 +111,10 @@ class ElementController 'id' => $element->getId(), 'title' => $element->getTitle(), 'description' => $element->getDescription(), - 'iconImageUrl' => $this->storageUrl( - $element->getIconImageUrl(), - 'element-icons/', - ), + 'iconImageUrl' => $element->getIconImageUrl(), 'richText' => $element->getRichText(), - 'pdfPath' => $this->storageUrl( - $element->getPdfPath(), - 'element-pdfs/', - ), + 'pdfPath' => $element->getPdfPath(), 'youtubeUrl' => $element->getYoutubeUrl(), ]; } - - private function storageUrl(?string $path, string $folderPrefix): ?string - { - if ($path === null) { - return null; - } - - if (str_starts_with($path, $folderPrefix)) { - return $this->fileUploader->url($path); - } - - return $path; - } } diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateElement.php b/backend/app/Element/UseCases/UpdateElement/UpdateElement.php index 0533b16..9f8811a 100644 --- a/backend/app/Element/UseCases/UpdateElement/UpdateElement.php +++ b/backend/app/Element/UseCases/UpdateElement/UpdateElement.php @@ -16,8 +16,6 @@ class UpdateElement private UpdateRichText $updateRichText, private UpdatePdfPath $updatePdfPath, private UpdateYoutubeUrl $updateYoutubeUrl, - private UpdateIconImage $updateIconImage, - private UpdatePdf $updatePdf, private ElementRepository $elementRepository, ) { } @@ -37,9 +35,7 @@ class UpdateElement && $request->iconImageUrl === null && $request->richText === null && $request->pdfPath === null - && $request->youtubeUrl === null - && $request->iconImageContents === null - && $request->pdfContents === null; + && $request->youtubeUrl === null; if ($hasNoFields) { throw new BadRequestException('nothing to update'); } @@ -82,28 +78,6 @@ 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, - ) - ); - } $element = $this->elementRepository->find($request->id); if ($element === null) { diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php index aa1aa06..0b5e766 100644 --- a/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php +++ b/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php @@ -12,14 +12,6 @@ class UpdateElementRequest public ?string $richText, 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, ) { } } diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateIconImage.php b/backend/app/Element/UseCases/UpdateElement/UpdateIconImage.php deleted file mode 100644 index 45d274f..0000000 --- a/backend/app/Element/UseCases/UpdateElement/UpdateIconImage.php +++ /dev/null @@ -1,81 +0,0 @@ -id === null) { - throw new BadRequestException('id is required'); - } - - if ( - $request->iconImageContents === null - || $request->iconImageOriginalName === null - || $request->iconImageMimeType === null - || $request->iconImageSizeBytes === null - ) { - throw new BadRequestException('icon image is required'); - } - - if ( - ! in_array( - $request->iconImageMimeType, - self::ALLOWED_MIME_TYPES, - true, - ) - ) { - throw new BadRequestException( - 'icon image must be a jpeg, png or webp image', - ); - } - - if ($request->iconImageSizeBytes > self::MAX_SIZE_BYTES) { - throw new BadRequestException( - 'icon image must be 5MB or smaller', - ); - } - - $element = $this->elementRepository->find($request->id); - if ($element === null) { - throw new NotFoundException('Element not found'); - } - - $iconImage = new FileToUpload( - contents: $request->iconImageContents, - originalName: $request->iconImageOriginalName, - mimeType: $request->iconImageMimeType, - sizeBytes: $request->iconImageSizeBytes, - ); - $path = $this->fileUploader->upload($iconImage, 'element-icons'); - $element->setIconImageUrl($path); - - return $this->elementRepository->update($element); - } -} diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateIconImageRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateIconImageRequest.php deleted file mode 100644 index a119053..0000000 --- a/backend/app/Element/UseCases/UpdateElement/UpdateIconImageRequest.php +++ /dev/null @@ -1,15 +0,0 @@ -nullableString($request->iconImageUrl); - if ($iconImageUrl === null) { - $this->deleteManagedFile($element->getIconImageUrl()); - } - - $element->setIconImageUrl($iconImageUrl); + $element->setIconImageUrl($this->nullableString( + $request->iconImageUrl, + )); return $this->elementRepository->update($element); } @@ -53,13 +47,4 @@ class UpdateIconImageUrl return $value; } - - private function deleteManagedFile(?string $path): void - { - if ($path === null) { - return; - } - - $this->fileUploader->delete($path); - } } diff --git a/backend/app/Element/UseCases/UpdateElement/UpdatePdf.php b/backend/app/Element/UseCases/UpdateElement/UpdatePdf.php deleted file mode 100644 index 0a13f09..0000000 --- a/backend/app/Element/UseCases/UpdateElement/UpdatePdf.php +++ /dev/null @@ -1,75 +0,0 @@ -id === null) { - throw new BadRequestException('id is required'); - } - - if ( - $request->pdfContents === null - || $request->pdfOriginalName === null - || $request->pdfMimeType === null - || $request->pdfSizeBytes === null - ) { - throw new BadRequestException('pdf is required'); - } - - if ( - ! in_array( - $request->pdfMimeType, - self::ALLOWED_MIME_TYPES, - true, - ) - ) { - throw new BadRequestException('pdf must be a pdf file'); - } - - if ($request->pdfSizeBytes > 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->pdfContents, - originalName: $request->pdfOriginalName, - mimeType: $request->pdfMimeType, - sizeBytes: $request->pdfSizeBytes, - ); - $path = $this->fileUploader->upload($pdf, 'element-pdfs'); - $element->setPdfPath($path); - - return $this->elementRepository->update($element); - } -} diff --git a/backend/app/Element/UseCases/UpdateElement/UpdatePdfPath.php b/backend/app/Element/UseCases/UpdateElement/UpdatePdfPath.php index 404c78b..d6fde1e 100644 --- a/backend/app/Element/UseCases/UpdateElement/UpdatePdfPath.php +++ b/backend/app/Element/UseCases/UpdateElement/UpdatePdfPath.php @@ -6,14 +6,11 @@ use App\Element\Element; use App\Element\ElementRepository; use App\Exceptions\BadRequestException; use App\Exceptions\NotFoundException; -use App\Shared\Files\FileUploader; class UpdatePdfPath { - public function __construct( - private ElementRepository $elementRepository, - private FileUploader $fileUploader, - ) { + public function __construct(private ElementRepository $elementRepository) + { } /** @@ -35,12 +32,7 @@ class UpdatePdfPath throw new NotFoundException('Element not found'); } - $pdfPath = $this->nullableString($request->pdfPath); - if ($pdfPath === null) { - $this->deleteManagedFile($element->getPdfPath()); - } - - $element->setPdfPath($pdfPath); + $element->setPdfPath($this->nullableString($request->pdfPath)); return $this->elementRepository->update($element); } @@ -53,13 +45,4 @@ class UpdatePdfPath return $value; } - - private function deleteManagedFile(?string $path): void - { - if ($path === null) { - return; - } - - $this->fileUploader->delete($path); - } } diff --git a/backend/app/Element/UseCases/UpdateElement/UpdatePdfRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdatePdfRequest.php deleted file mode 100644 index 98ac3bf..0000000 --- a/backend/app/Element/UseCases/UpdateElement/UpdatePdfRequest.php +++ /dev/null @@ -1,15 +0,0 @@ -app->bind( - FileUploader::class, - LaravelFileUploader::class, - ); } } diff --git a/backend/app/Shared/Files/FileToUpload.php b/backend/app/Shared/Files/FileToUpload.php deleted file mode 100644 index e8b8f41..0000000 --- a/backend/app/Shared/Files/FileToUpload.php +++ /dev/null @@ -1,34 +0,0 @@ -contents; - } - - public function getOriginalName(): string - { - return $this->originalName; - } - - public function getMimeType(): string - { - return $this->mimeType; - } - - public function getSizeBytes(): int - { - return $this->sizeBytes; - } -} diff --git a/backend/app/Shared/Files/FileUploader.php b/backend/app/Shared/Files/FileUploader.php deleted file mode 100644 index ac30916..0000000 --- a/backend/app/Shared/Files/FileUploader.php +++ /dev/null @@ -1,12 +0,0 @@ -extensionFor($file); - Storage::disk('public')->put($path, $file->getContents()); - - return $path; - } - - public function url(string $path): string - { - return Storage::disk('public')->url($path); - } - - public function delete(string $path): void - { - Storage::disk('public')->delete($path); - } - - private function extensionFor(FileToUpload $file): string - { - $mimeExtensions = [ - 'image/jpeg' => '.jpg', - 'image/png' => '.png', - 'image/webp' => '.webp', - 'application/pdf' => '.pdf', - ]; - if (isset($mimeExtensions[$file->getMimeType()])) { - return $mimeExtensions[$file->getMimeType()]; - } - - $originalExtension = pathinfo( - $file->getOriginalName(), - PATHINFO_EXTENSION, - ); - if ($originalExtension !== '') { - return '.' . $originalExtension; - } - - return ''; - } -} diff --git a/backend/database/seeders/ElementSeeder.php b/backend/database/seeders/ElementSeeder.php index c7df012..ba89ab1 100644 --- a/backend/database/seeders/ElementSeeder.php +++ b/backend/database/seeders/ElementSeeder.php @@ -6,7 +6,6 @@ use App\Element\CreateElementDto; use App\Element\ElementRepository; use App\Set\SetRepository; use Illuminate\Database\Seeder; -use Illuminate\Support\Facades\Storage; use RuntimeException; class ElementSeeder extends Seeder @@ -16,18 +15,11 @@ class ElementSeeder extends Seeder $setRepository = app(SetRepository::class); $elementRepository = app(ElementRepository::class); $baderechSet = $setRepository->find(1); - $rootIconImageUrl = $this->seedStorageFile( - 'element-icons/baderech-haavodah-icon.png', - ); - $introductionPdfPath = $this->seedStorageFile( - 'element-pdfs/baderech.pdf', - ); - $rootElement = $elementRepository->create(new CreateElementDto( set: $baderechSet, title: $baderechSet->getName(), description: $baderechSet->getDescription(), - iconImageUrl: $rootIconImageUrl, + iconImageUrl: '/assets/baderech-haavodah-icon.png', richText: '', pdfPath: null, youtubeUrl: null, @@ -44,7 +36,7 @@ class ElementSeeder extends Seeder . 'unlock the ability to live with strength, confidence, ' . 'and hope.', 'richText' => $this->introductionRichText(), - 'pdfPath' => $introductionPdfPath, + 'pdfPath' => '/assets/pdfs/baderech.pdf', ], [ 'title' => '2. Foundations', @@ -124,22 +116,6 @@ class ElementSeeder extends Seeder )); } - private function seedStorageFile( - string $storagePath, - ): string { - $sourcePath = base_path( - "database/seeders/assets/$storagePath", - ); - $contents = file_get_contents($sourcePath); - if ($contents === false) { - throw new RuntimeException("Seed file missing: $sourcePath"); - } - - Storage::disk('public')->put($storagePath, $contents); - - return $storagePath; - } - private function introductionRichText(): string { return '

' diff --git a/backend/database/seeders/assets/element-icons/baderech-haavodah-icon.png b/backend/database/seeders/assets/element-icons/baderech-haavodah-icon.png deleted file mode 100644 index 72603bb..0000000 Binary files a/backend/database/seeders/assets/element-icons/baderech-haavodah-icon.png and /dev/null differ diff --git a/backend/database/seeders/assets/element-pdfs/baderech.pdf b/backend/database/seeders/assets/element-pdfs/baderech.pdf deleted file mode 100644 index 8864294..0000000 Binary files a/backend/database/seeders/assets/element-pdfs/baderech.pdf and /dev/null differ diff --git a/backend/routes/api.php b/backend/routes/api.php index d3e0c43..9d253d0 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -12,5 +12,5 @@ Route::get('/me', [AuthController::class, 'me']) ->middleware(AuthMiddleware::class); Route::get('/sets', [SetController::class, 'index']); Route::get('/elements/{id}', [ElementController::class, 'show']); -Route::post('/element/update', [ElementController::class, 'update']) +Route::patch('/elements/{id}', [ElementController::class, 'update']) ->middleware(AuthMiddleware::class); diff --git a/backend/tests/Fakes/FakeFileUploader.php b/backend/tests/Fakes/FakeFileUploader.php deleted file mode 100644 index 9273887..0000000 --- a/backend/tests/Fakes/FakeFileUploader.php +++ /dev/null @@ -1,36 +0,0 @@ - - */ - public array $uploads = []; - - /** - * @var string[] - */ - public array $deletedPaths = []; - - public function upload(FileToUpload $file, string $folder): string - { - $this->uploads[] = ['file' => $file, 'folder' => $folder]; - - return $folder . '/fake-' . $file->getOriginalName(); - } - - public function url(string $path): string - { - return 'https://test.local/storage/' . $path; - } - - public function delete(string $path): void - { - $this->deletedPaths[] = $path; - } -} diff --git a/backend/tests/Feature/DemoSeedDataTest.php b/backend/tests/Feature/DemoSeedDataTest.php index b802b2b..fcc4fe7 100644 --- a/backend/tests/Feature/DemoSeedDataTest.php +++ b/backend/tests/Feature/DemoSeedDataTest.php @@ -6,27 +6,14 @@ use App\Element\ElementRepository; use App\Set\SetRepository; use Database\Seeders\DatabaseSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Support\Facades\Storage; use Tests\TestCase; class DemoSeedDataTest extends TestCase { use RefreshDatabase; - public function testElementSeederDoesNotReadFrontendAssets(): void - { - $seederSource = file_get_contents( - base_path('database/seeders/ElementSeeder.php'), - ); - - $this->assertIsString($seederSource); - $this->assertStringNotContainsString('../frontend/', $seederSource); - } - public function testBaderechRootElementHasDemoChildren(): void { - Storage::fake('public'); - $this->seed(DatabaseSeeder::class); $setRepository = app(SetRepository::class); @@ -36,13 +23,6 @@ class DemoSeedDataTest extends TestCase $childElements = $elementRepository->findByParentElement($rootElement); $this->assertSame('', $rootElement->getRichText()); - $this->assertSame( - 'element-icons/baderech-haavodah-icon.png', - $rootElement->getIconImageUrl(), - ); - Storage::disk('public')->assertExists( - 'element-icons/baderech-haavodah-icon.png', - ); $this->assertNull($rootElement->getPdfPath()); $this->assertNull($rootElement->getYoutubeUrl()); $this->assertCount(6, $childElements); @@ -96,10 +76,9 @@ class DemoSeedDataTest extends TestCase $childElements[0]->getRichText(), ); $this->assertSame( - 'element-pdfs/baderech.pdf', + '/assets/pdfs/baderech.pdf', $childElements[0]->getPdfPath(), ); - Storage::disk('public')->assertExists('element-pdfs/baderech.pdf'); $introductionChildElements = $elementRepository->findByParentElement( $childElements[0], diff --git a/backend/tests/Feature/ElementsEndpointTest.php b/backend/tests/Feature/ElementsEndpointTest.php index 48292ce..2ff9089 100644 --- a/backend/tests/Feature/ElementsEndpointTest.php +++ b/backend/tests/Feature/ElementsEndpointTest.php @@ -14,8 +14,6 @@ use App\User\UserRepository; use DateTimeImmutable; use DateTimeZone; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Http\UploadedFile; -use Illuminate\Support\Facades\Storage; use Tests\TestCase; class ElementsEndpointTest extends TestCase @@ -122,8 +120,7 @@ class ElementsEndpointTest extends TestCase parentElement: null, )); - $response = $this->postJson('/api/element/update', [ - 'elementId' => $element->getId(), + $response = $this->patchJson("/api/elements/{$element->getId()}", [ 'title' => 'Updated title', 'description' => 'Updated description', 'iconImageUrl' => '/assets/updated-icon.png', @@ -163,8 +160,7 @@ class ElementsEndpointTest extends TestCase $response = $this->withCredentials() ->withUnencryptedCookie('auth_token', 'valid-token') - ->postJson('/api/element/update', [ - 'elementId' => $element->getId(), + ->patchJson("/api/elements/{$element->getId()}", [ 'title' => 'Updated title', 'description' => 'Updated description', 'iconImageUrl' => '/assets/updated-icon.png', @@ -193,225 +189,6 @@ class ElementsEndpointTest extends TestCase ); } - public function testAuthenticatedUpdateClearsUploadedMedia(): void - { - Storage::fake('public'); - Storage::disk('public')->put( - 'element-icons/original-icon.png', - 'icon bytes', - ); - Storage::disk('public')->put( - 'element-pdfs/original.pdf', - 'pdf bytes', - ); - $setRepository = app(SetRepository::class); - $elementRepository = app(ElementRepository::class); - $set = $setRepository->create(new CreateSetDto( - name: 'Baderech HaAvodah', - description: 'A structured path for growth', - iconImageUrl: '/assets/baderech-haavodah-icon.png', - )); - $element = $elementRepository->create(new CreateElementDto( - set: $set, - title: 'Baderech HaAvodah', - description: 'A structured path for growth', - iconImageUrl: 'element-icons/original-icon.png', - richText: '

A structured path for growth

', - pdfPath: 'element-pdfs/original.pdf', - youtubeUrl: null, - parentElement: null, - )); - $this->createSession('valid-token'); - - $response = $this->withCredentials() - ->withUnencryptedCookie('auth_token', 'valid-token') - ->postJson('/api/element/update', [ - 'elementId' => $element->getId(), - 'iconImageUrl' => '', - 'pdfPath' => '', - ]); - - $response->assertOk(); - $body = $response->json(); - $this->assertNull($body['element']['iconImageUrl']); - $this->assertNull($body['element']['pdfPath']); - $updatedElement = $elementRepository->find($element->getId()); - $this->assertNotNull($updatedElement); - $this->assertNull($updatedElement->getIconImageUrl()); - $this->assertNull($updatedElement->getPdfPath()); - Storage::disk('public')->assertMissing( - 'element-icons/original-icon.png', - ); - Storage::disk('public')->assertMissing( - 'element-pdfs/original.pdf', - ); - } - - public function testIconImageUploadRequiresAuthentication(): void - { - $setRepository = app(SetRepository::class); - $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, - )); - - $response = $this->post( - '/api/element/update', - [ - 'elementId' => (string) $element->getId(), - 'iconImage' => $this->iconImageUpload(), - ], - ); - - $response->assertUnauthorized(); - $response->assertExactJson([ - 'error' => 'unauthenticated', - ]); - } - - public function testAuthenticatedIconImageUploadReturnsElementPayload(): 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, - )); - $this->createSession('valid-token'); - - $response = $this->withCredentials() - ->withUnencryptedCookie('auth_token', 'valid-token') - ->post( - '/api/element/update', - [ - 'elementId' => (string) $element->getId(), - 'iconImage' => $this->iconImageUpload(), - ], - ); - - $response->assertOk(); - $body = $response->json(); - $this->assertSame($element->getId(), $body['element']['id']); - $this->assertStringContainsString( - '/storage/element-icons/', - $body['element']['iconImageUrl'], - ); - $updatedElement = $elementRepository->find($element->getId()); - $this->assertNotNull($updatedElement); - $updatedIconImageUrl = $updatedElement->getIconImageUrl(); - $this->assertIsString($updatedIconImageUrl); - $this->assertStringStartsWith( - 'element-icons/', - $updatedIconImageUrl, - ); - Storage::disk('public')->assertExists($updatedIconImageUrl); - } - - public function testPdfUploadRequiresAuthentication(): 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, - )); - - $response = $this->post( - '/api/element/update', - [ - 'elementId' => (string) $element->getId(), - 'pdf' => $this->pdfUpload(), - ], - ); - - $response->assertUnauthorized(); - $response->assertExactJson([ - 'error' => 'unauthenticated', - ]); - } - - public function testAuthenticatedPdfUploadReturnsElementPayload(): 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, - )); - $this->createSession('valid-token'); - - $response = $this->withCredentials() - ->withUnencryptedCookie('auth_token', 'valid-token') - ->post( - '/api/element/update', - [ - 'elementId' => (string) $element->getId(), - 'pdf' => $this->pdfUpload(), - ], - ); - - $response->assertOk(); - $body = $response->json(); - $this->assertSame($element->getId(), $body['element']['id']); - $this->assertStringContainsString( - '/storage/element-pdfs/', - $body['element']['pdfPath'], - ); - $updatedElement = $elementRepository->find($element->getId()); - $this->assertNotNull($updatedElement); - $updatedPdfPath = $updatedElement->getPdfPath(); - $this->assertIsString($updatedPdfPath); - $this->assertStringStartsWith('element-pdfs/', $updatedPdfPath); - Storage::disk('public')->assertExists($updatedPdfPath); - } - private function createSession(string $token): void { $userRepository = app(UserRepository::class); @@ -434,30 +211,4 @@ class ElementsEndpointTest extends TestCase ), )); } - - private function iconImageUpload(): UploadedFile - { - $fixturePath = __DIR__ . '/../fixtures/icon.png'; - - return new UploadedFile( - $fixturePath, - 'icon.png', - 'image/png', - null, - true, - ); - } - - private function pdfUpload(): UploadedFile - { - $fixturePath = __DIR__ . '/../fixtures/baderech.pdf'; - - return new UploadedFile( - $fixturePath, - 'baderech.pdf', - 'application/pdf', - null, - true, - ); - } } diff --git a/backend/tests/Unit/Controllers/ElementControllerTest.php b/backend/tests/Unit/Controllers/ElementControllerTest.php index 5416d92..99bfa3e 100644 --- a/backend/tests/Unit/Controllers/ElementControllerTest.php +++ b/backend/tests/Unit/Controllers/ElementControllerTest.php @@ -8,18 +8,13 @@ use App\Element\Element; use App\Element\UseCases\GetElement\GetElement; 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\UpdateRichText; use App\Element\UseCases\UpdateElement\UpdateTitle; use App\Element\UseCases\UpdateElement\UpdateYoutubeUrl; use App\Set\Set as DomainSet; -use Illuminate\Http\Request; -use Illuminate\Http\UploadedFile; use Tests\Fakes\FakeElementRepository; -use Tests\Fakes\FakeFileUploader; use Tests\TestCase; class ElementControllerTest extends TestCase @@ -28,35 +23,20 @@ class ElementControllerTest extends TestCase private FakeElementRepository $elementRepo; - private FakeFileUploader $fileUploader; - protected function setUp(): void { $this->elementRepo = new FakeElementRepository(); - $this->fileUploader = new FakeFileUploader(); $getElement = new GetElement($this->elementRepo); $updateElement = new UpdateElement( new UpdateTitle($this->elementRepo), new UpdateDescription($this->elementRepo), - new UpdateIconImageUrl($this->elementRepo, $this->fileUploader), + new UpdateIconImageUrl($this->elementRepo), new UpdateRichText($this->elementRepo), - new UpdatePdfPath($this->elementRepo, $this->fileUploader), + new UpdatePdfPath($this->elementRepo), new UpdateYoutubeUrl($this->elementRepo), - new UpdateIconImage( - $this->elementRepo, - $this->fileUploader, - ), - new UpdatePdf( - $this->elementRepo, - $this->fileUploader, - ), $this->elementRepo, ); - $this->controller = new ElementController( - $getElement, - $updateElement, - $this->fileUploader, - ); + $this->controller = new ElementController($getElement, $updateElement); } public function testShowReturnsElementPayload(): void @@ -155,88 +135,6 @@ 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, - ); - $fixturePath = __DIR__ . '/../../fixtures/icon.png'; - $request = new Request([], [ - 'elementId' => (string) $element->getId(), - ], [], [], [ - 'iconImage' => new UploadedFile( - $fixturePath, - 'icon.png', - 'image/png', - 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-icons/fake-icon.png', - $body['element']['iconImageUrl'], - ); - $this->assertSame( - 'element-icons', - $this->fileUploader->uploads[0]['folder'], - ); - } - - public function testUpdateWithPdfReturnsElementPayload(): 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, - ); - $fixturePath = __DIR__ . '/../../fixtures/baderech.pdf'; - $request = new Request([], [ - 'elementId' => (string) $element->getId(), - ], [], [], [ - 'pdf' => 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/fake-baderech.pdf', - $body['element']['pdfPath'], - ); - $this->assertSame( - 'element-pdfs', - $this->fileUploader->uploads[0]['folder'], - ); - } - private function createSet(int $id, string $name): DomainSet { return new DomainSet( diff --git a/backend/tests/Unit/Element/UseCases/UpdateElementFieldsTest.php b/backend/tests/Unit/Element/UseCases/UpdateElementFieldsTest.php index ca39700..7c27c9c 100644 --- a/backend/tests/Unit/Element/UseCases/UpdateElementFieldsTest.php +++ b/backend/tests/Unit/Element/UseCases/UpdateElementFieldsTest.php @@ -19,19 +19,15 @@ use App\Element\UseCases\UpdateElement\UpdateYoutubeUrlRequest; use App\Exceptions\BadRequestException; use App\Set\Set as DomainSet; use Tests\Fakes\FakeElementRepository; -use Tests\Fakes\FakeFileUploader; use Tests\TestCase; class UpdateElementFieldsTest extends TestCase { private FakeElementRepository $elementRepo; - private FakeFileUploader $fileUploader; - protected function setUp(): void { $this->elementRepo = new FakeElementRepository(); - $this->fileUploader = new FakeFileUploader(); } public function testUpdateTitleUpdatesOnlyTitle(): void @@ -92,10 +88,7 @@ class UpdateElementFieldsTest extends TestCase public function testUpdateIconImageUrlClearsEmptyString(): void { $element = $this->createFilledElement(); - $updateIconImageUrl = new UpdateIconImageUrl( - $this->elementRepo, - $this->fileUploader, - ); + $updateIconImageUrl = new UpdateIconImageUrl($this->elementRepo); $updatedElement = $updateIconImageUrl->execute( new UpdateIconImageUrlRequest( @@ -106,10 +99,6 @@ class UpdateElementFieldsTest extends TestCase $this->assertNull($updatedElement->getIconImageUrl()); $this->assertSame($element->getTitle(), $updatedElement->getTitle()); - $this->assertSame( - ['/assets/original-icon.png'], - $this->fileUploader->deletedPaths, - ); } public function testUpdateRichTextUpdatesOnlyRichText(): void @@ -137,10 +126,7 @@ class UpdateElementFieldsTest extends TestCase public function testUpdatePdfPathClearsEmptyString(): void { $element = $this->createFilledElement(); - $updatePdfPath = new UpdatePdfPath( - $this->elementRepo, - $this->fileUploader, - ); + $updatePdfPath = new UpdatePdfPath($this->elementRepo); $updatedElement = $updatePdfPath->execute( new UpdatePdfPathRequest(id: $element->getId(), pdfPath: '') @@ -148,10 +134,6 @@ class UpdateElementFieldsTest extends TestCase $this->assertNull($updatedElement->getPdfPath()); $this->assertSame($element->getTitle(), $updatedElement->getTitle()); - $this->assertSame( - ['/assets/pdfs/original.pdf'], - $this->fileUploader->deletedPaths, - ); } public function testUpdateYoutubeUrlClearsEmptyString(): void @@ -172,16 +154,6 @@ class UpdateElementFieldsTest extends TestCase private function createFilledElement(): Element { - return $this->createElementWithMedia( - '/assets/original-icon.png', - '/assets/pdfs/original.pdf', - ); - } - - private function createElementWithMedia( - ?string $iconImageUrl, - ?string $pdfPath, - ): Element { $set = new DomainSet( id: 1, name: 'Baderech', @@ -193,9 +165,9 @@ class UpdateElementFieldsTest extends TestCase set: $set, title: 'Original title', description: 'Original description', - iconImageUrl: $iconImageUrl, + iconImageUrl: '/assets/original-icon.png', richText: '

Original rich text

', - pdfPath: $pdfPath, + pdfPath: '/assets/pdfs/original.pdf', 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..0ce0d88 100644 --- a/backend/tests/Unit/Element/UseCases/UpdateElementTest.php +++ b/backend/tests/Unit/Element/UseCases/UpdateElementTest.php @@ -5,12 +5,10 @@ namespace Tests\Unit\Element\UseCases; use App\Element\CreateElementDto; use App\Element\Element; use App\Element\UseCases\UpdateElement\UpdateDescription; +use App\Element\UseCases\UpdateElement\UpdateIconImageUrl; 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\UpdateRichText; use App\Element\UseCases\UpdateElement\UpdateTitle; use App\Element\UseCases\UpdateElement\UpdateYoutubeUrl; @@ -18,36 +16,24 @@ use App\Exceptions\BadRequestException; use App\Exceptions\NotFoundException; use App\Set\Set as DomainSet; use Tests\Fakes\FakeElementRepository; -use Tests\Fakes\FakeFileUploader; use Tests\TestCase; class UpdateElementTest extends TestCase { private FakeElementRepository $elementRepo; - private FakeFileUploader $fileUploader; - private UpdateElement $updateElement; protected function setUp(): void { $this->elementRepo = new FakeElementRepository(); - $this->fileUploader = new FakeFileUploader(); $this->updateElement = new UpdateElement( new UpdateTitle($this->elementRepo), new UpdateDescription($this->elementRepo), - new UpdateIconImageUrl($this->elementRepo, $this->fileUploader), + new UpdateIconImageUrl($this->elementRepo), new UpdateRichText($this->elementRepo), - new UpdatePdfPath($this->elementRepo, $this->fileUploader), + new UpdatePdfPath($this->elementRepo), new UpdateYoutubeUrl($this->elementRepo), - new UpdateIconImage( - $this->elementRepo, - $this->fileUploader, - ), - new UpdatePdf( - $this->elementRepo, - $this->fileUploader, - ), $this->elementRepo, ); } @@ -75,14 +61,6 @@ class UpdateElementTest extends TestCase richText: '

Updated rich text

', pdfPath: '/assets/pdfs/updated.pdf', youtubeUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', - iconImageContents: null, - iconImageOriginalName: null, - iconImageMimeType: null, - iconImageSizeBytes: null, - pdfContents: null, - pdfOriginalName: null, - pdfMimeType: null, - pdfSizeBytes: null, ) ); @@ -137,14 +115,6 @@ class UpdateElementTest extends TestCase richText: '

Updated rich text

', pdfPath: '', youtubeUrl: '', - iconImageContents: null, - iconImageOriginalName: null, - iconImageMimeType: null, - iconImageSizeBytes: null, - pdfContents: null, - pdfOriginalName: null, - pdfMimeType: null, - pdfSizeBytes: null, ) ); @@ -176,14 +146,6 @@ class UpdateElementTest extends TestCase richText: null, pdfPath: null, youtubeUrl: null, - iconImageContents: null, - iconImageOriginalName: null, - iconImageMimeType: null, - iconImageSizeBytes: null, - pdfContents: null, - pdfOriginalName: null, - pdfMimeType: null, - pdfSizeBytes: null, ) ); @@ -210,58 +172,6 @@ class UpdateElementTest extends TestCase ); } - public function testUpdatesUploadedFiles(): void - { - $set = $this->createSet(1, 'Baderech'); - $element = $this->createElement( - $set, - 'Original title', - 'Original description', - null, - '

Original rich text

', - null, - null, - null, - ); - - $updatedElement = $this->updateElement->execute( - new UpdateElementRequest( - id: $element->getId(), - title: null, - description: null, - iconImageUrl: null, - richText: 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, - ) - ); - - $this->assertSame( - 'element-icons/fake-icon.png', - $updatedElement->getIconImageUrl(), - ); - $this->assertSame( - 'element-pdfs/fake-baderech.pdf', - $updatedElement->getPdfPath(), - ); - $this->assertSame( - 'element-icons', - $this->fileUploader->uploads[0]['folder'], - ); - $this->assertSame( - 'element-pdfs', - $this->fileUploader->uploads[1]['folder'], - ); - } - public function testThrowsWhenIdMissing(): void { $this->expectException(BadRequestException::class); @@ -275,14 +185,6 @@ class UpdateElementTest extends TestCase richText: '

Updated rich text

', pdfPath: null, youtubeUrl: null, - iconImageContents: null, - iconImageOriginalName: null, - iconImageMimeType: null, - iconImageSizeBytes: null, - pdfContents: null, - pdfOriginalName: null, - pdfMimeType: null, - pdfSizeBytes: null, )); } @@ -299,14 +201,6 @@ class UpdateElementTest extends TestCase richText: null, pdfPath: null, youtubeUrl: null, - iconImageContents: null, - iconImageOriginalName: null, - iconImageMimeType: null, - iconImageSizeBytes: null, - pdfContents: null, - pdfOriginalName: null, - pdfMimeType: null, - pdfSizeBytes: null, )); } @@ -323,14 +217,6 @@ class UpdateElementTest extends TestCase richText: '

Updated rich text

', pdfPath: null, youtubeUrl: null, - iconImageContents: null, - iconImageOriginalName: null, - iconImageMimeType: null, - iconImageSizeBytes: null, - pdfContents: null, - pdfOriginalName: null, - pdfMimeType: null, - pdfSizeBytes: null, )); } @@ -347,14 +233,6 @@ class UpdateElementTest extends TestCase richText: '

Updated rich text

', pdfPath: null, youtubeUrl: null, - iconImageContents: null, - iconImageOriginalName: null, - iconImageMimeType: null, - iconImageSizeBytes: null, - pdfContents: null, - pdfOriginalName: null, - pdfMimeType: null, - pdfSizeBytes: null, )); } diff --git a/backend/tests/Unit/Element/UseCases/UpdateIconImageTest.php b/backend/tests/Unit/Element/UseCases/UpdateIconImageTest.php deleted file mode 100644 index af8ae7f..0000000 --- a/backend/tests/Unit/Element/UseCases/UpdateIconImageTest.php +++ /dev/null @@ -1,175 +0,0 @@ -elementRepository = new FakeElementRepository(); - $this->fileUploader = new FakeFileUploader(); - $this->useCase = new UpdateIconImage( - $this->elementRepository, - $this->fileUploader, - ); - } - - /** - * @return array{ - * iconImageContents: string, - * iconImageOriginalName: string, - * iconImageMimeType: string, - * iconImageSizeBytes: int - * } - */ - private function imageArgs(): array - { - return [ - 'iconImageContents' => 'binary-image-bytes', - 'iconImageOriginalName' => 'icon.png', - 'iconImageMimeType' => 'image/png', - 'iconImageSizeBytes' => 1024, - ]; - } - - public function testUploadsIconImageAndStoresPath(): void - { - $element = $this->createElement(); - - $updatedElement = $this->useCase->execute( - new UpdateIconImageRequest( - ...$this->imageArgs(), - id: $element->getId(), - ) - ); - - $this->assertSame( - 'element-icons/fake-icon.png', - $updatedElement->getIconImageUrl(), - ); - $this->assertSame( - 'element-icons/fake-icon.png', - $this->elementRepository - ->find($element->getId()) - ->getIconImageUrl(), - ); - $this->assertSame( - 'element-icons', - $this->fileUploader->uploads[0]['folder'], - ); - } - - public function testNullIdThrowsBadRequest(): void - { - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage('id is required'); - - $this->useCase->execute( - new UpdateIconImageRequest( - ...$this->imageArgs(), - id: null, - ) - ); - } - - public function testNullIconImageThrowsBadRequest(): void - { - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage('icon image is required'); - - $this->useCase->execute( - new UpdateIconImageRequest( - id: 1, - iconImageContents: null, - iconImageOriginalName: null, - iconImageMimeType: null, - iconImageSizeBytes: null, - ) - ); - } - - public function testDisallowedMimeThrowsBadRequest(): void - { - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage( - 'icon image must be a jpeg, png or webp image', - ); - - $this->useCase->execute( - new UpdateIconImageRequest( - id: 1, - iconImageContents: 'not-an-image', - iconImageOriginalName: 'icon.pdf', - iconImageMimeType: 'application/pdf', - iconImageSizeBytes: 1024, - ) - ); - } - - public function testOversizedIconImageThrowsBadRequest(): void - { - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage('icon image must be 5MB or smaller'); - - $this->useCase->execute( - new UpdateIconImageRequest( - id: 1, - iconImageContents: 'big', - iconImageOriginalName: 'icon.png', - iconImageMimeType: 'image/png', - iconImageSizeBytes: 6 * 1024 * 1024, - ) - ); - } - - public function testMissingElementThrowsNotFound(): void - { - $this->expectException(NotFoundException::class); - $this->expectExceptionMessage('Element not found'); - - $this->useCase->execute( - new UpdateIconImageRequest( - ...$this->imageArgs(), - id: 999, - ) - ); - } - - 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: '', - pdfPath: null, - youtubeUrl: null, - parentElement: null, - )); - } -} diff --git a/backend/tests/Unit/Element/UseCases/UpdatePdfTest.php b/backend/tests/Unit/Element/UseCases/UpdatePdfTest.php deleted file mode 100644 index 9a3b30b..0000000 --- a/backend/tests/Unit/Element/UseCases/UpdatePdfTest.php +++ /dev/null @@ -1,173 +0,0 @@ -elementRepository = new FakeElementRepository(); - $this->fileUploader = new FakeFileUploader(); - $this->useCase = new UpdatePdf( - $this->elementRepository, - $this->fileUploader, - ); - } - - /** - * @return array{ - * pdfContents: string, - * pdfOriginalName: string, - * pdfMimeType: string, - * pdfSizeBytes: int - * } - */ - private function pdfArgs(): array - { - return [ - 'pdfContents' => 'binary-pdf-bytes', - 'pdfOriginalName' => 'baderech.pdf', - 'pdfMimeType' => 'application/pdf', - 'pdfSizeBytes' => 1024, - ]; - } - - public function testUploadsPdfAndStoresPath(): void - { - $element = $this->createElement(); - - $updatedElement = $this->useCase->execute( - new UpdatePdfRequest( - ...$this->pdfArgs(), - id: $element->getId(), - ) - ); - - $this->assertSame( - 'element-pdfs/fake-baderech.pdf', - $updatedElement->getPdfPath(), - ); - $storedElement = $this->elementRepository->find($element->getId()); - $this->assertNotNull($storedElement); - $this->assertSame( - 'element-pdfs/fake-baderech.pdf', - $storedElement->getPdfPath(), - ); - $this->assertSame( - 'element-pdfs', - $this->fileUploader->uploads[0]['folder'], - ); - } - - public function testNullIdThrowsBadRequest(): void - { - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage('id is required'); - - $this->useCase->execute( - new UpdatePdfRequest( - ...$this->pdfArgs(), - id: null, - ) - ); - } - - public function testNullPdfThrowsBadRequest(): void - { - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage('pdf is required'); - - $this->useCase->execute( - new UpdatePdfRequest( - id: 1, - pdfContents: null, - pdfOriginalName: null, - pdfMimeType: null, - pdfSizeBytes: null, - ) - ); - } - - public function testDisallowedMimeThrowsBadRequest(): void - { - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage('pdf must be a pdf file'); - - $this->useCase->execute( - new UpdatePdfRequest( - id: 1, - pdfContents: 'not-a-pdf', - pdfOriginalName: 'icon.png', - pdfMimeType: 'image/png', - pdfSizeBytes: 1024, - ) - ); - } - - public function testOversizedPdfThrowsBadRequest(): void - { - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage('pdf must be 10MB or smaller'); - - $this->useCase->execute( - new UpdatePdfRequest( - id: 1, - pdfContents: 'big', - pdfOriginalName: 'baderech.pdf', - pdfMimeType: 'application/pdf', - pdfSizeBytes: 11 * 1024 * 1024, - ) - ); - } - - public function testMissingElementThrowsNotFound(): void - { - $this->expectException(NotFoundException::class); - $this->expectExceptionMessage('Element not found'); - - $this->useCase->execute( - new UpdatePdfRequest( - ...$this->pdfArgs(), - id: 999, - ) - ); - } - - 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: '', - pdfPath: null, - youtubeUrl: null, - parentElement: null, - )); - } -} diff --git a/backend/tests/fixtures/baderech.pdf b/backend/tests/fixtures/baderech.pdf deleted file mode 100644 index 8864294..0000000 Binary files a/backend/tests/fixtures/baderech.pdf and /dev/null differ diff --git a/backend/tests/fixtures/icon.png b/backend/tests/fixtures/icon.png deleted file mode 100644 index 72603bb..0000000 Binary files a/backend/tests/fixtures/icon.png and /dev/null differ diff --git a/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts index 38dc7a1..8678ec2 100644 --- a/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts +++ b/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts @@ -12,17 +12,26 @@ function loginAsAdmin(): void { function fillElementForm( title: string, description: string, + iconImageUrl: string, richText: string, + pdfPath: string, youtubeUrl: string, ): void { cy.get('[data-cy="admin-element-title"]').clear().type(title) cy.get('[data-cy="admin-element-description"]').clear().type(description) + cy.get('[data-cy="admin-element-icon-image-url"]') + .clear() + .type(iconImageUrl) cy.get('[data-cy="admin-element-rich-text"]').clear() if (richText !== '') { cy.get('[data-cy="admin-element-rich-text"]').type(richText, { parseSpecialCharSequences: false, }) } + cy.get('[data-cy="admin-element-pdf-path"]').clear() + if (pdfPath !== '') { + cy.get('[data-cy="admin-element-pdf-path"]').type(pdfPath) + } cy.get('[data-cy="admin-element-youtube-url"]').clear() if (youtubeUrl !== '') { cy.get('[data-cy="admin-element-youtube-url"]').type(youtubeUrl) @@ -70,6 +79,7 @@ describe('admin element editing', () => { it('saves element edits and restores the seed content through the UI', () => { const originalTitle = 'Baderech HaAvodah' const originalDescription = 'a structured path for inner and outer growth' + const originalIconImageUrl = '/assets/baderech-haavodah-icon.png' const updatedTitle = 'Baderech HaAvodah Updated' const updatedDescription = 'Updated admin description' const updatedRichText = '

Updated admin rich text

' @@ -80,8 +90,10 @@ describe('admin element editing', () => { fillElementForm( updatedTitle, updatedDescription, + originalIconImageUrl, updatedRichText, '', + '', ) cy.get('[data-cy="admin-element-save"]').click() @@ -98,6 +110,8 @@ describe('admin element editing', () => { fillElementForm( originalTitle, originalDescription, + originalIconImageUrl, + '', '', '', ) @@ -106,89 +120,4 @@ describe('admin element editing', () => { .should('be.visible') .and('contain.text', 'Saved') }) - - it('uploads icon and pdf files immediately through the UI', () => { - cy.resetDb() - loginAsAdmin() - cy.visit('/admin/element/1') - cy.intercept('POST', /\/api\/element\/update$/).as('updateElement') - - cy.get('[data-cy="admin-element-icon-image-input"]').selectFile( - 'public/assets/baderech-haavodah-icon.png', - { force: true }, - ) - cy.wait('@updateElement') - cy.get('[data-cy="admin-element-icon-image-status"]') - .should('be.visible') - .and('contain.text', 'Icon image updated') - cy.get('[data-cy="admin-element-current-icon"]') - .should('be.visible') - .and('have.attr', 'src') - .and('include', '/storage/element-icons/') - - cy.get('[data-cy="admin-element-pdf-input"]').selectFile( - 'public/assets/pdfs/baderech.pdf', - { force: true }, - ) - cy.wait('@updateElement') - cy.get('[data-cy="admin-element-pdf-status"]') - .should('be.visible') - .and('contain.text', 'PDF updated') - cy.get('[data-cy="admin-element-current-pdf"]') - .should('be.visible') - .and('have.attr', 'href') - .and('include', '/storage/element-pdfs/') - - 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"]') - .should('be.visible') - .and('have.attr', 'href') - .and('include', '/storage/element-pdfs/') - - cy.resetDb() - }) - - it('removes uploaded icon and pdf files immediately through the UI', () => { - cy.resetDb() - loginAsAdmin() - cy.visit('/admin/element/1') - cy.intercept('POST', /\/api\/element\/update$/).as('updateElement') - - cy.get('[data-cy="admin-element-icon-image-input"]').selectFile( - 'public/assets/baderech-haavodah-icon.png', - { force: true }, - ) - cy.wait('@updateElement') - cy.get('[data-cy="admin-element-pdf-input"]').selectFile( - 'public/assets/pdfs/baderech.pdf', - { force: true }, - ) - cy.wait('@updateElement') - - cy.contains('button', 'Remove icon image').click() - cy.wait('@updateElement') - cy.get('[data-cy="admin-element-icon-image-status"]') - .should('be.visible') - .and('contain.text', 'Icon image removed') - cy.get('[data-cy="admin-element-current-icon"]').should('not.exist') - cy.contains('No icon image').should('be.visible') - - cy.contains('button', 'Remove PDF').click() - cy.wait('@updateElement') - cy.get('[data-cy="admin-element-pdf-status"]') - .should('be.visible') - .and('contain.text', 'PDF removed') - cy.get('[data-cy="admin-element-current-pdf"]').should('not.exist') - cy.contains('No PDF').should('be.visible') - - cy.contains('header.site-header a', 'View Element').click() - cy.get('[data-cy="element-icon"]').should('not.exist') - cy.get('[data-cy="element-pdf-link"]').should('not.exist') - - cy.resetDb() - }) }) diff --git a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts index e0f361a..b3e1424 100644 --- a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts +++ b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts @@ -47,7 +47,7 @@ describe('media page sets', () => { cy.get('[data-cy="element-icon"]') .should('be.visible') .and('have.attr', 'src') - .and('include', '/storage/element-icons/baderech-haavodah-icon.png') + .and('include', '/assets/baderech-haavodah-icon.png') cy.get('[data-cy="element-rich-text"]').should('not.exist') cy.get('[data-cy="element-youtube-embed"]') .should('not.exist') @@ -117,12 +117,9 @@ 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') .should('be.visible') + .and('have.attr', 'href', '/assets/pdfs/baderech.pdf') .and('have.attr', 'target', '_blank') - cy.get('@pdfLink') - .and('have.attr', 'href') - .and('include', '/storage/element-pdfs/baderech.pdf') cy.contains('[data-cy="child-element-link"]', introductionAudioTitle) .as('introductionAudioLink') .should('have.attr', 'href', '/element/8') diff --git a/frontend/rabbi_gerzi/src/stores/elements.ts b/frontend/rabbi_gerzi/src/stores/elements.ts index 3155637..9da179f 100644 --- a/frontend/rabbi_gerzi/src/stores/elements.ts +++ b/frontend/rabbi_gerzi/src/stores/elements.ts @@ -22,7 +22,9 @@ interface ElementResponse { export interface UpdateElementInput { title: string description: string + iconImageUrl: string richText: string + pdfPath: string youtubeUrl: string } @@ -30,21 +32,11 @@ interface UpdateElementResponse { element: Element } -interface ElementPatchInput { - title?: string - description?: string - iconImageUrl?: string - richText?: string - pdfPath?: string - youtubeUrl?: string -} - interface ErrorResponse { error?: string } const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string -const ELEMENT_UPDATE_URL = `${API_BASE_URL}/api/element/update` export const useElementsStore = defineStore('elements', () => { const element = ref(null) @@ -52,8 +44,6 @@ export const useElementsStore = defineStore('elements', () => { const isLoading = ref(false) const error = ref(null) const isSaving = ref(false) - const isUploadingIconImage = ref(false) - const isUploadingPdf = ref(false) const saveError = ref(null) async function fetchElement(elementId: string): Promise { @@ -90,142 +80,59 @@ export const useElementsStore = defineStore('elements', () => { } 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') - } - - async function clearElementPdf(elementId: string): Promise { - return await saveElementPatch(elementId, { pdfPath: '' }, 'Could not remove PDF') - } - - async function saveElementPatch( - elementId: string, - input: ElementPatchInput, - failureMessage: string, - ): Promise { saveError.value = null isSaving.value = true try { - const response = await fetch(ELEMENT_UPDATE_URL, { - method: 'POST', + const encodedElementId = encodeURIComponent(elementId) + const elementUrl = `${API_BASE_URL}/api/elements/${encodedElementId}` + const response = await fetch(elementUrl, { + method: 'PATCH', headers: { 'Content-Type': 'application/json' }, credentials: 'include', - body: JSON.stringify({ elementId, ...input }), + body: JSON.stringify(input), }) - return await handleElementResponse(response, failureMessage) + if (response.status === 401) { + saveError.value = 'Please log in again' + return false + } + + if (response.status === 404) { + saveError.value = 'Element not found' + return false + } + + if (response.status === 400) { + const data: ErrorResponse = await response.json() + saveError.value = data.error ?? 'Could not save element' + return false + } + + if (!response.ok) { + saveError.value = 'Could not save element' + return false + } + + const data: UpdateElementResponse = await response.json() + element.value = data.element + return true } catch { - saveError.value = `Network error - ${failureMessage.toLowerCase()}` + saveError.value = 'Network error - could not save element' return false } finally { isSaving.value = false } } - 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 uploadElementPdf(elementId: string, file: File): Promise { - saveError.value = null - isUploadingPdf.value = true - - try { - const formData = new FormData() - formData.append('elementId', elementId) - formData.append('pdf', 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 PDF') - } catch { - saveError.value = 'Network error - could not upload PDF' - return false - } finally { - isUploadingPdf.value = false - } - } - - async function handleElementResponse( - response: Response, - failureMessage: string, - ): Promise { - if (response.status === 401) { - saveError.value = 'Please log in again' - return false - } - - if (response.status === 404) { - saveError.value = 'Element not found' - return false - } - - if (response.status === 400) { - saveError.value = await errorMessage(response, failureMessage) - return false - } - - if (!response.ok) { - saveError.value = failureMessage - return false - } - - const data: UpdateElementResponse = await response.json() - element.value = data.element - return true - } - - async function errorMessage(response: Response, fallbackMessage: string): Promise { - try { - const data: ErrorResponse = await response.json() - return data.error ?? fallbackMessage - } catch { - return fallbackMessage - } - } - return { element, childElements, isLoading, error, isSaving, - isUploadingIconImage, - isUploadingPdf, saveError, fetchElement, updateElement, - uploadElementIconImage, - uploadElementPdf, - clearElementIconImage, - clearElementPdf, } }) diff --git a/frontend/rabbi_gerzi/src/views/AdminElementPage.vue b/frontend/rabbi_gerzi/src/views/AdminElementPage.vue index 37f03ff..cc19049 100644 --- a/frontend/rabbi_gerzi/src/views/AdminElementPage.vue +++ b/frontend/rabbi_gerzi/src/views/AdminElementPage.vue @@ -8,24 +8,23 @@ import { useElementsStore } from '@/stores/elements' interface ElementForm { title: string description: string + iconImageUrl: string richText: string + pdfPath: string youtubeUrl: string } const route = useRoute() const elementsStore = useElementsStore() -const { element, isLoading, error, isSaving, isUploadingIconImage, isUploadingPdf, saveError } = - storeToRefs(elementsStore) +const { element, isLoading, error, isSaving, saveError } = storeToRefs(elementsStore) const savedMessage = ref(null) -const iconImageStatus = ref(null) -const pdfStatus = ref(null) -const iconImageInput = ref(null) -const pdfInput = ref(null) const form = reactive({ title: '', description: '', + iconImageUrl: '', richText: '', + pdfPath: '', youtubeUrl: '', }) @@ -55,8 +54,6 @@ watch( } savedMessage.value = null - iconImageStatus.value = null - pdfStatus.value = null void elementsStore.fetchElement(currentElementId) }, { immediate: true }, @@ -69,7 +66,9 @@ watch(element, (currentElement) => { form.title = currentElement.title form.description = currentElement.description + form.iconImageUrl = currentElement.iconImageUrl ?? '' form.richText = currentElement.richText + form.pdfPath = currentElement.pdfPath ?? '' form.youtubeUrl = currentElement.youtubeUrl ?? '' }) @@ -82,7 +81,9 @@ async function handleSubmit(): Promise { const saved = await elementsStore.updateElement(elementId.value, { title: form.title, description: form.description, + iconImageUrl: form.iconImageUrl, richText: form.richText, + pdfPath: form.pdfPath, youtubeUrl: form.youtubeUrl, }) @@ -90,92 +91,6 @@ async function handleSubmit(): Promise { savedMessage.value = 'Saved' } } - -function chooseIconImage(): void { - iconImageInput.value?.click() -} - -function choosePdf(): void { - pdfInput.value?.click() -} - -async function handleIconImageChange(changeEvent: Event): Promise { - const selectedFile = selectedInputFile(changeEvent) - if (selectedFile === null || elementId.value === '') { - return - } - - savedMessage.value = null - iconImageStatus.value = null - const uploaded = await elementsStore.uploadElementIconImage(elementId.value, selectedFile) - resetInput(changeEvent) - - if (uploaded) { - iconImageStatus.value = 'Icon image updated' - } -} - -async function handlePdfChange(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) - resetInput(changeEvent) - - if (uploaded) { - pdfStatus.value = 'PDF updated' - } -} - -async function handleRemoveIconImage(): Promise { - if (elementId.value === '') { - return - } - - savedMessage.value = null - iconImageStatus.value = null - const removed = await elementsStore.clearElementIconImage(elementId.value) - if (removed) { - iconImageStatus.value = 'Icon image removed' - } -} - -async function handleRemovePdf(): Promise { - if (elementId.value === '') { - return - } - - savedMessage.value = null - pdfStatus.value = null - const removed = await elementsStore.clearElementPdf(elementId.value) - if (removed) { - pdfStatus.value = 'PDF removed' - } -} - -function selectedInputFile(changeEvent: Event): File | null { - const target = changeEvent.target - if (!(target instanceof HTMLInputElement)) { - return null - } - - if (target.files === null || target.files.length === 0) { - return null - } - - return target.files.item(0) -} - -function resetInput(changeEvent: Event): void { - const target = changeEvent.target - if (target instanceof HTMLInputElement) { - target.value = '' - } -}