Plan berichtigt: zwei Annahmen im Vorabdurchgang gefunden

InstanceFactory kennt keinen active()-Zustand — die Tests im Plan haetten nicht
einmal kompiliert. Status und vmid stehen jetzt ausgeschrieben.

Und eine Zusicherung in Aufgabe 1 war leer: sie prueft, dass der Nachbar NICHT
auf 'info' zeigt, ohne ein Postfach fuer Abrechnung anzulegen — der Resolver
liefert dann null, und null ist nun einmal nicht 'info'. Genau die Sorte Test,
die gruen bleibt, waehrend die Sache kaputt ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat/versandtakt
nexxo 2026-08-03 12:32:11 +02:00
parent 850a9ebd5b
commit e3f2e9a88b
1 changed files with 16 additions and 10 deletions

View File

@ -20,6 +20,7 @@
- Kommentare auf Deutsch, im Ton des umliegenden Codes: sie erklären das **Warum**, nicht das Was.
- Repo-Regeln **R18R24** 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()