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

A structured path for growth

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

Foundations rich text

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

Daily practice rich text

', null, null, null, $element, ); $response = $this->getJson("/api/elements/{$element->getId()}"); $response->assertOk(); $response->assertExactJson([ 'childElements' => [ [ 'id' => $firstChildElement->getId(), 'title' => 'Avodah Foundations', 'description' => 'Foundations for steady avodah', ], [ 'id' => $secondChildElement->getId(), 'title' => 'Daily Practice', 'description' => 'Daily practices for growth', ], ], 'element' => [ 'id' => $element->getId(), 'title' => 'Baderech HaAvodah', 'description' => 'A structured path for growth', 'iconImageUrl' => '/assets/baderech-haavodah-icon.png', 'richText' => '

A structured path for growth

', 'shortPdfPath' => '/assets/pdfs/baderech.pdf', 'longPdfPath' => '/assets/pdfs/baderech-long.pdf', 'pdfPath' => '/assets/pdfs/baderech.pdf', 'youtubeUrl' => $sampleYoutubeUrl, ], ]); } public function testReturns404WhenElementDoesNotExist(): void { $response = $this->getJson('/api/elements/999'); $response->assertNotFound(); $response->assertExactJson([ 'error' => 'Element not found', ]); } public function testUpdateRequiresAuthentication(): void { $setRepository = app(SetRepository::class); $elementRepository = app(ElementRepository::class); $set = $this->createSet($setRepository); $element = $this->createElement( $elementRepository, $set, 'Baderech HaAvodah', 'A structured path for growth', '/assets/baderech-haavodah-icon.png', '

A structured path for growth

', '/assets/pdfs/baderech.pdf', null, null, null, ); $response = $this->postJson('/api/element/update', [ 'elementId' => $element->getId(), 'title' => 'Updated title', 'description' => 'Updated description', 'iconImageUrl' => '/assets/updated-icon.png', 'richText' => '

Updated rich text

', 'shortPdfPath' => '/assets/pdfs/updated.pdf', 'longPdfPath' => '/assets/pdfs/updated-long.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 = $this->createSet($setRepository); $element = $this->createElement( $elementRepository, $set, 'Baderech HaAvodah', 'A structured path for growth', '/assets/baderech-haavodah-icon.png', '

A structured path for growth

', '/assets/pdfs/baderech.pdf', null, null, null, ); $this->createSession('valid-token'); $response = $this->withCredentials() ->withUnencryptedCookie('auth_token', 'valid-token') ->postJson('/api/element/update', [ 'elementId' => $element->getId(), 'title' => 'Updated title', 'description' => 'Updated description', 'iconImageUrl' => '/assets/updated-icon.png', 'richText' => '

Updated rich text

', 'shortPdfPath' => '/assets/pdfs/updated.pdf', 'longPdfPath' => '/assets/pdfs/updated-long.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

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

A structured path for growth

', '/assets/pdfs/baderech.pdf', '/assets/pdfs/baderech-long.pdf', null, null, ); $this->createSession('valid-token'); $response = $this->withCredentials() ->withUnencryptedCookie('auth_token', 'valid-token') ->postJson('/api/element/update', [ 'elementId' => $element->getId(), 'pdfPath' => '/assets/pdfs/legacy-updated.pdf', ]); $response->assertOk(); $body = $response->json(); $this->assertSame( '/assets/pdfs/legacy-updated.pdf', $body['element']['shortPdfPath'], ); $this->assertSame( '/assets/pdfs/legacy-updated.pdf', $body['element']['pdfPath'], ); $this->assertSame( '/assets/pdfs/baderech-long.pdf', $body['element']['longPdfPath'], ); } public function testAuthenticatedUpdateClearsUploadedMedia(): void { Storage::fake('public'); Storage::disk('public')->put( 'element-icons/original-icon.png', 'icon bytes', ); Storage::disk('public')->put( 'element-pdfs/short/original.pdf', 'short pdf bytes', ); Storage::disk('public')->put( 'element-pdfs/long/original-long.pdf', 'long pdf bytes', ); $setRepository = app(SetRepository::class); $elementRepository = app(ElementRepository::class); $set = $this->createSet($setRepository); $element = $this->createElement( $elementRepository, $set, 'Baderech HaAvodah', 'A structured path for growth', 'element-icons/original-icon.png', '

A structured path for growth

', 'element-pdfs/short/original.pdf', 'element-pdfs/long/original-long.pdf', null, null, ); $this->createSession('valid-token'); $response = $this->withCredentials() ->withUnencryptedCookie('auth_token', 'valid-token') ->postJson('/api/element/update', [ 'elementId' => $element->getId(), 'iconImageUrl' => '', 'pdfPath' => '', 'longPdfPath' => '', ]); $response->assertOk(); $body = $response->json(); $this->assertNull($body['element']['iconImageUrl']); $this->assertNull($body['element']['shortPdfPath']); $this->assertNull($body['element']['longPdfPath']); $this->assertNull($body['element']['pdfPath']); $updatedElement = $elementRepository->find($element->getId()); $this->assertNotNull($updatedElement); $this->assertNull($updatedElement->getIconImageUrl()); $this->assertNull($updatedElement->getShortPdfPath()); $this->assertNull($updatedElement->getLongPdfPath()); Storage::disk('public')->assertMissing( 'element-icons/original-icon.png', ); Storage::disk('public')->assertMissing( 'element-pdfs/short/original.pdf', ); Storage::disk('public')->assertMissing( 'element-pdfs/long/original-long.pdf', ); } public function testIconImageUploadRequiresAuthentication(): void { $setRepository = app(SetRepository::class); $elementRepository = app(ElementRepository::class); $set = $this->createSet($setRepository); $element = $this->createElement( $elementRepository, $set, 'Baderech HaAvodah', 'A structured path for growth', null, '

A structured path for growth

', null, null, null, null, ); $response = $this->post( '/api/element/update', [ 'elementId' => (string) $element->getId(), 'fileType' => 'iconImage', 'file' => $this->iconImageUpload(), ], ); $response->assertUnauthorized(); $response->assertExactJson([ 'error' => 'unauthenticated', ]); } public function testAuthenticatedIconImageUploadReturnsElementPayload(): void { Storage::fake('public'); $setRepository = app(SetRepository::class); $elementRepository = app(ElementRepository::class); $set = $this->createSet($setRepository); $element = $this->createElement( $elementRepository, $set, 'Baderech HaAvodah', 'A structured path for growth', null, '

A structured path for growth

', null, null, null, null, ); $this->createSession('valid-token'); $response = $this->withCredentials() ->withUnencryptedCookie('auth_token', 'valid-token') ->post( '/api/element/update', [ 'elementId' => (string) $element->getId(), 'fileType' => 'iconImage', 'file' => $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 testShortPdfUploadRequiresAuthentication(): void { $setRepository = app(SetRepository::class); $elementRepository = app(ElementRepository::class); $set = $this->createSet($setRepository); $element = $this->createElement( $elementRepository, $set, 'Baderech HaAvodah', 'A structured path for growth', null, '

A structured path for growth

', null, null, null, null, ); $response = $this->post( '/api/element/update', [ 'elementId' => (string) $element->getId(), 'fileType' => 'shortPdf', 'file' => $this->pdfUpload(), ], ); $response->assertUnauthorized(); $response->assertExactJson([ 'error' => 'unauthenticated', ]); } public function testAuthenticatedShortPdfUploadReturnsElementPayload(): void { Storage::fake('public'); $setRepository = app(SetRepository::class); $elementRepository = app(ElementRepository::class); $set = $this->createSet($setRepository); $element = $this->createElement( $elementRepository, $set, 'Baderech HaAvodah', 'A structured path for growth', null, '

A structured path for growth

', null, null, null, null, ); $this->createSession('valid-token'); $response = $this->withCredentials() ->withUnencryptedCookie('auth_token', 'valid-token') ->post( '/api/element/update', [ 'elementId' => (string) $element->getId(), 'fileType' => 'shortPdf', 'file' => $this->pdfUpload(), ], ); $response->assertOk(); $body = $response->json(); $this->assertSame($element->getId(), $body['element']['id']); $this->assertStringContainsString( '/storage/element-pdfs/short/', $body['element']['shortPdfPath'], ); $this->assertSame( $body['element']['shortPdfPath'], $body['element']['pdfPath'], ); $updatedElement = $elementRepository->find($element->getId()); $this->assertNotNull($updatedElement); $updatedPdfPath = $updatedElement->getShortPdfPath(); $this->assertIsString($updatedPdfPath); $this->assertStringStartsWith( 'element-pdfs/short/', $updatedPdfPath, ); Storage::disk('public')->assertExists($updatedPdfPath); } public function testAuthenticatedLongPdfUploadReturnsElementPayload(): void { Storage::fake('public'); $setRepository = app(SetRepository::class); $elementRepository = app(ElementRepository::class); $set = $this->createSet($setRepository); $element = $this->createElement( $elementRepository, $set, 'Baderech HaAvodah', 'A structured path for growth', null, '

A structured path for growth

', null, null, null, null, ); $this->createSession('valid-token'); $response = $this->withCredentials() ->withUnencryptedCookie('auth_token', 'valid-token') ->post( '/api/element/update', [ 'elementId' => (string) $element->getId(), 'fileType' => 'longPdf', 'file' => $this->pdfUpload(), ], ); $response->assertOk(); $body = $response->json(); $this->assertSame($element->getId(), $body['element']['id']); $this->assertStringContainsString( '/storage/element-pdfs/long/', $body['element']['longPdfPath'], ); $updatedElement = $elementRepository->find($element->getId()); $this->assertNotNull($updatedElement); $updatedPdfPath = $updatedElement->getLongPdfPath(); $this->assertIsString($updatedPdfPath); $this->assertStringStartsWith( 'element-pdfs/long/', $updatedPdfPath, ); Storage::disk('public')->assertExists($updatedPdfPath); } private function createSet(SetRepository $setRepository): Set { return $setRepository->create(new CreateSetDto( name: 'Baderech HaAvodah', description: 'A structured path for growth', iconImageUrl: '/assets/baderech-haavodah-icon.png', )); } private function createElement( ElementRepository $elementRepository, Set $set, string $title, string $description, ?string $iconImageUrl, string $richText, ?string $shortPdfPath, ?string $longPdfPath, ?string $youtubeUrl, ?Element $parentElement, ): Element { return $elementRepository->create(new CreateElementDto( set: $set, title: $title, description: $description, iconImageUrl: $iconImageUrl, richText: $richText, shortPdfPath: $shortPdfPath, longPdfPath: $longPdfPath, youtubeUrl: $youtubeUrl, parentElement: $parentElement, )); } private function createSession(string $token): void { $userRepository = app(UserRepository::class); $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'), ), )); } 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, ); } }