test(services): add PlaylistExportTest for .proplaylist download scenarios

- Finalized service with matched songs returns .proplaylist file
- Non-finalized service returns 403
- Finalized service with no songs returns 422
- Skipped songs count reported via X-Skipped-Songs header
- Auth required for download endpoint
This commit is contained in:
Thorsten Bus 2026-03-02 12:29:14 +01:00
parent 747d2c3c07
commit 8cbda3b8bc

View file

@ -0,0 +1,128 @@
<?php
namespace Tests\Feature;
use App\Models\Service;
use App\Models\ServiceSong;
use App\Models\Song;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class PlaylistExportTest extends TestCase
{
use RefreshDatabase;
private User $user;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create();
}
private function createSongWithContent(string $title = 'Test Song', ?string $ccli = null): Song
{
$song = Song::create([
'title' => $title,
'ccli_id' => $ccli ?? fake()->unique()->numerify('#####'),
'author' => 'Test Author',
'copyright_text' => 'Test Publisher',
]);
$verse = $song->groups()->create(['name' => 'Verse 1', 'color' => '#2196F3', 'order' => 0]);
$verse->slides()->create(['order' => 0, 'text_content' => 'First 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]);
return $song;
}
public function test_download_finalisierter_service_mit_songs_gibt_proplaylist_datei(): void
{
$service = Service::factory()->create(['finalized_at' => now(), 'title' => 'Gottesdienst']);
$song = $this->createSongWithContent('Lobe den Herrn');
ServiceSong::create([
'service_id' => $service->id,
'song_id' => $song->id,
'cts_song_name' => 'Lobe den Herrn',
'order' => 1,
]);
$response = $this->actingAs($this->user)->get(route('services.download', $service));
$response->assertOk();
$contentType = $response->headers->get('content-type');
$this->assertTrue(
in_array($contentType, ['application/octet-stream', 'application/zip']),
"Expected octet-stream or zip content-type, got: {$contentType}"
);
$disposition = $response->headers->get('content-disposition');
$this->assertNotNull($disposition);
$this->assertTrue(
str_contains($disposition, '.proplaylist'),
"Expected .proplaylist in content-disposition, got: {$disposition}"
);
}
public function test_download_nicht_finalisierter_service_gibt_403(): void
{
$service = Service::factory()->create(['finalized_at' => null]);
$response = $this->actingAs($this->user)->getJson(route('services.download', $service));
$response->assertForbidden();
}
public function test_download_finalisierter_service_ohne_songs_gibt_422(): void
{
$service = Service::factory()->create(['finalized_at' => now()]);
$response = $this->actingAs($this->user)->getJson(route('services.download', $service));
$response->assertUnprocessable()
->assertJson(['message' => 'Keine Songs mit Inhalt zum Exportieren gefunden.']);
}
public function test_download_mit_ungematchten_songs_setzt_skipped_header(): void
{
$service = Service::factory()->create(['finalized_at' => now()]);
$song = $this->createSongWithContent('Matched Song');
// Matched song
ServiceSong::create([
'service_id' => $service->id,
'song_id' => $song->id,
'cts_song_name' => 'Matched Song',
'order' => 1,
]);
// Unmatched song (no song_id)
ServiceSong::create([
'service_id' => $service->id,
'song_id' => null,
'cts_song_name' => 'Unmatched Song',
'order' => 2,
]);
$response = $this->actingAs($this->user)->get(route('services.download', $service));
$response->assertOk();
$this->assertEquals('1', $response->headers->get('X-Skipped-Songs'));
}
public function test_download_erfordert_authentifizierung(): void
{
$service = Service::factory()->create(['finalized_at' => now()]);
$response = $this->getJson(route('services.download', $service));
$response->assertUnauthorized();
}
}