pp-planer/tests/Feature/SongControllerTest.php
Thorsten Bus bdbf0c65e3 refactor(php): rename SongGroup references throughout controllers/services/tests
Replace all SongGroup/SongArrangementGroup model usages with Label/SongArrangementLabel
after the schema migration to global labels. Updates 12 app files and 11 test files:

- SongService: createDefaultGroups now finds-or-creates global Labels by name
- ArrangementController: validates label_id (labels are global, no song-ownership)
- ProImportService: imports groups as Labels (firstOrCreate by name); does not
  overwrite existing label colors per spec
- ProExportService/SongPdfController/TranslationService/etc: traverse via
  arrangements -> arrangementLabels -> label -> songSlides chain
- All test factories and assertions adapted to label-based schema
2026-05-03 22:55:02 +02:00

290 lines
9.1 KiB
PHP

<?php
use App\Models\Label;
use App\Models\Service;
use App\Models\ServiceSong;
use App\Models\Song;
use App\Models\SongArrangement;
use App\Models\SongArrangementLabel;
use App\Models\User;
beforeEach(function () {
$this->user = User::factory()->create();
});
test('songs index returns paginated list', function () {
Song::factory()->count(3)->create();
$response = $this->actingAs($this->user)
->getJson('/api/songs');
$response->assertOk()
->assertJsonStructure([
'data' => [['id', 'title', 'ccli_id', 'author', 'has_translation']],
'meta' => ['current_page', 'last_page', 'per_page', 'total'],
]);
expect($response->json('meta.total'))->toBe(3);
});
test('songs index excludes soft-deleted songs', function () {
Song::factory()->count(2)->create();
Song::factory()->create(['deleted_at' => now()]);
$response = $this->actingAs($this->user)
->getJson('/api/songs');
$response->assertOk();
expect($response->json('meta.total'))->toBe(2);
});
test('songs index search by title', function () {
Song::factory()->create(['title' => 'Amazing Grace']);
Song::factory()->create(['title' => 'Holy Spirit']);
$response = $this->actingAs($this->user)
->getJson('/api/songs?search=Amazing');
$response->assertOk();
expect($response->json('meta.total'))->toBe(1);
expect($response->json('data.0.title'))->toBe('Amazing Grace');
});
test('songs index search by ccli id', function () {
Song::factory()->create(['ccli_id' => '123456', 'title' => 'Song A']);
Song::factory()->create(['ccli_id' => '789012', 'title' => 'Song B']);
$response = $this->actingAs($this->user)
->getJson('/api/songs?search=123456');
$response->assertOk();
expect($response->json('meta.total'))->toBe(1);
expect($response->json('data.0.ccli_id'))->toBe('123456');
});
test('songs index requires authentication', function () {
$response = $this->getJson('/api/songs');
$response->assertUnauthorized();
});
test('store creates song with default groups and arrangement', function () {
$response = $this->actingAs($this->user)
->postJson('/api/songs', [
'title' => 'Neues Lied',
'ccli_id' => '999999',
'author' => 'Test Author',
]);
$response->assertCreated()
->assertJsonFragment(['message' => 'Song erfolgreich erstellt']);
$song = Song::where('title', 'Neues Lied')->first();
expect($song)->not->toBeNull();
$arrangement = $song->arrangements()->where('is_default', true)->first();
expect($arrangement)->not->toBeNull();
expect($arrangement->name)->toBe('Normal');
expect($arrangement->arrangementLabels)->toHaveCount(3);
expect($arrangement->arrangementLabels->sortBy('order')->pluck('label.name')->toArray())
->toBe(['Strophe 1', 'Refrain', 'Bridge']);
});
test('store validates required title', function () {
$response = $this->actingAs($this->user)
->postJson('/api/songs', [
'ccli_id' => '111111',
]);
$response->assertUnprocessable()
->assertJsonValidationErrors(['title']);
});
test('store validates unique ccli_id', function () {
Song::factory()->create(['ccli_id' => '555555']);
$response = $this->actingAs($this->user)
->postJson('/api/songs', [
'title' => 'Duplicate Song',
'ccli_id' => '555555',
]);
$response->assertUnprocessable()
->assertJsonValidationErrors(['ccli_id']);
});
test('store allows null ccli_id', function () {
$response = $this->actingAs($this->user)
->postJson('/api/songs', [
'title' => 'Song ohne CCLI',
]);
$response->assertCreated();
expect(Song::where('title', 'Song ohne CCLI')->first()->ccli_id)->toBeNull();
});
test('show returns song with groups slides and arrangements', function () {
$song = Song::factory()->create();
$label = Label::factory()->create(['name' => 'Strophe 1']);
$arrangement = SongArrangement::factory()->create(['song_id' => $song->id, 'name' => 'Normal', 'is_default' => true]);
SongArrangementLabel::factory()->create([
'song_arrangement_id' => $arrangement->id,
'label_id' => $label->id,
'order' => 1,
]);
$response = $this->actingAs($this->user)
->getJson("/api/songs/{$song->id}");
$response->assertOk()
->assertJsonStructure([
'data' => [
'id', 'title', 'ccli_id', 'author', 'copyright_text',
'has_translation', 'last_used_in_service',
'groups' => [['id', 'name', 'color', 'order', 'slides']],
'arrangements' => [['id', 'name', 'is_default', 'arrangement_groups']],
],
]);
});
test('show returns 404 for nonexistent song', function () {
$response = $this->actingAs($this->user)
->getJson('/api/songs/99999');
$response->assertNotFound()
->assertJsonFragment(['message' => 'Song nicht gefunden']);
});
test('show returns 404 for soft-deleted song', function () {
$song = Song::factory()->create(['deleted_at' => now()]);
$response = $this->actingAs($this->user)
->getJson("/api/songs/{$song->id}");
$response->assertNotFound();
});
test('update modifies song metadata', function () {
$song = Song::factory()->create(['title' => 'Old Title']);
$response = $this->actingAs($this->user)
->putJson("/api/songs/{$song->id}", [
'title' => 'New Title',
'author' => 'New Author',
]);
$response->assertOk()
->assertJsonFragment(['message' => 'Song erfolgreich aktualisiert']);
$song->refresh();
expect($song->title)->toBe('New Title');
expect($song->author)->toBe('New Author');
});
test('update validates unique ccli_id excluding self', function () {
$songA = Song::factory()->create(['ccli_id' => '111111']);
$songB = Song::factory()->create(['ccli_id' => '222222']);
$response = $this->actingAs($this->user)
->putJson("/api/songs/{$songB->id}", [
'title' => $songB->title,
'ccli_id' => '111111',
]);
$response->assertUnprocessable()
->assertJsonValidationErrors(['ccli_id']);
});
test('update allows keeping own ccli_id', function () {
$song = Song::factory()->create(['ccli_id' => '333333']);
$response = $this->actingAs($this->user)
->putJson("/api/songs/{$song->id}", [
'title' => 'Updated Title',
'ccli_id' => '333333',
]);
$response->assertOk();
});
test('destroy soft-deletes a song', function () {
$song = Song::factory()->create();
$response = $this->actingAs($this->user)
->deleteJson("/api/songs/{$song->id}");
$response->assertOk()
->assertJsonFragment(['message' => 'Song erfolgreich gelöscht']);
expect(Song::find($song->id))->toBeNull();
expect(Song::withTrashed()->find($song->id))->not->toBeNull();
});
test('destroy returns 404 for nonexistent song', function () {
$response = $this->actingAs($this->user)
->deleteJson('/api/songs/99999');
$response->assertNotFound();
});
test('last_used_in_service returns correct date from service_songs', function () {
$song = Song::factory()->create();
$serviceOld = Service::factory()->create(['date' => '2025-06-01']);
$serviceNew = Service::factory()->create(['date' => '2026-01-15']);
ServiceSong::factory()->create([
'song_id' => $song->id,
'service_id' => $serviceOld->id,
]);
ServiceSong::factory()->create([
'song_id' => $song->id,
'service_id' => $serviceNew->id,
]);
$response = $this->actingAs($this->user)
->getJson("/api/songs/{$song->id}");
$response->assertOk();
expect($response->json('data.last_used_in_service'))->toBe('2026-01-15');
});
test('last_used_in_service returns null when never used', function () {
$song = Song::factory()->create();
$response = $this->actingAs($this->user)
->getJson("/api/songs/{$song->id}");
$response->assertOk();
expect($response->json('data.last_used_in_service'))->toBeNull();
});
test('duplicate arrangement clones arrangement with groups', function () {
$song = Song::factory()->create();
$label1 = Label::factory()->create();
$label2 = Label::factory()->create();
$arrangement = SongArrangement::factory()->create([
'song_id' => $song->id,
'name' => 'Original',
'is_default' => true,
]);
SongArrangementLabel::factory()->create([
'song_arrangement_id' => $arrangement->id,
'label_id' => $label1->id,
'order' => 1,
]);
SongArrangementLabel::factory()->create([
'song_arrangement_id' => $arrangement->id,
'label_id' => $label2->id,
'order' => 2,
]);
$service = app(\App\Services\SongService::class);
$clone = $service->duplicateArrangement($arrangement, 'Klone');
expect($clone->name)->toBe('Klone');
expect($clone->is_default)->toBeFalse();
expect($clone->arrangementLabels)->toHaveCount(2);
expect($clone->arrangementLabels->pluck('label_id')->toArray())
->toBe($arrangement->arrangementLabels->pluck('label_id')->toArray());
});