open($tmp, ZipArchive::CREATE | ZipArchive::OVERWRITE); $zip->addFromString('presentation.pro', $proBytes); foreach ($mediaFiles as $name => $content) { $zip->addFromString($name, $content); } $zip->close(); $bytes = file_get_contents($tmp); @unlink($tmp); return $bytes; } private function createSongWithContent(string $title): Song { $song = Song::create([ 'title' => $title, 'ccli_id' => fake()->unique()->numerify('#####'), 'author' => 'Test Author', 'copyright_text' => 'Test Publisher', ]); $label = Label::firstOrCreate( ['name' => 'Verse - '.$title], ['color' => '#2196F3'], ); $section = $song->sections()->create(['label_id' => $label->id, 'order' => 0]); $section->slides()->create(['order' => 0, 'text_content' => 'Erste Zeile']); $arrangement = $song->arrangements()->create(['name' => 'normal', 'is_default' => true]); $arrangement->arrangementSections()->create(['song_section_id' => $section->id, 'order' => 0]); return $song; } 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 cleanupTempDir(string $dir): void { if (! is_dir($dir)) { return; } $items = scandir($dir); if ($items === false) { return; } foreach ($items as $item) { if ($item === '.' || $item === '..') { continue; } $path = $dir.'/'.$item; is_dir($path) ? $this->cleanupTempDir($path) : unlink($path); } rmdir($dir); } public function test_probundle_mis_stored_as_zip_wird_mit_folien_injiziert(): void { $service = Service::factory()->create(['title' => 'Bundle Service', 'date' => now()]); $song = $this->createSongWithContent('Lied'); ServiceSong::create([ 'service_id' => $service->id, 'song_id' => $song->id, 'cts_song_name' => 'Lied', 'order' => 1, ]); // Real inner .pro with a group of 2 content slides. $proBytes = $this->realProBytes('intro', [ ['name' => 'Verse', 'color' => [0.5, 0.5, 0.5, 1.0], 'slides' => [ ['text' => 'Folie eins'], ['text' => 'Folie zwei'], ]], ], [['name' => 'normal', 'groupNames' => ['Verse']]]); $bundleBytes = $this->probundleBytes($proBytes, ['hintergrund.jpg' => 'jpg-bytes']); // Reproduce the mis-stored state: original name .probundle, stored path .zip. $storedPath = 'export-pro-files/prefix/'.\Illuminate\Support\Str::random(40).'.zip'; Storage::disk('local')->put($storedPath, $bundleBytes); ExportProFile::create([ 'type' => 'prefix', 'original_name' => 'intro.probundle', 'stored_path' => $storedPath, 'order' => 1, ]); $result = app(PlaylistExportService::class)->generatePlaylist($service); $playlist = ProPlaylistReader::read($result['path']); $entryNames = array_map(fn ($e) => $e->getName(), $playlist->getEntries()); $this->assertContains('intro', $entryNames, 'Prefix bundle presentation must be injected'); $embeddedSong = $playlist->getEmbeddedSong('PREFIX_1_intro.pro'); $this->assertNotNull($embeddedSong, 'Inner .pro must be embedded, not the raw ZIP'); $this->assertCount(2, $this->allParserSlides($embeddedSong), 'Inner .pro must keep its 2 content slides'); $embeddedFiles = $playlist->getEmbeddedFiles(); $this->assertArrayHasKey('hintergrund.jpg', $embeddedFiles, 'Bundle media must be embedded'); $this->assertSame('jpg-bytes', $embeddedFiles['hintergrund.jpg']); $this->assertSame([], $result['warnings'], 'A slide-bearing bundle must not be skipped'); $this->cleanupTempDir($result['temp_dir']); } public function test_probundle_ohne_folien_wird_uebersprungen_mit_warnung(): void { $service = Service::factory()->create(['title' => 'Leer Service', 'date' => now()]); $song = $this->createSongWithContent('Lied'); ServiceSong::create([ 'service_id' => $service->id, 'song_id' => $song->id, 'cts_song_name' => 'Lied', 'order' => 1, ]); // Real inner .pro that parses fine but has a group with ZERO slides. $proBytes = $this->realProBytes('leer', [ ['name' => 'Verse', 'color' => [0.5, 0.5, 0.5, 1.0], 'slides' => []], ], [['name' => 'normal', 'groupNames' => ['Verse']]]); $bundleBytes = $this->probundleBytes($proBytes); $storedPath = 'export-pro-files/prefix/'.\Illuminate\Support\Str::random(40).'.zip'; Storage::disk('local')->put($storedPath, $bundleBytes); ExportProFile::create([ 'type' => 'prefix', 'original_name' => 'leer.probundle', 'stored_path' => $storedPath, 'order' => 1, ]); $result = app(PlaylistExportService::class)->generatePlaylist($service); $playlist = ProPlaylistReader::read($result['path']); $entryNames = array_map(fn ($e) => $e->getName(), $playlist->getEntries()); $this->assertNotContains('leer', $entryNames, 'A contentless bundle must not be injected'); $this->assertNull($playlist->getEmbeddedSong('PREFIX_1_leer.pro'), 'A contentless bundle must not be embedded'); $this->assertContains( "Export-Datei 'leer.probundle' übersprungen: keine Folien gefunden.", $result['warnings'], 'A contentless bundle must be skipped with a German warning', ); // The valid song is still exported. $this->assertContains('Lied', $entryNames); $this->cleanupTempDir($result['temp_dir']); } }