create(); $fremder = Customer::factory()->create(); Host::factory()->active()->create([ 'datacenter' => 'fsn', 'total_gb' => 4000, 'reserved_for_customer_id' => $mieter->id, ]); expect(Host::placeableIn('fsn', 100, $fremder->id))->toBeNull() ->and(Host::placeableIn('fsn', 100, $mieter->id))->not->toBeNull(); }); it('zählt einen reservierten Host nicht zum allgemeinen Bestand', function () { $mieter = Customer::factory()->create(); Host::factory()->active()->create([ 'datacenter' => 'fsn', 'total_gb' => 4000, 'reserved_for_customer_id' => $mieter->id, ]); expect(app(HostCapacity::class)->largestPlaceableGb('fsn'))->toBe(0) ->and(app(HostCapacity::class)->canPlace('fsn', 40))->toBeFalse(); }); it('lässt einen freien Host frei', function () { Host::factory()->active()->create(['datacenter' => 'fsn', 'total_gb' => 500]); expect(app(HostCapacity::class)->canPlace('fsn', 40))->toBeTrue(); }); /** * preferredDatacenter() ruft placeableIn() nicht auf — sie fragt selbst nach * dem Rechenzentrum mit dem meisten freien Platz, für einen Checkout, der * noch keinen Kunden kennt. Ohne ->unreserved() sieht ein Rechenzentrum mit * einem großen, aber komplett reservierten Host geräumiger aus als eines mit * echtem allgemeinem Bestand — der Checkout legt die neue Bestellung dorthin, * placeableIn() findet dort dann niemanden, und die Bestellung parkt, obwohl * anderswo echter Platz war. */ it('bevorzugt kein Rechenzentrum, dessen größter Host nur reservierter Platz ist', function () { App\Models\Datacenter::factory()->create(['code' => 'fsn4']); App\Models\Datacenter::factory()->create(['code' => 'hel4']); $mieter = Customer::factory()->create(); // fsn4: der größere Host, aber vollständig einem Kunden versprochen. Host::factory()->active()->create(['datacenter' => 'fsn4', 'total_gb' => 4000, 'reserved_for_customer_id' => $mieter->id]); // hel4: kleiner, aber echter allgemeiner Bestand. Host::factory()->active()->create(['datacenter' => 'hel4', 'total_gb' => 500]); expect(app(HostCapacity::class)->preferredDatacenter())->toBe('hel4'); }); /** * Die Reservierung überlebt den Kunden — sie löst sich nicht von selbst. * * Vorgabe: "Die Beziehung löscht nicht mit dem Kunden … die Reservierung * wird beim Auszug von Hand gelöst — sonst stünde die Maschine still im * Bestand, ohne dass jemand sie angeboten hätte." Ein `nullOnDelete()` täte * genau das Verbotene: die Datenbank hebt die Reservierung von sich aus auf, * sobald der Kunde verschwindet (z. B. eine DSGVO-Löschung). `restrictOnDelete()` * macht daraus stattdessen eine bewusste Handlung — das Löschen selbst * scheitert, solange die Maschine noch jemandem gehört. */ it('verhindert das Löschen eines Kunden, dem noch ein Host gehört', function () { $mieter = Customer::factory()->create(); Host::factory()->active()->create(['reserved_for_customer_id' => $mieter->id]); expect(fn () => $mieter->delete())->toThrow(\Illuminate\Database\QueryException::class); // Nichts wurde stillschweigend aufgelöst — der Kunde steht noch da. expect(Customer::query()->whereKey($mieter->id)->exists())->toBeTrue(); }); it('lässt einen Kunden ohne Host normal löschen', function () { $customer = Customer::factory()->create(); $customer->delete(); expect(Customer::query()->whereKey($customer->id)->exists())->toBeFalse(); }); /** * Die Konsole: setzen und lösen. * * Setzen braucht keine Bestätigung — ein Host, der noch niemandem gehört, * bekommt einen Besitzer, nichts geht verloren. Lösen schon (R23): danach * ist die Maschine wieder allgemeiner Bestand, und der nächste Startkunde * kann darauf landen. */ it('reserviert einen Host für einen Kunden von der Konsole aus', function () { $host = Host::factory()->active()->create(); $customer = Customer::factory()->create(); Livewire::actingAs(admin(), 'operator') ->test(HostDetail::class, ['host' => $host]) ->call('reserveFor', $customer->uuid) ->assertDispatched('notify'); expect($host->fresh()->reserved_for_customer_id)->toBe($customer->id); }); it('verweigert das Reservieren ohne hosts.manage', function () { $host = Host::factory()->active()->create(); $customer = Customer::factory()->create(); Livewire::actingAs(operator('Read-only'), 'operator') ->test(HostDetail::class, ['host' => $host]) ->call('reserveFor', $customer->uuid) ->assertForbidden(); expect($host->fresh()->reserved_for_customer_id)->toBeNull(); }); it('bestätigt das Lösen im eigenen Modal, ohne dabei selbst etwas zu ändern', function () { $mieter = Customer::factory()->create(['name' => 'Acme GmbH']); $host = Host::factory()->active()->create(['reserved_for_customer_id' => $mieter->id]); Livewire::actingAs(admin(), 'operator') ->test(ConfirmReleaseReservation::class, ['uuid' => $host->uuid]) ->assertSee('Acme GmbH') ->call('confirmRelease') ->assertDispatched('host-reservation-release-confirmed'); // Das Modal selbst rührt nichts an — die Reservierung steht noch. expect($host->fresh()->reserved_for_customer_id)->toBe($mieter->id); }); it('löst die Reservierung, sobald die Seite das bestätigte Ereignis empfängt', function () { $mieter = Customer::factory()->create(); $host = Host::factory()->active()->create(['reserved_for_customer_id' => $mieter->id]); Livewire::actingAs(admin(), 'operator') ->test(HostDetail::class, ['host' => $host]) ->call('onReleaseConfirmed') ->assertDispatched('notify'); expect($host->fresh()->reserved_for_customer_id)->toBeNull(); }); /** * Erklärt statt versteckt (Aufgabenauflösung #4): eine reservierte Maschine * verschwindet nirgends kommentarlos aus dem Bestand — Host-Detailseite, * Host-Liste und Kapazitätsseite weisen sie jeweils als solche aus. */ it('zeigt die Reservierung auf der Host-Detailseite', function () { $mieter = Customer::factory()->create(['name' => 'Contoso KG']); $host = Host::factory()->active()->create(['reserved_for_customer_id' => $mieter->id]); $this->actingAs(admin(), 'operator')->get(route('admin.hosts.show', $host)) ->assertOk() ->assertSee(__('hosts.detail.reserved_for', ['name' => 'Contoso KG'])); }); it('bietet auf der Host-Detailseite eine Kundenauswahl, solange nichts reserviert ist', function () { $host = Host::factory()->active()->create(); $customer = Customer::factory()->create(['name' => 'Globex AG']); $this->actingAs(admin(), 'operator')->get(route('admin.hosts.show', $host)) ->assertOk() ->assertSee(__('hosts.detail.reserved_general')) ->assertSee('Globex AG'); }); it('weist die Reservierung auf der Host-Liste aus', function () { $mieter = Customer::factory()->create(['name' => 'Umbrella Corp']); Host::factory()->active()->create(['name' => 'pve-reserved-1', 'reserved_for_customer_id' => $mieter->id]); $this->actingAs(admin(), 'operator')->get(route('admin.hosts')) ->assertOk() ->assertSee(__('hosts.reserved_badge', ['name' => 'Umbrella Corp'])); }); it('weist die Reservierung auf der Kapazitätsseite aus', function () { App\Models\Datacenter::factory()->create(['code' => 'fsn2']); $mieter = Customer::factory()->create(['name' => 'Initech Ltd']); Host::factory()->active()->create(['datacenter' => 'fsn2', 'reserved_for_customer_id' => $mieter->id]); $this->actingAs(admin(), 'operator')->get(route('admin.capacity')) ->assertOk() ->assertSee(__('hosts.reserved_badge', ['name' => 'Initech Ltd'])); });