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('drosselt nicht, wenn gar keine Mail hinausging', function () { // Sonst schweigt eine Instanz eine volle Stunde, obwohl NULL Meldungen // ankamen — genau dann, wenn ohnehin schon etwas klemmt. Mail::fake(); // Ein Postfach, das den ERSTEN Aufruf von Mail::to() wirft und danach an // die echte Fake-Implementierung durchreicht — so laesst sich "der erste // Versandversuch scheitert, der zweite geht durch" nachstellen, ohne die // Zusicherung am Ende (Mail::assertQueued) aufzugeben. $fake = Mail::getFacadeRoot(); Mail::swap(new class($fake) { private int $calls = 0; public function __construct(private readonly object $inner) {} public function __call(string $name, array $arguments): mixed { if ($name === 'to' && ++$this->calls === 1) { throw new RuntimeException('Postfach kaputt'); } return $this->inner->{$name}(...$arguments); } }); $instance = Instance::factory()->create(['status' => 'active', 'vmid' => 101]); // erster Versuch: Versand scheitert app(BlockAddress::class)->forInstance($instance, '203.0.113.7', 12); // zweiter Versuch kurz darauf: Versand geht durch app(BlockAddress::class)->forInstance($instance, '203.0.113.8', 12); // Die zweite Sperre muss eine Mail erzeugen, obwohl die erste eine // Stunde 'verbraucht' zu haben schien. Mail::assertQueued(SecurityBlockMail::class); }); 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(); });