Zwei eigenstaendige Formulare unter /melden/rechtswidrig (DSA Art. 16:
URL, Name, E-Mail, Begruendung, Wahrheitsversicherung) und
/melden/kontaktdaten (DSGVO-Antrag) auf allen drei Domains. Gemeldete
URLs werden automatisch der passenden veroeffentlichten PM zugeordnet;
ohne Treffer bleibt die URL im payload. Jede Meldung erhaelt eine
Vorgangsnummer (M-{id}, analog K-{id} der Kuendigung), die meldende
Person unverzueglich eine Eingangsbestaetigung (DSA Art. 16 Abs. 4) und
das Melde-Postfach einen internen Hinweis — auch im bestehenden
Per-PM-Flow. Admin-Queue zeigt Vorgangsnummer, Name und gemeldete URL;
/melden verlinkt die neuen Formulare als ersten Weg.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
213 lines
7.9 KiB
PHP
213 lines
7.9 KiB
PHP
<?php
|
|
|
|
use App\Enums\LegalRequestStatus;
|
|
use App\Enums\LegalRequestType;
|
|
use App\Enums\Portal;
|
|
use App\Mail\LegalRequestReceived;
|
|
use App\Mail\LegalRequestSubmitted;
|
|
use App\Models\LegalRequest;
|
|
use App\Models\PressRelease;
|
|
use App\Models\User;
|
|
use Database\Seeders\RolesAndPermissionsSeeder;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Livewire\Volt\Volt;
|
|
use Tests\TestCase;
|
|
|
|
beforeEach(function () {
|
|
$this->seed(RolesAndPermissionsSeeder::class);
|
|
});
|
|
|
|
/**
|
|
* Default-Testhost ist „localhost" → der Controller löst auf Businessportal24
|
|
* auf. Daher die PM bewusst für dieses Portal veröffentlichen.
|
|
*/
|
|
function publishedRelease(): PressRelease
|
|
{
|
|
return PressRelease::factory()
|
|
->published()
|
|
->forPortal(Portal::Businessportal24)
|
|
->create();
|
|
}
|
|
|
|
test('the legal request form renders with per-type hints and hidden honeypots', function () {
|
|
/** @var TestCase $this */
|
|
$release = publishedRelease();
|
|
|
|
$this->get(route('legal-request.create', ['slug' => $release->slug, 'type' => 'report']))
|
|
->assertOk()
|
|
->assertSee($release->title)
|
|
// Hinweistexte je Typ sind serverseitig vorhanden (Umschaltung via Alpine).
|
|
->assertSee('Art. 17 DSGVO')
|
|
->assertSee('Persönlichkeitsrecht')
|
|
// Versteckte Honeypot-Felder im DOM.
|
|
->assertSee('name="website"', false)
|
|
->assertSee('name="homepage"', false);
|
|
});
|
|
|
|
test('the form 404s for a non-published release', function () {
|
|
/** @var TestCase $this */
|
|
$draft = PressRelease::factory()->forPortal(Portal::Businessportal24)->create();
|
|
|
|
$this->get(route('legal-request.create', ['slug' => $draft->slug]))
|
|
->assertNotFound();
|
|
});
|
|
|
|
test('a dsgvo request is stored in the queue', function () {
|
|
/** @var TestCase $this */
|
|
Mail::fake();
|
|
$release = publishedRelease();
|
|
|
|
$this->post(route('legal-request.store', ['slug' => $release->slug]), [
|
|
'type' => LegalRequestType::Dsgvo->value,
|
|
'requester_email' => 'betroffen@example.test',
|
|
'message' => 'Bitte meine personenbezogenen Daten anonymisieren.',
|
|
])->assertRedirect(route('release.detail', ['slug' => $release->slug]))
|
|
->assertSessionHas('legal-status');
|
|
|
|
$request = LegalRequest::query()->firstOrFail();
|
|
|
|
expect($request->type)->toBe(LegalRequestType::Dsgvo);
|
|
expect($request->status)->toBe(LegalRequestStatus::Open);
|
|
expect($request->press_release_id)->toBe($release->id);
|
|
expect($request->requester_email)->toBe('betroffen@example.test');
|
|
expect($request->portal)->toBe(Portal::Businessportal24->value);
|
|
|
|
// T10: Vorgangsnummer im Hinweis + Eingangsbestätigung/Queue-Hinweis.
|
|
expect(session('legal-status'))->toContain($request->reference());
|
|
Mail::assertSent(LegalRequestReceived::class, fn ($mail) => $mail->hasTo('betroffen@example.test'));
|
|
Mail::assertSent(LegalRequestSubmitted::class);
|
|
});
|
|
|
|
test('a personal rights request requires an email', function () {
|
|
/** @var TestCase $this */
|
|
$release = publishedRelease();
|
|
|
|
$this->post(route('legal-request.store', ['slug' => $release->slug]), [
|
|
'type' => LegalRequestType::PersonalRights->value,
|
|
'message' => 'Diese Meldung verletzt meine Persönlichkeitsrechte.',
|
|
])->assertSessionHasErrors('requester_email');
|
|
|
|
expect(LegalRequest::query()->count())->toBe(0);
|
|
});
|
|
|
|
test('a report can be submitted without an email and feeds the same queue', function () {
|
|
/** @var TestCase $this */
|
|
Mail::fake();
|
|
$release = publishedRelease();
|
|
|
|
$this->post(route('legal-request.store', ['slug' => $release->slug]), [
|
|
'type' => LegalRequestType::Report->value,
|
|
'message' => 'Diese Pressemitteilung wirkt wie Spam.',
|
|
])->assertRedirect(route('release.detail', ['slug' => $release->slug]));
|
|
|
|
$request = LegalRequest::query()->firstOrFail();
|
|
|
|
expect($request->type)->toBe(LegalRequestType::Report);
|
|
expect($request->requester_email)->toBeNull();
|
|
|
|
// Ohne E-Mail keine Eingangsbestätigung — der interne Hinweis geht trotzdem raus.
|
|
Mail::assertNotSent(LegalRequestReceived::class);
|
|
Mail::assertSent(LegalRequestSubmitted::class);
|
|
});
|
|
|
|
test('a filled honeypot field blocks bots without creating a request', function (string $field) {
|
|
/** @var TestCase $this */
|
|
$release = publishedRelease();
|
|
|
|
// Soll: still und neutral abnicken — kein Validierungsfehler, der einem
|
|
// Bot die Falle verrät, sondern der normale Erfolgs-Redirect.
|
|
$this->post(route('legal-request.store', ['slug' => $release->slug]), [
|
|
'type' => LegalRequestType::Report->value,
|
|
'message' => 'Spam spam spam spam.',
|
|
$field => 'http://spam.example',
|
|
])
|
|
->assertSessionHasNoErrors()
|
|
->assertRedirect(route('release.detail', ['slug' => $release->slug]));
|
|
|
|
expect(LegalRequest::query()->count())->toBe(0);
|
|
})->with(['website', 'homepage']);
|
|
|
|
test('only one open request per release and type, other types still allowed', function () {
|
|
/** @var TestCase $this */
|
|
$release = publishedRelease();
|
|
|
|
LegalRequest::factory()
|
|
->type(LegalRequestType::Dsgvo)
|
|
->create(['press_release_id' => $release->id, 'status' => LegalRequestStatus::Open]);
|
|
|
|
// Zweite offene DSGVO-Anfrage → keine Dublette.
|
|
$this->post(route('legal-request.store', ['slug' => $release->slug]), [
|
|
'type' => LegalRequestType::Dsgvo->value,
|
|
'requester_email' => 'noch@example.test',
|
|
'message' => 'Nochmal dieselbe Anfrage bitte bearbeiten.',
|
|
]);
|
|
|
|
expect(LegalRequest::query()->where('type', LegalRequestType::Dsgvo->value)->count())->toBe(1);
|
|
|
|
// Anderer Typ bleibt möglich.
|
|
$this->post(route('legal-request.store', ['slug' => $release->slug]), [
|
|
'type' => LegalRequestType::Report->value,
|
|
'message' => 'Andere Art von Anliegen zu dieser PM.',
|
|
]);
|
|
|
|
expect(LegalRequest::query()->where('type', LegalRequestType::Report->value)->count())->toBe(1);
|
|
});
|
|
|
|
test('the admin queue lists open requests', function () {
|
|
/** @var TestCase $this */
|
|
$admin = User::factory()->create(['is_active' => true]);
|
|
$admin->assignRole('admin');
|
|
|
|
// Kurzer, fixer Titel: die Queue kürzt Titel via Str::limit(…, 60).
|
|
$release = PressRelease::factory()
|
|
->published()
|
|
->forPortal(Portal::Businessportal24)
|
|
->create(['title' => 'Compliance-Test PM']);
|
|
LegalRequest::factory()
|
|
->type(LegalRequestType::PersonalRights)
|
|
->create(['press_release_id' => $release->id, 'status' => LegalRequestStatus::Open]);
|
|
|
|
Volt::actingAs($admin)
|
|
->test('admin.legal-requests.index')
|
|
->assertOk()
|
|
->assertSee('Compliance-Test PM')
|
|
->assertSee(LegalRequestType::PersonalRights->label());
|
|
});
|
|
|
|
test('an admin can resolve a request with a note', function () {
|
|
/** @var TestCase $this */
|
|
$admin = User::factory()->create(['is_active' => true]);
|
|
$admin->assignRole('admin');
|
|
|
|
$legalRequest = LegalRequest::factory()
|
|
->type(LegalRequestType::Dsgvo)
|
|
->create(['status' => LegalRequestStatus::Open]);
|
|
|
|
Volt::actingAs($admin)
|
|
->test('admin.legal-requests.show', ['id' => $legalRequest->id])
|
|
->set('adminNote', 'Daten anonymisiert.')
|
|
->call('resolve');
|
|
|
|
$legalRequest->refresh();
|
|
|
|
expect($legalRequest->status)->toBe(LegalRequestStatus::Resolved);
|
|
expect($legalRequest->resolved_by_user_id)->toBe($admin->id);
|
|
expect($legalRequest->resolved_at)->not->toBeNull();
|
|
expect($legalRequest->admin_note)->toBe('Daten anonymisiert.');
|
|
});
|
|
|
|
test('an admin can reject a request', function () {
|
|
/** @var TestCase $this */
|
|
$admin = User::factory()->create(['is_active' => true]);
|
|
$admin->assignRole('admin');
|
|
|
|
$legalRequest = LegalRequest::factory()
|
|
->type(LegalRequestType::Report)
|
|
->create(['status' => LegalRequestStatus::Open]);
|
|
|
|
Volt::actingAs($admin)
|
|
->test('admin.legal-requests.show', ['id' => $legalRequest->id])
|
|
->call('reject');
|
|
|
|
expect($legalRequest->fresh()->status)->toBe(LegalRequestStatus::Rejected);
|
|
});
|