pp-planer/tests/Feature/ProFileExportTest.php

309 lines
12 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Label;
use App\Models\Macro;
use App\Models\MacroAssignment;
use App\Models\MacroCollection;
use App\Models\Service;
use App\Models\Song;
use App\Models\User;
use App\Services\ProExportService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class ProFileExportTest extends TestCase
{
use RefreshDatabase;
private function createSongWithContent(): Song
{
$song = Song::create([
'title' => 'Export Test Song',
'ccli_id' => '54321',
'author' => 'Test Author',
'copyright_text' => 'Test Publisher',
'copyright_year' => 2024,
'publisher' => 'Test Publisher',
]);
$verse = Label::firstOrCreate(
['name' => 'Verse 1 - Export Test Song'],
['color' => '#2196F3'],
);
$verse->songSlides()->create(['order' => 0, 'text_content' => 'First line of verse']);
$verse->songSlides()->create(['order' => 1, 'text_content' => 'Second line of verse']);
$chorus = Label::firstOrCreate(
['name' => 'Chorus - Export Test Song'],
['color' => '#F44336'],
);
$chorus->songSlides()->create(['order' => 0, 'text_content' => 'Chorus text']);
$arrangement = $song->arrangements()->create(['name' => 'normal', 'is_default' => true]);
$arrangement->arrangementLabels()->create(['label_id' => $verse->id, 'order' => 0]);
$arrangement->arrangementLabels()->create(['label_id' => $chorus->id, 'order' => 1]);
$arrangement->arrangementLabels()->create(['label_id' => $verse->id, 'order' => 2]);
return $song;
}
public function test_download_pro_gibt_datei_zurueck(): void
{
$user = User::factory()->create();
$song = $this->createSongWithContent();
$response = $this->actingAs($user)->get("/api/songs/{$song->id}/download-pro");
$response->assertOk();
$response->assertHeader('content-disposition');
$this->assertStringContains('Export Test Song.pro', $response->headers->get('content-disposition'));
}
public function test_download_pro_song_ohne_gruppen_gibt_422(): void
{
$user = User::factory()->create();
$song = Song::factory()->create();
$response = $this->actingAs($user)->get("/api/songs/{$song->id}/download-pro");
$response->assertStatus(422);
}
public function test_download_pro_erfordert_authentifizierung(): void
{
$song = Song::factory()->create();
$response = $this->getJson("/api/songs/{$song->id}/download-pro");
$response->assertUnauthorized();
}
public function test_download_pro_roundtrip_import_export(): void
{
$user = User::factory()->create();
$sourcePath = base_path('tests/fixtures/propresenter/Test.pro');
$file = new \Illuminate\Http\UploadedFile($sourcePath, 'Test.pro', 'application/octet-stream', null, true);
$importResponse = $this->actingAs($user)->postJson(route('api.songs.import-pro'), ['file' => $file]);
$importResponse->assertOk();
$songId = $importResponse->json('songs.0.id');
$song = Song::find($songId);
$this->assertNotNull($song);
$labelCount = $song->arrangements()->withCount('arrangementLabels')->get()->sum('arrangement_labels_count');
$this->assertGreaterThan(0, $labelCount);
$exportResponse = $this->actingAs($user)->get("/api/songs/{$songId}/download-pro");
$exportResponse->assertOk();
}
public function test_download_pro_roundtrip_preserves_content(): void
{
$user = User::factory()->create();
$sourcePath = base_path('tests/fixtures/propresenter/Test.pro');
$file = new \Illuminate\Http\UploadedFile($sourcePath, 'Test.pro', 'application/octet-stream', null, true);
$importResponse = $this->actingAs($user)->postJson(route('api.songs.import-pro'), ['file' => $file]);
$importResponse->assertOk();
$songId = $importResponse->json('songs.0.id');
$originalSong = Song::with(['arrangements.arrangementLabels.label.songSlides'])->find($songId);
$this->assertNotNull($originalSong);
$defaultArr = $originalSong->arrangements->firstWhere('is_default', true) ?? $originalSong->arrangements->first();
$this->assertNotNull($defaultArr);
$originalArrangementLabels = $defaultArr->arrangementLabels->sortBy('order')->values();
$originalArrangements = $originalSong->arrangements;
$exportResponse = $this->actingAs($user)->get("/api/songs/{$songId}/download-pro");
$exportResponse->assertOk();
$tempPath = sys_get_temp_dir().'/roundtrip-test-'.uniqid().'.pro';
/** @var \Symfony\Component\HttpFoundation\BinaryFileResponse $baseResponse */
$baseResponse = $exportResponse->baseResponse;
copy($baseResponse->getFile()->getPathname(), $tempPath);
$reImported = \ProPresenter\Parser\ProFileReader::read($tempPath);
@unlink($tempPath);
$this->assertSame($originalSong->title, $reImported->getName());
$reImportedGroups = $reImported->getGroups();
$uniqueOriginalLabels = $originalArrangementLabels
->map(fn ($al) => $al->label)
->filter()
->unique('id')
->values();
$this->assertCount($uniqueOriginalLabels->count(), $reImportedGroups, 'Group count mismatch');
foreach ($uniqueOriginalLabels as $index => $originalLabel) {
$reImportedGroup = $reImportedGroups[$index];
$this->assertSame($originalLabel->name, $reImportedGroup->getName(), "Group name mismatch at index {$index}");
$originalSlides = $originalLabel->songSlides->sortBy('order')->values();
$reImportedSlides = $reImported->getSlidesForGroup($reImportedGroup);
$this->assertCount($originalSlides->count(), $reImportedSlides, "Slide count mismatch for group '{$originalLabel->name}'");
foreach ($originalSlides as $slideIndex => $originalSlide) {
$reImportedSlide = $reImportedSlides[$slideIndex];
$this->assertSame(
$originalSlide->text_content,
$reImportedSlide->getPlainText(),
"Slide text mismatch for group '{$originalLabel->name}' slide {$slideIndex}"
);
if ($originalSlide->text_content_translated) {
$this->assertTrue($reImportedSlide->hasTranslation(), "Expected translation for group '{$originalLabel->name}' slide {$slideIndex}");
$this->assertSame(
$originalSlide->text_content_translated,
$reImportedSlide->getTranslation()?->getPlainText(),
"Translation text mismatch for group '{$originalLabel->name}' slide {$slideIndex}"
);
}
}
}
$reImportedArrangements = $reImported->getArrangements();
$this->assertCount($originalArrangements->count(), $reImportedArrangements, 'Arrangement count mismatch');
foreach ($originalArrangements as $originalArrangement) {
$reImportedArrangement = $reImported->getArrangementByName($originalArrangement->name);
$this->assertNotNull($reImportedArrangement, "Arrangement '{$originalArrangement->name}' not found in re-import");
$originalGroupNames = $originalArrangement->arrangementLabels
->sortBy('order')
->map(fn ($al) => $al->label?->name)
->filter()
->values()
->toArray();
$reImportedGroupNames = array_map(
fn ($group) => $group->getName(),
$reImported->getGroupsForArrangement($reImportedArrangement)
);
$this->assertSame(
$originalGroupNames,
$reImportedGroupNames,
"Arrangement '{$originalArrangement->name}' group order mismatch"
);
}
if ($originalSong->ccli_id) {
$this->assertSame((int) $originalSong->ccli_id, $reImported->getCcliSongNumber());
}
if ($originalSong->author) {
$this->assertSame($originalSong->author, $reImported->getCcliAuthor());
}
}
public function test_export_ohne_service_context_enthaelt_keine_macros(): void
{
$song = $this->createSongWithContent();
$macro = $this->createMacroForExport('Service Macro');
MacroAssignment::create([
'part_type' => 'song',
'macro_id' => $macro->id,
'position' => 'all_slides',
'order' => 0,
]);
$parserSong = app(ProExportService::class)->generateParserSong($song);
foreach ($this->allParserSlides($parserSong) as $slide) {
$this->assertFalse($slide->hasMacro());
}
}
public function test_export_mit_globaler_song_zuweisung_enthaelt_macro_auf_allen_slides(): void
{
$service = Service::factory()->create();
$song = $this->createSongWithContent();
$macro = $this->createMacroForExport('Alle Folien Macro');
MacroAssignment::create([
'part_type' => 'song',
'macro_id' => $macro->id,
'position' => 'all_slides',
'order' => 0,
]);
$parserSong = app(ProExportService::class)->generateParserSong($song, $service);
$slides = $this->allParserSlides($parserSong);
$this->assertNotEmpty($slides);
foreach ($slides as $slide) {
$this->assertTrue($slide->hasMacro());
$this->assertSame('Alle Folien Macro', $slide->getMacroName());
$this->assertSame($macro->uuid, $slide->getMacroUuid());
$this->assertSame('Export Collection', $slide->getMacroCollectionName());
}
}
public function test_export_mit_ausgeblendeter_macro_enthaelt_keine_macro(): void
{
$service = Service::factory()->create();
$song = $this->createSongWithContent();
$macro = $this->createMacroForExport('Ausgeblendete Macro', ['hidden_at' => now()]);
MacroAssignment::create([
'part_type' => 'song',
'macro_id' => $macro->id,
'position' => 'all_slides',
'order' => 0,
]);
$parserSong = app(ProExportService::class)->generateParserSong($song, $service);
foreach ($this->allParserSlides($parserSong) as $slide) {
$this->assertFalse($slide->hasMacro());
}
}
private function createMacroForExport(string $name, array $attributes = []): Macro
{
$macro = Macro::factory()->create(array_merge([
'uuid' => '11111111-2222-4333-8444-555555555555',
'name' => $name,
], $attributes));
$collection = MacroCollection::create([
'uuid' => '99999999-8888-4777-8666-555555555555',
'name' => 'Export Collection',
]);
$collection->macros()->attach($macro->id, ['order' => 0]);
return $macro;
}
private function allParserSlides(\ProPresenter\Parser\Song $parserSong): array
{
$slides = [];
foreach ($parserSong->getGroups() as $group) {
foreach ($parserSong->getSlidesForGroup($group) as $slide) {
$slides[] = $slide;
}
}
return $slides;
}
private function assertStringContains(string $needle, ?string $haystack): void
{
$this->assertNotNull($haystack);
$this->assertTrue(
str_contains($haystack, $needle),
"Failed asserting that '{$haystack}' contains '{$needle}'"
);
}
}