From 6d0acaae56aa5fd332a4b441fdf6fb46fd10b940 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Thu, 28 May 2026 20:05:19 +0300 Subject: [PATCH 01/13] test element updates --- .../tests/Feature/ElementsEndpointTest.php | 118 +++++++++++ .../Element/UseCases/UpdateElementTest.php | 191 ++++++++++++++++++ 2 files changed, 309 insertions(+) create mode 100644 backend/tests/Unit/Element/UseCases/UpdateElementTest.php diff --git a/backend/tests/Feature/ElementsEndpointTest.php b/backend/tests/Feature/ElementsEndpointTest.php index 33725c8..251fddf 100644 --- a/backend/tests/Feature/ElementsEndpointTest.php +++ b/backend/tests/Feature/ElementsEndpointTest.php @@ -2,10 +2,17 @@ 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; @@ -92,4 +99,115 @@ 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->withCookie('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/Element/UseCases/UpdateElementTest.php b/backend/tests/Unit/Element/UseCases/UpdateElementTest.php new file mode 100644 index 0000000..0bb1195 --- /dev/null +++ b/backend/tests/Unit/Element/UseCases/UpdateElementTest.php @@ -0,0 +1,191 @@ +elementRepo = new FakeElementRepository(); + $this->updateElement = new UpdateElement($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 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 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, + )); + } +} From 73b0befc97dad304ed98bc068ff40a3bbfa8d05c Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Thu, 28 May 2026 20:08:36 +0300 Subject: [PATCH 02/13] add element updates --- backend/app/Controllers/ElementController.php | 86 ++++++++++++++++--- backend/app/Element/ElementRepository.php | 2 + .../app/Element/EloquentElementRepository.php | 30 +++++++ backend/app/Element/UpdateElementDto.php | 16 ++++ .../UseCases/UpdateElement/UpdateElement.php | 57 ++++++++++++ .../UpdateElement/UpdateElementRequest.php | 17 ++++ backend/routes/api.php | 2 + backend/tests/Fakes/FakeElementRepository.php | 19 ++++ .../tests/Feature/ElementsEndpointTest.php | 3 +- .../Controllers/ElementControllerTest.php | 4 +- 10 files changed, 223 insertions(+), 13 deletions(-) create mode 100644 backend/app/Element/UpdateElementDto.php create mode 100644 backend/app/Element/UseCases/UpdateElement/UpdateElement.php create mode 100644 backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php diff --git a/backend/app/Controllers/ElementController.php b/backend/app/Controllers/ElementController.php index 1c346c6..6a63a85 100644 --- a/backend/app/Controllers/ElementController.php +++ b/backend/app/Controllers/ElementController.php @@ -2,16 +2,22 @@ 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) - { + public function __construct( + private GetElement $getElement, + private UpdateElement $updateElement, + ) { } public function show(?int $id): JsonResponse @@ -42,15 +48,73 @@ class ElementController return new JsonResponse([ 'childElements' => $childElements, - 'element' => [ - 'id' => $element->getId(), - 'title' => $element->getTitle(), - 'description' => $element->getDescription(), - 'iconImageUrl' => $element->getIconImageUrl(), - 'richText' => $element->getRichText(), - 'pdfPath' => $element->getPdfPath(), - 'youtubeUrl' => $element->getYoutubeUrl(), - ], + 'element' => $this->buildElementPayload($element), ], 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/ElementRepository.php b/backend/app/Element/ElementRepository.php index 3e89628..b733611 100644 --- a/backend/app/Element/ElementRepository.php +++ b/backend/app/Element/ElementRepository.php @@ -8,6 +8,8 @@ interface ElementRepository { public function create(CreateElementDto $dto): Element; + public function update(Element $element, UpdateElementDto $dto): 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 b8c06d7..65dc061 100644 --- a/backend/app/Element/EloquentElementRepository.php +++ b/backend/app/Element/EloquentElementRepository.php @@ -38,6 +38,36 @@ class EloquentElementRepository implements ElementRepository ); } + public function update(Element $element, UpdateElementDto $dto): Element + { + $model = ElementModel::find($element->getId()); + if ($model === null) { + throw new DomainException( + "Element with id: {$element->getId()} doesnt exist" + ); + } + + $model->title = $dto->title; + $model->description = $dto->description; + $model->icon_image_url = $dto->iconImageUrl; + $model->rich_text = $dto->richText; + $model->pdf_path = $dto->pdfPath; + $model->youtube_url = $dto->youtubeUrl; + $model->save(); + + return new Element( + id: $element->getId(), + title: $dto->title, + description: $dto->description, + iconImageUrl: $dto->iconImageUrl, + richText: $dto->richText, + pdfPath: $dto->pdfPath, + youtubeUrl: $dto->youtubeUrl, + set: $element->getSet(), + parentElement: $element->getParentElement(), + ); + } + public function find(int $id): ?Element { $model = ElementModel::find($id); diff --git a/backend/app/Element/UpdateElementDto.php b/backend/app/Element/UpdateElementDto.php new file mode 100644 index 0000000..5c4b0b7 --- /dev/null +++ b/backend/app/Element/UpdateElementDto.php @@ -0,0 +1,16 @@ +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'); + } + + return $this->elementRepository->update( + $element, + new UpdateElementDto( + title: $request->title, + description: $request->description ?? '', + iconImageUrl: $this->nullableString($request->iconImageUrl), + richText: $request->richText ?? '', + pdfPath: $this->nullableString($request->pdfPath), + youtubeUrl: $this->nullableString($request->youtubeUrl), + ), + ); + } + + private function nullableString(?string $value): ?string + { + if ($value === null || $value === '') { + return null; + } + + return $value; + } +} diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php new file mode 100644 index 0000000..0b5e766 --- /dev/null +++ b/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php @@ -0,0 +1,17 @@ +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 132a526..037fae7 100644 --- a/backend/tests/Fakes/FakeElementRepository.php +++ b/backend/tests/Fakes/FakeElementRepository.php @@ -5,6 +5,7 @@ namespace Tests\Fakes; use App\Element\CreateElementDto; use App\Element\Element; use App\Element\ElementRepository; +use App\Element\UpdateElementDto; use App\Set\Set as DomainSet; class FakeElementRepository implements ElementRepository @@ -33,6 +34,24 @@ class FakeElementRepository implements ElementRepository return $element; } + public function update(Element $element, UpdateElementDto $dto): Element + { + $updatedElement = new Element( + id: $element->getId(), + title: $dto->title, + description: $dto->description, + iconImageUrl: $dto->iconImageUrl, + richText: $dto->richText, + pdfPath: $dto->pdfPath, + youtubeUrl: $dto->youtubeUrl, + set: $element->getSet(), + parentElement: $element->getParentElement(), + ); + $this->elementsById[$element->getId()] = $updatedElement; + + return $this->cloneElement($updatedElement); + } + public function find(int $id): ?Element { if (! isset($this->elementsById[$id])) { diff --git a/backend/tests/Feature/ElementsEndpointTest.php b/backend/tests/Feature/ElementsEndpointTest.php index 251fddf..2ff9089 100644 --- a/backend/tests/Feature/ElementsEndpointTest.php +++ b/backend/tests/Feature/ElementsEndpointTest.php @@ -158,7 +158,8 @@ class ElementsEndpointTest extends TestCase )); $this->createSession('valid-token'); - $response = $this->withCookie('auth_token', 'valid-token') + $response = $this->withCredentials() + ->withUnencryptedCookie('auth_token', 'valid-token') ->patchJson("/api/elements/{$element->getId()}", [ 'title' => 'Updated title', 'description' => 'Updated description', diff --git a/backend/tests/Unit/Controllers/ElementControllerTest.php b/backend/tests/Unit/Controllers/ElementControllerTest.php index d06c0f7..64fcfb5 100644 --- a/backend/tests/Unit/Controllers/ElementControllerTest.php +++ b/backend/tests/Unit/Controllers/ElementControllerTest.php @@ -6,6 +6,7 @@ use App\Controllers\ElementController; use App\Element\CreateElementDto; use App\Element\Element; use App\Element\UseCases\GetElement\GetElement; +use App\Element\UseCases\UpdateElement\UpdateElement; use App\Set\Set as DomainSet; use Tests\Fakes\FakeElementRepository; use Tests\TestCase; @@ -20,7 +21,8 @@ class ElementControllerTest extends TestCase { $this->elementRepo = new FakeElementRepository(); $getElement = new GetElement($this->elementRepo); - $this->controller = new ElementController($getElement); + $updateElement = new UpdateElement($this->elementRepo); + $this->controller = new ElementController($getElement, $updateElement); } public function testShowReturnsElementPayload(): void From 527cf2898dd3de84595dcb8894021a0bb72eb96c Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Thu, 28 May 2026 20:10:07 +0300 Subject: [PATCH 03/13] test admin element editing --- .../cypress/e2e/admin-element.cy.ts | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts diff --git a/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts new file mode 100644 index 0000000..8678ec2 --- /dev/null +++ b/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts @@ -0,0 +1,123 @@ +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') +} + +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) + } +} + +describe('admin element editing', () => { + it('does not show the edit link to logged-out users', () => { + cy.visit('/element/1') + + cy.contains('header.site-header a', 'Edit Element').should('not.exist') + }) + + it('shows the edit link on public element pages to logged-in users', () => { + loginAsAdmin() + + cy.visit('/element/1') + + cy.contains('header.site-header a', 'Edit Element') + .should('be.visible') + .and('have.attr', 'href', '/admin/element/1') + .click() + cy.location('pathname').should('eq', '/admin/element/1') + }) + + it('redirects logged-out admin element visits to login', () => { + cy.visit('/admin/element/1') + + cy.location('pathname').should('eq', '/login') + }) + + it('shows the view link on admin element pages', () => { + loginAsAdmin() + + cy.visit('/admin/element/1') + + cy.contains('header.site-header a', 'View Element') + .should('be.visible') + .and('have.attr', 'href', '/element/1') + cy.contains('header.site-header a', 'Edit Element').should('not.exist') + cy.get('[data-cy="admin-element-title"]') + .should('have.value', 'Baderech HaAvodah') + }) + + 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

' + + 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') + }) +}) From 21a4a94cf4c71a76c787469fd416825d70c5a07b Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Thu, 28 May 2026 20:11:09 +0300 Subject: [PATCH 04/13] test patch cors --- backend/tests/Feature/CorsTest.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/backend/tests/Feature/CorsTest.php b/backend/tests/Feature/CorsTest.php index b5ab81d..52273e4 100644 --- a/backend/tests/Feature/CorsTest.php +++ b/backend/tests/Feature/CorsTest.php @@ -21,4 +21,23 @@ 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') + ); + } } From eab26824bb584e4a6776cb5f687cf0d30b122000 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Thu, 28 May 2026 20:11:35 +0300 Subject: [PATCH 05/13] allow patch cors --- backend/config/cors.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/config/cors.php b/backend/config/cors.php index 4414a95..0f3088a 100644 --- a/backend/config/cors.php +++ b/backend/config/cors.php @@ -13,7 +13,7 @@ $allowedOrigins = array_values(array_filter(array_map( return [ 'paths' => ['api/*'], - 'allowed_methods' => ['GET', 'POST', 'OPTIONS'], + 'allowed_methods' => ['GET', 'POST', 'PATCH', 'OPTIONS'], 'allowed_origins' => $allowedOrigins, 'allowed_origins_patterns' => [], 'allowed_headers' => [ From 240ab4e7717b621b39c51497a8c1b8431d3fcd48 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Thu, 28 May 2026 20:17:17 +0300 Subject: [PATCH 06/13] add admin element editing --- .../rabbi_gerzi/src/components/SiteHeader.vue | 54 ++- frontend/rabbi_gerzi/src/router/index.ts | 24 ++ frontend/rabbi_gerzi/src/stores/elements.ts | 80 ++++- .../src/views/AdminElementPage.vue | 329 ++++++++++++++++++ 4 files changed, 484 insertions(+), 3 deletions(-) create mode 100644 frontend/rabbi_gerzi/src/views/AdminElementPage.vue diff --git a/frontend/rabbi_gerzi/src/components/SiteHeader.vue b/frontend/rabbi_gerzi/src/components/SiteHeader.vue index 65497a5..0ecdc90 100644 --- a/frontend/rabbi_gerzi/src/components/SiteHeader.vue +++ b/frontend/rabbi_gerzi/src/components/SiteHeader.vue @@ -1,9 +1,15 @@