user = User::factory()->create(); $this->actingAs($this->user); }); /* |-------------------------------------------------------------------------- | Upload (store) |-------------------------------------------------------------------------- */ test('upload image creates slide with 1920x1080 jpg', function () { $service = Service::factory()->create(); $file = makePngUploadForSlide('test-slide.png', 800, 600); $response = $this->post(route('slides.store'), [ 'file' => $file, 'type' => 'information', 'service_id' => $service->id, ]); $response->assertStatus(200); $response->assertJson(['success' => true]); $slide = Slide::first(); expect($slide)->not->toBeNull(); expect($slide->type)->toBe('information'); expect($slide->service_id)->toBe($service->id); expect($slide->uploader_name)->toBe($this->user->name); expect($slide->original_filename)->toBe('test-slide.png'); expect(Storage::disk('public')->exists($slide->stored_filename))->toBeTrue(); expect(Storage::disk('public')->exists($slide->thumbnail_filename))->toBeTrue(); }); test('upload image with expire_date stores date on slide', function () { $service = Service::factory()->create(); $file = makePngUploadForSlide('info.png', 400, 300); $response = $this->post(route('slides.store'), [ 'file' => $file, 'type' => 'information', 'service_id' => $service->id, 'expire_date' => '2026-06-15', ]); $response->assertStatus(200); $slide = Slide::first(); expect($slide->expire_date->format('Y-m-d'))->toBe('2026-06-15'); }); test('upload moderation slide without service_id fails', function () { $file = makePngUploadForSlide('mod.png', 400, 300); $response = $this->post(route('slides.store'), [ 'file' => $file, 'type' => 'moderation', ]); $response->assertStatus(422); }); test('upload information slide without service_id is allowed', function () { $file = makePngUploadForSlide('info.png', 400, 300); $response = $this->post(route('slides.store'), [ 'file' => $file, 'type' => 'information', ]); $response->assertStatus(200); expect(Slide::first()->service_id)->toBeNull(); }); test('upload rejects unsupported file types', function () { $file = UploadedFile::fake()->create('document.pdf', 100, 'application/pdf'); $response = $this->post(route('slides.store'), [ 'file' => $file, 'type' => 'information', ]); $response->assertStatus(422); }); test('upload rejects invalid type', function () { $file = makePngUploadForSlide('test.png', 400, 300); $response = $this->postJson(route('slides.store'), [ 'file' => $file, 'type' => 'invalid_type', ]); $response->assertStatus(422); }); test('upload pptx dispatches conversion job', function () { // Mock the conversion service to return a job ID $mockService = Mockery::mock(FileConversionService::class); $mockService->shouldReceive('convertPowerPoint') ->once() ->andReturn('test-job-uuid-1234'); app()->instance(FileConversionService::class, $mockService); // Create a real file with .pptx extension $tempPath = tempnam(sys_get_temp_dir(), 'cts-pptx-'); $pptxPath = $tempPath.'.pptx'; file_put_contents($pptxPath, str_repeat('x', 1024)); $file = new UploadedFile($pptxPath, 'slides.pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', null, true); $response = $this->post(route('slides.store'), [ 'file' => $file, 'type' => 'sermon', 'service_id' => Service::factory()->create()->id, ]); $response->assertStatus(200); $response->assertJsonStructure(['success', 'job_id']); $response->assertJson(['job_id' => 'test-job-uuid-1234']); // Cleanup @unlink($pptxPath); @unlink($tempPath); }); test('upload zip processes contained images', function () { // Create a small zip with a valid image $tempDir = sys_get_temp_dir().'/cts-zip-test-'.uniqid(); mkdir($tempDir, 0775, true); // Create an image inside $imgPath = $tempDir.'/slide1.png'; $image = imagecreatetruecolor(200, 150); $blue = imagecolorallocate($image, 0, 0, 255); imagefill($image, 0, 0, $blue); imagepng($image, $imgPath); imagedestroy($image); // Build zip $zipPath = $tempDir.'/slides.zip'; $zip = new ZipArchive(); $zip->open($zipPath, ZipArchive::CREATE); $zip->addFile($imgPath, 'slide1.png'); $zip->close(); $file = new UploadedFile($zipPath, 'slides.zip', 'application/zip', null, true); $response = $this->post(route('slides.store'), [ 'file' => $file, 'type' => 'moderation', 'service_id' => Service::factory()->create()->id, ]); $response->assertStatus(200); $response->assertJson(['success' => true]); // Cleanup @unlink($imgPath); @unlink($zipPath); @rmdir($tempDir); }); test('upload agenda_item slide with service_agenda_item_id links to agenda item', function () { $service = Service::factory()->create(); $agendaItem = ServiceAgendaItem::factory()->create(['service_id' => $service->id]); $file = makePngUploadForSlide('agenda-slide.png', 800, 600); $response = $this->post(route('slides.store'), [ 'file' => $file, 'type' => 'agenda_item', 'service_id' => $service->id, 'service_agenda_item_id' => $agendaItem->id, ]); $response->assertStatus(200); $response->assertJson(['success' => true]); $slide = Slide::first(); expect($slide)->not->toBeNull(); expect($slide->type)->toBe('agenda_item'); expect($slide->service_id)->toBe($service->id); expect($slide->service_agenda_item_id)->toBe($agendaItem->id); }); test('upload agenda_item slide without service_id fails', function () { $file = makePngUploadForSlide('agenda-slide.png', 400, 300); $response = $this->post(route('slides.store'), [ 'file' => $file, 'type' => 'agenda_item', ]); $response->assertStatus(422); }); test('agenda_item type is accepted in validation', function () { $file = makePngUploadForSlide('test.png', 400, 300); $response = $this->postJson(route('slides.store'), [ 'file' => $file, 'type' => 'agenda_item', 'service_id' => Service::factory()->create()->id, ]); $response->assertStatus(200); }); test('unauthenticated user cannot upload slides', function () { auth()->logout(); $file = makePngUploadForSlide('test.png', 400, 300); $response = $this->post(route('slides.store'), [ 'file' => $file, 'type' => 'information', ]); $response->assertRedirect('/login'); }); /* |-------------------------------------------------------------------------- | Soft Delete (destroy) |-------------------------------------------------------------------------- */ test('delete slide soft deletes it', function () { $slide = Slide::factory()->create([ 'service_id' => Service::factory()->create()->id, 'uploader_name' => $this->user->name, ]); $response = $this->delete(route('slides.destroy', $slide)); $response->assertStatus(200); $response->assertJson(['success' => true]); expect(Slide::find($slide->id))->toBeNull(); expect(Slide::withTrashed()->find($slide->id))->not->toBeNull(); }); test('delete non-existing slide returns 404', function () { $response = $this->delete(route('slides.destroy', 99999)); $response->assertStatus(404); }); /* |-------------------------------------------------------------------------- | Update Expire Date |-------------------------------------------------------------------------- */ test('update expire date on information slide', function () { $slide = Slide::factory()->create([ 'type' => 'information', 'expire_date' => '2026-04-01', 'service_id' => null, ]); $response = $this->patch(route('slides.update-expire-date', $slide), [ 'expire_date' => '2026-08-15', ]); $response->assertStatus(200); $response->assertJson(['success' => true]); expect($slide->fresh()->expire_date->format('Y-m-d'))->toBe('2026-08-15'); }); test('update expire date rejects non-information slides', function () { $slide = Slide::factory()->create([ 'type' => 'sermon', 'service_id' => Service::factory()->create()->id, ]); $response = $this->patch(route('slides.update-expire-date', $slide), [ 'expire_date' => '2026-08-15', ]); $response->assertStatus(422); }); test('expire date must be a valid date', function () { $slide = Slide::factory()->create([ 'type' => 'information', 'service_id' => null, ]); $response = $this->patchJson(route('slides.update-expire-date', $slide), [ 'expire_date' => 'not-a-date', ]); $response->assertStatus(422); }); test('expire date can be set to null', function () { $slide = Slide::factory()->create([ 'type' => 'information', 'expire_date' => '2026-04-01', 'service_id' => null, ]); $response = $this->patch(route('slides.update-expire-date', $slide), [ 'expire_date' => null, ]); $response->assertStatus(200); expect($slide->fresh()->expire_date)->toBeNull(); }); /* |-------------------------------------------------------------------------- | Helpers |-------------------------------------------------------------------------- */ function makePngUploadForSlide(string $name, int $width, int $height): UploadedFile { $path = tempnam(sys_get_temp_dir(), 'cts-slide-'); if ($path === false) { throw new RuntimeException('Temp-Datei konnte nicht erstellt werden.'); } $image = imagecreatetruecolor($width, $height); if ($image === false) { throw new RuntimeException('Bild konnte nicht erstellt werden.'); } $red = imagecolorallocate($image, 255, 0, 0); imagefill($image, 0, 0, $red); imagepng($image, $path); imagedestroy($image); return new UploadedFile($path, $name, 'image/png', null, true); }