create(new CreateSetDto( name: 'Baderech HaAvodah', description: 'Baderech HaAvodah is a way of living', iconImageUrl: '/assets/baderech-haavodah-icon.png', )); $dailyLearningSet = $setRepository->create(new CreateSetDto( name: 'Daily Learning', description: 'Daily learning for steady growth', iconImageUrl: '/assets/daily-learning-icon.svg', )); $baderechRootElement = $elementRepository->create( new CreateElementDto( set: $baderechSet, title: $baderechSet->getName(), description: $baderechSet->getDescription(), iconImageUrl: null, richText: '', shortPdfPath: null, longPdfPath: null, youtubeUrl: null, parentElement: null, ) ); $response = $this->getJson('/api/sets'); $response->assertOk(); $response->assertExactJson([ 'sets' => [ [ 'id' => $baderechSet->getId(), 'name' => $baderechSet->getName(), 'description' => $baderechSet->getDescription(), 'iconImageUrl' => $baderechSet->getIconImageUrl(), 'rootElementId' => $baderechRootElement->getId(), ], [ 'id' => $dailyLearningSet->getId(), 'name' => $dailyLearningSet->getName(), 'description' => $dailyLearningSet->getDescription(), 'iconImageUrl' => $dailyLearningSet->getIconImageUrl(), 'rootElementId' => null, ], ], ]); } public function testCreateSetRequiresAuthentication(): void { $response = $this->postJson('/api/sets', [ 'name' => 'New Series', 'description' => 'A new learning series', ]); $response->assertUnauthorized(); $response->assertExactJson([ 'error' => 'unauthenticated', ]); } public function testAuthenticatedCreateSetCreatesRootElement(): void { Storage::fake('public'); $setRepository = app(SetRepository::class); $elementRepository = app(ElementRepository::class); $this->createSession('valid-token'); $response = $this->withCredentials() ->withUnencryptedCookie('auth_token', 'valid-token') ->post('/api/sets', [ 'name' => 'New Series', 'description' => 'A new learning series', 'iconImage' => $this->iconImageUpload(), ]); $response->assertCreated(); $body = $response->json(); $setId = $body['set']['id']; $rootElementId = $body['set']['rootElementId']; $this->assertIsInt($setId); $this->assertIsInt($rootElementId); $this->assertSame('New Series', $body['set']['name']); $this->assertSame( 'A new learning series', $body['set']['description'], ); $this->assertStringContainsString( '/storage/set-icons/', $body['set']['iconImageUrl'], ); $createdSet = $setRepository->find($setId); $this->assertNotNull($createdSet); $rootElement = $elementRepository->findRootBySet($createdSet); $this->assertNotNull($rootElement); $this->assertSame($rootElementId, $rootElement->getId()); $this->assertSame('New Series', $rootElement->getTitle()); $this->assertSame( 'A new learning series', $rootElement->getDescription(), ); $storedIconPath = $createdSet->getIconImageUrl(); $this->assertStringStartsWith('set-icons/', $storedIconPath); $this->assertSame($storedIconPath, $rootElement->getIconImageUrl()); Storage::disk('public')->assertExists($storedIconPath); } public function testCreateSetRequiresIconImage(): void { $this->createSession('valid-token'); $response = $this->withCredentials() ->withUnencryptedCookie('auth_token', 'valid-token') ->post('/api/sets', [ 'name' => 'New Series', 'description' => 'A new learning series', ]); $response->assertBadRequest(); $response->assertExactJson([ 'error' => 'icon image is required', ]); } 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, ); } }