- Turnstile für Gäste und unbestätigte Konten (TurnstileVerifier, <x-turnstile>); ist Cloudflare nicht erreichbar, wird durchgelassen und im Verlauf, in der Kontakt-Queue und in der Betriebsüberwachung markiert - Inhaltsfilter des Kontaktformulars (fremde Links, Floskel mit Seitentitel, Zeichensalat, Sperrliste, Absenden unter 3 Sekunden); Treffer landen in der Spam-Quarantäne ohne Mail, Löschung nach 30 Tagen - Admin: Filter „Spam“, Sammelaktion, „Kein Spam“ holt die Mail nach - Registrierung lehnt Zufallsnamen ab; users:prune-bot-registrations (täglicher Lauf erst mit USERS_PRUNE_BOT_REGISTRATIONS=true) - contact-messages:quarantine-spam für den Altbestand - CSP erlaubt challenges.cloudflare.com, Log-Kanal spam - Datenschutzerklärung: Abschnitt 6a Cloudflare Turnstile (Vorschlag) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
89 lines
3 KiB
PHP
89 lines
3 KiB
PHP
<?php
|
|
|
|
use App\Models\Company;
|
|
use App\Models\User;
|
|
use Illuminate\Console\Scheduling\Schedule;
|
|
use Tests\TestCase;
|
|
|
|
/*
|
|
* users:prune-bot-registrations (Spamschutz B4): nur Formular-Konten, die
|
|
* nie bestätigt und nie genutzt wurden — Altkonten bleiben unberührt.
|
|
*/
|
|
|
|
/**
|
|
* @param array<string, mixed> $attributes
|
|
*/
|
|
function botRegistration(array $attributes = []): User
|
|
{
|
|
return User::factory()->unverified()->create([
|
|
'name' => 'ruLrlotwaCoVStmZetSxD',
|
|
'is_active' => false,
|
|
'legacy_id' => null,
|
|
'last_login_at' => null,
|
|
'created_at' => now()->subDays(20),
|
|
...$attributes,
|
|
]);
|
|
}
|
|
|
|
test('unused unconfirmed registrations older than the deadline are deleted', function () {
|
|
/** @var TestCase $this */
|
|
$bot = botRegistration();
|
|
|
|
$this->artisan('users:prune-bot-registrations')
|
|
->expectsOutputToContain('Bot-Registrierungen gelöscht: 1')
|
|
->assertSuccessful();
|
|
|
|
expect(User::withTrashed()->find($bot->id))->toBeNull();
|
|
});
|
|
|
|
test('accounts that are recent, legacy, confirmed, used or owned are kept', function () {
|
|
/** @var TestCase $this */
|
|
$kept = [
|
|
botRegistration(['created_at' => now()->subDays(5)]),
|
|
botRegistration(['legacy_id' => 4711]),
|
|
botRegistration(['email_verified_at' => now()]),
|
|
botRegistration(['last_login_at' => now()->subDays(3)]),
|
|
botRegistration(['oauth_provider' => 'google']),
|
|
botRegistration(['is_active' => true]),
|
|
];
|
|
$withCompany = botRegistration();
|
|
$withCompany->companies()->attach(Company::factory()->create(), ['role' => 'owner', 'granted_via' => 'manual']);
|
|
$kept[] = $withCompany;
|
|
|
|
$this->artisan('users:prune-bot-registrations')->assertSuccessful();
|
|
|
|
expect(User::query()->whereKey(collect($kept)->pluck('id'))->count())->toBe(count($kept));
|
|
});
|
|
|
|
test('the dry run lists candidates and deletes nothing', function () {
|
|
/** @var TestCase $this */
|
|
$bot = botRegistration(['email' => 'opfer@example.test']);
|
|
|
|
$this->artisan('users:prune-bot-registrations', ['--dry-run' => true])
|
|
->expectsOutputToContain('op***@example.test')
|
|
->expectsOutputToContain('Probelauf: 1 Konten älter als 14 Tage würden gelöscht, davon 1 mit Zufallsnamen.')
|
|
->assertSuccessful();
|
|
|
|
expect(User::query()->find($bot->id))->not->toBeNull();
|
|
});
|
|
|
|
test('a deadline below seven days is refused', function () {
|
|
/** @var TestCase $this */
|
|
botRegistration(['created_at' => now()->subDays(3)]);
|
|
|
|
$this->artisan('users:prune-bot-registrations', ['--days' => 2])->assertFailed();
|
|
|
|
expect(User::query()->count())->toBe(1);
|
|
});
|
|
|
|
test('the daily run stays off until it is switched on', function () {
|
|
$event = collect(app(Schedule::class)->events())
|
|
->first(fn ($event) => str_contains((string) $event->command, 'users:prune-bot-registrations'));
|
|
|
|
expect($event)->not->toBeNull()
|
|
->and($event->filtersPass(app()))->toBeFalse();
|
|
|
|
config()->set('auth.prune_bot_registrations.enabled', true);
|
|
|
|
expect($event->filtersPass(app()))->toBeTrue();
|
|
});
|