diff --git a/ai/backend-context.md b/ai/backend-context.md index 34729b2..dd69678 100644 --- a/ai/backend-context.md +++ b/ai/backend-context.md @@ -20,9 +20,6 @@ intentionally omitted here - update this section as entities land. - Do not write unit tests for concrete repository implementations (e.g. `Postgres*Repository`). They are exercised by e2e tests. Use cases are tested with fake repositories. - - Do not write reflection-only contract tests for repository interface - method names, parameter counts, parameter names, or type hints. PHP - interfaces already enforce those signatures; test behavior instead. - Repository methods that find records by a foreign key should accept the related entity, not a raw id (e.g. `findBySet(Set $set)`, not `findBySetId(int $setId)`). diff --git a/backend/app/Controllers/ElementController.php b/backend/app/Controllers/ElementController.php index 6a63a85..1c346c6 100644 --- a/backend/app/Controllers/ElementController.php +++ b/backend/app/Controllers/ElementController.php @@ -2,22 +2,16 @@ namespace App\Controllers; -use App\Element\Element; use App\Element\UseCases\GetElement\GetElement; use App\Element\UseCases\GetElement\GetElementRequest; -use App\Element\UseCases\UpdateElement\UpdateElement; -use App\Element\UseCases\UpdateElement\UpdateElementRequest; use App\Exceptions\BadRequestException; use App\Exceptions\NotFoundException; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; class ElementController { - public function __construct( - private GetElement $getElement, - private UpdateElement $updateElement, - ) { + public function __construct(private GetElement $getElement) + { } public function show(?int $id): JsonResponse @@ -48,73 +42,15 @@ class ElementController return new JsonResponse([ 'childElements' => $childElements, - 'element' => $this->buildElementPayload($element), + 'element' => [ + 'id' => $element->getId(), + 'title' => $element->getTitle(), + 'description' => $element->getDescription(), + 'iconImageUrl' => $element->getIconImageUrl(), + 'richText' => $element->getRichText(), + 'pdfPath' => $element->getPdfPath(), + 'youtubeUrl' => $element->getYoutubeUrl(), + ], ], 200); } - - public function update(?int $id, Request $request): JsonResponse - { - try { - $element = $this->updateElement->execute( - new UpdateElementRequest( - id: $id, - title: $this->stringInput($request, 'title'), - description: $this->stringInput($request, 'description'), - iconImageUrl: $this->stringInput( - $request, - 'iconImageUrl', - ), - richText: $this->stringInput($request, 'richText'), - pdfPath: $this->stringInput($request, 'pdfPath'), - youtubeUrl: $this->stringInput($request, 'youtubeUrl'), - ) - ); - } catch (BadRequestException $exception) { - return new JsonResponse([ - 'error' => $exception->getMessage(), - ], 400); - } catch (NotFoundException $exception) { - return new JsonResponse([ - 'error' => $exception->getMessage(), - ], 404); - } - - return new JsonResponse([ - 'element' => $this->buildElementPayload($element), - ], 200); - } - - private function stringInput(Request $request, string $key): ?string - { - $value = $request->input($key); - if (! is_string($value)) { - return null; - } - - return $value; - } - - /** - * @return array{ - * id: int, - * title: string, - * description: string, - * iconImageUrl: string|null, - * richText: string, - * pdfPath: string|null, - * youtubeUrl: string|null, - * } - */ - private function buildElementPayload(Element $element): array - { - return [ - 'id' => $element->getId(), - 'title' => $element->getTitle(), - 'description' => $element->getDescription(), - 'iconImageUrl' => $element->getIconImageUrl(), - 'richText' => $element->getRichText(), - 'pdfPath' => $element->getPdfPath(), - 'youtubeUrl' => $element->getYoutubeUrl(), - ]; - } } diff --git a/backend/app/Element/Element.php b/backend/app/Element/Element.php index 37be364..41fec0d 100644 --- a/backend/app/Element/Element.php +++ b/backend/app/Element/Element.php @@ -29,61 +29,31 @@ class Element return $this->title; } - public function setTitle(string $title): void - { - $this->title = $title; - } - public function getDescription(): string { return $this->description; } - public function setDescription(string $description): void - { - $this->description = $description; - } - public function getIconImageUrl(): ?string { return $this->iconImageUrl; } - public function setIconImageUrl(?string $iconImageUrl): void - { - $this->iconImageUrl = $iconImageUrl; - } - public function getRichText(): string { return $this->richText; } - public function setRichText(string $richText): void - { - $this->richText = $richText; - } - public function getPdfPath(): ?string { return $this->pdfPath; } - public function setPdfPath(?string $pdfPath): void - { - $this->pdfPath = $pdfPath; - } - public function getYoutubeUrl(): ?string { return $this->youtubeUrl; } - public function setYoutubeUrl(?string $youtubeUrl): void - { - $this->youtubeUrl = $youtubeUrl; - } - public function getSet(): Set { return $this->set; diff --git a/backend/app/Element/ElementRepository.php b/backend/app/Element/ElementRepository.php index e4fa74e..3e89628 100644 --- a/backend/app/Element/ElementRepository.php +++ b/backend/app/Element/ElementRepository.php @@ -8,8 +8,6 @@ interface ElementRepository { public function create(CreateElementDto $dto): Element; - public function update(Element $element): Element; - public function find(int $id): ?Element; public function findRootBySet(DomainSet $set): ?Element; diff --git a/backend/app/Element/EloquentElementRepository.php b/backend/app/Element/EloquentElementRepository.php index 379abe0..b8c06d7 100644 --- a/backend/app/Element/EloquentElementRepository.php +++ b/backend/app/Element/EloquentElementRepository.php @@ -38,28 +38,6 @@ class EloquentElementRepository implements ElementRepository ); } - public function update(Element $element): Element - { - $model = ElementModel::find($element->getId()); - if ($model === null) { - throw new DomainException( - "Element with id: {$element->getId()} doesnt exist" - ); - } - - $model->set_id = $element->getSet()->getId(); - $model->title = $element->getTitle(); - $model->description = $element->getDescription(); - $model->icon_image_url = $element->getIconImageUrl(); - $model->rich_text = $element->getRichText(); - $model->pdf_path = $element->getPdfPath(); - $model->youtube_url = $element->getYoutubeUrl(); - $model->parent_element_id = $element->getParentElement()?->getId(); - $model->save(); - - return $this->toDomain($model); - } - public function find(int $id): ?Element { $model = ElementModel::find($id); diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateDescription.php b/backend/app/Element/UseCases/UpdateElement/UpdateDescription.php deleted file mode 100644 index 6e277b9..0000000 --- a/backend/app/Element/UseCases/UpdateElement/UpdateDescription.php +++ /dev/null @@ -1,39 +0,0 @@ -id === null) { - throw new BadRequestException('id is required'); - } - - if ($request->description === null) { - throw new BadRequestException('description is required'); - } - - $element = $this->elementRepository->find($request->id); - if ($element === null) { - throw new NotFoundException('Element not found'); - } - - $element->setDescription($request->description); - - return $this->elementRepository->update($element); - } -} diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateDescriptionRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateDescriptionRequest.php deleted file mode 100644 index 882d4ff..0000000 --- a/backend/app/Element/UseCases/UpdateElement/UpdateDescriptionRequest.php +++ /dev/null @@ -1,12 +0,0 @@ -id === null) { - throw new BadRequestException('id is required'); - } - - $hasNoFields = $request->title === null - && $request->description === null - && $request->iconImageUrl === null - && $request->richText === null - && $request->pdfPath === null - && $request->youtubeUrl === null; - if ($hasNoFields) { - throw new BadRequestException('nothing to update'); - } - - if ($request->title !== null) { - $this->updateTitle->execute(new UpdateTitleRequest( - id: $request->id, - title: $request->title, - )); - } - if ($request->description !== null) { - $this->updateDescription->execute(new UpdateDescriptionRequest( - id: $request->id, - description: $request->description, - )); - } - if ($request->iconImageUrl !== null) { - $this->updateIconImageUrl->execute( - new UpdateIconImageUrlRequest( - id: $request->id, - iconImageUrl: $request->iconImageUrl, - ) - ); - } - if ($request->richText !== null) { - $this->updateRichText->execute(new UpdateRichTextRequest( - id: $request->id, - richText: $request->richText, - )); - } - if ($request->pdfPath !== null) { - $this->updatePdfPath->execute(new UpdatePdfPathRequest( - id: $request->id, - pdfPath: $request->pdfPath, - )); - } - if ($request->youtubeUrl !== null) { - $this->updateYoutubeUrl->execute(new UpdateYoutubeUrlRequest( - id: $request->id, - youtubeUrl: $request->youtubeUrl, - )); - } - - $element = $this->elementRepository->find($request->id); - if ($element === null) { - throw new NotFoundException('Element not found'); - } - - return $element; - } -} diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php deleted file mode 100644 index 0b5e766..0000000 --- a/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php +++ /dev/null @@ -1,17 +0,0 @@ -id === null) { - throw new BadRequestException('id is required'); - } - - if ($request->iconImageUrl === null) { - throw new BadRequestException('iconImageUrl is required'); - } - - $element = $this->elementRepository->find($request->id); - if ($element === null) { - throw new NotFoundException('Element not found'); - } - - $element->setIconImageUrl($this->nullableString( - $request->iconImageUrl, - )); - - return $this->elementRepository->update($element); - } - - private function nullableString(string $value): ?string - { - if ($value === '') { - return null; - } - - return $value; - } -} diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateIconImageUrlRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateIconImageUrlRequest.php deleted file mode 100644 index d328af3..0000000 --- a/backend/app/Element/UseCases/UpdateElement/UpdateIconImageUrlRequest.php +++ /dev/null @@ -1,12 +0,0 @@ -id === null) { - throw new BadRequestException('id is required'); - } - - if ($request->pdfPath === null) { - throw new BadRequestException('pdfPath is required'); - } - - $element = $this->elementRepository->find($request->id); - if ($element === null) { - throw new NotFoundException('Element not found'); - } - - $element->setPdfPath($this->nullableString($request->pdfPath)); - - return $this->elementRepository->update($element); - } - - private function nullableString(string $value): ?string - { - if ($value === '') { - return null; - } - - return $value; - } -} diff --git a/backend/app/Element/UseCases/UpdateElement/UpdatePdfPathRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdatePdfPathRequest.php deleted file mode 100644 index c48c47d..0000000 --- a/backend/app/Element/UseCases/UpdateElement/UpdatePdfPathRequest.php +++ /dev/null @@ -1,12 +0,0 @@ -id === null) { - throw new BadRequestException('id is required'); - } - - if ($request->richText === null) { - throw new BadRequestException('richText is required'); - } - - $element = $this->elementRepository->find($request->id); - if ($element === null) { - throw new NotFoundException('Element not found'); - } - - $element->setRichText($request->richText); - - return $this->elementRepository->update($element); - } -} diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateRichTextRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateRichTextRequest.php deleted file mode 100644 index fe29972..0000000 --- a/backend/app/Element/UseCases/UpdateElement/UpdateRichTextRequest.php +++ /dev/null @@ -1,12 +0,0 @@ -id === null) { - throw new BadRequestException('id is required'); - } - - if ($request->title === null || trim($request->title) === '') { - throw new BadRequestException('title is required'); - } - - $element = $this->elementRepository->find($request->id); - if ($element === null) { - throw new NotFoundException('Element not found'); - } - - $element->setTitle(trim($request->title)); - - return $this->elementRepository->update($element); - } -} diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateTitleRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateTitleRequest.php deleted file mode 100644 index 5a13ea9..0000000 --- a/backend/app/Element/UseCases/UpdateElement/UpdateTitleRequest.php +++ /dev/null @@ -1,12 +0,0 @@ -id === null) { - throw new BadRequestException('id is required'); - } - - if ($request->youtubeUrl === null) { - throw new BadRequestException('youtubeUrl is required'); - } - - $element = $this->elementRepository->find($request->id); - if ($element === null) { - throw new NotFoundException('Element not found'); - } - - $element->setYoutubeUrl($this->nullableString( - $request->youtubeUrl, - )); - - return $this->elementRepository->update($element); - } - - private function nullableString(string $value): ?string - { - if ($value === '') { - return null; - } - - return $value; - } -} diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateYoutubeUrlRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateYoutubeUrlRequest.php deleted file mode 100644 index 3a7e06a..0000000 --- a/backend/app/Element/UseCases/UpdateElement/UpdateYoutubeUrlRequest.php +++ /dev/null @@ -1,12 +0,0 @@ - ['api/*'], - 'allowed_methods' => ['GET', 'POST', 'PATCH', 'OPTIONS'], + 'allowed_methods' => ['GET', 'POST', 'OPTIONS'], 'allowed_origins' => $allowedOrigins, 'allowed_origins_patterns' => [], 'allowed_headers' => [ diff --git a/backend/routes/api.php b/backend/routes/api.php index 9d253d0..f32b2ca 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -12,5 +12,3 @@ Route::get('/me', [AuthController::class, 'me']) ->middleware(AuthMiddleware::class); Route::get('/sets', [SetController::class, 'index']); Route::get('/elements/{id}', [ElementController::class, 'show']); -Route::patch('/elements/{id}', [ElementController::class, 'update']) - ->middleware(AuthMiddleware::class); diff --git a/backend/tests/Fakes/FakeElementRepository.php b/backend/tests/Fakes/FakeElementRepository.php index 9c08e1c..132a526 100644 --- a/backend/tests/Fakes/FakeElementRepository.php +++ b/backend/tests/Fakes/FakeElementRepository.php @@ -33,14 +33,6 @@ class FakeElementRepository implements ElementRepository return $element; } - public function update(Element $element): Element - { - $updatedElement = $this->cloneElement($element); - $this->elementsById[$updatedElement->getId()] = $updatedElement; - - return $this->cloneElement($updatedElement); - } - public function find(int $id): ?Element { if (! isset($this->elementsById[$id])) { diff --git a/backend/tests/Feature/CorsTest.php b/backend/tests/Feature/CorsTest.php index 52273e4..b5ab81d 100644 --- a/backend/tests/Feature/CorsTest.php +++ b/backend/tests/Feature/CorsTest.php @@ -21,23 +21,4 @@ class CorsTest extends TestCase ); $response->assertHeader('Access-Control-Allow-Credentials', 'true'); } - - public function testAllowsPatchPreflight(): void - { - $response = $this->withHeaders([ - 'Origin' => 'http://localhost:5173', - 'Access-Control-Request-Method' => 'PATCH', - 'Access-Control-Request-Headers' => 'content-type', - ])->options('/api/elements/1'); - - $response->assertNoContent(); - $response->assertHeader( - 'Access-Control-Allow-Origin', - 'http://localhost:5173' - ); - $this->assertStringContainsString( - 'PATCH', - $response->headers->get('Access-Control-Allow-Methods') - ); - } } diff --git a/backend/tests/Feature/ElementsEndpointTest.php b/backend/tests/Feature/ElementsEndpointTest.php index 2ff9089..33725c8 100644 --- a/backend/tests/Feature/ElementsEndpointTest.php +++ b/backend/tests/Feature/ElementsEndpointTest.php @@ -2,17 +2,10 @@ namespace Tests\Feature; -use App\Auth\CreateSessionDto; -use App\Auth\SessionRepository; use App\Element\CreateElementDto; use App\Element\ElementRepository; use App\Set\CreateSetDto; use App\Set\SetRepository; -use App\Shared\ValueObject\EmailAddress; -use App\User\CreateUserDto; -use App\User\UserRepository; -use DateTimeImmutable; -use DateTimeZone; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -99,116 +92,4 @@ class ElementsEndpointTest extends TestCase 'error' => 'Element not found', ]); } - - public function testUpdateRequiresAuthentication(): 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: '/assets/baderech-haavodah-icon.png', - richText: '
A structured path for growth
', - pdfPath: '/assets/pdfs/baderech.pdf', - youtubeUrl: null, - parentElement: null, - )); - - $response = $this->patchJson("/api/elements/{$element->getId()}", [ - 'title' => 'Updated title', - 'description' => 'Updated description', - 'iconImageUrl' => '/assets/updated-icon.png', - 'richText' => 'Updated rich text
', - 'pdfPath' => '/assets/pdfs/updated.pdf', - 'youtubeUrl' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', - ]); - - $response->assertUnauthorized(); - $response->assertExactJson([ - 'error' => 'unauthenticated', - ]); - } - - public function testAuthenticatedUpdateReturnsElementPayload(): void - { - $sampleYoutubeUrl = 'https://www.youtube.com/watch?v=' - . '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, - )); - $this->createSession('valid-token'); - - $response = $this->withCredentials() - ->withUnencryptedCookie('auth_token', 'valid-token') - ->patchJson("/api/elements/{$element->getId()}", [ - 'title' => 'Updated title', - 'description' => 'Updated description', - 'iconImageUrl' => '/assets/updated-icon.png', - 'richText' => 'Updated rich text
', - 'pdfPath' => '/assets/pdfs/updated.pdf', - 'youtubeUrl' => $sampleYoutubeUrl, - ]); - - $response->assertOk(); - $response->assertExactJson([ - 'element' => [ - 'id' => $element->getId(), - 'title' => 'Updated title', - 'description' => 'Updated description', - 'iconImageUrl' => '/assets/updated-icon.png', - 'richText' => 'Updated rich text
', - 'pdfPath' => '/assets/pdfs/updated.pdf', - 'youtubeUrl' => $sampleYoutubeUrl, - ], - ]); - $updatedElement = $elementRepository->find($element->getId()); - $this->assertSame('Updated title', $updatedElement->getTitle()); - $this->assertSame( - 'Updated rich text
', - $updatedElement->getRichText(), - ); - } - - private function createSession(string $token): void - { - $userRepository = app(UserRepository::class); - $sessionRepository = app(SessionRepository::class); - $user = $userRepository->create(new CreateUserDto( - email: new EmailAddress('admin@example.com'), - passwordHash: 'password-hash', - )); - $now = new DateTimeImmutable( - '2026-05-01T00:00:00', - new DateTimeZone('UTC'), - ); - $sessionRepository->create(new CreateSessionDto( - token: $token, - user: $user, - createdAt: $now, - expiresAt: new DateTimeImmutable( - '2099-01-01T00:00:00', - new DateTimeZone('UTC'), - ), - )); - } } diff --git a/backend/tests/Unit/Controllers/ElementControllerTest.php b/backend/tests/Unit/Controllers/ElementControllerTest.php index 99bfa3e..d06c0f7 100644 --- a/backend/tests/Unit/Controllers/ElementControllerTest.php +++ b/backend/tests/Unit/Controllers/ElementControllerTest.php @@ -6,13 +6,6 @@ use App\Controllers\ElementController; use App\Element\CreateElementDto; 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\UpdateIconImageUrl; -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 Tests\Fakes\FakeElementRepository; use Tests\TestCase; @@ -27,16 +20,7 @@ class ElementControllerTest extends TestCase { $this->elementRepo = new FakeElementRepository(); $getElement = new GetElement($this->elementRepo); - $updateElement = new UpdateElement( - new UpdateTitle($this->elementRepo), - new UpdateDescription($this->elementRepo), - new UpdateIconImageUrl($this->elementRepo), - new UpdateRichText($this->elementRepo), - new UpdatePdfPath($this->elementRepo), - new UpdateYoutubeUrl($this->elementRepo), - $this->elementRepo, - ); - $this->controller = new ElementController($getElement, $updateElement); + $this->controller = new ElementController($getElement); } public function testShowReturnsElementPayload(): void diff --git a/backend/tests/Unit/Element/ElementTest.php b/backend/tests/Unit/Element/ElementTest.php index 89e9c84..16c08a7 100644 --- a/backend/tests/Unit/Element/ElementTest.php +++ b/backend/tests/Unit/Element/ElementTest.php @@ -68,39 +68,4 @@ class ElementTest extends TestCase $this->assertNull($rootElement->getYoutubeUrl()); $this->assertNull($rootElement->getParentElement()); } - - public function testUpdatesEditableFields(): void - { - $set = new DomainSet( - id: 1, - name: 'Daily learning', - description: 'Daily learning description', - iconImageUrl: '/assets/daily-learning-icon.svg', - ); - $element = new Element( - id: 1, - title: 'Original', - description: 'Original description', - iconImageUrl: '/assets/original.svg', - richText: 'Original
', - pdfPath: '/assets/pdfs/original.pdf', - youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU', - set: $set, - parentElement: null, - ); - - $element->setTitle('Updated'); - $element->setDescription('Updated description'); - $element->setIconImageUrl(null); - $element->setRichText('Updated
'); - $element->setPdfPath(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->getYoutubeUrl()); - } } diff --git a/backend/tests/Unit/Element/UseCases/UpdateElementFieldsTest.php b/backend/tests/Unit/Element/UseCases/UpdateElementFieldsTest.php deleted file mode 100644 index 7c27c9c..0000000 --- a/backend/tests/Unit/Element/UseCases/UpdateElementFieldsTest.php +++ /dev/null @@ -1,175 +0,0 @@ -elementRepo = new FakeElementRepository(); - } - - public function testUpdateTitleUpdatesOnlyTitle(): void - { - $element = $this->createFilledElement(); - $updateTitle = new UpdateTitle($this->elementRepo); - - $updatedElement = $updateTitle->execute(new UpdateTitleRequest( - id: $element->getId(), - title: 'Updated title', - )); - - $this->assertSame('Updated title', $updatedElement->getTitle()); - $this->assertSame( - $element->getDescription(), - $updatedElement->getDescription(), - ); - $this->assertSame( - $element->getRichText(), - $updatedElement->getRichText(), - ); - } - - public function testUpdateTitleRejectsBlankTitle(): void - { - $this->createFilledElement(); - $updateTitle = new UpdateTitle($this->elementRepo); - - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage('title is required'); - - $updateTitle->execute(new UpdateTitleRequest(id: 1, title: '')); - } - - public function testUpdateDescriptionUpdatesOnlyDescription(): void - { - $element = $this->createFilledElement(); - $updateDescription = new UpdateDescription($this->elementRepo); - - $updatedElement = $updateDescription->execute( - new UpdateDescriptionRequest( - id: $element->getId(), - description: 'Updated description', - ) - ); - - $this->assertSame( - 'Updated description', - $updatedElement->getDescription(), - ); - $this->assertSame($element->getTitle(), $updatedElement->getTitle()); - $this->assertSame( - $element->getYoutubeUrl(), - $updatedElement->getYoutubeUrl(), - ); - } - - public function testUpdateIconImageUrlClearsEmptyString(): void - { - $element = $this->createFilledElement(); - $updateIconImageUrl = new UpdateIconImageUrl($this->elementRepo); - - $updatedElement = $updateIconImageUrl->execute( - new UpdateIconImageUrlRequest( - id: $element->getId(), - iconImageUrl: '', - ) - ); - - $this->assertNull($updatedElement->getIconImageUrl()); - $this->assertSame($element->getTitle(), $updatedElement->getTitle()); - } - - public function testUpdateRichTextUpdatesOnlyRichText(): void - { - $element = $this->createFilledElement(); - $updateRichText = new UpdateRichText($this->elementRepo); - - $updatedElement = $updateRichText->execute( - new UpdateRichTextRequest( - id: $element->getId(), - richText: 'Updated rich text
', - ) - ); - - $this->assertSame( - 'Updated rich text
', - $updatedElement->getRichText(), - ); - $this->assertSame( - $element->getDescription(), - $updatedElement->getDescription(), - ); - } - - public function testUpdatePdfPathClearsEmptyString(): void - { - $element = $this->createFilledElement(); - $updatePdfPath = new UpdatePdfPath($this->elementRepo); - - $updatedElement = $updatePdfPath->execute( - new UpdatePdfPathRequest(id: $element->getId(), pdfPath: '') - ); - - $this->assertNull($updatedElement->getPdfPath()); - $this->assertSame($element->getTitle(), $updatedElement->getTitle()); - } - - public function testUpdateYoutubeUrlClearsEmptyString(): void - { - $element = $this->createFilledElement(); - $updateYoutubeUrl = new UpdateYoutubeUrl($this->elementRepo); - - $updatedElement = $updateYoutubeUrl->execute( - new UpdateYoutubeUrlRequest( - id: $element->getId(), - youtubeUrl: '', - ) - ); - - $this->assertNull($updatedElement->getYoutubeUrl()); - $this->assertSame($element->getTitle(), $updatedElement->getTitle()); - } - - private function createFilledElement(): Element - { - $set = new DomainSet( - id: 1, - name: 'Baderech', - description: 'Baderech description', - iconImageUrl: '/assets/baderech-icon.png', - ); - - return $this->elementRepo->create(new CreateElementDto( - set: $set, - title: 'Original title', - description: 'Original description', - iconImageUrl: '/assets/original-icon.png', - richText: 'Original rich text
', - 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 deleted file mode 100644 index 0ce0d88..0000000 --- a/backend/tests/Unit/Element/UseCases/UpdateElementTest.php +++ /dev/null @@ -1,270 +0,0 @@ -elementRepo = new FakeElementRepository(); - $this->updateElement = new UpdateElement( - new UpdateTitle($this->elementRepo), - new UpdateDescription($this->elementRepo), - new UpdateIconImageUrl($this->elementRepo), - new UpdateRichText($this->elementRepo), - new UpdatePdfPath($this->elementRepo), - new UpdateYoutubeUrl($this->elementRepo), - $this->elementRepo, - ); - } - - public function testUpdatesContentFields(): void - { - $set = $this->createSet(1, 'Baderech'); - $element = $this->createElement( - $set, - 'Original title', - 'Original description', - '/assets/original-icon.png', - 'Original rich text
', - '/assets/pdfs/original.pdf', - 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', - null, - ); - - $updatedElement = $this->updateElement->execute( - new UpdateElementRequest( - id: $element->getId(), - title: 'Updated title', - description: 'Updated description', - iconImageUrl: '/assets/updated-icon.png', - richText: 'Updated rich text
', - pdfPath: '/assets/pdfs/updated.pdf', - youtubeUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', - ) - ); - - $this->assertSame($element->getId(), $updatedElement->getId()); - $this->assertSame('Updated title', $updatedElement->getTitle()); - $this->assertSame( - 'Updated description', - $updatedElement->getDescription(), - ); - $this->assertSame( - '/assets/updated-icon.png', - $updatedElement->getIconImageUrl(), - ); - $this->assertSame( - 'Updated rich text
', - $updatedElement->getRichText(), - ); - $this->assertSame( - '/assets/pdfs/updated.pdf', - $updatedElement->getPdfPath(), - ); - $this->assertSame( - 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', - $updatedElement->getYoutubeUrl(), - ); - - $persistedElement = $this->elementRepo->find($element->getId()); - $this->assertInstanceOf(Element::class, $persistedElement); - $this->assertSame('Updated title', $persistedElement->getTitle()); - } - - public function testConvertsEmptyNullableFieldsToNull(): void - { - $set = $this->createSet(1, 'Baderech'); - $element = $this->createElement( - $set, - 'Original title', - 'Original description', - '/assets/original-icon.png', - 'Original rich text
', - '/assets/pdfs/original.pdf', - 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', - null, - ); - - $updatedElement = $this->updateElement->execute( - new UpdateElementRequest( - id: $element->getId(), - title: 'Updated title', - description: 'Updated description', - iconImageUrl: '', - richText: 'Updated rich text
', - pdfPath: '', - youtubeUrl: '', - ) - ); - - $this->assertNull($updatedElement->getIconImageUrl()); - $this->assertNull($updatedElement->getPdfPath()); - $this->assertNull($updatedElement->getYoutubeUrl()); - } - - public function testUpdatesOnlySuppliedFields(): void - { - $set = $this->createSet(1, 'Baderech'); - $element = $this->createElement( - $set, - 'Original title', - 'Original description', - '/assets/original-icon.png', - 'Original rich text
', - '/assets/pdfs/original.pdf', - 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', - null, - ); - - $updatedElement = $this->updateElement->execute( - new UpdateElementRequest( - id: $element->getId(), - title: 'Updated title', - description: null, - iconImageUrl: null, - richText: null, - pdfPath: null, - youtubeUrl: null, - ) - ); - - $this->assertSame('Updated title', $updatedElement->getTitle()); - $this->assertSame( - 'Original description', - $updatedElement->getDescription(), - ); - $this->assertSame( - '/assets/original-icon.png', - $updatedElement->getIconImageUrl(), - ); - $this->assertSame( - 'Original rich text
', - $updatedElement->getRichText(), - ); - $this->assertSame( - '/assets/pdfs/original.pdf', - $updatedElement->getPdfPath(), - ); - $this->assertSame( - 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', - $updatedElement->getYoutubeUrl(), - ); - } - - public function testThrowsWhenIdMissing(): void - { - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage('id is required'); - - $this->updateElement->execute(new UpdateElementRequest( - id: null, - title: 'Updated title', - description: 'Updated description', - iconImageUrl: null, - richText: 'Updated rich text
', - pdfPath: null, - youtubeUrl: null, - )); - } - - public function testThrowsWhenNothingProvided(): void - { - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage('nothing to update'); - - $this->updateElement->execute(new UpdateElementRequest( - id: 1, - title: null, - description: null, - iconImageUrl: null, - richText: null, - pdfPath: null, - youtubeUrl: null, - )); - } - - public function testThrowsWhenTitleIsBlank(): void - { - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage('title is required'); - - $this->updateElement->execute(new UpdateElementRequest( - id: 1, - title: '', - description: 'Updated description', - iconImageUrl: null, - richText: 'Updated rich text
', - pdfPath: null, - youtubeUrl: null, - )); - } - - public function testThrowsWhenElementDoesNotExist(): void - { - $this->expectException(NotFoundException::class); - $this->expectExceptionMessage('Element not found'); - - $this->updateElement->execute(new UpdateElementRequest( - id: 999, - title: 'Updated title', - description: 'Updated description', - iconImageUrl: null, - richText: 'Updated rich text
', - pdfPath: null, - youtubeUrl: null, - )); - } - - private function createSet(int $id, string $name): DomainSet - { - return new DomainSet( - id: $id, - name: $name, - description: "$name description", - iconImageUrl: '/assets/baderech-icon.png', - ); - } - - private function createElement( - DomainSet $set, - string $title, - string $description, - ?string $iconImageUrl, - string $richText, - ?string $pdfPath, - ?string $youtubeUrl, - ?Element $parentElement, - ): Element { - return $this->elementRepo->create(new CreateElementDto( - set: $set, - title: $title, - description: $description, - iconImageUrl: $iconImageUrl, - richText: $richText, - pdfPath: $pdfPath, - youtubeUrl: $youtubeUrl, - parentElement: $parentElement, - )); - } -} diff --git a/frontend/rabbi_gerzi/cypress.config.mjs b/frontend/rabbi_gerzi/cypress.config.mjs new file mode 100644 index 0000000..d215a17 --- /dev/null +++ b/frontend/rabbi_gerzi/cypress.config.mjs @@ -0,0 +1,16 @@ +import { defineConfig } from 'cypress' +import createBundler from '@bahmutov/cypress-esbuild-preprocessor' + +export default defineConfig({ + e2e: { + baseUrl: 'http://localhost:5173', + specPattern: 'cypress/e2e/**/*.cy.ts', + supportFile: 'cypress/support/e2e.ts', + fixturesFolder: false, + video: false, + screenshotOnRunFailure: false, + setupNodeEvents(onEvent) { + onEvent('file:preprocessor', createBundler()) + }, + }, +}) diff --git a/frontend/rabbi_gerzi/cypress.config.ts b/frontend/rabbi_gerzi/cypress.config.ts deleted file mode 100644 index 82a9470..0000000 --- a/frontend/rabbi_gerzi/cypress.config.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { execSync } from 'node:child_process' -import { dirname, resolve } from 'node:path' -import { fileURLToPath } from 'node:url' -import { defineConfig } from 'cypress' -import createBundler from '@bahmutov/cypress-esbuild-preprocessor' -import { Client } from 'pg' - -const configDirectory = dirname(fileURLToPath(import.meta.url)) -const backendDirectory = resolve(configDirectory, '../../backend') -const socketDirectory = resolve(configDirectory, '../../.postgres') -const appDatabase = 'postgres' -const templateDatabase = 'postgres_cypress_template' - -function controlClient(): Client { - return new Client({ - host: socketDirectory, - database: 'template1', - user: 'postgres', - }) -} - -async function buildTemplate(): PromiseUpdated admin rich text
' - - loginAsAdmin() - cy.visit('/admin/element/1') - - fillElementForm( - updatedTitle, - updatedDescription, - originalIconImageUrl, - updatedRichText, - '', - '', - ) - cy.get('[data-cy="admin-element-save"]').click() - - cy.get('[data-cy="admin-element-status"]') - .should('be.visible') - .and('contain.text', 'Saved') - cy.contains('header.site-header a', 'View Element').click() - cy.location('pathname').should('eq', '/element/1') - cy.contains('h1', updatedTitle).should('be.visible') - cy.get('[data-cy="element-rich-text"]') - .should('contain.text', 'Updated admin rich text') - - cy.visit('/admin/element/1') - fillElementForm( - originalTitle, - originalDescription, - originalIconImageUrl, - '', - '', - '', - ) - cy.get('[data-cy="admin-element-save"]').click() - cy.get('[data-cy="admin-element-status"]') - .should('be.visible') - .and('contain.text', 'Saved') - }) -}) diff --git a/frontend/rabbi_gerzi/cypress/e2e/database-reset.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/database-reset.cy.ts deleted file mode 100644 index dd8ec44..0000000 --- a/frontend/rabbi_gerzi/cypress/e2e/database-reset.cy.ts +++ /dev/null @@ -1,41 +0,0 @@ -function loginAsAdmin(): void { - cy.visit('/login') - - cy.get('[data-cy="login-email"]').type('admin@rabbigerzi.test') - cy.get('[data-cy="login-password"]').type('password123!@#') - cy.get('[data-cy="login-submit"]').click() - - cy.url().should('eq', Cypress.config().baseUrl + '/') - cy.getCookie('auth_token').should('exist') -} - -describe('cypress database reset', () => { - it('can dirty element seed data', () => { - loginAsAdmin() - - cy.visit('/admin/element/1') - cy.get('[data-cy="admin-element-title"]') - .clear() - .type('Dirty Cypress Element') - cy.get('[data-cy="admin-element-description"]') - .clear() - .type('Dirty Cypress description') - cy.get('[data-cy="admin-element-rich-text"]') - .clear() - .type('Dirty Cypress rich text
', { - parseSpecialCharSequences: false, - }) - cy.get('[data-cy="admin-element-save"]').click() - - cy.get('[data-cy="admin-element-status"]') - .should('be.visible') - .and('contain.text', 'Saved') - }) - - it('starts the next test from seeded element data', () => { - cy.visit('/element/1') - - cy.contains('h1', 'Baderech HaAvodah').should('be.visible') - cy.get('[data-cy="element-rich-text"]').should('not.exist') - }) -}) diff --git a/frontend/rabbi_gerzi/cypress/support/commands.ts b/frontend/rabbi_gerzi/cypress/support/commands.ts deleted file mode 100644 index 65696c1..0000000 --- a/frontend/rabbi_gerzi/cypress/support/commands.ts +++ /dev/null @@ -1,16 +0,0 @@ -///