CluPilotCloud/tests/Feature/Admin/HostReservationTest.php

199 lines
8.1 KiB
PHP

<?php
use App\Livewire\Admin\ConfirmReleaseReservation;
use App\Livewire\Admin\HostDetail;
use App\Models\Customer;
use App\Models\Host;
use App\Services\Provisioning\HostCapacity;
use Livewire\Livewire;
/**
* Ein Host, der jemandem gehört.
*
* Enterprise verkauft eine eigene Maschine. Ohne Markierung landet der nächste
* Startkunde darauf, und die Kapazitätszahlen versprechen den Platz obendrein
* ein zweites Mal.
*/
it('platziert keinen fremden Kunden auf einem reservierten Host', function () {
$mieter = Customer::factory()->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']));
});