pp-planer/tests/Feature/CcliPasteControllerTest.php
Thorsten Bus 35d3298251 feat(ccli): add CcliPasteController endpoints
- POST /api/ccli/preview: parse-only endpoint (no DB writes)
- POST /api/songs/import-from-ccli-paste: 3 modes (create / pair-with-song / assign-to-service-song)
- GET /songs/import-from-ccli-paste: Inertia page with base64 bookmarklet prefill
- Routes guarded by auth:sanctum + throttle:30,1 (API); auth + web stack (web)
- Maps DuplicateCcliSongException to 409 with existing_song_id and edit_url
- Pest tests (10 cases, 63 assertions): preview, all 3 import modes, 409 dup, 422 errors, unauth, prefill happy/error, login redirect
2026-05-11 09:23:11 +02:00

172 lines
5.8 KiB
PHP

<?php
use App\Models\Service;
use App\Models\ServiceSong;
use App\Models\Song;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
function ccliPasteControllerFixture(string $name): string
{
return file_get_contents(base_path("tests/fixtures/ccli/{$name}"));
}
test('preview returns parsed DTO without writing to DB', function () {
$user = User::factory()->create();
$content = ccliPasteControllerFixture('english-only-multi-verse.txt');
$songCountBefore = Song::count();
$response = $this->actingAs($user)
->postJson(route('api.ccli.preview'), ['raw_text' => $content]);
$response->assertStatus(200);
$response->assertJsonStructure(['title', 'author', 'ccliId', 'year', 'copyrightText', 'sections']);
expect($response->json('title'))->toBe('Test Song 1');
expect($response->json('ccliId'))->toBe('9999001');
expect(Song::count())->toBe($songCountBefore);
});
test('importPaste create mode persists song and returns 201', function () {
$user = User::factory()->create();
$content = ccliPasteControllerFixture('english-only-multi-verse.txt');
$response = $this->actingAs($user)
->postJson(route('api.ccli.import'), [
'raw_text' => $content,
'mode' => 'create',
]);
$response->assertStatus(201);
$response->assertJsonStructure(['song_id', 'status', 'warnings']);
expect($response->json('status'))->toBe('created');
expect(Song::find($response->json('song_id')))->not->toBeNull();
});
test('importPaste returns 409 on duplicate ccli_id with existing_song_id', function () {
$user = User::factory()->create();
$content = ccliPasteControllerFixture('english-only-multi-verse.txt');
$first = $this->actingAs($user)
->postJson(route('api.ccli.import'), ['raw_text' => $content, 'mode' => 'create']);
$existingId = $first->json('song_id');
$response = $this->actingAs($user)
->postJson(route('api.ccli.import'), ['raw_text' => $content, 'mode' => 'create']);
$response->assertStatus(409);
$response->assertJsonStructure(['message', 'existing_song_id', 'edit_url']);
expect($response->json('existing_song_id'))->toBe($existingId);
expect($response->json('message'))->toContain('existiert bereits');
});
test('importPaste returns 422 on malformed paste with German error', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)
->postJson(route('api.ccli.import'), [
'raw_text' => 'This has no section labels at all',
'mode' => 'create',
]);
$response->assertStatus(422);
$response->assertJsonStructure(['message']);
expect($response->json('message'))->not->toBeEmpty();
});
test('importPaste returns 422 on empty raw_text', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)
->postJson(route('api.ccli.import'), [
'raw_text' => '',
'mode' => 'create',
]);
$response->assertStatus(422);
});
test('importPaste assign-to-service-song imports and assigns song to service song', function () {
$user = User::factory()->create();
$content = ccliPasteControllerFixture('english-only-multi-verse.txt');
$service = Service::factory()->create();
$serviceSong = ServiceSong::factory()->create([
'service_id' => $service->id,
'song_id' => null,
'song_arrangement_id' => null,
'matched_at' => null,
]);
$response = $this->actingAs($user)
->postJson(route('api.ccli.import'), [
'raw_text' => $content,
'mode' => 'assign-to-service-song',
'target_id' => $serviceSong->id,
]);
$response->assertStatus(201);
$response->assertJsonStructure(['song_id', 'service_song_id', 'status']);
$serviceSong->refresh();
expect($serviceSong->song_id)->toBe($response->json('song_id'));
expect($serviceSong->matched_at)->not->toBeNull();
});
test('importPaste rejects unauthenticated requests with 401', function () {
$response = $this->postJson(route('api.ccli.import'), [
'raw_text' => 'test',
'mode' => 'create',
]);
$response->assertUnauthorized();
});
test('showImportPage renders Inertia page with prefilled data from valid base64', function () {
$user = User::factory()->create();
$this->withoutVite();
$payload = json_encode([
'title' => 'Amazing Grace',
'author' => 'John Newton',
'ccliId' => '4760',
'sourceUrl' => 'https://songselect.ccli.com/Songs/4760',
'rawText' => "Amazing Grace\nJohn Newton\n\nVerse 1\nAmazing grace how sweet the sound\n\n© 1779\nCCLI # 4760",
]);
$encoded = base64_encode($payload);
$response = $this->actingAs($user)
->get(route('songs.import-from-ccli-paste', ['prefill' => $encoded]));
$response->assertStatus(200);
$response->assertInertia(
fn ($page) => $page->component('Songs/ImportFromCcliPaste', false)
->has('prefilledText')
->has('prefilledMetadata')
->where('prefilledMetadata.title', 'Amazing Grace')
->where('prefilledMetadata.ccliId', '4760')
);
});
test('showImportPage renders with error for invalid base64', function () {
$user = User::factory()->create();
$this->withoutVite();
$response = $this->actingAs($user)
->get(route('songs.import-from-ccli-paste', ['prefill' => 'NOT_VALID_BASE64!!!']));
$response->assertStatus(200);
$response->assertInertia(
fn ($page) => $page->component('Songs/ImportFromCcliPaste', false)
->where('prefillError', fn ($v) => $v !== null)
);
});
test('showImportPage redirects unauthenticated to login', function () {
$response = $this->get(route('songs.import-from-ccli-paste'));
$response->assertRedirect(route('login'));
});