Auto-formatted by Laravel Pint (default Laravel preset): string concatenation spacing, anonymous class brace placement, constructor body shorthand, import ordering, and assertion indentation.
377 lines
11 KiB
PHP
377 lines
11 KiB
PHP
<?php
|
|
|
|
use App\Models\Song;
|
|
use App\Models\SongGroup;
|
|
use App\Models\SongSlide;
|
|
use App\Models\User;
|
|
use App\Services\TranslationService;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Translation Service Tests
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
|
|
beforeEach(function () {
|
|
$this->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('<html><body><p>Zeile 1</p><p>Zeile 2</p></body></html>', 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('<p>');
|
|
expect($result)->not->toContain('<html>');
|
|
});
|
|
|
|
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('<div>Liedtext Zeile 1</div>', 200),
|
|
]);
|
|
|
|
$response = $this->actingAs($this->user)
|
|
->postJson('/api/translation/fetch-url', [
|
|
'url' => 'https://lyrics.example.com/song',
|
|
]);
|
|
|
|
$response->assertOk()
|
|
->assertJsonStructure(['text']);
|
|
|
|
expect($response->json('text'))->toContain('Liedtext Zeile 1');
|
|
});
|
|
|
|
test('POST translation/fetch-url returns error on failure', function () {
|
|
Http::fake([
|
|
'https://broken.example.com/*' => Http::response('', 500),
|
|
]);
|
|
|
|
$response = $this->actingAs($this->user)
|
|
->postJson('/api/translation/fetch-url', [
|
|
'url' => 'https://broken.example.com/song',
|
|
]);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonFragment(['message' => 'Konnte Text nicht abrufen']);
|
|
});
|
|
|
|
test('POST translation/fetch-url validates url field', function () {
|
|
$response = $this->actingAs($this->user)
|
|
->postJson('/api/translation/fetch-url', []);
|
|
|
|
$response->assertUnprocessable()
|
|
->assertJsonValidationErrors(['url']);
|
|
});
|
|
|
|
test('POST songs/{song}/translation/import distributes and saves translation', function () {
|
|
$song = Song::factory()->create(['has_translation' => false]);
|
|
|
|
$group = SongGroup::factory()->create([
|
|
'song_id' => $song->id,
|
|
'order' => 1,
|
|
]);
|
|
|
|
$slide = SongSlide::factory()->create([
|
|
'song_group_id' => $group->id,
|
|
'order' => 1,
|
|
'text_content' => "Line 1\nLine 2",
|
|
]);
|
|
|
|
$response = $this->actingAs($this->user)
|
|
->postJson("/api/songs/{$song->id}/translation/import", [
|
|
'text' => "Zeile 1\nZeile 2",
|
|
]);
|
|
|
|
$response->assertOk()
|
|
->assertJsonFragment(['message' => 'Übersetzung erfolgreich importiert']);
|
|
|
|
$slide->refresh();
|
|
$song->refresh();
|
|
|
|
expect($slide->text_content_translated)->toBe("Zeile 1\nZeile 2");
|
|
expect($song->has_translation)->toBeTrue();
|
|
});
|
|
|
|
test('POST songs/{song}/translation/import validates text field', function () {
|
|
$song = Song::factory()->create();
|
|
|
|
$response = $this->actingAs($this->user)
|
|
->postJson("/api/songs/{$song->id}/translation/import", []);
|
|
|
|
$response->assertUnprocessable()
|
|
->assertJsonValidationErrors(['text']);
|
|
});
|
|
|
|
test('POST songs/{song}/translation/import returns 404 for missing song', function () {
|
|
$response = $this->actingAs($this->user)
|
|
->postJson('/api/songs/99999/translation/import', [
|
|
'text' => 'Some text',
|
|
]);
|
|
|
|
$response->assertNotFound();
|
|
});
|
|
|
|
test('DELETE songs/{song}/translation removes translation', function () {
|
|
$song = Song::factory()->create(['has_translation' => true]);
|
|
|
|
$group = SongGroup::factory()->create([
|
|
'song_id' => $song->id,
|
|
'order' => 1,
|
|
]);
|
|
|
|
SongSlide::factory()->create([
|
|
'song_group_id' => $group->id,
|
|
'order' => 1,
|
|
'text_content' => 'Original',
|
|
'text_content_translated' => 'Übersetzt',
|
|
]);
|
|
|
|
$response = $this->actingAs($this->user)
|
|
->deleteJson("/api/songs/{$song->id}/translation");
|
|
|
|
$response->assertOk()
|
|
->assertJsonFragment(['message' => 'Übersetzung entfernt']);
|
|
|
|
$song->refresh();
|
|
expect($song->has_translation)->toBeFalse();
|
|
});
|
|
|
|
test('translation endpoints require authentication', function () {
|
|
$this->postJson('/api/translation/fetch-url', ['url' => 'https://example.com'])
|
|
->assertUnauthorized();
|
|
|
|
$this->postJson('/api/songs/1/translation/import', ['text' => 'test'])
|
|
->assertUnauthorized();
|
|
|
|
$this->deleteJson('/api/songs/1/translation')
|
|
->assertUnauthorized();
|
|
});
|