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(); });