- 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
172 lines
5.8 KiB
PHP
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'));
|
|
});
|