active()->create(['datacenter' => 'fsn', 'node' => 'pve']); $order = Order::factory()->withSubscription()->create(['datacenter' => 'fsn', 'plan' => 'business']); return Instance::factory()->create(array_merge([ 'order_id' => $order->id, 'customer_id' => $order->customer_id, 'host_id' => $host->id, 'vmid' => 101, 'status' => 'active', 'quota_gb' => 500, 'quota_applied_gb' => null, ], $attributes)); } /** Carry out every quota run the command started. */ function runQuotaRuns(): void { $runner = app(RunRunner::class); ProvisioningRun::query()->where('pipeline', 'quota')->get() ->each(fn (ProvisioningRun $run) => $runner->advance($run)); } it('applies the allowance to an instance that never had one, and records that it did', function () { $services = fakeServices(); Queue::fake(); $instance = unquotedInstance(); $this->artisan('clupilot:apply-quotas')->assertSuccessful(); runQuotaRuns(); expect($services['pve']->guestRan("config:app:set files default_quota --value='500 GB'"))->toBeTrue() ->and($instance->fresh()->quota_applied_gb)->toBe(500) ->and($instance->fresh()->quotaIsEnforced())->toBeTrue(); }); it('is a no-op the second time round', function () { $services = fakeServices(); Queue::fake(); unquotedInstance(); $this->artisan('clupilot:apply-quotas')->assertSuccessful(); runQuotaRuns(); $callsAfterFirstSweep = count($services['pve']->guestCommands); $this->artisan('clupilot:apply-quotas') ->expectsOutputToContain('bereits gesetzt') ->assertSuccessful(); runQuotaRuns(); expect(ProvisioningRun::query()->where('pipeline', 'quota')->count())->toBe(1) ->and($services['pve']->guestCommands)->toHaveCount($callsAfterFirstSweep); }); it('changes nothing at all in dry-run mode', function () { $services = fakeServices(); Queue::fake(); $instance = unquotedInstance(); $this->artisan('clupilot:apply-quotas', ['--dry-run' => true]) ->expectsOutputToContain('Probelauf') ->assertSuccessful(); expect(ProvisioningRun::query()->where('pipeline', 'quota')->count())->toBe(0) ->and($services['pve']->guestCommands)->toBe([]) ->and($instance->fresh()->quota_applied_gb)->toBeNull(); }); it('skips what it has no business touching, and says which', function () { fakeServices(); Queue::fake(); // Still being built: its own run is writing this machine. unquotedInstance(['status' => 'provisioning']); // Nothing to reach at all. unquotedInstance(['status' => 'failed', 'vmid' => null]); // No allowance on the row. Writing "0 GB" would lock the customer out of // their own files, which is worse than leaving what is there. unquotedInstance(['quota_gb' => 0]); // A service that has run out. unquotedInstance(['status' => 'ended']); $busy = unquotedInstance(); ProvisioningRun::factory()->create([ 'subject_type' => Order::class, 'subject_id' => $busy->order_id, 'pipeline' => 'plan-change', 'status' => ProvisioningRun::STATUS_RUNNING, ]); $this->artisan('clupilot:apply-quotas') ->expectsOutputToContain('keine erreichbare Maschine') ->expectsOutputToContain('kein Kontingent hinterlegt') ->expectsOutputToContain('anderer Lauf aktiv') ->assertSuccessful(); expect(ProvisioningRun::query()->where('pipeline', 'quota')->count())->toBe(0); }); it('re-applies an allowance that has changed since it was last enforced', function () { $services = fakeServices(); Queue::fake(); // A downgrade landed on the row but the run that should have followed it // never finished — the customer is paying for less and still holds more. $instance = unquotedInstance(['quota_gb' => 200, 'quota_applied_gb' => 500]); $this->artisan('clupilot:apply-quotas')->assertSuccessful(); runQuotaRuns(); expect($services['pve']->guestRan("config:app:set files default_quota --value='200 GB'"))->toBeTrue() ->and($instance->fresh()->quota_applied_gb)->toBe(200); }); it('repairs one named instance when asked to', function () { fakeServices(); Queue::fake(); $wanted = unquotedInstance(); unquotedInstance(); $this->artisan('clupilot:apply-quotas', ['--instance' => $wanted->uuid])->assertSuccessful(); expect(ProvisioningRun::query()->where('pipeline', 'quota')->count())->toBe(1) ->and(ProvisioningRun::query()->where('pipeline', 'quota')->first()->context('instance_id')) ->toBe($wanted->id); });