- 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"
200 lines
6.7 KiB
PHP
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);
|
|
}
|
|
}
|