presseportale/tests/Feature/Images/ImageRightsConfirmationTest.php
Kevin Adametz 214aea8910 images: Datenmodell, Textarchiv, Herkunftsprüfung und gemeinsamer Uploader für Formular v2 (Bildkennzeichnung Block A)
- press_release_images: Kundenangabe/Portalkennzeichnung getrennt, Herkunftsbefund, Beweisoriginal, Snapshot-Pfade, published_ever_at, rules_version, Fokus/Letterbox; Bestand unangetastet
- legal_texts (insert-only) mit img-confirm-v1 (alter Wortlaut), v2 (AGB 0.6), v3 (AGB 0.7, noch ohne Datum); image-rights:backfill-confirmations
- image_rights_confirmations polymorph, insert-only, IP als versionierter HMAC; Löschung nur für nie veröffentlichte Uploads
- ImageProvenanceInspector (Imagick/exif): IPTC, XMP, EXIF; algorithmicallyEnhanced nur Vermerk; unlesbare Datei = error
- PressReleaseImageUploader: Original vor Derivaten, Hash-Bindung, Titelbild-Regel unter Sperre, Dateien bei Fehlern zurückgerollt; Admin-Upload ohne Bestätigung
- ImageService: kein Rückfall auf die Quelldatei, og-Variante, Fokuspunkt und no_crop

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 09:22:05 +00:00

101 lines
4 KiB
PHP

<?php
use App\Enums\ConfirmationChannel;
use App\Models\ImageRightsConfirmation;
use App\Models\PressRelease;
use App\Models\PressReleaseImage;
function makeConfirmationFor(PressReleaseImage $image): 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::Form,
'legal_text_key' => 'img-confirm-v2',
'text_snapshot' => 'Ich bestätige …',
'agb_version' => '0.6',
'file_sha256' => $image->original_sha256,
'declaration_snapshot' => ['copyright' => $image->copyright],
'ip_hmac' => ImageRightsConfirmation::hashIp('203.0.113.7'),
'user_agent' => 'PestBrowser/1.0',
'confirmed_at' => now(),
'created_at' => now(),
]);
}
test('confirmations are insert-only', function () {
$image = PressReleaseImage::factory()->rulesV2()->create();
$confirmation = makeConfirmationFor($image);
expect(fn () => $confirmation->update(['agb_version' => '0.7']))->toThrow(LogicException::class)
->and(fn () => $confirmation->delete())->toThrow(LogicException::class)
->and(ImageRightsConfirmation::query()->count())->toBe(1);
});
test('only the purge for never published uploads may remove a confirmation', function () {
$image = PressReleaseImage::factory()->rulesV2()->create();
$confirmation = makeConfirmationFor($image);
$confirmation->purgeForUnpublished();
expect(ImageRightsConfirmation::query()->count())->toBe(0);
});
test('a hard deleted image leaves its confirmation with the snapshot in place', function () {
$image = PressReleaseImage::factory()->rulesV2()->create();
$confirmation = makeConfirmationFor($image);
$pressReleaseId = $image->press_release_id;
$image->forceDelete();
$confirmation->refresh();
expect($confirmation->subject_id)->toBe($image->getKey())
->and($confirmation->press_release_id)->toBe($pressReleaseId)
->and($confirmation->file_sha256)->toBe($image->original_sha256)
->and($confirmation->declaration_snapshot['copyright'])->toBe($image->copyright);
});
test('a hard deleted press release nulls the reference but keeps the row', function () {
$pressRelease = PressRelease::factory()->create();
$image = PressReleaseImage::factory()->for($pressRelease)->rulesV2()->create();
$confirmation = makeConfirmationFor($image);
$pressRelease->forceDelete();
expect($confirmation->fresh())->not->toBeNull()
->and($confirmation->fresh()?->press_release_id)->toBeNull();
});
test('the ip derivation is keyed, versioned and not a plain hash', function () {
config()->set('images.confirmation_ip_key', 'geheim-1');
$first = ImageRightsConfirmation::hashIp('203.0.113.7');
config()->set('images.confirmation_ip_key', 'geheim-2');
$second = ImageRightsConfirmation::hashIp('203.0.113.7');
expect($first)->toStartWith('v1:')
->and($first)->not->toBe($second)
->and($first)->not->toContain(hash('sha256', '203.0.113.7'))
->and(ImageRightsConfirmation::hashIp(null))->toBeNull();
});
test('the image exposes its latest valid confirmation bound to the current file hash', function () {
$image = PressReleaseImage::factory()->rulesV2()->create();
expect($image->hasValidRightsConfirmation())->toBeFalse()
->and($image->isLegacyRules())->toBeFalse();
makeConfirmationFor($image);
expect($image->fresh()->hasValidRightsConfirmation())->toBeTrue();
// Bildtausch: neuer Hash, alte Bestätigung zählt nicht mehr (F09).
$image->update(['original_sha256' => hash('sha256', 'neue-datei')]);
expect($image->fresh()->hasValidRightsConfirmation())->toBeFalse()
->and(PressReleaseImage::factory()->legacy()->create()->isLegacyRules())->toBeTrue();
});