/dev/null', escapeshellarg($ffmpeg), escapeshellarg($path) ); shell_exec($command); return is_file($path) && filesize($path) > 0 ? $path : null; } // ======================================== // MODEL TESTS // ======================================== it('creates an uploaded image media', function () { $media = DisplayMedia::factory()->create(); expect($media->isUpload())->toBeTrue() ->and($media->isExternal())->toBeFalse() ->and($media->isImage())->toBeTrue() ->and($media->isVideo())->toBeFalse() ->and($media->source_type)->toBe('upload'); }); it('creates an external video media', function () { $media = DisplayMedia::factory()->externalVideo()->create(); expect($media->isExternal())->toBeTrue() ->and($media->isUpload())->toBeFalse() ->and($media->isVideo())->toBeTrue() ->and($media->external_url)->toStartWith('https://'); }); it('returns correct URL for uploaded media', function () { $media = DisplayMedia::factory()->create(['path' => 'display-media/2026/03/test.webp']); expect($media->getUrl())->toContain('display-media/2026/03/test.webp'); }); it('returns external URL for external media', function () { $url = 'https://drive.google.com/file/d/abc123/view'; $media = DisplayMedia::factory()->external()->create(['external_url' => $url]); expect($media->getUrl())->toBe($url); }); it('returns human readable file size', function () { $media = DisplayMedia::factory()->create(['file_size' => 1536000]); expect($media->getHumanFileSize())->toBe('1.5 MB'); $external = DisplayMedia::factory()->external()->create(); expect($external->getHumanFileSize())->toBe('Extern'); }); it('returns display name from title or filename', function () { $withTitle = DisplayMedia::factory()->create(['title' => 'Mein Video', 'filename' => 'file.mp4']); expect($withTitle->getDisplayName())->toBe('Mein Video'); $withoutTitle = DisplayMedia::factory()->create(['title' => null, 'filename' => 'file.mp4']); expect($withoutTitle->getDisplayName())->toBe('file.mp4'); }); it('returns the stored poster as thumbnail url for a video', function () { $media = DisplayMedia::factory()->video()->create([ 'thumbnail_path' => 'display-media/2026/05/tour-poster.jpg', ]); expect($media->getThumbnailUrl())->toContain('display-media/2026/05/tour-poster.jpg'); }); it('has no thumbnail url for a video without a poster', function () { $media = DisplayMedia::factory()->video()->create(['thumbnail_path' => null]); expect($media->getThumbnailUrl())->toBeNull(); }); // ======================================== // SCOPE TESTS // ======================================== it('filters by image scope', function () { DisplayMedia::factory()->create(['type' => 'image']); DisplayMedia::factory()->video()->create(); expect(DisplayMedia::images()->count())->toBe(1); }); it('filters by video scope', function () { DisplayMedia::factory()->create(['type' => 'image']); DisplayMedia::factory()->video()->create(); expect(DisplayMedia::videos()->count())->toBe(1); }); it('filters by upload scope', function () { DisplayMedia::factory()->create(); DisplayMedia::factory()->external()->create(); expect(DisplayMedia::uploads()->count())->toBe(1); }); it('filters by external scope', function () { DisplayMedia::factory()->create(); DisplayMedia::factory()->external()->create(); expect(DisplayMedia::externals()->count())->toBe(1); }); it('filters by collection scope', function () { DisplayMedia::factory()->create(['collection' => 'immobilien']); DisplayMedia::factory()->create(['collection' => 'moebel']); expect(DisplayMedia::inCollection('immobilien')->count())->toBe(1); }); it('keeps preceding filters when combined with the search scope', function () { DisplayMedia::factory()->create([ 'type' => 'image', 'filename' => 'haus.webp', 'title' => 'Schaufenster', ]); DisplayMedia::factory()->video()->create([ 'filename' => 'tour.mp4', 'title' => 'Haus Tour', ]); $results = DisplayMedia::images()->search('haus')->get(); expect($results)->toHaveCount(1) ->and($results->first()->filename)->toBe('haus.webp'); }); it('search scope respects the active filter on the media picker', function () { DisplayMedia::factory()->create([ 'filename' => 'aktiv.webp', 'title' => 'Goya', 'is_active' => true, ]); DisplayMedia::factory()->create([ 'filename' => 'inaktiv.webp', 'title' => 'Goya', 'is_active' => false, ]); $results = DisplayMedia::active()->search('goya')->get(); expect($results)->toHaveCount(1) ->and($results->first()->filename)->toBe('aktiv.webp'); }); it('filters by active scope', function () { DisplayMedia::factory()->create(['is_active' => true]); DisplayMedia::factory()->create(['is_active' => false]); expect(DisplayMedia::active()->count())->toBe(1); }); // ======================================== // SERVICE TESTS // ======================================== it('stores an uploaded file', function () { $service = app(DisplayMediaService::class); $file = UploadedFile::fake()->image('test-photo.jpg', 800, 600); $media = $service->storeUpload($file, 'immobilien'); expect($media)->toBeInstanceOf(DisplayMedia::class) ->and($media->filename)->toBe('test-photo.jpg') ->and($media->source_type)->toBe('upload') ->and($media->type)->toBe('image') ->and($media->collection)->toBe('immobilien') ->and($media->path)->not->toBeNull(); expect(Storage::disk('public')->exists($media->path))->toBeTrue(); }); it('stores a video upload', function () { $service = app(DisplayMediaService::class); $file = UploadedFile::fake()->create('showroom.mp4', 10000, 'video/mp4'); $media = $service->storeUpload($file); expect($media->type)->toBe('video') ->and($media->mime_type)->toBe('video/mp4'); }); it('generates a poster thumbnail when a real video is uploaded', function () { $videoPath = makeRealTestVideo(); if ($videoPath === null) { $this->markTestSkipped('ffmpeg is not available.'); } $service = app(DisplayMediaService::class); $file = new UploadedFile($videoPath, 'showroom.mp4', 'video/mp4', null, true); $media = $service->storeUpload($file); expect($media->thumbnail_path)->not->toBeNull(); expect(Storage::disk('public')->exists($media->thumbnail_path))->toBeTrue(); @unlink($videoPath); }); it('does not set a thumbnail when the video cannot be decoded', function () { $service = app(DisplayMediaService::class); $file = UploadedFile::fake()->create('broken.mp4', 500, 'video/mp4'); $media = $service->storeUpload($file); expect($media->thumbnail_path)->toBeNull(); }); it('stores an svg upload as image media', function () { $service = app(DisplayMediaService::class); $file = UploadedFile::fake()->createWithContent( 'logo.svg', '' ); $media = $service->storeUpload($file); expect($media->filename)->toBe('logo.svg') ->and($media->type)->toBe('image') ->and($media->mime_type)->toBe('image/svg+xml'); expect(Storage::disk('public')->exists($media->path))->toBeTrue(); }); it('resets pagination to page one when a filter changes', function () { Volt::test('admin.cms.display-media-library') ->call('gotoPage', 3) ->set('filterType', 'video') ->assertSet('paginators.page', 1); }); it('renders external image media with an inline thumbnail', function () { $media = DisplayMedia::factory()->external()->create([ 'type' => 'image', 'external_url' => 'https://example.com/foto.jpg', ]); Volt::test('admin.cms.display-media-library') ->assertSeeHtml('src="https://example.com/foto.jpg"'); }); it('renders a stored poster image for a video with a thumbnail', function () { DisplayMedia::factory()->video()->create([ 'path' => 'display-media/2026/05/tour.mp4', 'thumbnail_path' => 'display-media/2026/05/tour-poster.jpg', ]); Volt::test('admin.cms.display-media-library') ->assertSeeHtml('tour-poster.jpg'); }); it('renders an inline video frame for an uploaded video without a poster', function () { DisplayMedia::factory()->video()->create([ 'path' => 'display-media/2026/05/clip.mp4', 'thumbnail_path' => null, 'mime_type' => 'video/mp4', ]); Volt::test('admin.cms.display-media-library') ->assertSeeHtml('clip.mp4#t=1'); }); it('accepts svg uploads in the display media library', function () { $file = UploadedFile::fake()->createWithContent( 'brand.svg', '' ); Volt::test('admin.cms.display-media-library') ->set('uploads', [$file]) ->call('handleUploads') ->assertHasNoErrors(); $media = DisplayMedia::query()->first(); expect($media)->not->toBeNull() ->and($media->filename)->toBe('brand.svg') ->and($media->type)->toBe('image') ->and($media->mime_type)->toBe('image/svg+xml'); }); it('accepts display media videos up to 200 mb', function () { $file = UploadedFile::fake()->create('showroom.mp4', 204800, 'video/mp4'); Volt::test('admin.cms.display-media-library') ->set('uploads', [$file]) ->call('handleUploads') ->assertHasNoErrors(); $media = DisplayMedia::query()->first(); expect($media)->not->toBeNull() ->and($media->filename)->toBe('showroom.mp4') ->and($media->type)->toBe('video'); }); it('configures livewire temporary uploads up to 200 mb', function () { expect(config('livewire.temporary_file_upload.rules'))->toContain('max:204800'); }); it('creates media from external URL', function () { $service = app(DisplayMediaService::class); $media = $service->createFromUrl( url: 'https://drive.google.com/file/d/abc123/view', type: 'video', title: 'Showroom Tour 4K', collection: 'brand', ); expect($media->source_type)->toBe('external') ->and($media->external_url)->toBe('https://drive.google.com/file/d/abc123/view') ->and($media->title)->toBe('Showroom Tour 4K') ->and($media->type)->toBe('video') ->and($media->collection)->toBe('brand') ->and($media->file_size)->toBe(0); }); it('deletes uploaded media and its file', function () { $service = app(DisplayMediaService::class); $file = UploadedFile::fake()->image('delete-me.jpg'); $media = $service->storeUpload($file); $path = $media->path; expect(Storage::disk('public')->exists($path))->toBeTrue(); $service->delete($media); expect(Storage::disk('public')->exists($path))->toBeFalse(); expect(DisplayMedia::find($media->id))->toBeNull(); }); it('deletes external media record', function () { $service = app(DisplayMediaService::class); $media = $service->createFromUrl('https://example.com/video.mp4', 'video'); $service->delete($media); expect(DisplayMedia::find($media->id))->toBeNull(); }); // ======================================== // COMMAND TESTS // ======================================== it('reports when there are no videos to process', function () { $this->artisan('display-media:generate-video-thumbnails') ->expectsOutputToContain('Keine Videos zum Verarbeiten gefunden.') ->assertSuccessful(); }); it('backfills posters for existing uploaded videos', function () { $videoPath = makeRealTestVideo(); if ($videoPath === null) { $this->markTestSkipped('ffmpeg is not available.'); } Storage::disk('public')->putFileAs( 'display-media/2026/05', new UploadedFile($videoPath, 'clip.mp4', 'video/mp4', null, true), 'clip.mp4' ); $media = DisplayMedia::factory()->video()->create([ 'source_type' => 'upload', 'path' => 'display-media/2026/05/clip.mp4', 'thumbnail_path' => null, ]); $this->artisan('display-media:generate-video-thumbnails')->assertSuccessful(); expect($media->fresh()->thumbnail_path)->not->toBeNull(); @unlink($videoPath); }); // ======================================== // ROUTE TESTS // ======================================== it('requires authentication for display media library', function () { $this->get(route('admin.cms.display-media')) ->assertRedirect(); }); it('loads display media library for authenticated admin', function () { Role::findOrCreate('Super-Admin', 'web'); $user = User::factory()->create(); $user->assignRole('Super-Admin'); $this->actingAs($user) ->get(route('admin.cms.display-media')) ->assertSuccessful(); });