- 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>
181 lines
6.8 KiB
PHP
181 lines
6.8 KiB
PHP
<?php
|
|
|
|
use App\Enums\ActivityType;
|
|
use App\Enums\Portal;
|
|
use App\Models\PressRelease;
|
|
use App\Models\PressReleaseImage;
|
|
use App\Models\User;
|
|
use App\Services\ActivityLog;
|
|
use App\Services\Admin\AdminSystemHealth;
|
|
use Database\Seeders\RolesAndPermissionsSeeder;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Spatie\Permission\Models\Permission;
|
|
use Spatie\Permission\PermissionRegistrar;
|
|
use Tests\TestCase;
|
|
|
|
beforeEach(function () {
|
|
$this->seed(RolesAndPermissionsSeeder::class);
|
|
// In der Testumgebung ist die Queue `sync`; für die Queue-Metriken den
|
|
// Produktions-Treiber (database) verwenden, der die jobs-Tabellen liest.
|
|
config()->set('queue.default', 'database');
|
|
});
|
|
|
|
test('editor cannot open the system health report', function () {
|
|
/** @var TestCase $this */
|
|
$editor = User::factory()->create(['is_active' => true]);
|
|
$editor->assignRole('editor');
|
|
$this->actingAs($editor);
|
|
|
|
$this->get('https://pressekonto.test/admin/reports/system-health')->assertForbidden();
|
|
});
|
|
|
|
test('admin can open the system health report', function () {
|
|
/** @var TestCase $this */
|
|
$admin = User::factory()->create(['is_active' => true]);
|
|
$admin->assignRole('admin');
|
|
$this->actingAs($admin);
|
|
|
|
$this->get('https://pressekonto.test/admin/reports/system-health')
|
|
->assertSuccessful()
|
|
->assertSee('Betriebsüberwachung')
|
|
->assertSee('Queue-Worker');
|
|
});
|
|
|
|
test('the system health report shows the deployed version', function () {
|
|
/** @var TestCase $this */
|
|
config()->set('app.version', 'v1.1.0');
|
|
$admin = User::factory()->create(['is_active' => true]);
|
|
$admin->assignRole('admin');
|
|
$this->actingAs($admin);
|
|
|
|
$this->get('https://pressekonto.test/admin/reports/system-health')
|
|
->assertSuccessful()
|
|
->assertSee('Version: v1.1.0');
|
|
});
|
|
|
|
test('snapshot reports an ok queue when there are no waiting jobs', function () {
|
|
/** @var TestCase $this */
|
|
$snapshot = app(AdminSystemHealth::class)->snapshot();
|
|
|
|
expect($snapshot['queue']['pending'])->toBe(0)
|
|
->and($snapshot['queue']['worker_running'])->toBeTrue()
|
|
->and($snapshot['queue']['status'])->toBe('ok');
|
|
});
|
|
|
|
test('snapshot flags a stalled worker when a job has waited too long', function () {
|
|
/** @var TestCase $this */
|
|
DB::table('jobs')->insert([
|
|
'queue' => 'default',
|
|
'payload' => json_encode(['displayName' => 'App\\Jobs\\Example']),
|
|
'attempts' => 0,
|
|
'reserved_at' => null,
|
|
'available_at' => now()->subMinutes(10)->getTimestamp(),
|
|
'created_at' => now()->subMinutes(10)->getTimestamp(),
|
|
]);
|
|
|
|
$snapshot = app(AdminSystemHealth::class)->snapshot();
|
|
|
|
expect($snapshot['queue']['status'])->toBe('critical')
|
|
->and($snapshot['queue']['worker_running'])->toBeFalse()
|
|
->and($snapshot['overall'])->toBe('critical');
|
|
});
|
|
|
|
test('snapshot surfaces recent failed jobs', function () {
|
|
/** @var TestCase $this */
|
|
DB::table('failed_jobs')->insert([
|
|
'uuid' => (string) Str::uuid(),
|
|
'connection' => 'database',
|
|
'queue' => 'default',
|
|
'payload' => json_encode(['displayName' => 'App\\Jobs\\Broken']),
|
|
'exception' => "RuntimeException: boom\nStack trace...",
|
|
'failed_at' => now(),
|
|
]);
|
|
|
|
$snapshot = app(AdminSystemHealth::class)->snapshot();
|
|
|
|
expect($snapshot['queue']['failed_recent'])->toBe(1)
|
|
->and($snapshot['queue']['recent_failures'][0]['job'])->toBe('App\\Jobs\\Broken')
|
|
->and($snapshot['queue']['recent_failures'][0]['error'])->toContain('boom')
|
|
->and($snapshot['queue']['status'])->toBe('warn');
|
|
});
|
|
|
|
test('snapshot flags missing permissions as critical', function () {
|
|
/** @var TestCase $this */
|
|
Permission::query()->where('name', 'billing:manage')->delete();
|
|
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
|
|
|
$checks = collect(app(AdminSystemHealth::class)->snapshot()['checks'])
|
|
->keyBy('key');
|
|
|
|
expect($checks['permissions']['status'])->toBe('critical')
|
|
->and($checks['permissions']['detail'])->toContain('billing:manage');
|
|
});
|
|
|
|
test('snapshot flags pending media privatization', function () {
|
|
/** @var TestCase $this */
|
|
$pr = PressRelease::factory()->forPortal(Portal::Presseecho)->create();
|
|
PressReleaseImage::query()->create([
|
|
'press_release_id' => $pr->id,
|
|
'disk' => 'public',
|
|
'path' => 'press-releases/'.$pr->id.'/images/legacy.jpg',
|
|
]);
|
|
|
|
$checks = collect(app(AdminSystemHealth::class)->snapshot()['checks'])->keyBy('key');
|
|
|
|
expect($checks['media_privatization']['status'])->toBe('warn')
|
|
->and($checks['media_privatization']['detail'])->toContain('1');
|
|
});
|
|
|
|
test('snapshot flags a missing imagick extension', function (bool $precheckEnabled, string $status) {
|
|
config()->set('images.precheck.enabled', $precheckEnabled);
|
|
app()->bind(AdminSystemHealth::class, fn () => new class extends AdminSystemHealth
|
|
{
|
|
protected function imagickAvailable(): bool
|
|
{
|
|
return false;
|
|
}
|
|
});
|
|
|
|
$checks = collect(app(AdminSystemHealth::class)->snapshot()['checks'])->keyBy('key');
|
|
|
|
expect($checks['imagick']['status'])->toBe($status)
|
|
->and($checks['imagick']['detail'])->toContain('php8.4-imagick');
|
|
})->with([
|
|
'precheck off' => [false, 'warn'],
|
|
'precheck on' => [true, 'critical'],
|
|
]);
|
|
|
|
test('snapshot reports imagick as ok when it is loaded', function () {
|
|
$checks = collect(app(AdminSystemHealth::class)->snapshot()['checks'])->keyBy('key');
|
|
|
|
expect($checks['imagick']['status'])->toBe(class_exists(Imagick::class) ? 'ok' : 'warn');
|
|
});
|
|
|
|
test('snapshot warns in production when the turnstile keys are missing', function () {
|
|
app()->instance('env', 'production');
|
|
|
|
$checks = collect(app(AdminSystemHealth::class)->snapshot()['checks'])->keyBy('key');
|
|
|
|
expect($checks['turnstile']['status'])->toBe('warn')
|
|
->and($checks['turnstile']['detail'])->toContain('TURNSTILE_SITE_KEY');
|
|
|
|
config()->set('services.turnstile.site_key', '0x4AAAAAAAsitekey');
|
|
config()->set('services.turnstile.secret_key', '0x4AAAAAAAsecret');
|
|
|
|
$checks = collect(app(AdminSystemHealth::class)->snapshot()['checks'])->keyBy('key');
|
|
|
|
expect($checks['turnstile']['status'])->toBe('ok');
|
|
});
|
|
|
|
test('snapshot asks to review forms accepted without bot check', function () {
|
|
$checks = collect(app(AdminSystemHealth::class)->snapshot()['checks'])->keyBy('key');
|
|
expect($checks['bot_check_unavailable']['status'])->toBe('ok');
|
|
|
|
app(ActivityLog::class)->record(ActivityType::BotCheckUnavailable, 'Kontaktanfrage KT-1 ohne Bot-Prüfung angenommen.');
|
|
|
|
$checks = collect(app(AdminSystemHealth::class)->snapshot()['checks'])->keyBy('key');
|
|
|
|
expect($checks['bot_check_unavailable']['status'])->toBe('warn')
|
|
->and($checks['bot_check_unavailable']['detail'])->toContain('1 Formular(e)');
|
|
});
|