'Export Test Song', 'ccli_id' => '54321', 'author' => 'Test Author', 'copyright_text' => 'Test Publisher', 'copyright_year' => 2024, 'publisher' => 'Test Publisher', ]); $verse = Label::firstOrCreate( ['name' => 'Verse 1 - Export Test Song'], ['color' => '#2196F3'], ); $verse->songSlides()->create(['order' => 0, 'text_content' => 'First line of verse']); $verse->songSlides()->create(['order' => 1, 'text_content' => 'Second line of verse']); $chorus = Label::firstOrCreate( ['name' => 'Chorus - Export Test Song'], ['color' => '#F44336'], ); $chorus->songSlides()->create(['order' => 0, 'text_content' => 'Chorus text']); $arrangement = $song->arrangements()->create(['name' => 'normal', 'is_default' => true]); $arrangement->arrangementLabels()->create(['label_id' => $verse->id, 'order' => 0]); $arrangement->arrangementLabels()->create(['label_id' => $chorus->id, 'order' => 1]); $arrangement->arrangementLabels()->create(['label_id' => $verse->id, 'order' => 2]); return $song; } public function test_download_pro_gibt_datei_zurueck(): void { $user = User::factory()->create(); $song = $this->createSongWithContent(); $response = $this->actingAs($user)->get("/api/songs/{$song->id}/download-pro"); $response->assertOk(); $response->assertHeader('content-disposition'); $this->assertStringContains('Export Test Song.pro', $response->headers->get('content-disposition')); } public function test_download_pro_song_ohne_gruppen_gibt_422(): void { $user = User::factory()->create(); $song = Song::factory()->create(); $response = $this->actingAs($user)->get("/api/songs/{$song->id}/download-pro"); $response->assertStatus(422); } public function test_download_pro_erfordert_authentifizierung(): void { $song = Song::factory()->create(); $response = $this->getJson("/api/songs/{$song->id}/download-pro"); $response->assertUnauthorized(); } public function test_download_pro_roundtrip_import_export(): void { $user = User::factory()->create(); $sourcePath = base_path('tests/fixtures/propresenter/Test.pro'); $file = new \Illuminate\Http\UploadedFile($sourcePath, 'Test.pro', 'application/octet-stream', null, true); $importResponse = $this->actingAs($user)->postJson(route('api.songs.import-pro'), ['file' => $file]); $importResponse->assertOk(); $songId = $importResponse->json('songs.0.id'); $song = Song::find($songId); $this->assertNotNull($song); $labelCount = $song->arrangements()->withCount('arrangementLabels')->get()->sum('arrangement_labels_count'); $this->assertGreaterThan(0, $labelCount); $exportResponse = $this->actingAs($user)->get("/api/songs/{$songId}/download-pro"); $exportResponse->assertOk(); } public function test_download_pro_roundtrip_preserves_content(): void { $user = User::factory()->create(); $sourcePath = base_path('tests/fixtures/propresenter/Test.pro'); $file = new \Illuminate\Http\UploadedFile($sourcePath, 'Test.pro', 'application/octet-stream', null, true); $importResponse = $this->actingAs($user)->postJson(route('api.songs.import-pro'), ['file' => $file]); $importResponse->assertOk(); $songId = $importResponse->json('songs.0.id'); $originalSong = Song::with(['arrangements.arrangementLabels.label.songSlides'])->find($songId); $this->assertNotNull($originalSong); $defaultArr = $originalSong->arrangements->firstWhere('is_default', true) ?? $originalSong->arrangements->first(); $this->assertNotNull($defaultArr); $originalArrangementLabels = $defaultArr->arrangementLabels->sortBy('order')->values(); $originalArrangements = $originalSong->arrangements; $exportResponse = $this->actingAs($user)->get("/api/songs/{$songId}/download-pro"); $exportResponse->assertOk(); $tempPath = sys_get_temp_dir().'/roundtrip-test-'.uniqid().'.pro'; /** @var \Symfony\Component\HttpFoundation\BinaryFileResponse $baseResponse */ $baseResponse = $exportResponse->baseResponse; copy($baseResponse->getFile()->getPathname(), $tempPath); $reImported = \ProPresenter\Parser\ProFileReader::read($tempPath); @unlink($tempPath); $this->assertSame($originalSong->title, $reImported->getName()); $reImportedGroups = $reImported->getGroups(); $uniqueOriginalLabels = $originalArrangementLabels ->map(fn ($al) => $al->label) ->filter() ->unique('id') ->values(); $this->assertCount($uniqueOriginalLabels->count(), $reImportedGroups, 'Group count mismatch'); foreach ($uniqueOriginalLabels as $index => $originalLabel) { $reImportedGroup = $reImportedGroups[$index]; $this->assertSame($originalLabel->name, $reImportedGroup->getName(), "Group name mismatch at index {$index}"); $originalSlides = $originalLabel->songSlides->sortBy('order')->values(); $reImportedSlides = $reImported->getSlidesForGroup($reImportedGroup); $this->assertCount($originalSlides->count(), $reImportedSlides, "Slide count mismatch for group '{$originalLabel->name}'"); foreach ($originalSlides as $slideIndex => $originalSlide) { $reImportedSlide = $reImportedSlides[$slideIndex]; $this->assertSame( $originalSlide->text_content, $reImportedSlide->getPlainText(), "Slide text mismatch for group '{$originalLabel->name}' slide {$slideIndex}" ); if ($originalSlide->text_content_translated) { $this->assertTrue($reImportedSlide->hasTranslation(), "Expected translation for group '{$originalLabel->name}' slide {$slideIndex}"); $this->assertSame( $originalSlide->text_content_translated, $reImportedSlide->getTranslation()?->getPlainText(), "Translation text mismatch for group '{$originalLabel->name}' slide {$slideIndex}" ); } } } $reImportedArrangements = $reImported->getArrangements(); $this->assertCount($originalArrangements->count(), $reImportedArrangements, 'Arrangement count mismatch'); foreach ($originalArrangements as $originalArrangement) { $reImportedArrangement = $reImported->getArrangementByName($originalArrangement->name); $this->assertNotNull($reImportedArrangement, "Arrangement '{$originalArrangement->name}' not found in re-import"); $originalGroupNames = $originalArrangement->arrangementLabels ->sortBy('order') ->map(fn ($al) => $al->label?->name) ->filter() ->values() ->toArray(); $reImportedGroupNames = array_map( fn ($group) => $group->getName(), $reImported->getGroupsForArrangement($reImportedArrangement) ); $this->assertSame( $originalGroupNames, $reImportedGroupNames, "Arrangement '{$originalArrangement->name}' group order mismatch" ); } if ($originalSong->ccli_id) { $this->assertSame((int) $originalSong->ccli_id, $reImported->getCcliSongNumber()); } if ($originalSong->author) { $this->assertSame($originalSong->author, $reImported->getCcliAuthor()); } } public function test_export_ohne_service_context_enthaelt_keine_macros(): void { $song = $this->createSongWithContent(); $macro = $this->createMacroForExport('Service Macro'); MacroAssignment::create([ 'part_type' => 'song', 'macro_id' => $macro->id, 'position' => 'all_slides', 'order' => 0, ]); $parserSong = app(ProExportService::class)->generateParserSong($song); foreach ($this->allParserSlides($parserSong) as $slide) { $this->assertFalse($slide->hasMacro()); } } public function test_export_mit_globaler_song_zuweisung_enthaelt_macro_auf_allen_slides(): void { $service = Service::factory()->create(); $song = $this->createSongWithContent(); $macro = $this->createMacroForExport('Alle Folien Macro'); MacroAssignment::create([ 'part_type' => 'song', 'macro_id' => $macro->id, 'position' => 'all_slides', 'order' => 0, ]); $parserSong = app(ProExportService::class)->generateParserSong($song, $service); $slides = $this->allParserSlides($parserSong); $this->assertNotEmpty($slides); foreach ($slides as $slide) { $this->assertTrue($slide->hasMacro()); $this->assertSame('Alle Folien Macro', $slide->getMacroName()); $this->assertSame($macro->uuid, $slide->getMacroUuid()); $this->assertSame('Export Collection', $slide->getMacroCollectionName()); } } public function test_export_mit_ausgeblendeter_macro_enthaelt_keine_macro(): void { $service = Service::factory()->create(); $song = $this->createSongWithContent(); $macro = $this->createMacroForExport('Ausgeblendete Macro', ['hidden_at' => now()]); MacroAssignment::create([ 'part_type' => 'song', 'macro_id' => $macro->id, 'position' => 'all_slides', 'order' => 0, ]); $parserSong = app(ProExportService::class)->generateParserSong($song, $service); foreach ($this->allParserSlides($parserSong) as $slide) { $this->assertFalse($slide->hasMacro()); } } private function createMacroForExport(string $name, array $attributes = []): Macro { $macro = Macro::factory()->create(array_merge([ 'uuid' => '11111111-2222-4333-8444-555555555555', 'name' => $name, ], $attributes)); $collection = MacroCollection::create([ 'uuid' => '99999999-8888-4777-8666-555555555555', 'name' => 'Export Collection', ]); $collection->macros()->attach($macro->id, ['order' => 0]); return $macro; } private function allParserSlides(\ProPresenter\Parser\Song $parserSong): array { $slides = []; foreach ($parserSong->getGroups() as $group) { foreach ($parserSong->getSlidesForGroup($group) as $slide) { $slides[] = $slide; } } return $slides; } private function assertStringContains(string $needle, ?string $haystack): void { $this->assertNotNull($haystack); $this->assertTrue( str_contains($haystack, $needle), "Failed asserting that '{$haystack}' contains '{$needle}'" ); } }