actingAs(User::factory()->create()); $label = Label::factory()->create(['name' => 'Chorus']); [$songA, $sectionA] = $this->createSongWithSection($label, ['Alter Refrain']); [, $sectionB] = $this->createSongWithSection($label, ['Anderer Refrain']); $response = $this->patchJson(route('songs.sections.update', [$songA, $sectionA]), [ 'slides' => [ ['text_content' => 'Neuer Refrain', 'text_content_translated' => 'New chorus'], ['text_content' => 'Zweiter Block', 'text_content_translated' => null], ], ]); $response->assertOk() ->assertJsonPath('data.has_translation', true) ->assertJsonPath('data.groups.0.slides.0.text_content', 'Neuer Refrain'); $this->assertSame(['Neuer Refrain', 'Zweiter Block'], $sectionA->slides()->orderBy('order')->pluck('text_content')->all()); $this->assertSame(['Anderer Refrain'], $sectionB->slides()->orderBy('order')->pluck('text_content')->all()); $this->assertTrue($songA->fresh()->has_translation); } public function test_post_adds_section_reuses_normalized_label_and_appends_default_arrangement(): void { $this->actingAs(User::factory()->create()); $song = Song::factory()->create(['has_translation' => false]); $arrangement = SongArrangement::factory()->create([ 'song_id' => $song->id, 'name' => 'Normal', 'is_default' => true, ]); $existingLabel = Label::factory()->create(['name' => 'Verse 3', 'color' => '#111111']); $response = $this->postJson(route('songs.sections.store', $song), [ 'label_name' => 'Strophe 3', 'color' => '#ABCDEF', 'slides' => [ ['text_content' => 'Neue Strophe', 'text_content_translated' => null], ], ]); $response->assertCreated() ->assertJsonPath('data.groups.0.name', 'Verse 3') ->assertJsonPath('data.groups.0.slides.0.text_content', 'Neue Strophe'); $this->assertSame(1, Label::query()->where('name', 'Verse 3')->count()); $section = SongSection::query()->where('song_id', $song->id)->where('label_id', $existingLabel->id)->first(); $this->assertNotNull($section); $this->assertDatabaseHas('song_slides', [ 'song_section_id' => $section->id, 'text_content' => 'Neue Strophe', ]); $this->assertDatabaseHas('song_arrangement_labels', [ 'song_arrangement_id' => $arrangement->id, 'song_section_id' => $section->id, 'order' => 1, ]); } public function test_delete_removes_section_slides_and_junction_for_this_song_only_but_keeps_global_label(): void { $this->actingAs(User::factory()->create()); $label = Label::factory()->create(['name' => 'Bridge']); [$songA, $sectionA, $arrangementA] = $this->createSongWithSection($label, ['Bridge A']); [, $sectionB] = $this->createSongWithSection($label, ['Bridge B']); $slideId = $sectionA->slides()->first()->id; $junctionId = SongArrangementLabel::query() ->where('song_arrangement_id', $arrangementA->id) ->where('song_section_id', $sectionA->id) ->value('id'); $response = $this->deleteJson(route('songs.sections.destroy', [$songA, $sectionA])); $response->assertOk() ->assertJsonPath('data.groups', []); $this->assertDatabaseMissing('song_sections', ['id' => $sectionA->id]); $this->assertDatabaseMissing('song_slides', ['id' => $slideId]); $this->assertDatabaseMissing('song_arrangement_labels', ['id' => $junctionId]); $this->assertDatabaseHas('labels', ['id' => $label->id]); $this->assertDatabaseHas('song_sections', ['id' => $sectionB->id]); $this->assertSame(['Bridge B'], $sectionB->slides()->pluck('text_content')->all()); } public function test_validation_and_ownership_errors_are_german(): void { $this->actingAs(User::factory()->create()); $label = Label::factory()->create(['name' => 'Verse 1']); [$songA, $sectionA] = $this->createSongWithSection($label, ['Song A']); [$songB] = $this->createSongWithSection(Label::factory()->create(['name' => 'Chorus']), ['Song B']); $this->patchJson(route('songs.sections.update', [$songB, $sectionA]), [ 'slides' => [ ['text_content' => 'Falsch'], ], ])->assertNotFound() ->assertJsonPath('message', 'Sektion nicht gefunden.'); $this->postJson(route('songs.sections.store', $songA), [ 'slides' => [ ['text_content' => 'Ohne Label'], ], ])->assertUnprocessable() ->assertJsonPath('message', 'Bitte gib einen Namen für die Sektion ein.'); $this->postJson(route('songs.sections.store', $songA), [ 'label_name' => 'Verse 1', 'slides' => [ ['text_content' => 'Doppelt'], ], ])->assertUnprocessable() ->assertJsonPath('message', 'Dieser Abschnitt existiert bereits in diesem Lied.'); } public function test_has_translation_is_recomputed_after_edits(): void { $this->actingAs(User::factory()->create()); [$song, $section] = $this->createSongWithSection(Label::factory()->create(['name' => 'Verse 1']), ['Original']); $this->patchJson(route('songs.sections.update', [$song, $section]), [ 'slides' => [ ['text_content' => 'Original', 'text_content_translated' => 'Übersetzt'], ], ])->assertOk() ->assertJsonPath('data.has_translation', true); $this->assertTrue($song->fresh()->has_translation); $this->patchJson(route('songs.sections.update', [$song, $section]), [ 'slides' => [ ['text_content' => 'Original', 'text_content_translated' => ''], ], ])->assertOk() ->assertJsonPath('data.has_translation', false); $this->assertFalse($song->fresh()->has_translation); } private function createSongWithSection(Label $label, array $slides): array { $song = Song::factory()->create(['has_translation' => false]); $section = SongSection::factory()->create([ 'song_id' => $song->id, 'label_id' => $label->id, 'order' => 1, ]); foreach ($slides as $index => $text) { SongSlide::factory()->create([ 'song_section_id' => $section->id, 'order' => $index + 1, 'text_content' => $text, 'text_content_translated' => null, ]); } $arrangement = SongArrangement::factory()->create([ 'song_id' => $song->id, 'name' => 'Normal', 'is_default' => true, ]); SongArrangementLabel::factory()->create([ 'song_arrangement_id' => $arrangement->id, 'song_section_id' => $section->id, 'order' => 1, ]); return [$song, $section, $arrangement]; } }