pp-planer/tests/Feature/ExportPreAddedProBundleTest.php
Thorsten Bus 42b8b5f428 fix(export): copyright/blank slides, bundle-relative media, probundle injection, song links
- populate COPYRIGHT (title/author/copyright/CCLI) + blank slides on every song; songHasContent ignores locked sections
- foreground info/moderation images now bundle-relative (fixes blank images)
- pre-added .probundle injection: Zip64-fix + verbatim .pro extraction (fixes empty bundle)
- nametag subtitle split (text + subtitle); smaller non-bold render
- skip songs with no content slides at export with German warning
- link service agenda songs to SongDB edit modal via #song-<id>
- allow CCLI import of metadata-only songs (no lyric sections)
- expose has_content_slides on service songs; show "Keine Inhaltsfolien"
2026-06-21 09:58:55 +02:00

161 lines
5.4 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\ExportProFile;
use App\Models\Label;
use App\Models\Service;
use App\Models\ServiceSong;
use App\Models\Song;
use App\Services\PlaylistExportService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use ProPresenter\Parser\PresentationBundle;
use ProPresenter\Parser\ProBundleWriter;
use ProPresenter\Parser\ProFileGenerator;
use Tests\TestCase;
/**
* FIX 3: A pre-added .probundle (prefix/postfix ExportProFile) must be unpacked
* with the parser's ProBundleReader (Zip64-aware) so the embedded .pro bytes are
* read correctly and the presentation appears in the exported playlist.
*/
final class ExportPreAddedProBundleTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Storage::fake('public');
}
private function createSongWithContent(string $title = 'Test Song'): Song
{
$song = Song::create([
'title' => $title,
'ccli_id' => fake()->unique()->numerify('#####'),
'author' => 'Test Author',
]);
$label = Label::firstOrCreate(['name' => 'Verse 1 - '.$title], ['color' => '#2196F3']);
$section = $song->sections()->create(['label_id' => $label->id, 'order' => 0]);
$section->slides()->create(['order' => 0, 'text_content' => 'Inhalt']);
$arrangement = $song->arrangements()->create(['name' => 'normal', 'is_default' => true]);
$arrangement->arrangementSections()->create(['song_section_id' => $section->id, 'order' => 0]);
return $song;
}
/** Build a minimal valid .probundle and register it as a prefix ExportProFile on the local disk. */
private function registerPrefixBundle(string $proName = 'IntroBundle'): ExportProFile
{
$song = ProFileGenerator::generate(
$proName,
[[
'name' => 'Intro',
'color' => [0, 0, 0, 1],
'slides' => [['text' => 'Willkommen']],
]],
[['name' => 'normal', 'groupNames' => ['Intro']]],
);
$bundle = new PresentationBundle($song, $proName.'.pro', []);
$tmp = tempnam(sys_get_temp_dir(), 'fixture-').'.probundle';
ProBundleWriter::write($bundle, $tmp);
$bytes = file_get_contents($tmp);
@unlink($tmp);
$storedPath = 'export-pro-files/'.$proName.'.probundle';
Storage::disk('local')->put($storedPath, $bytes);
return ExportProFile::create([
'type' => 'prefix',
'original_name' => $proName.'.probundle',
'stored_path' => $storedPath,
'order' => 1,
]);
}
public function test_vorab_hinzugefuegtes_probundle_ergibt_nicht_leere_playlist(): void
{
$service = Service::factory()->create(['finalized_at' => now(), 'title' => 'Mit Prefix']);
$song = $this->createSongWithContent('Hauptlied');
ServiceSong::create([
'service_id' => $service->id,
'song_id' => $song->id,
'cts_song_name' => 'Hauptlied',
'order' => 1,
]);
$this->registerPrefixBundle('IntroBundle');
$capturedItems = [];
$capturedFiles = [];
$exportService = new class($capturedItems, $capturedFiles) extends PlaylistExportService
{
/** @var array<int, array> */
private array $items;
/** @var array<string, string> */
private array $files;
public function __construct(array &$items, array &$files)
{
$this->items = &$items;
$this->files = &$files;
}
protected function writeProFile(string $path, string $name, array $groups, array $arrangements): void
{
file_put_contents($path, 'mock-pro:'.$name);
}
protected function writePlaylistFile(string $path, string $name, array $items, array $embeddedFiles): void
{
$this->items = $items;
$this->files = $embeddedFiles;
file_put_contents($path, 'mock-playlist:'.$name);
}
};
$result = $exportService->generatePlaylist($service);
$this->assertFileExists($result['path']);
// The prefix .pro must be present as a playlist item and embedded with non-empty bytes.
$prefixItem = collect($capturedItems)->first(
fn ($item) => str_starts_with($item['path'] ?? '', 'PREFIX_') && str_ends_with($item['path'], '.pro')
);
$this->assertNotNull($prefixItem, 'Pre-added .probundle should produce a PREFIX_*.pro playlist item.');
$embeddedKey = $prefixItem['path'];
$this->assertArrayHasKey($embeddedKey, $capturedFiles);
$this->assertNotSame('', $capturedFiles[$embeddedKey], 'Embedded .pro from .probundle must be non-empty.');
$this->assertGreaterThan(0, strlen($capturedFiles[$embeddedKey]));
$this->cleanupTempDir($result['temp_dir']);
}
private function cleanupTempDir(string $dir): void
{
if (! is_dir($dir)) {
return;
}
foreach (scandir($dir) as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$path = $dir.'/'.$item;
is_dir($path) ? $this->cleanupTempDir($path) : unlink($path);
}
rmdir($dir);
}
}