seed(RolesAndPermissionsSeeder::class); Queue::fake(); }); function makeAiDeclarationCustomer(): array { $customer = User::factory()->create(['is_active' => true]); $customer->assignRole('customer'); $company = Company::factory()->presseecho()->create(); $customer->companies()->attach($company->id, ['role' => 'owner']); return [$customer, $company]; } test('the text declaration maps to the attributed public label per decision update', function () { // Zwei Gruppen (Vereinfachung 10.07.2026): None/Assisted → kein Label, // MostlyAi/FullyAi → gemeinsames „KI-generiert"-Label in Einreicher-Stimme. expect(AiTextDeclaration::None->publicLabel())->toBeNull(); expect(AiTextDeclaration::Assisted->publicLabel())->toBeNull(); expect(AiTextDeclaration::MostlyAi->publicLabel()) ->toBe('KI-generiert: Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung ganz oder überwiegend mit generativer KI erstellt.'); expect(AiTextDeclaration::FullyAi->publicLabel()) ->toBe('KI-generiert: Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung ganz oder überwiegend mit generativer KI erstellt.'); }); test('every stored value maps onto one of the two form options', function () { expect(AiTextDeclaration::None->formOption())->toBe(AiTextDeclaration::None); expect(AiTextDeclaration::Assisted->formOption())->toBe(AiTextDeclaration::None); expect(AiTextDeclaration::MostlyAi->formOption())->toBe(AiTextDeclaration::MostlyAi); expect(AiTextDeclaration::FullyAi->formOption())->toBe(AiTextDeclaration::MostlyAi); expect(AiTextDeclaration::formCases())->toBe([AiTextDeclaration::None, AiTextDeclaration::MostlyAi]); }); test('the image declaration maps to the public image hint per decision update', function () { // Kurzform als Badge direkt am Bild (User-Entscheidung 10.07.2026). expect(AiImageDeclaration::Original->publicLabel())->toBeNull(); expect(AiImageDeclaration::AiGenerated->publicLabel())->toBe('KI-generiert'); expect(AiImageDeclaration::AiEdited->publicLabel())->toBe('KI-bearbeitet'); }); test('submit is blocked without a complete ai declaration', function () { /** @var TestCase $this */ [$customer] = makeAiDeclarationCustomer(); $pr = PressRelease::factory()->aiUndeclared()->create([ 'user_id' => $customer->id, 'status' => PressReleaseStatus::Draft->value, ]); $this->actingAs($customer); expect(fn () => app(PressReleaseService::class)->submitForReview($pr)) ->toThrow(AiDeclarationRequiredException::class); expect($pr->fresh()->status)->toBe(PressReleaseStatus::Draft); expect(PressReleaseAiDeclaration::query()->count())->toBe(0); }); test('submit is blocked while any image lacks its ai declaration', function () { /** @var TestCase $this */ [$customer] = makeAiDeclarationCustomer(); $pr = PressRelease::factory()->create([ 'user_id' => $customer->id, 'status' => PressReleaseStatus::Draft->value, ]); $pr->images()->create([ 'disk' => 'public', 'path' => 'press-releases/'.$pr->id.'/images/title.jpg', 'author' => 'Jane Doe', 'license_type' => ImageLicenseType::Own->value, 'rights_confirmed_at' => now(), 'is_preview' => true, 'sort_order' => 1, 'ai_declaration' => null, ]); $this->actingAs($customer); expect(fn () => app(PressReleaseService::class)->submitForReview($pr)) ->toThrow(AiDeclarationRequiredException::class); expect($pr->fresh()->status)->toBe(PressReleaseStatus::Draft); }); test('each submit writes a versioned immutable audit entry with the image snapshot', function () { /** @var TestCase $this */ [$customer, $company] = makeAiDeclarationCustomer(); $pr = PressRelease::factory()->create([ 'user_id' => $customer->id, 'company_id' => $company->id, 'status' => PressReleaseStatus::Draft->value, 'ai_text_declaration' => AiTextDeclaration::MostlyAi->value, 'ai_declaration_confirmed_at' => now(), ]); $image = $pr->images()->create([ 'disk' => 'public', 'path' => 'press-releases/'.$pr->id.'/images/title.jpg', 'author' => 'Beispiel GmbH', 'license_type' => ImageLicenseType::AiGenerated->value, 'rights_confirmed_at' => now(), 'is_ai_generated' => true, 'ai_declaration' => AiImageDeclaration::AiGenerated->value, 'is_preview' => true, 'sort_order' => 1, ]); $this->actingAs($customer); app(PressReleaseService::class)->submitForReview($pr); $entry = PressReleaseAiDeclaration::query()->sole(); expect($entry->press_release_id)->toBe($pr->id); expect($entry->user_id)->toBe($customer->id); expect($entry->company_id)->toBe($company->id); expect($entry->text_declaration)->toBe(AiTextDeclaration::MostlyAi); expect($entry->confirmed_at)->not->toBeNull(); expect($entry->created_at)->not->toBeNull(); expect($entry->image_declarations)->toBe([ ['image_id' => $image->id, 'path' => $image->path, 'declaration' => 'ai_generated'], ]); // Re-Submit nach Ablehnung erzeugt einen ZWEITEN Eintrag (Versionierung). $pr->fresh()->update(['status' => PressReleaseStatus::Rejected->value]); app(PressReleaseService::class)->submitForReview($pr->fresh()); expect(PressReleaseAiDeclaration::query()->count())->toBe(2); }); test('the create editor requires the ai declaration only when submitting for review', function () { /** @var TestCase $this */ [$customer] = makeAiDeclarationCustomer(); $category = Category::factory()->create(); $this->actingAs($customer); $component = LivewireVolt::test('customer.press-releases.create') ->set('categoryId', $category->id) ->set('title', 'Ein gültiger Titel für die PM') ->set('text', str_repeat('Aussagekräftiger Fließtext. ', 5)); // Einreichen ohne Deklaration → Fehler im Modal, keine PM angelegt. $component ->call('save', 'review') ->assertHasErrors(['aiTextDeclaration', 'aiDeclarationConfirmed']); expect(PressRelease::query()->where('user_id', $customer->id)->count())->toBe(0); // Entwurf-Speichern bleibt ohne Deklaration möglich. $component ->call('save', 'draft') ->assertHasNoErrors(); $draft = PressRelease::query()->where('user_id', $customer->id)->sole(); expect($draft->ai_text_declaration)->toBeNull(); expect($draft->ai_declaration_confirmed_at)->toBeNull(); }); test('submitting from the create editor persists the declaration and the audit entry', function () { /** @var TestCase $this */ [$customer] = makeAiDeclarationCustomer(); $category = Category::factory()->create(); $this->actingAs($customer); LivewireVolt::test('customer.press-releases.create') ->set('categoryId', $category->id) ->set('title', 'Ein gültiger Titel für die PM') ->set('text', str_repeat('Aussagekräftiger Fließtext. ', 5)) ->set('aiTextDeclaration', AiTextDeclaration::FullyAi->value) ->set('aiDeclarationConfirmed', true) ->call('save', 'review') ->assertHasNoErrors(); $pr = PressRelease::query()->where('user_id', $customer->id)->sole(); expect($pr->status)->toBe(PressReleaseStatus::Review); expect($pr->ai_text_declaration)->toBe(AiTextDeclaration::FullyAi); expect($pr->ai_declaration_confirmed_at)->not->toBeNull(); expect($pr->aiTextPublicLabel()) ->toBe('KI-generiert: Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung ganz oder überwiegend mit generativer KI erstellt.'); $entry = PressReleaseAiDeclaration::query()->sole(); expect($entry->press_release_id)->toBe($pr->id); expect($entry->text_declaration)->toBe(AiTextDeclaration::FullyAi); }); test('submitting from the detail page validates and persists the declaration', function () { /** @var TestCase $this */ [$customer, $company] = makeAiDeclarationCustomer(); $pr = PressRelease::factory()->aiUndeclared()->create([ 'user_id' => $customer->id, 'company_id' => $company->id, 'portal' => $company->portal->value, 'status' => PressReleaseStatus::Draft->value, ]); $this->actingAs($customer); LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id]) ->call('submitForReview') ->assertHasErrors(['aiTextDeclaration', 'aiDeclarationConfirmed']); expect($pr->fresh()->status)->toBe(PressReleaseStatus::Draft); LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id]) ->set('aiTextDeclaration', AiTextDeclaration::Assisted->value) ->set('aiDeclarationConfirmed', true) ->call('submitForReview') ->assertHasNoErrors(); $pr->refresh(); expect($pr->status)->toBe(PressReleaseStatus::Review); expect($pr->ai_text_declaration)->toBe(AiTextDeclaration::Assisted); // Formulierungshilfe = Entlastungsfall → kein öffentliches Pflicht-Label. expect($pr->aiTextPublicLabel())->toBeNull(); expect(PressReleaseAiDeclaration::query()->count())->toBe(1); }); test('the submit modal renders the declaration options and the binding confirmation', function () { /** @var TestCase $this */ [$customer, $company] = makeAiDeclarationCustomer(); $pr = PressRelease::factory()->aiUndeclared()->create([ 'user_id' => $customer->id, 'company_id' => $company->id, 'portal' => $company->portal->value, 'status' => PressReleaseStatus::Draft->value, ]); $this->actingAs($customer); LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id]) ->assertSee('KI-Kennzeichnung') ->assertSee('Ohne generative KI erstellt – oder KI nur als Formulierungshilfe, mit anschließender menschlicher Überarbeitung und Verantwortung.') ->assertSee('Ganz oder überwiegend mit generativer KI erstellt.') ->assertSee('Ich bestätige, dass meine Angaben zum Einsatz generativer KI vollständig und wahrheitsgemäß sind.'); }); test('image uploads require the per-image ai declaration', function () { /** @var TestCase $this */ [$customer, $company] = makeAiDeclarationCustomer(); $pr = PressRelease::factory()->create([ 'user_id' => $customer->id, 'company_id' => $company->id, 'category_id' => Category::factory()->create()->id, 'portal' => $company->portal->value, 'status' => 'draft', ]); Storage::fake('public'); $this->actingAs($customer); LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id]) ->set('newImage', UploadedFile::fake()->image('foto.jpg', 1200, 800)) ->set('newCopyright', 'Foto: Jane Doe') ->set('newAuthor', 'Jane Doe') ->set('newLicenseType', ImageLicenseType::Own->value) ->set('newPeopleRightsStatus', 'none') ->set('newPropertyRightsStatus', 'none') ->set('newRightsConfirmed', true) ->call('saveImage') ->assertHasErrors(['newAiDeclaration']); expect($pr->images()->count())->toBe(0); });