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>
This commit is contained in:
parent
0240c143c8
commit
62723b551a
27 changed files with 866 additions and 15 deletions
136
tests/Feature/Legal/LegalAcceptanceGateTest.php
Normal file
136
tests/Feature/Legal/LegalAcceptanceGateTest.php
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
<?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');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue