- 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"
279 lines
9.5 KiB
PHP
279 lines
9.5 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\Models\User;
|
|
use App\Services\PlaylistExportService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use ProPresenter\Parser\ProPlaylistReader;
|
|
use Tests\TestCase;
|
|
use ZipArchive;
|
|
|
|
final class ExportProFileInjectionTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
Storage::fake('local');
|
|
Storage::fake('public');
|
|
}
|
|
|
|
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 buildProbundleZip(string $proContent, array $mediaFiles = []): string
|
|
{
|
|
$tmpPath = tempnam(sys_get_temp_dir(), 'test-probundle-').'.probundle';
|
|
|
|
$zip = new ZipArchive;
|
|
$zip->open($tmpPath, ZipArchive::CREATE | ZipArchive::OVERWRITE);
|
|
$zip->addFromString('song.pro', $proContent);
|
|
|
|
foreach ($mediaFiles as $name => $content) {
|
|
$zip->addFromString($name, $content);
|
|
}
|
|
|
|
$zip->close();
|
|
|
|
return $tmpPath;
|
|
}
|
|
|
|
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_prefix_wird_korrekt_injiziert(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
|
|
$service = Service::factory()->create([
|
|
'title' => 'Bundle Test Service',
|
|
'date' => now(),
|
|
]);
|
|
|
|
$song = $this->createSongWithContent('Test Song');
|
|
ServiceSong::create([
|
|
'service_id' => $service->id,
|
|
'song_id' => $song->id,
|
|
'cts_song_name' => 'Test Song',
|
|
'order' => 1,
|
|
]);
|
|
|
|
$proContent = 'dummy-pro-content';
|
|
$mediaContent = 'dummy-media-bytes';
|
|
$bundlePath = $this->buildProbundleZip($proContent, ['background.jpg' => $mediaContent]);
|
|
|
|
$storedPath = 'export-pro-files/intro.probundle';
|
|
Storage::disk('local')->put($storedPath, file_get_contents($bundlePath));
|
|
unlink($bundlePath);
|
|
|
|
ExportProFile::create([
|
|
'type' => 'prefix',
|
|
'original_name' => 'intro.probundle',
|
|
'stored_path' => $storedPath,
|
|
'order' => 1,
|
|
]);
|
|
|
|
$result = app(PlaylistExportService::class)->generatePlaylist($service);
|
|
$playlist = ProPlaylistReader::read($result['path']);
|
|
|
|
$entries = $playlist->getEntries();
|
|
$entryNames = array_map(fn ($e) => $e->getName(), $entries);
|
|
|
|
$this->assertContains('intro', $entryNames, 'Expected prefix presentation entry named "intro"');
|
|
$songPos = array_search('Test Song', $entryNames, true);
|
|
$prefixPos = array_search('intro', $entryNames, true);
|
|
$this->assertNotFalse($prefixPos, 'Prefix entry not found');
|
|
$this->assertNotFalse($songPos, 'Song entry not found');
|
|
$this->assertLessThan($songPos, $prefixPos, 'Prefix should appear before song');
|
|
|
|
$embeddedFiles = $playlist->getEmbeddedFiles();
|
|
$this->assertArrayHasKey('PREFIX_1_intro.pro', $embeddedFiles, 'Inner .pro should be embedded as PREFIX_1_intro.pro');
|
|
$this->assertSame($proContent, $embeddedFiles['PREFIX_1_intro.pro']);
|
|
|
|
$this->assertArrayHasKey('background.jpg', $embeddedFiles, 'Media file from .probundle should be embedded');
|
|
$this->assertSame($mediaContent, $embeddedFiles['background.jpg']);
|
|
|
|
$this->cleanupTempDir($result['temp_dir']);
|
|
}
|
|
|
|
public function test_probundle_postfix_wird_nach_songs_injiziert(): void
|
|
{
|
|
$service = Service::factory()->create([
|
|
'title' => 'Postfix Bundle Service',
|
|
'date' => now(),
|
|
]);
|
|
|
|
$song = $this->createSongWithContent('Worship Song');
|
|
ServiceSong::create([
|
|
'service_id' => $service->id,
|
|
'song_id' => $song->id,
|
|
'cts_song_name' => 'Worship Song',
|
|
'order' => 1,
|
|
]);
|
|
|
|
$bundlePath = $this->buildProbundleZip('outro-pro-content', ['outro-media.jpg' => 'outro-media-bytes']);
|
|
$storedPath = 'export-pro-files/outro.probundle';
|
|
Storage::disk('local')->put($storedPath, file_get_contents($bundlePath));
|
|
unlink($bundlePath);
|
|
|
|
ExportProFile::create([
|
|
'type' => 'postfix',
|
|
'original_name' => 'outro.probundle',
|
|
'stored_path' => $storedPath,
|
|
'order' => 1,
|
|
]);
|
|
|
|
$result = app(PlaylistExportService::class)->generatePlaylist($service);
|
|
$playlist = ProPlaylistReader::read($result['path']);
|
|
|
|
$entries = $playlist->getEntries();
|
|
$entryNames = array_map(fn ($e) => $e->getName(), $entries);
|
|
|
|
$songPos = array_search('Worship Song', $entryNames, true);
|
|
$postfixPos = array_search('outro', $entryNames, true);
|
|
$this->assertNotFalse($postfixPos, 'Postfix entry not found');
|
|
$this->assertNotFalse($songPos, 'Song entry not found');
|
|
$this->assertLessThan($postfixPos, $songPos, 'Song should appear before postfix');
|
|
|
|
$embeddedFiles = $playlist->getEmbeddedFiles();
|
|
$this->assertArrayHasKey('POSTFIX_1_outro.pro', $embeddedFiles);
|
|
$this->assertSame('outro-pro-content', $embeddedFiles['POSTFIX_1_outro.pro']);
|
|
$this->assertArrayHasKey('outro-media.jpg', $embeddedFiles);
|
|
|
|
$this->cleanupTempDir($result['temp_dir']);
|
|
}
|
|
|
|
public function test_probundle_ohne_pro_eintrag_wird_uebersprungen(): void
|
|
{
|
|
$service = Service::factory()->create([
|
|
'title' => 'Skip Bundle Service',
|
|
'date' => now(),
|
|
]);
|
|
|
|
$song = $this->createSongWithContent('Einziger Song');
|
|
ServiceSong::create([
|
|
'service_id' => $service->id,
|
|
'song_id' => $song->id,
|
|
'cts_song_name' => 'Einziger Song',
|
|
'order' => 1,
|
|
]);
|
|
|
|
$tmpPath = tempnam(sys_get_temp_dir(), 'test-probundle-').'.probundle';
|
|
$zip = new ZipArchive;
|
|
$zip->open($tmpPath, ZipArchive::CREATE | ZipArchive::OVERWRITE);
|
|
$zip->addFromString('media.jpg', 'media-only');
|
|
$zip->close();
|
|
|
|
$storedPath = 'export-pro-files/broken.probundle';
|
|
Storage::disk('local')->put($storedPath, file_get_contents($tmpPath));
|
|
unlink($tmpPath);
|
|
|
|
ExportProFile::create([
|
|
'type' => 'prefix',
|
|
'original_name' => 'broken.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('broken', $entryNames);
|
|
$this->assertContains('Einziger Song', $entryNames);
|
|
|
|
$this->cleanupTempDir($result['temp_dir']);
|
|
}
|
|
|
|
public function test_plain_pro_prefix_weiterhin_korrekt(): void
|
|
{
|
|
$service = Service::factory()->create([
|
|
'title' => 'Plain Pro Service',
|
|
'date' => now(),
|
|
]);
|
|
|
|
$song = $this->createSongWithContent('Lied');
|
|
ServiceSong::create([
|
|
'service_id' => $service->id,
|
|
'song_id' => $song->id,
|
|
'cts_song_name' => 'Lied',
|
|
'order' => 1,
|
|
]);
|
|
|
|
$storedPath = 'export-pro-files/welcome.pro';
|
|
Storage::disk('local')->put($storedPath, 'welcome-pro-bytes');
|
|
|
|
ExportProFile::create([
|
|
'type' => 'prefix',
|
|
'original_name' => 'welcome.pro',
|
|
'stored_path' => $storedPath,
|
|
'order' => 1,
|
|
]);
|
|
|
|
$result = app(PlaylistExportService::class)->generatePlaylist($service);
|
|
$playlist = ProPlaylistReader::read($result['path']);
|
|
|
|
$entries = $playlist->getEntries();
|
|
$entryNames = array_map(fn ($e) => $e->getName(), $entries);
|
|
|
|
$this->assertContains('welcome', $entryNames);
|
|
$prefixPos = array_search('welcome', $entryNames, true);
|
|
$songPos = array_search('Lied', $entryNames, true);
|
|
$this->assertLessThan($songPos, $prefixPos, 'Plain .pro prefix should appear before song');
|
|
|
|
$embeddedFiles = $playlist->getEmbeddedFiles();
|
|
$this->assertArrayHasKey('PREFIX_1_welcome.pro', $embeddedFiles);
|
|
$this->assertSame('welcome-pro-bytes', $embeddedFiles['PREFIX_1_welcome.pro']);
|
|
|
|
$this->cleanupTempDir($result['temp_dir']);
|
|
}
|
|
}
|