10-04-2026
This commit is contained in:
parent
4d6b4930b2
commit
4bb89aad8c
836 changed files with 52961 additions and 5950 deletions
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue