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(); });