102 lines
4.5 KiB
PHP
102 lines
4.5 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Admin\Overview;
|
|
use App\Models\Host;
|
|
use App\Models\Instance;
|
|
use App\Services\Provisioning\HostCapacity;
|
|
use Livewire\Livewire;
|
|
|
|
/**
|
|
* Saying so before a paid order finds out.
|
|
*
|
|
* The platform books thick: a package reserves its whole `disk_gb` on ONE host
|
|
* at placement. So the catalogue can go on offering something no machine has
|
|
* room for, and the first anyone hears of it is a paid order failing at the
|
|
* reservation step — after the money has moved.
|
|
*/
|
|
beforeEach(function () {
|
|
// hosts.datacenter is a foreign key onto datacenters.code, so the code has
|
|
// to exist before a host can carry it.
|
|
App\Models\Datacenter::factory()->create(['code' => 'fsn1']);
|
|
});
|
|
|
|
it('measures the largest single reservation the estate could take', function () {
|
|
// The largest free HOST, not the sum. A package goes on one machine, so two
|
|
// hosts with 300 GB each cannot take a 540 GB instance between them.
|
|
Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 300, 'reserve_pct' => 0]);
|
|
Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 300, 'reserve_pct' => 0]);
|
|
|
|
expect(app(HostCapacity::class)->largestPlaceableGb('fsn1'))->toBe(300)
|
|
->and(app(HostCapacity::class)->canPlace('fsn1', 540))->toBeFalse();
|
|
});
|
|
|
|
it('counts what is already committed, not what is on the disk', function () {
|
|
// Thick booking: an instance holds its whole disk_gb whether the customer
|
|
// has put a byte in it or not.
|
|
$host = Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 1000, 'reserve_pct' => 0]);
|
|
Instance::factory()->create(['host_id' => $host->id, 'status' => 'active', 'disk_gb' => 700]);
|
|
|
|
expect(app(HostCapacity::class)->largestPlaceableGb('fsn1'))->toBe(300);
|
|
});
|
|
|
|
it('respects the reserve the operator set aside', function () {
|
|
// reserve_pct is headroom for snapshots and for the host's own growth. A
|
|
// capacity answer that spends it is not an answer.
|
|
Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 1000, 'reserve_pct' => 20]);
|
|
|
|
expect(app(HostCapacity::class)->largestPlaceableGb('fsn1'))->toBe(800);
|
|
});
|
|
|
|
it('warns on the console about a package that can no longer be placed', function () {
|
|
// The whole point of the feature: this is the sentence that has to appear
|
|
// BEFORE somebody pays for the package it names.
|
|
Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 400, 'reserve_pct' => 0]);
|
|
|
|
$atRisk = app(HostCapacity::class)->unplaceablePlans();
|
|
|
|
expect($atRisk)->not->toBeEmpty();
|
|
|
|
$page = Livewire::actingAs(operator('Owner'), 'operator')->test(Overview::class);
|
|
|
|
$page->assertViewHas('notices', fn (array $notices) => collect($notices)
|
|
->contains(fn (array $n) => str_contains($n['text'], 'Host')));
|
|
});
|
|
|
|
it('says nothing while everything still fits', function () {
|
|
// A warning that is always there is furniture. This one has to mean
|
|
// something the moment it appears.
|
|
Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 20000, 'reserve_pct' => 0]);
|
|
|
|
expect(app(HostCapacity::class)->unplaceablePlans())->toBeEmpty();
|
|
});
|
|
|
|
it('does not take the overview down when the catalogue cannot be read', function () {
|
|
// A capacity warning does not get to be the thing that breaks the page an
|
|
// operator opens when something is already wrong.
|
|
// PlanCatalogue is final — deliberately, it is the single reader of the
|
|
// price tables — so it cannot be subclassed. The call site does not
|
|
// type-hint what comes back from the container, which is enough to put a
|
|
// throwing stand-in in its place.
|
|
app()->bind(App\Services\Billing\PlanCatalogue::class, fn () => new class
|
|
{
|
|
public function sellable(): array
|
|
{
|
|
throw new RuntimeException('overlapping availability windows');
|
|
}
|
|
});
|
|
|
|
expect(app(HostCapacity::class)->unplaceablePlans())->toBe([]);
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')->test(Overview::class)->assertOk();
|
|
});
|
|
|
|
it('does not shout at an installation that has no host yet', function () {
|
|
// Zero hosts is not a capacity problem — it is a machine nobody has
|
|
// onboarded, and that has its own notice. Without this guard a fresh
|
|
// console greeted the operator with one warning per sellable package
|
|
// before they had done anything at all.
|
|
App\Models\Host::query()->delete();
|
|
|
|
expect(app(HostCapacity::class)->unplaceablePlans())->toBe([]);
|
|
});
|