10-04-2026

This commit is contained in:
Kevin Adametz 2026-04-10 17:18:17 +02:00
parent 4d6b4930b2
commit 4bb89aad8c
836 changed files with 52961 additions and 5950 deletions

View file

@ -0,0 +1,64 @@
<?php
use App\Models\User;
it('redirects unauthenticated users from cms dashboard', function () {
$this->get(route('cms.dashboard'))
->assertRedirect(route('login'));
});
it('allows authenticated users to access cms dashboard', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get(route('cms.dashboard'))
->assertSuccessful();
});
it('allows authenticated users to access content index', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get(route('cms.content.index'))
->assertSuccessful();
});
it('allows authenticated users to access news index', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get(route('cms.news.index'))
->assertSuccessful();
});
it('allows authenticated users to access faq index', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get(route('cms.faqs.index'))
->assertSuccessful();
});
it('allows authenticated users to access industries index', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get(route('cms.industries.index'))
->assertSuccessful();
});
it('allows authenticated users to access linkedin index', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get(route('cms.linkedin.index'))
->assertSuccessful();
});
it('allows authenticated users to access downloads index', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get(route('cms.downloads.index'))
->assertSuccessful();
});

View file

@ -0,0 +1,92 @@
<?php
use FluxCms\Core\Models\CmsContent;
use FluxCms\Core\Services\CmsContentService;
beforeEach(function () {
CmsContent::create([
'group' => 'welcome',
'key' => 'hero.heading',
'type' => 'text',
'value' => ['de' => 'Willkommen', 'en' => 'Welcome'],
'order' => 0,
]);
CmsContent::create([
'group' => 'welcome',
'key' => 'hero.description',
'type' => 'html',
'value' => ['de' => '<p>Beschreibung</p>', 'en' => '<p>Description</p>'],
'order' => 1,
]);
});
it('resolves content by dot-notation key', function () {
app()->setLocale('de');
$service = app(CmsContentService::class);
expect($service->get('welcome.hero.heading'))->toBe('Willkommen');
});
it('resolves content in english locale', function () {
app()->setLocale('en');
$service = app(CmsContentService::class);
expect($service->get('welcome.hero.heading'))->toBe('Welcome');
});
it('falls back to lang file when no db entry', function () {
$service = app(CmsContentService::class);
$result = $service->get('nonexistent.key.here');
expect($result)->toBe('nonexistent.key.here');
});
it('returns html content correctly', function () {
app()->setLocale('de');
$service = app(CmsContentService::class);
expect($service->get('welcome.hero.description'))->toBe('<p>Beschreibung</p>');
});
it('replaces placeholders in content', function () {
CmsContent::create([
'group' => 'test',
'key' => 'greeting',
'type' => 'text',
'value' => ['de' => 'Hallo :name!'],
'order' => 0,
]);
app()->setLocale('de');
$service = app(CmsContentService::class);
expect($service->get('test.greeting', ['name' => 'Welt']))->toBe('Hallo Welt!');
});
it('gets all content for a group', function () {
app()->setLocale('de');
$service = app(CmsContentService::class);
$group = $service->getGroup('welcome');
expect($group)->toHaveKey('hero.heading', 'Willkommen')
->toHaveKey('hero.description', '<p>Beschreibung</p>');
});
it('clears cache for a group', function () {
$service = app(CmsContentService::class);
$service->get('welcome.hero.heading');
$service->clearCache('welcome');
expect(true)->toBeTrue();
});
it('cms helper function works', function () {
app()->setLocale('en');
expect(cms('welcome.hero.heading'))->toBe('Welcome');
});

View file

@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
use FluxCms\Core\Models\CmsMedia;
use FluxCms\Core\Services\MediaConversionService;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
beforeEach(function () {
Storage::fake('public');
});
test('CmsMedia model can be created', function () {
$media = CmsMedia::create([
'filename' => 'test-image.jpg',
'disk' => 'public',
'path' => 'cms/media/originals/test-image.jpg',
'type' => 'image',
'mime_type' => 'image/jpeg',
'file_size' => 102400,
'original_width' => 1920,
'original_height' => 1080,
]);
expect($media)->toBeInstanceOf(CmsMedia::class)
->and($media->filename)->toBe('test-image.jpg')
->and($media->isImage())->toBeTrue()
->and($media->isPdf())->toBeFalse()
->and($media->getHumanFileSize())->toBe('100 KB')
->and($media->getDimensionsLabel())->toBe('1920 × 1080');
});
test('CmsMedia scopes filter correctly', function () {
CmsMedia::create(['filename' => 'photo.jpg', 'disk' => 'public', 'path' => 'a.jpg', 'type' => 'image', 'collection' => 'hero']);
CmsMedia::create(['filename' => 'doc.pdf', 'disk' => 'public', 'path' => 'b.pdf', 'type' => 'pdf', 'collection' => 'downloads']);
CmsMedia::create(['filename' => 'draft.jpg', 'disk' => 'public', 'path' => 'c.jpg', 'type' => 'image', 'is_published' => false]);
expect(CmsMedia::images()->count())->toBe(2)
->and(CmsMedia::pdfs()->count())->toBe(1)
->and(CmsMedia::published()->count())->toBe(2)
->and(CmsMedia::inCollection('hero')->count())->toBe(1);
});
test('CmsMedia stores translatable alt_text and title', function () {
$media = CmsMedia::create([
'filename' => 'hero.jpg',
'disk' => 'public',
'path' => 'cms/media/originals/hero.jpg',
'type' => 'image',
]);
$media->setTranslation('alt_text', 'de', 'Heldenbild');
$media->setTranslation('alt_text', 'en', 'Hero image');
$media->setTranslation('title', 'de', 'Startseite Hero');
$media->save();
$media->refresh();
expect($media->getTranslation('alt_text', 'de'))->toBe('Heldenbild')
->and($media->getTranslation('alt_text', 'en'))->toBe('Hero image')
->and($media->getTranslation('title', 'de'))->toBe('Startseite Hero');
});
test('CmsMedia conversions tracking works', function () {
$media = CmsMedia::create([
'filename' => 'test.jpg',
'disk' => 'public',
'path' => 'cms/media/originals/test.jpg',
'type' => 'image',
'conversions' => [],
]);
expect($media->hasConversion('hero'))->toBeFalse()
->and($media->getExistingConversions())->toBe([]);
$media->update(['conversions' => ['hero' => 'cms/media/conversions/hero/test-hero.webp']]);
Storage::disk('public')->put('cms/media/conversions/hero/test-hero.webp', 'fake');
$media->refresh();
expect($media->hasConversion('hero'))->toBeTrue()
->and($media->getExistingConversions())->toHaveKey('hero');
});
test('MediaConversionService storeUpload creates media record', function () {
$file = UploadedFile::fake()->image('photo.jpg', 800, 600);
$service = app(MediaConversionService::class);
$media = $service->storeUpload($file, 'test');
expect($media)->toBeInstanceOf(CmsMedia::class)
->and($media->filename)->toBe('photo.jpg')
->and($media->type)->toBe('image')
->and($media->collection)->toBe('test')
->and($media->original_width)->toBe(800)
->and($media->original_height)->toBe(600)
->and(Storage::disk('public')->exists($media->path))->toBeTrue();
});
test('MediaConversionService generates thumbnail on upload', function () {
$file = UploadedFile::fake()->image('big-photo.jpg', 1920, 1080);
$service = app(MediaConversionService::class);
$media = $service->storeUpload($file);
$media->refresh();
expect($media->hasConversion('thumb'))->toBeTrue();
});
test('MediaConversionService can generate specific profile conversion', function () {
$file = UploadedFile::fake()->image('hero-bg.jpg', 2400, 1600);
$service = app(MediaConversionService::class);
$media = $service->storeUpload($file, generateThumbnail: false);
$result = $service->convert($media, 'hero');
expect($result)->not->toBeNull();
$media->refresh();
expect($media->hasConversion('hero'))->toBeTrue();
});
test('MediaConversionService deleteAll removes files', function () {
$file = UploadedFile::fake()->image('delete-me.jpg', 400, 400);
$service = app(MediaConversionService::class);
$media = $service->storeUpload($file);
$path = $media->path;
expect(Storage::disk('public')->exists($path))->toBeTrue();
$service->deleteAll($media);
expect(Storage::disk('public')->exists($path))->toBeFalse();
});
test('MediaConversionService skips SVG files for conversions', function () {
$file = UploadedFile::fake()->create('logo.svg', 5, 'image/svg+xml');
$service = app(MediaConversionService::class);
$media = $service->storeUpload($file, generateThumbnail: false);
expect($media->type)->toBe('image')
->and($media->mime_type)->toBe('image/svg+xml');
});
test('media profiles are configured', function () {
$profiles = config('flux-cms.media.profiles');
expect($profiles)->toHaveKey('hero')
->and($profiles)->toHaveKey('thumb')
->and($profiles)->toHaveKey('avatar')
->and($profiles)->toHaveKey('news')
->and($profiles)->toHaveKey('thumbnail')
->and($profiles)->toHaveKey('service')
->and($profiles)->toHaveKey('og_image')
->and($profiles['hero']['width'])->toBe(1920)
->and($profiles['hero']['height'])->toBe(800)
->and($profiles['avatar']['width'])->toBe(400);
});
test('media library admin page loads', function () {
$user = \App\Models\User::factory()->create();
$this->actingAs($user)
->get(route('cms.media.index'))
->assertSuccessful();
});

View file

@ -0,0 +1,125 @@
<?php
use FluxCms\Core\Models\CmsDownload;
use FluxCms\Core\Models\CmsFaq;
use FluxCms\Core\Models\CmsIndustry;
use FluxCms\Core\Models\CmsLinkedinPost;
use FluxCms\Core\Models\CmsNewsItem;
it('creates a cms faq with translations', function () {
$faq = CmsFaq::create([
'category' => 'general',
'question' => ['de' => 'Was ist das?', 'en' => 'What is this?'],
'answer' => ['de' => 'Eine Antwort', 'en' => 'An answer'],
'is_published' => true,
'order' => 0,
]);
expect($faq->getTranslation('question', 'de'))->toBe('Was ist das?')
->and($faq->getTranslation('question', 'en'))->toBe('What is this?');
});
it('scopes faqs by category', function () {
CmsFaq::create([
'category' => 'general',
'question' => ['de' => 'Frage 1'],
'answer' => ['de' => 'Antwort 1'],
'order' => 0,
]);
CmsFaq::create([
'category' => 'technical',
'question' => ['de' => 'Frage 2'],
'answer' => ['de' => 'Antwort 2'],
'order' => 0,
]);
expect(CmsFaq::byCategory('general')->count())->toBe(1)
->and(CmsFaq::byCategory('technical')->count())->toBe(1);
});
it('scopes published items', function () {
CmsFaq::create([
'category' => 'test',
'question' => ['de' => 'Sichtbar'],
'answer' => ['de' => 'Ja'],
'is_published' => true,
'order' => 0,
]);
CmsFaq::create([
'category' => 'test',
'question' => ['de' => 'Versteckt'],
'answer' => ['de' => 'Nein'],
'is_published' => false,
'order' => 1,
]);
expect(CmsFaq::published()->count())->toBe(1);
});
it('creates a news item with all fields', function () {
$item = CmsNewsItem::create([
'icon' => 'document-check',
'text' => ['de' => 'Capability: Test', 'en' => 'Capability: Test'],
'title' => ['de' => 'Titel DE', 'en' => 'Title EN'],
'excerpt' => ['de' => 'Kurztext', 'en' => 'Short text'],
'content' => ['de' => '<p>Inhalt</p>', 'en' => '<p>Content</p>'],
'image' => '/images/test.jpg',
'date' => '2026-01-01',
'author' => 'Test Author',
'pdf_path' => '/pdfs/test.pdf',
'pdf_open_text' => ['de' => 'Öffnen', 'en' => 'Open'],
'is_published' => true,
'order' => 0,
]);
expect($item->toFrontendArray('de'))
->toHaveKey('icon', 'document-check')
->toHaveKey('title', 'Titel DE')
->toHaveKey('pdf_path', '/pdfs/test.pdf');
});
it('creates and queries industries', function () {
CmsIndustry::create(['name' => ['de' => 'FMCG', 'en' => 'FMCG'], 'order' => 0, 'is_published' => true]);
CmsIndustry::create(['name' => ['de' => 'Beauty', 'en' => 'Beauty'], 'order' => 1, 'is_published' => true]);
CmsIndustry::create(['name' => ['de' => 'Entwurf', 'en' => 'Draft'], 'order' => 2, 'is_published' => false]);
expect(CmsIndustry::published()->count())->toBe(2)
->and(CmsIndustry::ordered()->first()->getTranslation('name', 'de'))->toBe('FMCG');
});
it('creates a download with category', function () {
$dl = CmsDownload::create([
'title' => ['de' => 'Case Study', 'en' => 'Case Study'],
'description' => ['de' => 'Beschreibung', 'en' => 'Description'],
'category' => 'case_study',
'file_path' => '/pdfs/case-study.pdf',
'is_published' => true,
'order' => 0,
]);
expect(CmsDownload::byCategory('case_study')->count())->toBe(1)
->and($dl->getTranslation('title', 'de'))->toBe('Case Study');
});
it('creates a linkedin post with source flag', function () {
CmsLinkedinPost::create([
'title' => ['de' => 'Post Titel'],
'content' => ['de' => '<p>Inhalt</p>'],
'author' => 'Test',
'date' => '2026-01-01',
'source' => 'manual',
'is_published' => true,
'order' => 0,
]);
CmsLinkedinPost::create([
'title' => ['de' => 'API Post'],
'content' => ['de' => '<p>API</p>'],
'linkedin_id' => 'ext-123',
'source' => 'api',
'is_published' => true,
'order' => 1,
]);
expect(CmsLinkedinPost::manual()->count())->toBe(1)
->and(CmsLinkedinPost::fromApi()->count())->toBe(1);
});

View file

@ -0,0 +1,74 @@
<?php
use Database\Seeders\CmsContentSeeder;
use Database\Seeders\CmsFaqSeeder;
use Database\Seeders\CmsIndustrySeeder;
use Database\Seeders\CmsLinkedinPostSeeder;
use Database\Seeders\CmsNewsItemSeeder;
use FluxCms\Core\Models\CmsContent;
use FluxCms\Core\Models\CmsFaq;
use FluxCms\Core\Models\CmsIndustry;
use FluxCms\Core\Models\CmsLinkedinPost;
use FluxCms\Core\Models\CmsNewsItem;
it('seeds cms content from lang files', function () {
$this->seed(CmsContentSeeder::class);
expect(CmsContent::count())->toBeGreaterThan(0);
$welcomeContent = CmsContent::forGroup('welcome')->get();
expect($welcomeContent->count())->toBeGreaterThan(0);
$heroHeading = CmsContent::where('group', 'welcome')
->where('key', 'hero.heading_main')
->first();
expect($heroHeading)->not->toBeNull()
->and($heroHeading->getTranslation('value', 'de'))->not->toBeEmpty();
});
it('seeds news items from lang files', function () {
$this->seed(CmsNewsItemSeeder::class);
expect(CmsNewsItem::count())->toBeGreaterThan(0);
$first = CmsNewsItem::ordered()->first();
expect($first->getTranslation('title', 'de'))->not->toBeEmpty();
});
it('seeds industries from lang files', function () {
$this->seed(CmsIndustrySeeder::class);
expect(CmsIndustry::count())->toBeGreaterThan(0);
$first = CmsIndustry::ordered()->first();
expect($first->getTranslation('name', 'de'))->toBe('FMCG');
});
it('seeds faqs from lang files', function () {
$this->seed(CmsFaqSeeder::class);
expect(CmsFaq::count())->toBeGreaterThan(0);
$categories = CmsFaq::distinct()->pluck('category');
expect($categories)->toContain('general');
});
it('seeds linkedin posts', function () {
$this->seed(CmsLinkedinPostSeeder::class);
expect(CmsLinkedinPost::count())->toBe(3);
$first = CmsLinkedinPost::ordered()->first();
expect($first->source)->toBe('manual')
->and($first->getTranslation('title', 'de'))->not->toBeEmpty();
});
it('does not create duplicates when run twice', function () {
$this->seed(CmsIndustrySeeder::class);
$countAfterFirst = CmsIndustry::count();
$this->seed(CmsIndustrySeeder::class);
$countAfterSecond = CmsIndustry::count();
expect($countAfterSecond)->toBe($countAfterFirst);
});