CluPilotCloud/tests/Feature/Billing/StoragePackCapacityTest.php

116 lines
4.8 KiB
PHP

<?php
use App\Actions\BookAddon;
use App\Models\Host;
use App\Models\Instance;
use App\Models\Order;
use App\Models\Subscription;
use App\Services\Billing\AddonCatalogue;
/**
* Ein Block ist Platz auf einer bestimmten Maschine.
*
* Eine laufende Instanz zieht nicht um — es gibt keinen Umzug zwischen Hosts.
* Die Frage ist deshalb nicht "hat irgendein Host Platz", sondern "hat DIESER
* Host Platz", und sie muss gestellt werden, bevor Geld fließt.
*/
function packedInstanceOn(Host $host): Subscription
{
$order = Order::factory()->create(['plan' => 'start', 'datacenter' => $host->datacenter, 'status' => 'paid']);
$subscription = app(App\Actions\OpenSubscription::class)($order);
$instance = Instance::factory()->create([
'customer_id' => $subscription->customer_id,
'host_id' => $host->id,
'status' => 'active',
'quota_gb' => 30,
'disk_gb' => 40,
]);
$subscription->update(['instance_id' => $instance->id]);
return $subscription->refresh();
}
it('lehnt einen Block ab, für den der Host keinen Platz hat', function () {
config(['provisioning.storage_addon.gb' => 20, 'provisioning.storage_addon.disk_gb' => 22]);
// 60 GB gesamt, davon 40 durch die Instanz gebunden: für 22 weitere ist
// kein Platz. `active()` setzt total_gb/RAM/Kerne — ohne diesen Zustand
// hat ein Host gar keine Größe.
$host = Host::factory()->active()->create(['datacenter' => 'fsn', 'total_gb' => 60]);
$subscription = packedInstanceOn($host);
expect(fn () => app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1))
->toThrow(RuntimeException::class);
expect($subscription->addons()->count())->toBe(0);
});
it('lässt einen Block durch, für den der Host Platz hat', function () {
config(['provisioning.storage_addon.gb' => 20, 'provisioning.storage_addon.disk_gb' => 22]);
$host = Host::factory()->active()->create(['datacenter' => 'fsn', 'total_gb' => 500]);
$subscription = packedInstanceOn($host);
app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1);
expect((int) $subscription->addons()->active()->sum('quantity'))->toBe(1);
});
it('fragt nicht, solange es keine Maschine gibt', function () {
config(['provisioning.storage_addon.gb' => 20, 'provisioning.storage_addon.disk_gb' => 22]);
$order = Order::factory()->create(['plan' => 'start', 'datacenter' => 'fsn', 'status' => 'paid']);
$subscription = app(App\Actions\OpenSubscription::class)($order);
app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1);
expect((int) $subscription->addons()->active()->sum('quantity'))->toBe(1);
});
/**
* Nachtrag aus der Durchsicht: die Kapazitätsprüfung darf einen wiederholt
* zugestellten Webhook nicht treffen.
*
* Stripe liefert bei einem 5xx oder Timeout denselben Webhook ein zweites Mal
* aus — BookAddon::__invoke() wird mit demselben `$order` erneut aufgerufen,
* und book()'s eigener Kurzschluss ("Idempotent against a retried webhook")
* soll dafür nur die bestehende Buchung zurückgeben, ohne irgendetwas neu zu
* entscheiden. Läge die Kapazitätsprüfung VOR diesem Kurzschluss, würde die
* Wiederholung erneut fragen "passt NOCH ein Block drauf" — obwohl gar keiner
* hinzukommt — und einen längst bezahlten, längst gebuchten Vorgang mit einem
* Fehler quittieren, sobald der Host in der Zwischenzeit enger geworden ist.
*/
it('gibt bei einem wiederholten Webhook die vorhandene Buchung zurück, auch wenn der Host inzwischen eng geworden ist', function () {
config(['provisioning.storage_addon.gb' => 20, 'provisioning.storage_addon.disk_gb' => 22]);
// 100 GB gesamt → 85 GB frei (reserve_pct 15). Die gepackte Instanz bindet
// 40, für den einen Block (22) bleiben 45 - 22 = 23 übrig: reichlich für
// den ERSTEN Aufruf.
$host = Host::factory()->active()->create(['datacenter' => 'fsn', 'total_gb' => 100]);
$subscription = packedInstanceOn($host);
$order = Order::factory()->create([
'customer_id' => $subscription->customer_id, 'type' => 'addon', 'addon_key' => 'storage', 'status' => 'paid',
]);
$first = app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1, $order);
// Der Host wird zwischen den beiden Zustellversuchen enger — hier durch
// eine zweite Instanz, die den Rest praktisch aufbraucht (40 + 22 + 30 =
// 92 committed, nur noch 85 - 92 < 0 → 0 frei). Die Ursache ist beliebig
// (ein zweiter Kunde, ein weiterer Block); die Frage ist nur, ob die
// WIEDERHOLUNG sie stellen darf.
Instance::factory()->create([
'host_id' => $host->id,
'status' => 'active',
'disk_gb' => 30,
]);
$second = app(BookAddon::class)($subscription, AddonCatalogue::STORAGE, 1, $order);
expect($second->id)->toBe($first->id)
->and((int) $subscription->addons()->active()->sum('quantity'))->toBe(1);
});