create(['code' => 'fsn1']);
// The capacity page quotes live server prices. A test suite that reaches
// the internet is a test suite that fails when somebody else's site is
// down, and it puts requests on that site on every run.
Http::fake([
'*' => Http::response(['server' => []]),
]);
});
/** A paid order with its contract frozen, standing at the reservation step. */
function parkedOrder(string $plan = 'start', string $datacenter = 'fsn1'): ProvisioningRun
{
$order = Order::factory()->withSubscription()->create([
'plan' => $plan,
'datacenter' => $datacenter,
'status' => 'paid',
]);
$pipeline = (array) config('provisioning.pipelines.customer');
return ProvisioningRun::factory()->create([
'subject_type' => Order::class,
'subject_id' => $order->id,
'pipeline' => 'customer',
'current_step' => array_search(ReserveResources::class, $pipeline, true),
'status' => ProvisioningRun::STATUS_WAITING,
]);
}
it('parks a paid order instead of failing it when nothing has room', function () {
// No host at all: the customer has paid and there is nowhere to put them.
$run = parkedOrder();
$run->update(['status' => ProvisioningRun::STATUS_RUNNING]);
$result = app(ReserveResources::class)->execute($run->fresh());
expect($result->type)->toBe('poll')
->and($result->reason)->toBe('awaiting_capacity')
// poll(), not retry(): the wait is measured in days and must not eat
// the run's attempt budget.
->and($result->afterSeconds)->toBe(ReserveResources::PARK_POLL_SECONDS);
});
it('keeps a park alive through the runner, not merely through the step', function () {
// Both park tests around this one call execute() directly, and that is exactly
// why this stayed invisible for the whole life of the feature. The step
// returned poll(120) correctly; the RUNNER then measured the step's own
// maxDuration from a started_at it deliberately does not reset on a poll, and
// maxDuration was 60. So every single re-entry was ruled timed out BEFORE the
// body ran, a timeout consumes an attempt, and five of them had failRun()
// marking a paid order and its instance failed and releasing the instance —
// about six minutes for a wait that promises fourteen days. The console's
// capacity queue, HostCapacity::parked(), queueDemand() and the whole "go and
// buy a server" workflow were unreachable code.
Queue::fake();
$run = parkedOrder();
$run->update(['status' => ProvisioningRun::STATUS_RUNNING]);
$runner = app(RunRunner::class);
// Four full poll intervals: further than the old sixty-second budget, and
// further than five attempts would ever have carried it.
for ($i = 0; $i < 4; $i++) {
$runner->advance($run->fresh());
$polled = $run->fresh();
expect($polled->status)->toBe(ProvisioningRun::STATUS_WAITING)
->and($polled->attempt)->toBe(0) // a poll costs nothing
->and($polled->error)->toBeNull();
$this->travel(ReserveResources::PARK_POLL_SECONDS + 1)->seconds();
}
// Still parked, still one of the runs the console is asking somebody to buy a
// machine for, and nothing has been declared failed along the way.
expect(app(HostCapacity::class)->isParked($run->fresh()))->toBeTrue()
->and($run->fresh()->events()->where('outcome', 'failed')->exists())->toBeFalse()
->and($run->fresh()->events()->where('message', 'like', '%timed out%')->exists())->toBeFalse();
});
it('fails only once the order has waited longer than a machine can take', function () {
// Long enough to buy, rack and onboard a server over a weekend; short
// enough that a forgotten order surfaces instead of waiting for ever.
$run = parkedOrder();
// forceFill, because created_at is not fillable — update() drops it
// silently and the run stays a second old, which is a green test for a
// deadline that never fires.
$run->forceFill([
'status' => ProvisioningRun::STATUS_RUNNING,
'created_at' => now()->subDays(ReserveResources::PARK_DAYS + 1),
])->save();
expect(app(ReserveResources::class)->execute($run->fresh())->type)->toBe('fail');
});
it('finds parked orders by where they stand, not by a string that casts to zero', function () {
// current_step is an INDEX. Comparing it to 'reserve_resources' reads as
// correct and matches step 0 of every pipeline instead — including a host
// onboarding that is merely waiting on SSH.
$parked = parkedOrder();
ProvisioningRun::factory()->create([
'pipeline' => 'host',
'current_step' => 0,
'status' => ProvisioningRun::STATUS_WAITING,
]);
$found = app(HostCapacity::class)->parked();
expect($found)->toHaveCount(1)
->and($found[0]['run']->id)->toBe($parked->id);
});
it('recommends what to buy from the mix that is actually waiting', function () {
// The BIGGEST parked package decides the minimum machine — an instance
// lives on one host, so a server that cannot take the largest one leaves it
// parked however much total space it has.
parkedOrder('start');
parkedOrder('start');
parkedOrder('business');
$demand = app(HostCapacity::class)->queueDemand();
expect($demand['count'])->toBe(3)
->and($demand['largest'])->toBe(200) // business
->and($demand['total'])->toBe(280) // 2 × 40 + 200
->and($demand['by_plan'])->toHaveCount(2);
});
it('sizes the queue from the contract the customer signed, not today catalogue', function () {
// A plan edited after the sale must not change the machine we go and buy
// for the customer who bought the old one.
$run = parkedOrder('start');
Subscription::query()
->where('order_id', $run->subject_id)
->update(['disk_gb' => 4000]);
expect(app(HostCapacity::class)->queueDemand()['largest'])->toBe(4000);
});
it('promises immediate delivery only where a host could actually take it', function () {
Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 500, 'reserve_pct' => 0]);
$capacity = app(HostCapacity::class);
expect($capacity->deliveryFor(120))->toBe('immediate') // a small one fits
->and($capacity->deliveryFor(1050))->toBe('scheduled') // a big one does not
// Nothing sized is nothing promised.
->and($capacity->deliveryFor(0))->toBe('scheduled');
});
it('stops promising immediate delivery once the room is committed', function () {
// Thick booking: the instance holds its whole disk whether the customer has
// put a byte in it or not, and the promise has to follow the booking.
$host = Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 500, 'reserve_pct' => 0]);
expect(app(HostCapacity::class)->deliveryFor(120))->toBe('immediate');
Instance::factory()->create(['host_id' => $host->id, 'status' => 'active', 'disk_gb' => 450]);
expect(app(HostCapacity::class)->deliveryFor(120))->toBe('scheduled');
});
it('shows the delivery promise for every package on the console front page', function () {
// 150 GB: Start und Team passen darauf, Business (200 GB Platte) nicht —
// die Seite muss beide Antworten zeigen können, und auf der neuen Leiter
// fänden auf 500 GB alle drei Platz.
Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 150, 'reserve_pct' => 0]);
Livewire::actingAs(operator('Owner'), 'operator')
->test(Overview::class)
->assertViewHas('delivery', fn (array $d) => count($d['plans']) > 0
&& collect($d['plans'])->contains(fn (array $p) => $p['state'] === 'immediate')
&& collect($d['plans'])->contains(fn (array $p) => $p['state'] === 'scheduled'));
});
it('lets the operator decide which parked order goes on which host', function () {
$run = parkedOrder('start');
$small = Host::factory()->active()->create(['datacenter' => 'fsn1', 'name' => 'pve-a', 'total_gb' => 1000, 'reserve_pct' => 0]);
$chosen = Host::factory()->active()->create(['datacenter' => 'fsn1', 'name' => 'pve-b', 'total_gb' => 1000, 'reserve_pct' => 0]);
Livewire::actingAs(operator('Owner'), 'operator')
->test(Capacity::class)
->call('pin', $run->id, (string) $chosen->id);
expect($run->fresh()->context('preferred_host_id'))->toBe($chosen->id);
// And the step honours it — placeableIn() would have taken pve-a, which
// sorts first by name and has just as much room.
$run->update(['status' => ProvisioningRun::STATUS_RUNNING]);
app(ReserveResources::class)->execute($run->fresh());
expect(Instance::query()->where('order_id', $run->subject_id)->first()?->host_id)->toBe($chosen->id)
->and($small->instances()->count())->toBe(0);
});
it('waits rather than quietly placing a pinned order somewhere else', function () {
// A pin is honoured or it waits. Placing it elsewhere because the chosen
// host happened to be full answers a different question than the one the
// operator answered — and they would find out from the finished instance.
$run = parkedOrder('start');
// Zu klein für Start (40 GB Platte auf der neuen Leiter), damit der Pin
// wirklich wartet, statt bequem doch zu passen.
$full = Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 20, 'reserve_pct' => 0]);
Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 1000, 'reserve_pct' => 0]);
Livewire::actingAs(operator('Owner'), 'operator')
->test(Capacity::class)
->call('pin', $run->id, (string) $full->id);
$run->update(['status' => ProvisioningRun::STATUS_RUNNING]);
$result = app(ReserveResources::class)->execute($run->fresh());
expect($result->type)->toBe('poll')
// Named apart from 'awaiting_capacity': this one is a decision to
// revisit, not a server to buy.
->and($result->reason)->toBe('awaiting_pinned_host')
->and(Instance::query()->where('order_id', $run->subject_id)->exists())->toBeFalse();
});
/**
* Nachtrag aus der Durchsicht (Befund 2): ein Anheften ist eine Wahl unter den
* erlaubten Hosts, keine Umgehung der Reservierung.
*
* Der Anheft-Zweig prüfte nur die Kapazität (`canTake`) — nicht, ob die
* Maschine jemand anderem gehört. Die frisch gebaute Reservierung ließ sich
* damit über einen ganz normalen Weg durch die Oberfläche aushebeln: das
* Auswahlfeld auf der Kapazitätsseite bot alle aktiven Hosts an.
*/
it('platziert eine angeheftete Bestellung nicht auf einem fremd reservierten Host', function () {
$run = parkedOrder('start');
$fremder = Customer::factory()->create();
$reserviert = Host::factory()->active()->create([
'datacenter' => 'fsn1', 'name' => 'pve-fremd', 'total_gb' => 1000, 'reserve_pct' => 0,
'reserved_for_customer_id' => $fremder->id,
]);
// Ein freier Host daneben: der Pin darf auch nicht still dorthin ausweichen
// — eine Bestellung, die woanders landet, beantwortet eine andere Frage als
// die, die der Betreiber beantwortet hat.
$frei = Host::factory()->active()->create(['datacenter' => 'fsn1', 'total_gb' => 1000, 'reserve_pct' => 0]);
$run->mergeContext(['preferred_host_id' => $reserviert->id]);
$run->update(['status' => ProvisioningRun::STATUS_RUNNING]);
$result = app(ReserveResources::class)->execute($run->fresh());
expect($result->type)->toBe('poll')
->and($result->reason)->toBe('awaiting_pinned_host')
->and(Instance::query()->where('order_id', $run->subject_id)->exists())->toBeFalse()
->and($frei->instances()->count())->toBe(0);
});
it('platziert eine angeheftete Bestellung auf dem Host, der ihrem eigenen Kunden reserviert ist', function () {
// Der ganze Sinn einer Reservierung: die Maschine gehört diesem Kunden, und
// seine eigene Bestellung gehört genau dorthin.
$run = parkedOrder('start');
$eigen = Host::factory()->active()->create([
'datacenter' => 'fsn1', 'total_gb' => 1000, 'reserve_pct' => 0,
'reserved_for_customer_id' => $run->subject->customer_id,
]);
$run->mergeContext(['preferred_host_id' => $eigen->id]);
$run->update(['status' => ProvisioningRun::STATUS_RUNNING]);
app(ReserveResources::class)->execute($run->fresh());
expect(Instance::query()->where('order_id', $run->subject_id)->first()?->host_id)->toBe($eigen->id);
});
it('stellt einen fremd reservierten Host im Anheft-Auswahlfeld gar nicht erst zur Wahl', function () {
// Die Ursache eine Ebene höher. Ein Knopf, der etwas anbietet, das die
// Aktion danach ablehnt, ist eine Falle — und die Aktion lehnt jetzt ab.
$run = parkedOrder('start');
$fremder = Customer::factory()->create();
$reserviert = Host::factory()->active()->create([
'datacenter' => 'fsn1', 'name' => 'pve-fremd', 'total_gb' => 1000, 'reserve_pct' => 0,
'reserved_for_customer_id' => $fremder->id,
]);
$eigen = Host::factory()->active()->create([
'datacenter' => 'fsn1', 'name' => 'pve-eigen', 'total_gb' => 1000, 'reserve_pct' => 0,
'reserved_for_customer_id' => $run->subject->customer_id,
]);
$page = Livewire::actingAs(operator('Owner'), 'operator')->test(Capacity::class);
$page->assertDontSeeHtml('