- Update propresenter ref path from ../propresenter-work/ to ../propresenter/ - Fix ProFileImportTest assertion for CCLI-based upsert behavior - Replace Mockery alias mocks with testable subclass pattern in PlaylistExportTest, eliminating @runInSeparateProcess requirement - Use DI (app()) for PlaylistExportService in controller for testability - All 302 tests pass (was 285 pass + 17 fail)
207 lines
8.5 KiB
PHP
207 lines
8.5 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Song;
|
|
use App\Models\User;
|
|
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 = $song->groups()->create(['name' => 'Verse 1', 'color' => '#2196F3', 'order' => 0]);
|
|
$verse->slides()->create(['order' => 0, 'text_content' => 'First line of verse']);
|
|
$verse->slides()->create(['order' => 1, 'text_content' => 'Second line of verse']);
|
|
|
|
$chorus = $song->groups()->create(['name' => 'Chorus', 'color' => '#F44336', 'order' => 1]);
|
|
$chorus->slides()->create(['order' => 0, 'text_content' => 'Chorus text']);
|
|
|
|
$arrangement = $song->arrangements()->create(['name' => 'normal', 'is_default' => true]);
|
|
$arrangement->arrangementGroups()->create(['song_group_id' => $verse->id, 'order' => 0]);
|
|
$arrangement->arrangementGroups()->create(['song_group_id' => $chorus->id, 'order' => 1]);
|
|
$arrangement->arrangementGroups()->create(['song_group_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('../propresenter/ref/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);
|
|
$this->assertGreaterThan(0, $song->groups()->count());
|
|
|
|
$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();
|
|
|
|
// 1. Import the reference .pro file
|
|
$sourcePath = base_path('../propresenter/ref/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(['groups.slides', 'arrangements.arrangementGroups.group'])->find($songId);
|
|
$this->assertNotNull($originalSong);
|
|
|
|
// Snapshot original data
|
|
$originalGroups = $originalSong->groups->sortBy('order')->values();
|
|
$originalArrangements = $originalSong->arrangements;
|
|
|
|
// 2. Export as .pro
|
|
$exportResponse = $this->actingAs($user)->get("/api/songs/{$songId}/download-pro");
|
|
$exportResponse->assertOk();
|
|
|
|
// Save exported content to temp file — BinaryFileResponse delivers a real file
|
|
$tempPath = sys_get_temp_dir().'/roundtrip-test-'.uniqid().'.pro';
|
|
/** @var \Symfony\Component\HttpFoundation\BinaryFileResponse $baseResponse */
|
|
$baseResponse = $exportResponse->baseResponse;
|
|
copy($baseResponse->getFile()->getPathname(), $tempPath);
|
|
|
|
// 3. Re-import the exported file as a new song (different ccli to avoid upsert)
|
|
// Use the ProPresenter parser directly to read and verify
|
|
$reImported = \ProPresenter\Parser\ProFileReader::read($tempPath);
|
|
@unlink($tempPath);
|
|
|
|
// 4. Assert song name
|
|
$this->assertSame($originalSong->title, $reImported->getName());
|
|
|
|
// 5. Assert groups match (same names, same order)
|
|
$reImportedGroups = $reImported->getGroups();
|
|
$this->assertCount($originalGroups->count(), $reImportedGroups, 'Group count mismatch');
|
|
|
|
foreach ($originalGroups as $index => $originalGroup) {
|
|
$reImportedGroup = $reImportedGroups[$index];
|
|
$this->assertSame($originalGroup->name, $reImportedGroup->getName(), "Group name mismatch at index {$index}");
|
|
|
|
// Assert slides within group
|
|
$originalSlides = $originalGroup->slides->sortBy('order')->values();
|
|
$reImportedSlides = $reImported->getSlidesForGroup($reImportedGroup);
|
|
$this->assertCount($originalSlides->count(), $reImportedSlides, "Slide count mismatch for group '{$originalGroup->name}'");
|
|
|
|
foreach ($originalSlides as $slideIndex => $originalSlide) {
|
|
$reImportedSlide = $reImportedSlides[$slideIndex];
|
|
|
|
$this->assertSame(
|
|
$originalSlide->text_content,
|
|
$reImportedSlide->getPlainText(),
|
|
"Slide text mismatch for group '{$originalGroup->name}' slide {$slideIndex}"
|
|
);
|
|
|
|
// Assert translation if present
|
|
if ($originalSlide->text_content_translated) {
|
|
$this->assertTrue($reImportedSlide->hasTranslation(), "Expected translation for group '{$originalGroup->name}' slide {$slideIndex}");
|
|
$this->assertSame(
|
|
$originalSlide->text_content_translated,
|
|
$reImportedSlide->getTranslation()?->getPlainText(),
|
|
"Translation text mismatch for group '{$originalGroup->name}' slide {$slideIndex}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 6. Assert arrangements match (same names, same group order)
|
|
$reImportedArrangements = $reImported->getArrangements();
|
|
$this->assertCount($originalArrangements->count(), $reImportedArrangements, 'Arrangement count mismatch');
|
|
|
|
foreach ($originalArrangements as $index => $originalArrangement) {
|
|
$reImportedArrangement = $reImported->getArrangementByName($originalArrangement->name);
|
|
$this->assertNotNull($reImportedArrangement, "Arrangement '{$originalArrangement->name}' not found in re-import");
|
|
|
|
$originalGroupNames = $originalArrangement->arrangementGroups
|
|
->sortBy('order')
|
|
->map(fn ($ag) => $ag->group?->name)
|
|
->filter()
|
|
->values()
|
|
->toArray();
|
|
|
|
$reImportedGroupNames = array_map(
|
|
fn ($group) => $group->getName(),
|
|
$reImported->getGroupsForArrangement($reImportedArrangement)
|
|
);
|
|
|
|
$this->assertSame(
|
|
$originalGroupNames,
|
|
$reImportedGroupNames,
|
|
"Arrangement '{$originalArrangement->name}' group order mismatch"
|
|
);
|
|
}
|
|
|
|
// 7. Assert CCLI metadata
|
|
if ($originalSong->ccli_id) {
|
|
$this->assertSame((int) $originalSong->ccli_id, $reImported->getCcliSongNumber());
|
|
}
|
|
if ($originalSong->author) {
|
|
$this->assertSame($originalSong->author, $reImported->getCcliAuthor());
|
|
}
|
|
}
|
|
|
|
private function assertStringContains(string $needle, ?string $haystack): void
|
|
{
|
|
$this->assertNotNull($haystack);
|
|
$this->assertTrue(
|
|
str_contains($haystack, $needle),
|
|
"Failed asserting that '{$haystack}' contains '{$needle}'"
|
|
);
|
|
}
|
|
}
|