pp-planer/tests/Feature/ExportSkipEmptyLockedSongTest.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

200 lines
6.7 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Label;
use App\Models\Service;
use App\Models\ServiceSong;
use App\Models\Song;
use App\Services\PlaylistExportService;
use App\Services\ProBundleExportService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use RuntimeException;
use Tests\TestCase;
/**
* FIX 5: A song whose default arrangement contains only EMPTY locked prefix/postfix
* sections has zero content slides. It must be skipped (not exported as an empty .pro)
* and a German warning must be surfaced. A normal song still exports.
*/
final class ExportSkipEmptyLockedSongTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Storage::fake('public');
}
/** Song whose default arrangement only references an empty, locked section. */
private function createEmptyLockedSong(string $title = 'Leeres Lied'): Song
{
$song = Song::create([
'title' => $title,
'ccli_id' => fake()->unique()->numerify('#####'),
'author' => 'Author',
]);
$label = Label::firstOrCreate(['name' => 'Intro - '.$title], ['color' => '#000000']);
// Locked section with NO slides.
$section = $song->sections()->create([
'label_id' => $label->id,
'order' => 0,
'locked' => true,
]);
$arrangement = $song->arrangements()->create(['name' => 'normal', 'is_default' => true]);
$arrangement->arrangementSections()->create(['song_section_id' => $section->id, 'order' => 0]);
return $song;
}
private function createNormalSong(string $title = 'Normales Lied'): Song
{
$song = Song::create([
'title' => $title,
'ccli_id' => fake()->unique()->numerify('#####'),
'author' => 'Author',
]);
$label = Label::firstOrCreate(['name' => 'Verse 1 - '.$title], ['color' => '#2196F3']);
$section = $song->sections()->create(['label_id' => $label->id, 'order' => 0, 'locked' => false]);
$section->slides()->create(['order' => 0, 'text_content' => 'Strophentext']);
$arrangement = $song->arrangements()->create(['name' => 'normal', 'is_default' => true]);
$arrangement->arrangementSections()->create(['song_section_id' => $section->id, 'order' => 0]);
return $song;
}
private function testable_export_service(): PlaylistExportService
{
return new class extends PlaylistExportService
{
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
{
$content = 'mock-playlist:'.$name;
foreach ($items as $item) {
$content .= "\n".($item['name'] ?? '');
}
file_put_contents($path, $content);
}
};
}
public function test_playlist_legacy_ueberspringt_leeres_locked_lied_mit_warnung(): void
{
$service = Service::factory()->create(['finalized_at' => now(), 'title' => 'Mix Service']);
$emptySong = $this->createEmptyLockedSong('Leeres Lied');
$normalSong = $this->createNormalSong('Normales Lied');
ServiceSong::create([
'service_id' => $service->id,
'song_id' => $emptySong->id,
'cts_song_name' => 'Leeres Lied',
'order' => 1,
]);
ServiceSong::create([
'service_id' => $service->id,
'song_id' => $normalSong->id,
'cts_song_name' => 'Normales Lied',
'order' => 2,
]);
$result = $this->testable_export_service()->generatePlaylist($service);
$content = file_get_contents($result['path']);
// Empty locked song skipped, normal song exported.
$this->assertStringNotContainsString('Leeres Lied', $content);
$this->assertStringContainsString('Normales Lied', $content);
// German warning present.
$this->assertContains(
"Lied 'Leeres Lied' übersprungen: keine Inhaltsfolien.",
$result['warnings'],
);
$this->assertGreaterThanOrEqual(1, $result['skipped']);
$this->cleanupTempDir($result['temp_dir']);
}
public function test_probundle_agenda_song_wirft_fehler_bei_leerem_locked_lied(): void
{
$service = Service::factory()->create(['finalized_at' => now()]);
$emptySong = $this->createEmptyLockedSong('Leeres Lied');
$serviceSong = ServiceSong::create([
'service_id' => $service->id,
'song_id' => $emptySong->id,
'cts_song_name' => 'Leeres Lied',
'order' => 1,
]);
$agendaItem = \App\Models\ServiceAgendaItem::factory()->create([
'service_id' => $service->id,
'title' => 'Leeres Lied',
'service_song_id' => $serviceSong->id,
'sort_order' => 1,
'is_before_event' => false,
]);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('keine Inhaltsfolien');
app(ProBundleExportService::class)->generateAgendaItemBundle($agendaItem);
}
public function test_probundle_agenda_song_exportiert_normales_lied(): void
{
$service = Service::factory()->create(['finalized_at' => now()]);
$normalSong = $this->createNormalSong('Normales Lied');
$serviceSong = ServiceSong::create([
'service_id' => $service->id,
'song_id' => $normalSong->id,
'cts_song_name' => 'Normales Lied',
'order' => 1,
]);
$agendaItem = \App\Models\ServiceAgendaItem::factory()->create([
'service_id' => $service->id,
'title' => 'Normales Lied',
'service_song_id' => $serviceSong->id,
'sort_order' => 1,
'is_before_event' => false,
]);
$bundlePath = app(ProBundleExportService::class)->generateAgendaItemBundle($agendaItem);
$this->assertFileExists($bundlePath);
$this->assertGreaterThan(0, filesize($bundlePath));
@unlink($bundlePath);
}
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);
}
}