seed(LegalDocumentSeeder::class); $this->seed(LegalTextSeeder::class); config()->set('images.rights_v2', true); }); function rightsV2ApiDraft(): array { $user = User::factory()->create(); $pressRelease = PressRelease::factory()->create([ 'user_id' => $user->id, 'status' => PressReleaseStatus::Draft->value, ]); Sanctum::actingAs($user, ['press-releases:read', 'press-release-images:write']); return compact('user', 'pressRelease'); } function rightsV2ApiFixture(string $name): UploadedFile { return UploadedFile::fake()->createWithContent($name, (string) file_get_contents(base_path('tests/Fixtures/images/'.$name))); } test('credit, confirmation and text version are required under formular v2', function () { /** @var TestCase $this */ ['pressRelease' => $pressRelease] = rightsV2ApiDraft(); $this->postJson("/api/v1/press-releases/{$pressRelease->id}/images", [ 'image' => UploadedFile::fake()->image('foto.jpg', 1200, 800), 'ai_declaration' => 'original', ])->assertUnprocessable() ->assertInvalid(['copyright', 'rights_confirmed', 'confirm_text_version']); expect($pressRelease->images()->count())->toBe(0) ->and(Storage::disk('local')->allFiles())->toBe([]); }); test('an outdated text version is rejected with the current key in the message', function () { /** @var TestCase $this */ ['pressRelease' => $pressRelease] = rightsV2ApiDraft(); $response = $this->postJson("/api/v1/press-releases/{$pressRelease->id}/images", [ 'image' => UploadedFile::fake()->image('foto.jpg', 1200, 800), 'ai_declaration' => 'original', 'copyright' => 'Muster GmbH', 'rights_confirmed' => true, 'confirm_text_version' => 'img-confirm-v1', ]); $response->assertUnprocessable()->assertInvalid(['confirm_text_version']); expect($response->json('errors.confirm_text_version.0'))->toContain('img-confirm-v2'); }); test('a valid upload stores the confirmation with the token and returns the new fields', function () { /** @var TestCase $this */ ['pressRelease' => $pressRelease, 'user' => $user] = rightsV2ApiDraft(); $response = $this->postJson("/api/v1/press-releases/{$pressRelease->id}/images", [ 'image' => rightsV2ApiFixture('xmp-ai-generated.jpg'), 'ai_declaration' => 'ai_generated', 'copyright' => 'KI-Bild / Musterfirma', 'rights_confirmed' => true, 'confirm_text_version' => 'img-confirm-v2', 'license_url' => 'https://example.com/lizenz', 'focus_x' => 20, 'focus_y' => 80, 'is_preview' => true, ], ['User-Agent' => 'ConnektarBot/2.0']); $response->assertCreated() ->assertJsonPath('data.ai_declaration', 'ai_generated') ->assertJsonPath('data.ai_declaration_user', 'ai_generated') ->assertJsonPath('data.ai_declaration_source', 'iptc_prefill') ->assertJsonPath('data.ai_declaration_locked', false) ->assertJsonPath('data.provenance_status', 'unsigned_metadata') ->assertJsonPath('data.license_url', 'https://example.com/lizenz') ->assertJsonPath('data.rights_confirmation.version', 'img-confirm-v2') ->assertJsonPath('data.rights_confirmation.agb_version', '0.6') ->assertJsonPath('data.rights_confirmation.channel', 'api') ->assertJsonPath('data.warnings', []); $image = PressReleaseImage::query()->sole(); $confirmation = ImageRightsConfirmation::query()->sole(); expect($image->rules_version)->toBe(config('images.rules_version')) ->and($image->focus_x)->toBe(20) ->and($image->focus_y)->toBe(80) ->and($confirmation->channel)->toBe(ConfirmationChannel::Api) ->and($confirmation->user_id)->toBe($user->id) ->and($confirmation->personal_access_token_id)->not->toBeNull() ->and($confirmation->user_agent)->toBe('ConnektarBot/2.0') ->and($response->json('data.urls.original'))->toBe(route('media.press-release-images.show', ['image' => $image->id])) ->and($response->json('data.path'))->not->toContain('press-release-originals'); }); test('a second title image is rejected with 422 and the press release keeps its first', function () { /** @var TestCase $this */ ['pressRelease' => $pressRelease] = rightsV2ApiDraft(); $payload = fn (string $name) => [ 'image' => UploadedFile::fake()->image($name, 800, 600), 'ai_declaration' => 'original', 'copyright' => 'Muster GmbH', 'rights_confirmed' => true, 'confirm_text_version' => 'img-confirm-v2', ]; $this->postJson("/api/v1/press-releases/{$pressRelease->id}/images", $payload('eins.jpg'))->assertCreated(); $this->postJson("/api/v1/press-releases/{$pressRelease->id}/images", $payload('zwei.jpg')) ->assertUnprocessable() ->assertJsonPath('errors.image.0', 'Bitte löschen Sie zuerst das vorhandene Titelbild.'); expect($pressRelease->images()->count())->toBe(1); }); test('deleting a never published image via api removes the original and the confirmation', function () { /** @var TestCase $this */ ['pressRelease' => $pressRelease] = rightsV2ApiDraft(); $this->postJson("/api/v1/press-releases/{$pressRelease->id}/images", [ 'image' => UploadedFile::fake()->image('foto.jpg', 800, 600), 'ai_declaration' => 'original', 'copyright' => 'Muster GmbH', 'rights_confirmed' => true, 'confirm_text_version' => 'img-confirm-v2', ])->assertCreated(); $image = PressReleaseImage::query()->sole(); $this->deleteJson("/api/v1/press-release-images/{$image->id}")->assertNoContent(); expect(PressReleaseImage::withTrashed()->count())->toBe(0) ->and(ImageRightsConfirmation::query()->count())->toBe(0) ->and(Storage::disk('local')->allFiles())->toBe([]); }); test('the legacy service upload answers 410 with a pointer to v1', function () { /** @var TestCase $this */ ['pressRelease' => $pressRelease] = rightsV2ApiDraft(); $this->postJson("/service/pressreleases/{$pressRelease->id}/images", [ 'press_release_image' => ['image' => UploadedFile::fake()->image('alt.jpg')], ])->assertStatus(410) ->assertJsonPath('global_errors.0', fn (string $message) => str_contains($message, '/api/v1/press-releases/{id}/images')); expect($pressRelease->images()->count())->toBe(0); }); test('the api docs page lists the rights fields with the current text version', function () { /** @var TestCase $this */ $this->get('https://pressekonto.test/de/api') ->assertSuccessful() ->assertSeeText('rights_confirmed') ->assertSeeText('confirm_text_version') ->assertSeeText('img-confirm-v2'); });