user = User::factory()->create(); }); test('song pdf download returns pdf with correct content type', function () { $song = Song::factory()->create(['title' => 'Amazing Grace']); $arrangement = SongArrangement::factory()->create([ 'song_id' => $song->id, 'name' => 'Normal', ]); $group = SongGroup::factory()->create([ 'song_id' => $song->id, 'name' => 'Verse 1', 'color' => '#3B82F6', 'order' => 1, ]); SongSlide::factory()->create([ 'song_group_id' => $group->id, 'order' => 1, 'text_content' => 'Amazing grace how sweet the sound', ]); SongArrangementGroup::factory()->create([ 'song_arrangement_id' => $arrangement->id, 'song_group_id' => $group->id, 'order' => 1, ]); $response = $this->actingAs($this->user) ->get(route('songs.pdf', [$song, $arrangement])); $response->assertOk(); $response->assertHeader('Content-Type', 'application/pdf'); }); test('song pdf contains song title in filename', function () { $song = Song::factory()->create(['title' => 'Amazing Grace']); $arrangement = SongArrangement::factory()->create([ 'song_id' => $song->id, 'name' => 'Normal', ]); $response = $this->actingAs($this->user) ->get(route('songs.pdf', [$song, $arrangement])); $response->assertOk(); $contentDisposition = $response->headers->get('Content-Disposition'); expect($contentDisposition)->toContain('amazing-grace'); expect($contentDisposition)->toContain('normal'); }); test('song pdf includes arrangement groups in order', function () { $song = Song::factory()->create(['title' => 'Großer Gott']); $arrangement = SongArrangement::factory()->create([ 'song_id' => $song->id, 'name' => 'Normal', ]); $verse = SongGroup::factory()->create([ 'song_id' => $song->id, 'name' => 'Strophe 1', 'color' => '#3B82F6', 'order' => 1, ]); $chorus = SongGroup::factory()->create([ 'song_id' => $song->id, 'name' => 'Refrain', 'color' => '#10B981', 'order' => 2, ]); SongSlide::factory()->create([ 'song_group_id' => $verse->id, 'order' => 1, 'text_content' => 'Großer Gott wir loben dich', ]); SongSlide::factory()->create([ 'song_group_id' => $chorus->id, 'order' => 1, 'text_content' => 'Heilig heilig heilig', ]); // Arrangement: Strophe 1 -> Refrain SongArrangementGroup::factory()->create([ 'song_arrangement_id' => $arrangement->id, 'song_group_id' => $verse->id, 'order' => 1, ]); SongArrangementGroup::factory()->create([ 'song_arrangement_id' => $arrangement->id, 'song_group_id' => $chorus->id, 'order' => 2, ]); $response = $this->actingAs($this->user) ->get(route('songs.pdf', [$song, $arrangement])); $response->assertOk(); $response->assertHeader('Content-Type', 'application/pdf'); }); test('song pdf includes translated text when present', function () { $song = Song::factory()->create([ 'title' => 'Amazing Grace', 'has_translation' => true, ]); $arrangement = SongArrangement::factory()->create([ 'song_id' => $song->id, 'name' => 'Normal', ]); $group = SongGroup::factory()->create([ 'song_id' => $song->id, 'name' => 'Verse 1', 'order' => 1, ]); SongSlide::factory()->create([ 'song_group_id' => $group->id, 'order' => 1, 'text_content' => 'Amazing grace how sweet the sound', 'text_content_translated' => 'Erstaunliche Gnade wie süß der Klang', ]); SongArrangementGroup::factory()->create([ 'song_arrangement_id' => $arrangement->id, 'song_group_id' => $group->id, 'order' => 1, ]); $response = $this->actingAs($this->user) ->get(route('songs.pdf', [$song, $arrangement])); $response->assertOk(); $response->assertHeader('Content-Type', 'application/pdf'); }); test('song pdf includes copyright footer', function () { $song = Song::factory()->create([ 'title' => 'Amazing Grace', 'copyright_text' => 'John Newton, 1779', 'ccli_id' => '22025', ]); $arrangement = SongArrangement::factory()->create([ 'song_id' => $song->id, 'name' => 'Normal', ]); $response = $this->actingAs($this->user) ->get(route('songs.pdf', [$song, $arrangement])); $response->assertOk(); $response->assertHeader('Content-Type', 'application/pdf'); }); test('song pdf returns 404 when arrangement does not belong to song', function () { $song = Song::factory()->create(); $otherSong = Song::factory()->create(); $arrangement = SongArrangement::factory()->create([ 'song_id' => $otherSong->id, ]); $response = $this->actingAs($this->user) ->get(route('songs.pdf', [$song, $arrangement])); $response->assertNotFound(); }); test('song pdf requires authentication', function () { $song = Song::factory()->create(); $arrangement = SongArrangement::factory()->create([ 'song_id' => $song->id, ]); $response = $this->get(route('songs.pdf', [$song, $arrangement])); $response->assertRedirect(route('login')); }); test('song pdf handles german umlauts correctly', function () { $song = Song::factory()->create([ 'title' => 'Großer Gott wir loben dich', 'copyright_text' => 'Überliefert', ]); $arrangement = SongArrangement::factory()->create([ 'song_id' => $song->id, 'name' => 'Übung', ]); $group = SongGroup::factory()->create([ 'song_id' => $song->id, 'name' => 'Strophe 1', 'order' => 1, ]); SongSlide::factory()->create([ 'song_group_id' => $group->id, 'order' => 1, 'text_content' => 'Großer Gott wir loben dich', ]); SongArrangementGroup::factory()->create([ 'song_arrangement_id' => $arrangement->id, 'song_group_id' => $group->id, 'order' => 1, ]); $response = $this->actingAs($this->user) ->get(route('songs.pdf', [$song, $arrangement])); $response->assertOk(); $response->assertHeader('Content-Type', 'application/pdf'); // Filename should contain slug with umlauts handled $contentDisposition = $response->headers->get('Content-Disposition'); expect($contentDisposition)->toContain('.pdf'); }); test('song pdf works with empty arrangement (no groups)', function () { $song = Song::factory()->create(['title' => 'Empty Song']); $arrangement = SongArrangement::factory()->create([ 'song_id' => $song->id, 'name' => 'Leer', ]); $response = $this->actingAs($this->user) ->get(route('songs.pdf', [$song, $arrangement])); $response->assertOk(); $response->assertHeader('Content-Type', 'application/pdf'); }); test('song preview returns json with groups in arrangement order', function () { $song = Song::factory()->create([ 'title' => 'Testlied', 'copyright_text' => 'Test Copyright', 'ccli_id' => '123456', ]); $verse = SongGroup::factory()->create([ 'song_id' => $song->id, 'name' => 'Strophe 1', 'color' => '#3b82f6', ]); $chorus = SongGroup::factory()->create([ 'song_id' => $song->id, 'name' => 'Refrain', 'color' => '#ef4444', ]); SongSlide::factory()->create([ 'song_group_id' => $verse->id, 'order' => 1, 'text_content' => 'Strophe Text', ]); SongSlide::factory()->create([ 'song_group_id' => $chorus->id, 'order' => 1, 'text_content' => 'Refrain Text', ]); $arrangement = SongArrangement::factory()->create([ 'song_id' => $song->id, 'name' => 'Normal', ]); // Order: Chorus first, then Verse SongArrangementGroup::factory()->create([ 'song_arrangement_id' => $arrangement->id, 'song_group_id' => $chorus->id, 'order' => 1, ]); SongArrangementGroup::factory()->create([ 'song_arrangement_id' => $arrangement->id, 'song_group_id' => $verse->id, 'order' => 2, ]); $response = $this->actingAs($this->user)->getJson(route('songs.preview', [$song, $arrangement])); $response->assertOk(); $response->assertJsonStructure([ 'song' => ['id', 'title', 'copyright_text', 'ccli_id'], 'arrangement' => ['id', 'name'], 'groups' => [ '*' => [ 'name', 'color', 'slides' => [ '*' => ['text_content', 'text_content_translated'], ], ], ], ]); // Chorus should be first (order=1), Verse second (order=2) $data = $response->json(); expect($data['groups'][0]['name'])->toBe('Refrain'); expect($data['groups'][1]['name'])->toBe('Strophe 1'); expect($data['groups'][0]['slides'][0]['text_content'])->toBe('Refrain Text'); }); test('song preview includes translation text when slides have translations', function () { $song = Song::factory()->create(['title' => 'Lied mit Übersetzung']); $group = SongGroup::factory()->create([ 'song_id' => $song->id, 'name' => 'Verse', ]); SongSlide::factory()->create([ 'song_group_id' => $group->id, 'order' => 1, 'text_content' => 'Original Text', 'text_content_translated' => 'Translated Text', ]); $arrangement = SongArrangement::factory()->create([ 'song_id' => $song->id, ]); SongArrangementGroup::factory()->create([ 'song_arrangement_id' => $arrangement->id, 'song_group_id' => $group->id, 'order' => 1, ]); $response = $this->actingAs($this->user)->getJson(route('songs.preview', [$song, $arrangement])); $response->assertOk(); $data = $response->json(); expect($data['groups'][0]['slides'][0]['text_content'])->toBe('Original Text'); expect($data['groups'][0]['slides'][0]['text_content_translated'])->toBe('Translated Text'); }); test('song preview returns 404 when arrangement does not belong to song', function () { $song = Song::factory()->create(); $otherSong = Song::factory()->create(); $arrangement = SongArrangement::factory()->create(['song_id' => $otherSong->id]); $response = $this->actingAs($this->user)->getJson(route('songs.preview', [$song, $arrangement])); $response->assertNotFound(); }); test('song preview requires authentication', function () { $song = Song::factory()->create(); $arrangement = SongArrangement::factory()->create(['song_id' => $song->id]); $response = $this->getJson(route('songs.preview', [$song, $arrangement])); $response->assertUnauthorized(); });