diff --git a/app/Http/Controllers/ServiceImageController.php b/app/Http/Controllers/ServiceImageController.php new file mode 100644 index 0000000..481fa58 --- /dev/null +++ b/app/Http/Controllers/ServiceImageController.php @@ -0,0 +1,48 @@ +store($request, $service, 'key_visual_filename', 'current_key_visual'); + } + + public function storeBackground(Request $request, Service $service): RedirectResponse + { + return $this->store($request, $service, 'background_filename', 'current_background'); + } + + private function store(Request $request, Service $service, string $column, string $settingKey): RedirectResponse + { + $request->validate([ + 'file' => ['required', 'file', 'mimes:jpg,jpeg,png', 'max:20480'], + 'scope' => ['required', Rule::in(['service', 'default'])], + ], [ + 'file.required' => 'Bitte wähle eine Bilddatei aus.', + 'file.file' => 'Die hochgeladene Datei ist ungültig.', + 'file.mimes' => 'Nur Bilddateien (jpg, png) sind erlaubt.', + 'file.max' => 'Die Datei darf maximal 20 MB groß sein.', + 'scope.required' => 'Bitte wähle einen Geltungsbereich.', + 'scope.in' => 'Der gewählte Geltungsbereich ist ungültig.', + ]); + + $result = app(FileConversionService::class)->convertImageCover($request->file('file')); + + $service->update([$column => $result['filename']]); + + if ($request->input('scope') === 'default') { + Setting::set($settingKey, $result['filename']); + } + + return back()->with('success', 'Bild wurde gespeichert.'); + } +} diff --git a/routes/web.php b/routes/web.php index 8c7dff1..876eb8b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -8,6 +8,7 @@ use App\Http\Controllers\MacroAssignmentController; use App\Http\Controllers\MacroImportController; use App\Http\Controllers\ServiceController; +use App\Http\Controllers\ServiceImageController; use App\Http\Controllers\ServiceMacroOverrideController; use App\Http\Controllers\SettingsController; use App\Http\Controllers\SongPdfController; @@ -67,6 +68,8 @@ Route::get('/services/{service}/download-bundle/{blockType}', [ServiceController::class, 'downloadBundle'])->name('services.download-bundle'); Route::get('/services/{service}/agenda-items/{agendaItem}/download', [ServiceController::class, 'downloadAgendaItem'])->name('services.agenda-item.download'); Route::get('/services/{service}/edit', [ServiceController::class, 'edit'])->name('services.edit'); + Route::post('/services/{service}/key-visual', [ServiceImageController::class, 'storeKeyVisual'])->name('services.key-visual.store'); + Route::post('/services/{service}/background', [ServiceImageController::class, 'storeBackground'])->name('services.background.store'); Route::get('/songs/{song}/translate', [TranslationController::class, 'page'])->name('songs.translate'); Route::get('/songs', function () { diff --git a/tests/Feature/ServiceImageControllerTest.php b/tests/Feature/ServiceImageControllerTest.php new file mode 100644 index 0000000..953240d --- /dev/null +++ b/tests/Feature/ServiceImageControllerTest.php @@ -0,0 +1,159 @@ +user = User::factory()->create(); + $this->actingAs($this->user); + $this->service = Service::factory()->create(); +}); + +/* +|-------------------------------------------------------------------------- +| Key-Visual Upload +|-------------------------------------------------------------------------- +*/ + +test('key visual upload with service scope sets column only', function () { + $file = makeImageUpload('keyvisual.png', 800, 600); + + $response = $this->post(route('services.key-visual.store', $this->service), [ + 'file' => $file, + 'scope' => 'service', + ]); + + $response->assertRedirect(); + $response->assertSessionHas('success'); + + $this->service->refresh(); + expect($this->service->key_visual_filename)->not->toBeNull(); + expect($this->service->key_visual_filename)->toStartWith('slides/'); + expect($this->service->key_visual_filename)->toEndWith('.jpg'); + expect(Storage::disk('public')->exists($this->service->key_visual_filename))->toBeTrue(); + + expect(Setting::get('current_key_visual'))->toBeNull(); +}); + +/* +|-------------------------------------------------------------------------- +| Background Upload (default scope) +|-------------------------------------------------------------------------- +*/ + +test('background upload with default scope sets column and global setting', function () { + $file = makeImageUpload('background.png', 1920, 1080); + + $response = $this->post(route('services.background.store', $this->service), [ + 'file' => $file, + 'scope' => 'default', + ]); + + $response->assertRedirect(); + $response->assertSessionHas('success'); + + $this->service->refresh(); + expect($this->service->background_filename)->not->toBeNull(); + expect($this->service->background_filename)->toStartWith('slides/'); + expect(Storage::disk('public')->exists($this->service->background_filename))->toBeTrue(); + + expect(Setting::get('current_background'))->toBe($this->service->background_filename); +}); + +/* +|-------------------------------------------------------------------------- +| Validation +|-------------------------------------------------------------------------- +*/ + +test('key visual upload rejects non-image file with german error', function () { + $file = UploadedFile::fake()->create('notes.txt', 10, 'text/plain'); + + $response = $this->postJson(route('services.key-visual.store', $this->service), [ + 'file' => $file, + 'scope' => 'service', + ]); + + $response->assertStatus(422); + $response->assertJsonValidationErrors(['file']); + expect($response->json('errors.file.0'))->toContain('Bild'); + + $this->service->refresh(); + expect($this->service->key_visual_filename)->toBeNull(); +}); + +test('background upload rejects invalid scope', function () { + $file = makeImageUpload('background.png', 800, 600); + + $response = $this->postJson(route('services.background.store', $this->service), [ + 'file' => $file, + 'scope' => 'bogus', + ]); + + $response->assertStatus(422); + $response->assertJsonValidationErrors(['scope']); +}); + +test('key visual upload does not delete previous file', function () { + $first = makeImageUpload('first.png', 800, 600); + $this->post(route('services.key-visual.store', $this->service), [ + 'file' => $first, + 'scope' => 'service', + ]); + $this->service->refresh(); + $oldFilename = $this->service->key_visual_filename; + + $second = makeImageUpload('second.png', 800, 600); + $this->post(route('services.key-visual.store', $this->service), [ + 'file' => $second, + 'scope' => 'service', + ]); + $this->service->refresh(); + + expect($this->service->key_visual_filename)->not->toBe($oldFilename); + expect(Storage::disk('public')->exists($oldFilename))->toBeTrue(); +}); + +/* +|-------------------------------------------------------------------------- +| Auth +|-------------------------------------------------------------------------- +*/ + +test('key visual upload requires authentication', function () { + auth()->logout(); + $file = makeImageUpload('keyvisual.png', 800, 600); + + $response = $this->post(route('services.key-visual.store', $this->service), [ + 'file' => $file, + 'scope' => 'service', + ]); + + $response->assertRedirect(route('login')); +}); + +function makeImageUpload(string $name = 'test.png', int $w = 800, int $h = 600): UploadedFile +{ + $path = tempnam(sys_get_temp_dir(), 'cts-svc-img-'); + if ($path === false) { + throw new RuntimeException('Temp-Datei konnte nicht erstellt werden.'); + } + + $image = imagecreatetruecolor($w, $h); + if ($image === false) { + throw new RuntimeException('Bild konnte nicht erstellt werden.'); + } + + $blue = imagecolorallocate($image, 0, 0, 255); + imagefill($image, 0, 0, $blue); + imagepng($image, $path); + imagedestroy($image); + + return new UploadedFile($path, $name, 'image/png', null, true); +}