presseportale/tests/Feature/Legal/LegalAcceptanceGateTest.php
Kevin Adametz 075d0d931f legal/web: Zustimmungsgate je Dokumentversion (aus bis AGB 0.7), OG-Variante mit eingebranntem Bildnachweis, Nachweis in Teasern (Bildkennzeichnung Blöcke G und H)
- legal_document_acceptances (insert-only), LegalAcceptanceGate, Pflicht-Schalter im Einreichungs-Modal über den gemeinsamen Trait; API: GET/POST /api/v1/legal/acceptances, submit antwortet 428 mit den geforderten Fassungen; LEGAL_ACCEPTANCE_GATE
- ImageService: og-Variante (1200×630) mit halbtransparentem Balken und Inter SemiBold, regenerateOgVariant() für Korrekturen und Bestand; Fokuspunkt/Letterbox getestet
- Uploader brennt den Nachweis beim Upload ein, ImageCorrectionService baut die OG-Variante nach einer Korrektur neu; Artikelseite nutzt og für og:image
- Teaser: Nachweis in alt/title (Feed, Hero, Kategorie, Newsroom), Unterschrift am Hero-Bild; PressRelease::cardImageCredit()

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

136 lines
5.5 KiB
PHP

<?php
use App\Enums\PressReleaseStatus;
use App\Models\Category;
use App\Models\Company;
use App\Models\LegalDocument;
use App\Models\LegalDocumentAcceptance;
use App\Models\PressRelease;
use App\Models\User;
use App\Services\Legal\LegalAcceptanceGate;
use Database\Seeders\LegalDocumentSeeder;
use Database\Seeders\RolesAndPermissionsSeeder;
use Illuminate\Support\Facades\Mail;
use Laravel\Sanctum\Sanctum;
use Livewire\Volt\Volt as LivewireVolt;
use Tests\TestCase;
/**
* Zustimmungsgate (Bildkennzeichnung Block G, Decision-Update E12, O2).
*/
beforeEach(function (): void {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
$this->seed(LegalDocumentSeeder::class);
Mail::fake();
config()->set('billing.enforce_booking', false);
});
/**
* @return array{owner: User, pr: PressRelease}
*/
function gateDraft(): array
{
$owner = User::factory()->create(['is_active' => true]);
$owner->assignRole('customer');
$company = Company::factory()->presseecho()->create();
$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');
}
test('with the gate off nothing is required and submitting works as before', function () {
/** @var TestCase $this */
['owner' => $owner, 'pr' => $pr] = gateDraft();
$this->actingAs($owner);
expect(app(LegalAcceptanceGate::class)->requiredFor($owner))->toBeEmpty();
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->assertDontSee('Neue Fassung unserer Bedingungen')
->set('aiTextDeclaration', 'none')
->set('aiDeclarationConfirmed', true)
->call('submitForReview')
->assertHasNoErrors();
expect($pr->fresh()->status)->not->toBe(PressReleaseStatus::Draft)
->and(LegalDocumentAcceptance::query()->count())->toBe(0);
});
test('with the gate on the submit requires an active acceptance of agb and guidelines and records both', function () {
/** @var TestCase $this */
config()->set('legal.acceptance_gate', true);
['owner' => $owner, 'pr' => $pr] = gateDraft();
$this->actingAs($owner);
expect(app(LegalAcceptanceGate::class)->requiredFor($owner)->pluck('type')->all())->toBe(['agb', 'richtlinien']);
$component = LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->assertSee('Neue Fassung unserer Bedingungen')
->assertSee('Version 0.6')
->set('aiTextDeclaration', 'none')
->set('aiDeclarationConfirmed', true)
->call('submitForReview')
->assertHasErrors(['legalAcceptanceConfirmed']);
expect($pr->fresh()->status)->toBe(PressReleaseStatus::Draft);
$component->set('legalAcceptanceConfirmed', true)->call('submitForReview')->assertHasNoErrors();
// Der synchrone Prüf-Job kann die Meldung direkt veröffentlichen — entscheidend ist: nicht mehr Entwurf.
expect($pr->fresh()->status)->not->toBe(PressReleaseStatus::Draft)
->and(LegalDocumentAcceptance::query()->where('user_id', $owner->id)->pluck('type')->sort()->values()->all())->toBe(['agb', 'richtlinien'])
->and(LegalDocumentAcceptance::query()->first()->context)->toBe('submit')
->and(app(LegalAcceptanceGate::class)->requiredFor($owner))->toBeEmpty();
// Neue AGB-Fassung: das Gate fordert genau diese eine Fassung erneut.
LegalDocument::factory()->agb()->create(['version' => '0.7', 'effective_from' => now()->subDay()->toDateString()]);
expect(app(LegalAcceptanceGate::class)->requiredFor($owner)->pluck('version')->all())->toBe(['0.7']);
});
test('acceptances are insert-only', function () {
config()->set('legal.acceptance_gate', true);
['owner' => $owner] = gateDraft();
$gate = app(LegalAcceptanceGate::class);
$rows = $gate->accept($owner, $gate->requiredFor($owner), 'gate', '203.0.113.5', 'Pest');
expect($rows)->toHaveCount(2)
->and($rows[0]->ip_hmac)->toStartWith('v1:')
->and(fn () => $rows[0]->delete())->toThrow(LogicException::class)
->and($gate->accept($owner, $gate->requiredFor($owner), 'gate'))->toBe([]);
});
test('the api answers 428 with the required documents until they are accepted', function () {
/** @var TestCase $this */
config()->set('legal.acceptance_gate', true);
['owner' => $owner, 'pr' => $pr] = gateDraft();
Sanctum::actingAs($owner, ['press-releases:write', 'press-releases:read']);
$this->postJson("/api/v1/press-releases/{$pr->id}/submit", ['ai_text_declaration' => 'none'])
->assertStatus(428)
->assertJsonPath('required_documents.0.type', 'agb')
->assertJsonPath('required_documents.1.version', '0.2');
$this->postJson('/api/v1/legal/acceptances', ['documents' => [['type' => 'agb', 'version' => '0.5']]])
->assertUnprocessable();
$this->postJson('/api/v1/legal/acceptances', ['documents' => [
['type' => 'agb', 'version' => '0.6'],
['type' => 'richtlinien', 'version' => '0.2'],
]])->assertCreated()->assertJsonPath('required', []);
expect(LegalDocumentAcceptance::query()->where('context', 'api')->count())->toBe(2);
$this->postJson("/api/v1/press-releases/{$pr->id}/submit", ['ai_text_declaration' => 'none'])->assertSuccessful();
$this->getJson('/api/v1/legal/acceptances')->assertSuccessful()->assertJsonPath('gate_enabled', true)->assertJsonCount(2, 'accepted');
});