images: Titelbild-Formular v2, Admin-Upload mit Kundenbestätigung, Entfernen mit Sofortlöschung (Bildkennzeichnung Block B)
- press-release-images-manager-v2 hinter IMAGE_RIGHTS_V2: drei Blöcke, Vorbelegung aus Metadaten, Bestätigung mit archiviertem Wortlaut, Bild-Bindung über Hash, Fokuspunkt und Letterbox, Zustände absent/prefilled/error - CustomerImageConfirmation: Magic-Link an den Kontoinhaber, Karte auf der Kundenseite, Kanal magic_link - PressReleaseImageRemover: nie veröffentlicht = alles sofort weg, veröffentlicht = Soft Delete, Original bleibt - ConversionSource::MailImageConfirmation Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
214aea8910
commit
3d3ddc2dc8
11 changed files with 1380 additions and 5 deletions
297
tests/Feature/Images/PressReleaseImageRightsV2Test.php
Normal file
297
tests/Feature/Images/PressReleaseImageRightsV2Test.php
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\AiImageDeclaration;
|
||||
use App\Enums\ConfirmationChannel;
|
||||
use App\Mail\PressReleaseImageConfirmationRequested;
|
||||
use App\Models\Category;
|
||||
use App\Models\Company;
|
||||
use App\Models\ImageRightsConfirmation;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\PressReleaseImage;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\LegalDocumentSeeder;
|
||||
use Database\Seeders\LegalTextSeeder;
|
||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Volt\Volt as LivewireVolt;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Bildkennzeichnung Block B: Formular v2 im Titelbild-Manager, Admin-Upload
|
||||
* mit Kundenbestätigung, Entfernen mit Sofortlöschung.
|
||||
*/
|
||||
beforeEach(function (): void {
|
||||
/** @var TestCase $this */
|
||||
$this->seed(RolesAndPermissionsSeeder::class);
|
||||
$this->seed(LegalDocumentSeeder::class);
|
||||
$this->seed(LegalTextSeeder::class);
|
||||
Storage::fake('public');
|
||||
Storage::fake('local');
|
||||
config()->set('images.rights_v2', true);
|
||||
});
|
||||
|
||||
/**
|
||||
* @return array{owner: User, pr: PressRelease, company: Company}
|
||||
*/
|
||||
function makeRightsV2Draft(): array
|
||||
{
|
||||
$owner = User::factory()->create(['is_active' => true]);
|
||||
$owner->assignRole('customer');
|
||||
|
||||
$company = Company::factory()->presseecho()->create(['name' => 'Beispiel AG']);
|
||||
$owner->companies()->attach($company->id, ['role' => 'owner']);
|
||||
|
||||
$pr = PressRelease::factory()->create([
|
||||
'user_id' => $owner->id,
|
||||
'company_id' => $company->id,
|
||||
'category_id' => Category::factory()->create()->id,
|
||||
'portal' => $company->portal->value,
|
||||
'status' => 'draft',
|
||||
]);
|
||||
|
||||
return compact('owner', 'pr', 'company');
|
||||
}
|
||||
|
||||
function rightsV2Fixture(string $name): UploadedFile
|
||||
{
|
||||
return UploadedFile::fake()->createWithContent($name, (string) file_get_contents(base_path('tests/Fixtures/images/'.$name)));
|
||||
}
|
||||
|
||||
test('the edit page embeds the v2 manager only while the flag is on', function () {
|
||||
/** @var TestCase $this */
|
||||
['owner' => $owner, 'pr' => $pr] = makeRightsV2Draft();
|
||||
$this->actingAs($owner);
|
||||
|
||||
LivewireVolt::test('customer.press-releases.edit', ['id' => $pr->id])
|
||||
->assertSeeLivewire('components.press-release-images-manager-v2');
|
||||
|
||||
config()->set('images.rights_v2', false);
|
||||
|
||||
LivewireVolt::test('customer.press-releases.edit', ['id' => $pr->id])
|
||||
->assertSeeLivewire('components.press-release-images-manager')
|
||||
->assertDontSeeLivewire('components.press-release-images-manager-v2');
|
||||
});
|
||||
|
||||
test('nachweis, kennzeichnung and the confirmation are the only required inputs', function () {
|
||||
/** @var TestCase $this */
|
||||
['owner' => $owner, 'pr' => $pr] = makeRightsV2Draft();
|
||||
$this->actingAs($owner);
|
||||
|
||||
LivewireVolt::test('components.press-release-images-manager-v2', ['pressReleaseId' => $pr->id])
|
||||
->call('openUploadForm')
|
||||
->set('newImage', UploadedFile::fake()->image('foto.jpg', 1200, 800))
|
||||
->set('newCopyright', '')
|
||||
->call('saveImage')
|
||||
->assertHasErrors(['newCopyright', 'newAiDeclaration', 'newRightsConfirmed'])
|
||||
->assertHasNoErrors(['newTitle', 'newLicenseUrl', 'newSourceUrl']);
|
||||
|
||||
expect($pr->images()->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('without metadata the company name is suggested as credit and the state says so', function () {
|
||||
/** @var TestCase $this */
|
||||
['owner' => $owner, 'pr' => $pr] = makeRightsV2Draft();
|
||||
$this->actingAs($owner);
|
||||
|
||||
LivewireVolt::test('components.press-release-images-manager-v2', ['pressReleaseId' => $pr->id])
|
||||
->call('openUploadForm')
|
||||
->set('newImage', UploadedFile::fake()->image('foto.jpg', 1200, 800))
|
||||
->assertSet('newCopyright', 'Beispiel AG')
|
||||
->assertSet('provenanceStatus', 'absent')
|
||||
->assertSee('Keine Herkunftsdaten in der Datei gefunden.');
|
||||
});
|
||||
|
||||
test('iptc and xmp metadata prefill credit and ai declaration and can be changed', function () {
|
||||
/** @var TestCase $this */
|
||||
['owner' => $owner, 'pr' => $pr] = makeRightsV2Draft();
|
||||
$this->actingAs($owner);
|
||||
|
||||
$component = LivewireVolt::test('components.press-release-images-manager-v2', ['pressReleaseId' => $pr->id])
|
||||
->call('openUploadForm')
|
||||
->set('newImage', rightsV2Fixture('camera-iptc.jpg'))
|
||||
->assertSet('newCopyright', 'Musterfirma GmbH / Anna Beispiel')
|
||||
->assertSet('newAiDeclaration', '')
|
||||
->assertSee('Angaben aus den Metadaten der Datei vorbelegt.');
|
||||
|
||||
$component
|
||||
->set('newImage', rightsV2Fixture('xmp-ai-generated.jpg'))
|
||||
->assertSet('newCopyright', 'KI-Bild / Musterfirma')
|
||||
->assertSet('newAiDeclaration', 'ai_generated')
|
||||
->assertSee('Sie können die Auswahl ändern')
|
||||
->set('newAiDeclaration', 'original');
|
||||
|
||||
expect($component->get('newAiDeclaration'))->toBe('original');
|
||||
});
|
||||
|
||||
test('a manual credit survives a file change but the confirmation toggle does not', function () {
|
||||
/** @var TestCase $this */
|
||||
['owner' => $owner, 'pr' => $pr] = makeRightsV2Draft();
|
||||
$this->actingAs($owner);
|
||||
|
||||
LivewireVolt::test('components.press-release-images-manager-v2', ['pressReleaseId' => $pr->id])
|
||||
->call('openUploadForm')
|
||||
->set('newImage', UploadedFile::fake()->image('a.jpg', 640, 480))
|
||||
->set('newCopyright', 'Eigener Nachweis')
|
||||
->set('newRightsConfirmed', true)
|
||||
->set('newImage', rightsV2Fixture('camera-iptc.jpg'))
|
||||
->assertSet('newCopyright', 'Eigener Nachweis')
|
||||
->assertSet('newRightsConfirmed', false);
|
||||
});
|
||||
|
||||
test('an unreadable file shows the error state and is not accepted', function () {
|
||||
/** @var TestCase $this */
|
||||
['owner' => $owner, 'pr' => $pr] = makeRightsV2Draft();
|
||||
$this->actingAs($owner);
|
||||
|
||||
LivewireVolt::test('components.press-release-images-manager-v2', ['pressReleaseId' => $pr->id])
|
||||
->call('openUploadForm')
|
||||
->set('newImage', rightsV2Fixture('broken.jpg'))
|
||||
->assertHasErrors(['newImage']);
|
||||
});
|
||||
|
||||
test('a valid upload stores the image under the new rules with a confirmation and shows it on the card', function () {
|
||||
/** @var TestCase $this */
|
||||
['owner' => $owner, 'pr' => $pr] = makeRightsV2Draft();
|
||||
$this->actingAs($owner);
|
||||
|
||||
LivewireVolt::test('components.press-release-images-manager-v2', ['pressReleaseId' => $pr->id])
|
||||
->call('openUploadForm')
|
||||
->set('newImage', rightsV2Fixture('camera-iptc.jpg'))
|
||||
->set('newAiDeclaration', AiImageDeclaration::Original->value)
|
||||
->set('newTitle', 'Werkshalle')
|
||||
->set('newLicenseUrl', 'https://example.com/lizenz')
|
||||
->set('focus', '100,0')
|
||||
->set('newRightsConfirmed', true)
|
||||
->call('saveImage')
|
||||
->assertHasNoErrors()
|
||||
->assertSet('isUploadFormOpen', false)
|
||||
->assertDispatched('title-image-changed')
|
||||
->assertSee('Rechte bestätigt am')
|
||||
->assertSee('img-confirm-v2');
|
||||
|
||||
$image = $pr->images()->sole();
|
||||
|
||||
expect($image->rules_version)->toBe(config('images.rules_version'))
|
||||
->and($image->copyright)->toBe('Musterfirma GmbH / Anna Beispiel')
|
||||
->and($image->credit_source?->value)->toBe('metadata')
|
||||
->and($image->title)->toBe('Werkshalle')
|
||||
->and($image->license_url)->toBe('https://example.com/lizenz')
|
||||
->and($image->focus_x)->toBe(100)
|
||||
->and($image->focus_y)->toBe(0)
|
||||
->and($image->license_type)->toBeNull()
|
||||
->and($image->people_rights_status)->toBeNull()
|
||||
->and(Storage::disk('local')->exists($image->original_path))->toBeTrue()
|
||||
->and($image->hasValidRightsConfirmation())->toBeTrue();
|
||||
|
||||
$confirmation = ImageRightsConfirmation::query()->sole();
|
||||
|
||||
expect($confirmation->channel)->toBe(ConfirmationChannel::Form)
|
||||
->and($confirmation->user_id)->toBe($owner->id)
|
||||
->and($confirmation->legal_text_key)->toBe('img-confirm-v2')
|
||||
->and($confirmation->text_snapshot)->toContain('/de/agb');
|
||||
});
|
||||
|
||||
test('an admin upload skips the confirmation, mails the customer and blocks nothing else', function () {
|
||||
/** @var TestCase $this */
|
||||
Mail::fake();
|
||||
['pr' => $pr, 'owner' => $owner] = makeRightsV2Draft();
|
||||
$admin = User::factory()->create(['is_active' => true]);
|
||||
$admin->assignRole('admin');
|
||||
$this->actingAs($admin);
|
||||
|
||||
LivewireVolt::test('components.press-release-images-manager-v2', ['pressReleaseId' => $pr->id])
|
||||
->call('openUploadForm')
|
||||
->set('newImage', UploadedFile::fake()->image('foto.jpg', 1200, 800))
|
||||
->set('newCopyright', 'Beispiel AG')
|
||||
->set('newAiDeclaration', AiImageDeclaration::Original->value)
|
||||
->assertSee('Sie laden im Auftrag des Kunden hoch')
|
||||
->call('saveImage')
|
||||
->assertHasNoErrors()
|
||||
->assertSee('wartet auf Kundenbestätigung');
|
||||
|
||||
$image = $pr->images()->sole();
|
||||
|
||||
expect($image->customer_confirmation_requested_at)->not->toBeNull()
|
||||
->and($image->awaitsCustomerConfirmation())->toBeTrue()
|
||||
->and(ImageRightsConfirmation::query()->count())->toBe(0);
|
||||
|
||||
Mail::assertQueued(PressReleaseImageConfirmationRequested::class, fn ($mail) => $mail->hasTo($owner->email) && $mail->image->is($image));
|
||||
});
|
||||
|
||||
test('the customer confirms an admin upload on the detail page with the magic link channel', function () {
|
||||
/** @var TestCase $this */
|
||||
['pr' => $pr, 'owner' => $owner] = makeRightsV2Draft();
|
||||
$image = PressReleaseImage::factory()->for($pr)->rulesV2()->create([
|
||||
'rights_confirmed_at' => null,
|
||||
'customer_confirmation_requested_at' => now(),
|
||||
]);
|
||||
$this->actingAs($owner);
|
||||
|
||||
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
|
||||
->assertSee('Titelbild bestätigen')
|
||||
->call('confirmImageRights')
|
||||
->assertHasErrors(['imageRightsConfirmed'])
|
||||
->set('imageRightsConfirmed', true)
|
||||
->call('confirmImageRights')
|
||||
->assertHasNoErrors()
|
||||
->assertDontSee('Titelbild bestätigen');
|
||||
|
||||
$confirmation = $image->rightsConfirmations()->sole();
|
||||
|
||||
expect($confirmation->channel)->toBe(ConfirmationChannel::MagicLink)
|
||||
->and($confirmation->user_id)->toBe($owner->id)
|
||||
->and($confirmation->file_sha256)->toBe($image->original_sha256)
|
||||
->and($image->fresh()->awaitsCustomerConfirmation())->toBeFalse()
|
||||
->and($image->fresh()->rights_confirmed_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
test('removing a never published image deletes original, derivatives and confirmation for good', function () {
|
||||
/** @var TestCase $this */
|
||||
['owner' => $owner, 'pr' => $pr] = makeRightsV2Draft();
|
||||
$this->actingAs($owner);
|
||||
|
||||
$component = LivewireVolt::test('components.press-release-images-manager-v2', ['pressReleaseId' => $pr->id])
|
||||
->call('openUploadForm')
|
||||
->set('newImage', UploadedFile::fake()->image('foto.jpg', 1200, 800))
|
||||
->set('newAiDeclaration', AiImageDeclaration::Original->value)
|
||||
->set('newRightsConfirmed', true)
|
||||
->call('saveImage')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$image = $pr->images()->sole();
|
||||
expect(Storage::disk('local')->allFiles())->not->toBe([]);
|
||||
|
||||
$component->call('remove', $image->id)->assertDispatched('title-image-changed');
|
||||
|
||||
expect(PressReleaseImage::withTrashed()->whereKey($image->id)->exists())->toBeFalse()
|
||||
->and(ImageRightsConfirmation::query()->count())->toBe(0)
|
||||
->and(Storage::disk('local')->allFiles())->toBe([]);
|
||||
});
|
||||
|
||||
test('removing a once published image keeps the original and the confirmation and only soft deletes', function () {
|
||||
/** @var TestCase $this */
|
||||
['owner' => $owner, 'pr' => $pr] = makeRightsV2Draft();
|
||||
$admin = User::factory()->create(['is_active' => true]);
|
||||
$admin->assignRole('admin');
|
||||
$this->actingAs($admin);
|
||||
|
||||
LivewireVolt::test('components.press-release-images-manager-v2', ['pressReleaseId' => $pr->id])
|
||||
->call('openUploadForm')
|
||||
->set('newImage', UploadedFile::fake()->image('foto.jpg', 1200, 800))
|
||||
->set('newCopyright', 'Beispiel AG')
|
||||
->set('newAiDeclaration', AiImageDeclaration::Original->value)
|
||||
->call('saveImage')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$image = $pr->images()->sole();
|
||||
$image->forceFill(['published_ever_at' => now()])->save();
|
||||
|
||||
LivewireVolt::test('components.press-release-images-manager-v2', ['pressReleaseId' => $pr->id])
|
||||
->call('remove', $image->id);
|
||||
|
||||
expect(PressReleaseImage::withTrashed()->whereKey($image->id)->sole()->trashed())->toBeTrue()
|
||||
->and(Storage::disk('local')->exists($image->original_path))->toBeTrue()
|
||||
->and(Storage::disk('local')->exists($image->path))->toBeFalse();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue