user = User::factory()->create(); $this->service = app(TranslationService::class); }); // --- URL FETCH --- test('fetchFromUrl returns text from successful HTTP response', function () { Http::fake([ 'https://example.com/lyrics' => Http::response('
Zeile 1
Zeile 2
', 200), ]); $result = $this->service->fetchFromUrl('https://example.com/lyrics'); expect($result)->not->toBeNull(); expect($result)->toContain('Zeile 1'); expect($result)->toContain('Zeile 2'); // HTML tags should be stripped expect($result)->not->toContain(''); expect($result)->not->toContain(''); }); test('fetchFromUrl returns null on HTTP failure', function () { Http::fake([ 'https://example.com/broken' => Http::response('Not Found', 404), ]); $result = $this->service->fetchFromUrl('https://example.com/broken'); expect($result)->toBeNull(); }); test('fetchFromUrl returns null on connection error', function () { Http::fake([ 'https://timeout.example.com/*' => fn () => throw new \Illuminate\Http\Client\ConnectionException('Timeout'), ]); $result = $this->service->fetchFromUrl('https://timeout.example.com/lyrics'); expect($result)->toBeNull(); }); test('fetchFromUrl returns null for empty response body', function () { Http::fake([ 'https://example.com/empty' => Http::response('', 200), ]); $result = $this->service->fetchFromUrl('https://example.com/empty'); expect($result)->toBeNull(); }); // --- IMPORT TRANSLATION (LINE-COUNT DISTRIBUTION) --- test('importTranslation distributes lines by slide line counts', function () { $song = Song::factory()->create(['has_translation' => false]); $group = SongGroup::factory()->create([ 'song_id' => $song->id, 'name' => 'Strophe 1', 'order' => 1, ]); // Slide 1: 4 lines $slide1 = SongSlide::factory()->create([ 'song_group_id' => $group->id, 'order' => 1, 'text_content' => "Original 1\nOriginal 2\nOriginal 3\nOriginal 4", ]); // Slide 2: 2 lines $slide2 = SongSlide::factory()->create([ 'song_group_id' => $group->id, 'order' => 2, 'text_content' => "Original 5\nOriginal 6", ]); // Slide 3: 4 lines $slide3 = SongSlide::factory()->create([ 'song_group_id' => $group->id, 'order' => 3, 'text_content' => "Original 7\nOriginal 8\nOriginal 9\nOriginal 10", ]); $translatedText = "Zeile 1\nZeile 2\nZeile 3\nZeile 4\nZeile 5\nZeile 6\nZeile 7\nZeile 8\nZeile 9\nZeile 10"; $this->service->importTranslation($song, $translatedText); $slide1->refresh(); $slide2->refresh(); $slide3->refresh(); // Slide 1 gets lines 1-4 expect($slide1->text_content_translated)->toBe("Zeile 1\nZeile 2\nZeile 3\nZeile 4"); // Slide 2 gets lines 5-6 expect($slide2->text_content_translated)->toBe("Zeile 5\nZeile 6"); // Slide 3 gets lines 7-10 expect($slide3->text_content_translated)->toBe("Zeile 7\nZeile 8\nZeile 9\nZeile 10"); }); test('importTranslation distributes across multiple groups', function () { $song = Song::factory()->create(['has_translation' => false]); $group1 = SongGroup::factory()->create([ 'song_id' => $song->id, 'name' => 'Strophe 1', 'order' => 1, ]); $group2 = SongGroup::factory()->create([ 'song_id' => $song->id, 'name' => 'Refrain', 'order' => 2, ]); $slide1 = SongSlide::factory()->create([ 'song_group_id' => $group1->id, 'order' => 1, 'text_content' => "Line A\nLine B", ]); $slide2 = SongSlide::factory()->create([ 'song_group_id' => $group2->id, 'order' => 1, 'text_content' => "Line C\nLine D\nLine E", ]); $translatedText = "Über A\nÜber B\nÜber C\nÜber D\nÜber E"; $this->service->importTranslation($song, $translatedText); $slide1->refresh(); $slide2->refresh(); expect($slide1->text_content_translated)->toBe("Über A\nÜber B"); expect($slide2->text_content_translated)->toBe("Über C\nÜber D\nÜber E"); }); test('importTranslation handles fewer translation lines than original', function () { $song = Song::factory()->create(['has_translation' => false]); $group = SongGroup::factory()->create([ 'song_id' => $song->id, 'order' => 1, ]); $slide1 = SongSlide::factory()->create([ 'song_group_id' => $group->id, 'order' => 1, 'text_content' => "Line 1\nLine 2\nLine 3", ]); $slide2 = SongSlide::factory()->create([ 'song_group_id' => $group->id, 'order' => 2, 'text_content' => "Line 4\nLine 5", ]); // Only 3 lines for 5 lines total $translatedText = "Zeile 1\nZeile 2\nZeile 3"; $this->service->importTranslation($song, $translatedText); $slide1->refresh(); $slide2->refresh(); // Slide 1 gets all 3 available lines expect($slide1->text_content_translated)->toBe("Zeile 1\nZeile 2\nZeile 3"); // Slide 2 gets empty (no lines left) expect($slide2->text_content_translated)->toBe(''); }); test('importTranslation marks song as translated', function () { $song = Song::factory()->create(['has_translation' => false]); $group = SongGroup::factory()->create([ 'song_id' => $song->id, 'order' => 1, ]); SongSlide::factory()->create([ 'song_group_id' => $group->id, 'order' => 1, 'text_content' => "Line 1", ]); $this->service->importTranslation($song, "Zeile 1"); $song->refresh(); expect($song->has_translation)->toBeTrue(); }); // --- MARK AS TRANSLATED --- test('markAsTranslated sets has_translation to true', function () { $song = Song::factory()->create(['has_translation' => false]); $this->service->markAsTranslated($song); $song->refresh(); expect($song->has_translation)->toBeTrue(); }); // --- REMOVE TRANSLATION --- test('removeTranslation clears all translated text and sets flag to false', function () { $song = Song::factory()->create(['has_translation' => true]); $group = SongGroup::factory()->create([ 'song_id' => $song->id, 'order' => 1, ]); $slide1 = SongSlide::factory()->create([ 'song_group_id' => $group->id, 'order' => 1, 'text_content' => "Original", 'text_content_translated' => "Übersetzt", ]); $slide2 = SongSlide::factory()->create([ 'song_group_id' => $group->id, 'order' => 2, 'text_content' => "Original 2", 'text_content_translated' => "Übersetzt 2", ]); $this->service->removeTranslation($song); $song->refresh(); $slide1->refresh(); $slide2->refresh(); expect($song->has_translation)->toBeFalse(); expect($slide1->text_content_translated)->toBeNull(); expect($slide2->text_content_translated)->toBeNull(); }); // --- CONTROLLER ENDPOINTS --- test('POST translation/fetch-url returns scraped text', function () { Http::fake([ 'https://lyrics.example.com/song' => Http::response('