- 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"
161 lines
5.4 KiB
PHP
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);
|
|
}
|
|
}
|