diff --git a/docs/superpowers/plans/2026-08-03-fruehwarnsystem.md b/docs/superpowers/plans/2026-08-03-fruehwarnsystem.md index f2b2c5a..04f8e64 100644 --- a/docs/superpowers/plans/2026-08-03-fruehwarnsystem.md +++ b/docs/superpowers/plans/2026-08-03-fruehwarnsystem.md @@ -20,6 +20,7 @@ - Kommentare auf Deutsch, im Ton des umliegenden Codes: sie erklären das **Warum**, nicht das Was. - Repo-Regeln **R18–R24** aus `CLAUDE.md` gelten und werden per Test erzwungen. Besonders: R20 (Bearbeiten im Modal), R23 (Bestätigen im Modal, nie `wire:confirm`), R24.3 (keine Blade-Direktive in der Attributliste eines Komponenten-Tags). - Jeder Befehl im `app`-Container nennt seinen Benutzer (`-u www-data` bzw. `-u root`) — erzwungen durch `tests/Feature/DeploymentRunsAsTheAppUserTest.php`. +- **`InstanceFactory` kennt keinen `active()`-Zustand** — Status und `vmid` werden in den Tests ausgeschrieben. Wer einen Zustand ergänzen will, tut das in einer eigenen Aufgabe, nicht nebenbei. - Kommandos laufen im Container: `docker compose exec -u 1000:1000 -T app php artisan test` `docker compose exec -u 1000:1000 -e npm_config_cache=/tmp/npm-cache -T app npm run build` @@ -86,9 +87,14 @@ it('legt eine einzelne Mailart auf ein anderes Postfach', function () { Settings::set(MailPurpose::settingKey(MailPurpose::SYSTEM), 'no-reply'); Settings::set(MailRoute::settingKey('new-device'), 'info'); + // Und nur diese eine: der Nachbar bleibt, wo er war. Mit einem EIGENEN + // Postfach fuer Abrechnung — ohne das waere die Zusicherung leer, weil der + // Resolver dann null liefert und null nun einmal nicht 'info' ist. + $billing = Mailbox::factory()->create(['key' => 'billing', 'active' => true]); + Settings::set(MailPurpose::settingKey(MailPurpose::BILLING), 'billing'); + expect(MailRoute::purposeOrMailbox('new-device', MailPurpose::SYSTEM)?->id)->toBe($info->id) - // Und nur diese eine: der Nachbar bleibt, wo er war. - ->and(MailRoute::purposeOrMailbox('invoice', MailPurpose::BILLING)?->key)->not->toBe('info'); + ->and(MailRoute::purposeOrMailbox('invoice', MailPurpose::BILLING)?->id)->toBe($billing->id); }); it('faellt auf den Zweck zurueck, wenn das eingetragene Postfach abgeschaltet ist', function () { @@ -541,7 +547,7 @@ it('sperrt ab zehn Fehlversuchen im Fenster', function () { $pve->guestScripts['nextcloud.log'] = ['out-data' => protokollZeilen('203.0.113.7', 10), 'exitcode' => 0]; app()->instance(\App\Services\Proxmox\ProxmoxClient::class, $pve); - $instance = Instance::factory()->active()->create(); + $instance = Instance::factory()->create(['status' => 'active', 'vmid' => 101]); app(ScanForIntrusions::class)->handle(); expect(SecurityBlock::where('ip', '203.0.113.7')->exists())->toBeTrue(); @@ -552,7 +558,7 @@ it('sperrt bei neun Fehlversuchen nicht', function () { $pve->guestScripts['nextcloud.log'] = ['out-data' => protokollZeilen('203.0.113.7', 9), 'exitcode' => 0]; app()->instance(\App\Services\Proxmox\ProxmoxClient::class, $pve); - Instance::factory()->active()->create(); + Instance::factory()->create(['status' => 'active', 'vmid' => 101]); app(ScanForIntrusions::class)->handle(); expect(SecurityBlock::count())->toBe(0); @@ -572,7 +578,7 @@ it('sperrt nicht, wenn sich die Versuche ueber zwei Fenster verteilen', function $pve->guestScripts['nextcloud.log'] = ['out-data' => $alt."\n".protokollZeilen('203.0.113.7', 6), 'exitcode' => 0]; app()->instance(\App\Services\Proxmox\ProxmoxClient::class, $pve); - Instance::factory()->active()->create(); + Instance::factory()->create(['status' => 'active', 'vmid' => 101]); app(ScanForIntrusions::class)->handle(); expect(SecurityBlock::count())->toBe(0); @@ -586,7 +592,7 @@ it('faengt bei einem rotierten Protokoll wieder bei null an', function () { $pve->guestScripts['nextcloud.log'] = ['out-data' => protokollZeilen('203.0.113.7', 10), 'exitcode' => 0]; app()->instance(\App\Services\Proxmox\ProxmoxClient::class, $pve); - $instance = Instance::factory()->active()->create(['security_log_offset' => 999999]); + $instance = Instance::factory()->create(['status' => 'active', 'vmid' => 101, 'security_log_offset' => 999999]); app(ScanForIntrusions::class)->handle(); expect($instance->fresh()->security_log_offset)->toBeLessThan(999999) @@ -616,7 +622,7 @@ it('ueberspringt einen Gast, der nicht antwortet, ohne den Versatz zu verlieren' $pve->guestScripts['nextcloud.log'] = ['exitcode' => 1]; app()->instance(\App\Services\Proxmox\ProxmoxClient::class, $pve); - $instance = Instance::factory()->active()->create(['security_log_offset' => 4711]); + $instance = Instance::factory()->create(['status' => 'active', 'vmid' => 101, 'security_log_offset' => 4711]); app(ScanForIntrusions::class)->handle(); expect($instance->fresh()->security_log_offset)->toBe(4711); @@ -694,7 +700,7 @@ use Illuminate\Support\Facades\Mail; it('schickt dem Inhaber eine Mail, wenn seine Instanz gesperrt wird', function () { Mail::fake(); - $instance = Instance::factory()->active()->create(); + $instance = Instance::factory()->create(['status' => 'active', 'vmid' => 101]); app(BlockAddress::class)->forInstance($instance, '203.0.113.7', 12); @@ -705,7 +711,7 @@ 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()->active()->create(); + $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); @@ -720,7 +726,7 @@ it('laesst die Sperre stehen, wenn die Mail scheitert', function () { // im Argen liegt. Mail::shouldReceive('to')->andThrow(new RuntimeException('Postfach kaputt')); - $instance = Instance::factory()->active()->create(); + $instance = Instance::factory()->create(['status' => 'active', 'vmid' => 101]); $block = app(BlockAddress::class)->forInstance($instance, '203.0.113.7', 12); expect($block)->not->toBeNull()