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