CluPilotCloud/tests/Feature/Security/SecurityBlockMailTest.php

98 lines
4.1 KiB
PHP

<?php
// tests/Feature/Security/SecurityBlockMailTest.php
use App\Mail\SecurityBlockMail;
use App\Models\Host;
use App\Models\Instance;
use App\Models\Mailbox;
use App\Models\SecurityBlock;
use App\Services\Mail\MailPurpose;
use App\Services\Mail\MailRoute;
use App\Services\Security\BlockAddress;
use App\Support\Settings;
use Illuminate\Support\Facades\Mail;
// Blank slate statt der migrationseigenen fuenf Postfaecher (no-reply,
// support, billing, office, info): der letzte Test unten legt selbst eines
// mit dem Schluessel 'no-reply' an, und das kollidiert sonst mit
// mailboxes.key's Unique-Constraint gegen die von RefreshDatabase stehen
// gelassene Saat (siehe MailRoutingTest, derselbe Helfer aus tests/Pest.php).
beforeEach(fn () => clearMailboxSeed());
it('schickt dem Inhaber eine Mail, wenn seine Instanz gesperrt wird', function () {
Mail::fake();
$instance = Instance::factory()->create(['status' => 'active', 'vmid' => 101]);
app(BlockAddress::class)->forInstance($instance, '203.0.113.7', 12);
Mail::assertQueued(SecurityBlockMail::class, fn ($m) => $m->hasTo($instance->customer->email));
});
it('schickt hoechstens eine Mail je Instanz und Stunde', function () {
// Ein Angreifer, der Adressen durchwechselt, erzeugt sonst zwanzig Mails —
// und die zwanzigste liest niemand mehr.
Mail::fake();
$instance = Instance::factory()->create(['status' => 'active', 'vmid' => 101]);
app(BlockAddress::class)->forInstance($instance, '203.0.113.7', 12);
app(BlockAddress::class)->forInstance($instance, '203.0.113.8', 12);
app(BlockAddress::class)->forInstance($instance, '203.0.113.9', 12);
Mail::assertQueuedCount(1);
});
it('laesst die Sperre stehen, wenn die Mail scheitert', function () {
// Zustellung ist nicht die Bedingung fuer Schutz. Eine Sperre, die von einem
// kaputten Postfach abhinge, waere genau dann weg, wenn ohnehin schon etwas
// im Argen liegt.
Mail::shouldReceive('to')->andThrow(new RuntimeException('Postfach kaputt'));
$instance = Instance::factory()->create(['status' => 'active', 'vmid' => 101]);
$block = app(BlockAddress::class)->forInstance($instance, '203.0.113.7', 12);
expect($block)->not->toBeNull()
->and(SecurityBlock::count())->toBe(1);
});
it('geht standardmaessig aus dem System-Postfach und folgt der Wegwahl', function () {
Mailbox::factory()->create(['key' => 'no-reply', 'address' => 'no-reply@clupilot.com', 'active' => true]);
$info = Mailbox::factory()->create(['key' => 'info', 'active' => true]);
Settings::set(MailPurpose::settingKey(MailPurpose::SYSTEM), 'no-reply');
$block = SecurityBlock::factory()->create();
expect((new SecurityBlockMail($block))->envelope()->from->address)->toBe('no-reply@clupilot.com');
Settings::set(MailRoute::settingKey('security-block'), 'info');
expect((new SecurityBlockMail($block))->envelope()->from->address)->toBe($info->address);
});
// ---- Host-Sperren: an jeden aktiven Betreiber mit hosts.manage (Koordinator-Entscheidung, Fix-Runde 1) ----
it('schickt bei einer Host-Sperre an jeden Betreiber, der Hosts verwalten darf', function () {
Mail::fake();
// Owner und Admin haben hosts.manage (seed_roles_and_permissions), Support
// nicht — siehe database/migrations/2026_07_25_133900_seed_roles_and_permissions.php.
$darf = operator('Admin');
$darfNicht = operator('Support');
$host = Host::factory()->create();
app(BlockAddress::class)->forHost($host, '203.0.113.7', 12);
// Beide Haelften: der Zustaendige bekommt sie, der Unzustaendige nicht.
Mail::assertQueued(SecurityBlockMail::class, fn ($m) => $m->hasTo($darf->email));
Mail::assertNotQueued(SecurityBlockMail::class, fn ($m) => $m->hasTo($darfNicht->email));
});
it('sperrt auch dann, wenn es keinen zustaendigen Betreiber gibt', function () {
Mail::fake();
// Keine Betreiber mit hosts.manage — kein Empfaenger, kein Fehler, aber
// sehr wohl eine Sperre. Schutz haengt nicht an Zustellung.
$host = Host::factory()->create();
expect(app(BlockAddress::class)->forHost($host, '203.0.113.7', 12))->not->toBeNull();
Mail::assertNothingQueued();
});