T20: Song DB Page - Songs/Index.vue with search, action buttons, pagination - Upload area for .pro files (calls T23 placeholder) - Song-Datenbank nav link added to AuthenticatedLayout - Tests: 9 new (44 assertions) T21: Song DB Edit Popup - SongEditModal.vue with metadata + ArrangementConfigurator - Auto-save with fetch (500ms debounce for text, immediate on blur) - Tests: 11 new (53 assertions) T22: Song DB Translate Page - Songs/Translate.vue with two-column editor - URL fetch or manual paste, line-count constraints - Group headers with colors, save marks has_translation=true - Tests: 1 new (12 assertions) T23: .pro File Placeholders - ProParserNotImplementedException with HTTP 501 - ProFileController with importPro/downloadPro placeholders - German error messages - Tests: 5 new (7 assertions) T24: Service Finalization + Status - Two-step finalization with warnings (unmatched songs, missing slides) - Download placeholder toast - isReadyToFinalize accessor on Service model - Tests: 11 new (30 assertions) All tests passing: 174/174 (905 assertions) Build: ✓ Vite production build successful German UI: All user-facing text in German with 'Du' form
228 lines
6.5 KiB
PHP
228 lines
6.5 KiB
PHP
<?php
|
|
|
|
use App\Models\Song;
|
|
use App\Models\SongArrangement;
|
|
use App\Models\SongArrangementGroup;
|
|
use App\Models\SongGroup;
|
|
use App\Models\User;
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Song Edit Modal Tests
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| Tests verifying the API endpoints used by SongEditModal.vue:
|
|
| - GET /api/songs/{id} (show with groups + arrangements)
|
|
| - PUT /api/songs/{id} (auto-save metadata)
|
|
|
|
|
*/
|
|
|
|
beforeEach(function () {
|
|
$this->user = User::factory()->create();
|
|
});
|
|
|
|
// --- Modal Data Loading ---
|
|
|
|
test('show returns song with full detail for modal', function () {
|
|
$song = Song::factory()->create([
|
|
'title' => 'Großer Gott wir loben Dich',
|
|
'ccli_id' => '100200',
|
|
'copyright_text' => '© Public Domain',
|
|
]);
|
|
|
|
$group1 = SongGroup::factory()->create([
|
|
'song_id' => $song->id,
|
|
'name' => 'Strophe 1',
|
|
'color' => '#3B82F6',
|
|
'order' => 1,
|
|
]);
|
|
|
|
$group2 = SongGroup::factory()->create([
|
|
'song_id' => $song->id,
|
|
'name' => 'Refrain',
|
|
'color' => '#10B981',
|
|
'order' => 2,
|
|
]);
|
|
|
|
$arrangement = SongArrangement::factory()->create([
|
|
'song_id' => $song->id,
|
|
'name' => 'Normal',
|
|
'is_default' => true,
|
|
]);
|
|
|
|
SongArrangementGroup::factory()->create([
|
|
'song_arrangement_id' => $arrangement->id,
|
|
'song_group_id' => $group1->id,
|
|
'order' => 1,
|
|
]);
|
|
|
|
SongArrangementGroup::factory()->create([
|
|
'song_arrangement_id' => $arrangement->id,
|
|
'song_group_id' => $group2->id,
|
|
'order' => 2,
|
|
]);
|
|
|
|
$response = $this->actingAs($this->user)
|
|
->getJson("/api/songs/{$song->id}");
|
|
|
|
$response->assertOk()
|
|
->assertJsonPath('data.title', 'Großer Gott wir loben Dich')
|
|
->assertJsonPath('data.ccli_id', '100200')
|
|
->assertJsonPath('data.copyright_text', '© Public Domain')
|
|
->assertJsonStructure([
|
|
'data' => [
|
|
'id', 'title', 'ccli_id', 'copyright_text',
|
|
'groups' => [['id', 'name', 'color', 'order', 'slides']],
|
|
'arrangements' => [['id', 'name', 'is_default', 'arrangement_groups']],
|
|
],
|
|
]);
|
|
|
|
// Groups in correct order
|
|
expect($response->json('data.groups.0.name'))->toBe('Strophe 1');
|
|
expect($response->json('data.groups.1.name'))->toBe('Refrain');
|
|
|
|
// Arrangement with arrangement_groups
|
|
expect($response->json('data.arrangements.0.name'))->toBe('Normal');
|
|
expect($response->json('data.arrangements.0.arrangement_groups'))->toHaveCount(2);
|
|
});
|
|
|
|
// --- Metadata Auto-Save ---
|
|
|
|
test('update saves title via auto-save', function () {
|
|
$song = Song::factory()->create(['title' => 'Original Title']);
|
|
|
|
$response = $this->actingAs($this->user)
|
|
->putJson("/api/songs/{$song->id}", [
|
|
'title' => 'Neuer Titel',
|
|
]);
|
|
|
|
$response->assertOk()
|
|
->assertJsonFragment(['message' => 'Song erfolgreich aktualisiert']);
|
|
|
|
$song->refresh();
|
|
expect($song->title)->toBe('Neuer Titel');
|
|
});
|
|
|
|
test('update saves ccli_id via auto-save', function () {
|
|
$song = Song::factory()->create(['ccli_id' => '111111']);
|
|
|
|
$response = $this->actingAs($this->user)
|
|
->putJson("/api/songs/{$song->id}", [
|
|
'title' => $song->title,
|
|
'ccli_id' => '999888',
|
|
]);
|
|
|
|
$response->assertOk();
|
|
|
|
$song->refresh();
|
|
expect($song->ccli_id)->toBe('999888');
|
|
});
|
|
|
|
test('update saves copyright_text via auto-save', function () {
|
|
$song = Song::factory()->create(['copyright_text' => null]);
|
|
|
|
$response = $this->actingAs($this->user)
|
|
->putJson("/api/songs/{$song->id}", [
|
|
'title' => $song->title,
|
|
'copyright_text' => '© 2024 Neuer Copyright-Text',
|
|
]);
|
|
|
|
$response->assertOk();
|
|
|
|
$song->refresh();
|
|
expect($song->copyright_text)->toBe('© 2024 Neuer Copyright-Text');
|
|
});
|
|
|
|
test('update can clear optional fields with null', function () {
|
|
$song = Song::factory()->create([
|
|
'ccli_id' => '555555',
|
|
'copyright_text' => 'Some copyright',
|
|
]);
|
|
|
|
$response = $this->actingAs($this->user)
|
|
->putJson("/api/songs/{$song->id}", [
|
|
'title' => $song->title,
|
|
'ccli_id' => null,
|
|
'copyright_text' => null,
|
|
]);
|
|
|
|
$response->assertOk();
|
|
|
|
$song->refresh();
|
|
expect($song->ccli_id)->toBeNull();
|
|
expect($song->copyright_text)->toBeNull();
|
|
});
|
|
|
|
test('update returns full song detail with arrangements', function () {
|
|
$song = Song::factory()->create();
|
|
SongGroup::factory()->create(['song_id' => $song->id]);
|
|
SongArrangement::factory()->create(['song_id' => $song->id, 'is_default' => true]);
|
|
|
|
$response = $this->actingAs($this->user)
|
|
->putJson("/api/songs/{$song->id}", [
|
|
'title' => 'Updated Song',
|
|
]);
|
|
|
|
$response->assertOk()
|
|
->assertJsonStructure([
|
|
'data' => [
|
|
'id', 'title', 'ccli_id', 'copyright_text',
|
|
'groups', 'arrangements',
|
|
],
|
|
]);
|
|
});
|
|
|
|
test('update validates title is required', function () {
|
|
$song = Song::factory()->create();
|
|
|
|
$response = $this->actingAs($this->user)
|
|
->putJson("/api/songs/{$song->id}", [
|
|
'title' => '',
|
|
]);
|
|
|
|
$response->assertUnprocessable()
|
|
->assertJsonValidationErrors(['title']);
|
|
});
|
|
|
|
test('update validates unique ccli_id against other songs', function () {
|
|
Song::factory()->create(['ccli_id' => '777777']);
|
|
$song = Song::factory()->create(['ccli_id' => '888888']);
|
|
|
|
$response = $this->actingAs($this->user)
|
|
->putJson("/api/songs/{$song->id}", [
|
|
'title' => $song->title,
|
|
'ccli_id' => '777777',
|
|
]);
|
|
|
|
$response->assertUnprocessable()
|
|
->assertJsonValidationErrors(['ccli_id']);
|
|
});
|
|
|
|
test('update requires authentication', function () {
|
|
$song = Song::factory()->create();
|
|
|
|
$response = $this->putJson("/api/songs/{$song->id}", [
|
|
'title' => 'Should Fail',
|
|
]);
|
|
|
|
$response->assertUnauthorized();
|
|
});
|
|
|
|
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 returns 404 for nonexistent song', function () {
|
|
$response = $this->actingAs($this->user)
|
|
->putJson('/api/songs/99999', [
|
|
'title' => 'Ghost Song',
|
|
]);
|
|
|
|
$response->assertNotFound();
|
|
});
|