web/admin/pdf: gemeinsame Bildnachweis-Auflösung, Detailfenster am Artikelbild, Bestätigungsstand in Kunden- und Adminansicht, Wortwahl „automatisierte Prüfung" (Bildkennzeichnung Block D)
- App\Support\ImageCredit: Foto/Quelle-Zeile, KI-Hinweis mit Herkunft, Verantwortlich-Zeile nur mit Protokoll, sichere Links; PDF-Zeilen mit Bestätigungskennung - Artikelseite: Overlay (eigenes i-Zeichen, Alpine ohne Plugin, Bottom-Sheet auf Mobil) mit Nachweis, Lizenz, Quelle, KI-Hinweis, Verantwortlich-Zeile, „Bild melden" - Richtlinien 0.2: Prüfung als automatisiert beschrieben, Abschnitt „Bilder" (Decision-Update 3.5); Taglines, Suche, Newsrooms, Startseiten-CTA, Mails ohne „qualitätsgeprüft" - Cache-Invalidierung bei Bildwechsel an veröffentlichten Meldungen; PHPStan-Baseline für das Resource bereinigt Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
65ac72ce05
commit
2124ffd1f2
26 changed files with 578 additions and 199 deletions
|
|
@ -76,9 +76,13 @@ test('an outdated text version is rejected with the current key in the message',
|
|||
|
||||
test('a valid upload stores the confirmation with the token and returns the new fields', function () {
|
||||
/** @var TestCase $this */
|
||||
['pressRelease' => $pressRelease, 'user' => $user] = rightsV2ApiDraft();
|
||||
// Echter Bearer-Token statt Sanctum::actingAs, damit die Token-ID im
|
||||
// Protokoll landet (TransientToken hat keinen Datensatz).
|
||||
$user = User::factory()->create();
|
||||
$pressRelease = PressRelease::factory()->create(['user_id' => $user->id, 'status' => PressReleaseStatus::Draft->value]);
|
||||
$plainToken = $user->createToken('connektar', ['press-releases:read', 'press-release-images:write'])->plainTextToken;
|
||||
|
||||
$response = $this->postJson("/api/v1/press-releases/{$pressRelease->id}/images", [
|
||||
$response = $this->withToken($plainToken)->postJson("/api/v1/press-releases/{$pressRelease->id}/images", [
|
||||
'image' => rightsV2ApiFixture('xmp-ai-generated.jpg'),
|
||||
'ai_declaration' => 'ai_generated',
|
||||
'copyright' => 'KI-Bild / Musterfirma',
|
||||
|
|
@ -110,7 +114,7 @@ test('a valid upload stores the confirmation with the token and returns the new
|
|||
->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->personal_access_token_id)->toBe($user->tokens()->sole()->getKey())
|
||||
->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');
|
||||
|
|
|
|||
141
tests/Feature/Images/ImageCreditOutputTest.php
Normal file
141
tests/Feature/Images/ImageCreditOutputTest.php
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\ConfirmationChannel;
|
||||
use App\Enums\Portal;
|
||||
use App\Models\Category;
|
||||
use App\Models\Company;
|
||||
use App\Models\ImageRightsConfirmation;
|
||||
use App\Models\LegalDocument;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\PressReleaseImage;
|
||||
use App\Support\ImageCredit;
|
||||
use Database\Seeders\LegalDocumentSeeder;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Bildkennzeichnung Block D: eine Auflösung für Nachweiszeile, Overlay,
|
||||
* Kunden-/Adminansicht und PDF; Verantwortlich-Zeile nur mit Protokoll.
|
||||
*/
|
||||
function creditPublishedRelease(): array
|
||||
{
|
||||
$category = Category::factory()->create(['portal' => Portal::Presseecho]);
|
||||
$category->translations()->create(['locale' => 'de', 'name' => 'Energie', 'slug' => 'energie']);
|
||||
$company = Company::factory()->presseecho()->create(['name' => 'Beispiel AG', 'is_active' => true]);
|
||||
|
||||
$release = PressRelease::factory()
|
||||
->published()
|
||||
->forPortal(Portal::Presseecho)
|
||||
->for($category)
|
||||
->for($company)
|
||||
->create(['slug' => 'bild-nachweis-test', 'published_at' => now()->subDay()]);
|
||||
|
||||
return compact('category', 'company', 'release');
|
||||
}
|
||||
|
||||
function creditConfirmationFor(PressReleaseImage $image, string $channel = 'form'): ImageRightsConfirmation
|
||||
{
|
||||
return ImageRightsConfirmation::query()->create([
|
||||
'subject_type' => $image->getMorphClass(),
|
||||
'subject_id' => $image->getKey(),
|
||||
'press_release_id' => $image->press_release_id,
|
||||
'company_id' => $image->pressRelease->company_id,
|
||||
'user_id' => $image->pressRelease->user_id,
|
||||
'channel' => ConfirmationChannel::from($channel),
|
||||
'legal_text_key' => 'img-confirm-v2',
|
||||
'text_snapshot' => 'Ich bestätige …',
|
||||
'agb_version' => '0.6',
|
||||
'file_sha256' => $image->original_sha256,
|
||||
'declaration_snapshot' => [],
|
||||
'confirmed_at' => now()->setDate(2026, 9, 22)->setTime(10, 0),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
test('a formular v2 image renders credit line, ai hint with source and the responsible line', function () {
|
||||
/** @var TestCase $this */
|
||||
['release' => $release, 'company' => $company] = creditPublishedRelease();
|
||||
$image = PressReleaseImage::factory()->for($release)->rulesV2()->create([
|
||||
'copyright' => 'Anna Beispiel / Beispiel AG',
|
||||
'ai_declaration' => 'ai_edited',
|
||||
'ai_declaration_user' => 'ai_edited',
|
||||
'ai_declaration_source' => 'customer',
|
||||
'license_url' => 'https://example.com/lizenz',
|
||||
'source_url' => 'javascript:alert(1)',
|
||||
]);
|
||||
creditConfirmationFor($image);
|
||||
|
||||
$credit = ImageCredit::for($image->fresh(), $company);
|
||||
|
||||
expect($credit->line())->toBe('Foto: Anna Beispiel / Beispiel AG · KI-bearbeitet')
|
||||
->and($credit->responsibleLine)->toBe('Bild und Angaben: Beispiel AG, eingereicht am 22. September 2026')
|
||||
->and($credit->aiSourceLabel)->toBe('Angabe des Einreichers')
|
||||
->and($credit->licenseUrl)->toBe('https://example.com/lizenz')
|
||||
->and($credit->sourceUrl)->toBeNull()
|
||||
->and($credit->pdfLines())->toContain('Rechte bestätigt am 22.09.2026 10:00 (img-confirm-v2, AGB 0.6)');
|
||||
|
||||
$this->get('https://presseecho.test/de/press-release/bild-nachweis-test')
|
||||
->assertSuccessful()
|
||||
->assertSeeText('Foto: Anna Beispiel / Beispiel AG · KI-bearbeitet')
|
||||
->assertSee('data-image-credit', false)
|
||||
->assertSeeText('Dieses Bild wurde mit KI bearbeitet.')
|
||||
->assertSeeText('Kennzeichnung: Angabe des Einreichers')
|
||||
->assertSeeText('Bild und Angaben: Beispiel AG, eingereicht am 22. September 2026')
|
||||
->assertSeeText('Bild melden')
|
||||
->assertDontSee('javascript:alert', false);
|
||||
});
|
||||
|
||||
test('a legacy image without credit falls back to the company as source and shows no responsible line', function () {
|
||||
/** @var TestCase $this */
|
||||
['release' => $release, 'company' => $company] = creditPublishedRelease();
|
||||
$image = PressReleaseImage::factory()->for($release)->legacy()->create(['is_ai_generated' => true]);
|
||||
|
||||
$credit = ImageCredit::for($image, $company);
|
||||
|
||||
expect($credit->line())->toBe('Quelle: Beispiel AG · KI-generiert')
|
||||
->and($credit->responsibleLine)->toBeNull()
|
||||
->and($credit->aiSourceLabel)->toBeNull()
|
||||
->and($credit->isLegacy)->toBeTrue();
|
||||
|
||||
$this->get('https://presseecho.test/de/press-release/bild-nachweis-test')
|
||||
->assertSuccessful()
|
||||
->assertSeeText('Quelle: Beispiel AG · KI-generiert')
|
||||
->assertDontSeeText('Bild und Angaben');
|
||||
});
|
||||
|
||||
test('a legacy image with author but without copyright uses the author and the old fields stay in the pdf lines', function () {
|
||||
['release' => $release, 'company' => $company] = creditPublishedRelease();
|
||||
$image = PressReleaseImage::factory()->for($release)->legacy()->create([
|
||||
'author' => 'Max Mustermann',
|
||||
'license_type' => 'own',
|
||||
]);
|
||||
|
||||
$credit = ImageCredit::for($image, $company);
|
||||
|
||||
expect($credit->line())->toBe('Foto: Max Mustermann')
|
||||
->and($credit->pdfLines())->toBe(['Bildnachweis: Max Mustermann']);
|
||||
});
|
||||
|
||||
test('an image without any detail renders no line and no overlay', function () {
|
||||
/** @var TestCase $this */
|
||||
['release' => $release] = creditPublishedRelease();
|
||||
PressReleaseImage::factory()->for($release)->legacy()->create();
|
||||
$release->company()->update(['name' => '']);
|
||||
|
||||
$credit = ImageCredit::for($release->images()->first(), $release->company()->first());
|
||||
|
||||
expect($credit->line())->toBeNull()
|
||||
->and($credit->hasDetails())->toBeFalse();
|
||||
});
|
||||
|
||||
test('the guidelines page describes the automated check and the image rules under version 0.2', function () {
|
||||
/** @var TestCase $this */
|
||||
$this->seed(LegalDocumentSeeder::class);
|
||||
|
||||
expect(LegalDocument::current(LegalDocument::TYPE_RICHTLINIEN)?->version)->toBe('0.2');
|
||||
|
||||
$this->get('https://presseecho.test/de/richtlinien')
|
||||
->assertSuccessful()
|
||||
->assertSeeText('Jede Mitteilung durchläuft vor der Veröffentlichung eine automatisierte Prüfung.')
|
||||
->assertSeeText('Zu jedem Bild gehört ein Bildnachweis')
|
||||
->assertDontSeeText('geprüft, den Richtlinien entsprechend');
|
||||
});
|
||||
|
|
@ -21,12 +21,13 @@ test('the agb version falls back to the config while the table is empty', functi
|
|||
expect(LegalDocument::currentAgbVersion())->toBe('9.9');
|
||||
});
|
||||
|
||||
test('the seeder registers all five legal documents idempotently', function () {
|
||||
test('the seeder registers all legal documents idempotently', function () {
|
||||
/** @var TestCase $this */
|
||||
$this->seed(LegalDocumentSeeder::class);
|
||||
$this->seed(LegalDocumentSeeder::class); // Wiederholung darf nichts duplizieren.
|
||||
|
||||
expect(LegalDocument::query()->count())->toBe(5);
|
||||
expect(LegalDocument::query()->count())->toBe(6);
|
||||
expect(LegalDocument::current(LegalDocument::TYPE_RICHTLINIEN)?->version)->toBe('0.2');
|
||||
expect(LegalDocument::currentAgbVersion())->toBe('0.6');
|
||||
expect(LegalDocument::current(LegalDocument::TYPE_DATENSCHUTZ)?->version)->toBe('0.3');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ test('businessportal24 homepage renders the editorial shell with real data', fun
|
|||
->assertSeeText('Im Fokus')
|
||||
->assertSeeText('Aktive Newsrooms')
|
||||
->assertSeeText('Veröffentlichen Sie Ihre Pressemitteilung')
|
||||
->assertSeeText('Ihre Pressemitteilung, qualitätsgeprüft veröffentlicht')
|
||||
->assertSeeText('Ihre Pressemitteilung, automatisiert geprüft und veröffentlicht')
|
||||
->assertSeeText('Konto anlegen')
|
||||
->assertSeeText('Unser Qualitätsstandard')
|
||||
->assertDontSeeText('redaktionell geprüft')
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ test('presseecho homepage renders the editorial shell with real data', function
|
|||
->assertSeeText('Im Fokus')
|
||||
->assertSeeText('Aktive Newsrooms')
|
||||
->assertSeeText('Veröffentlichen Sie Ihre Pressemitteilung')
|
||||
->assertSeeText('Ihre Pressemitteilung, qualitätsgeprüft veröffentlicht')
|
||||
->assertSeeText('Ihre Pressemitteilung, automatisiert geprüft und veröffentlicht')
|
||||
->assertSeeText('Konto anlegen')
|
||||
->assertSeeText('Unser Qualitätsstandard')
|
||||
->assertDontSeeText('redaktionell geprüft')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue