CluPilotCloud/tests/Feature/Console/ApplyStorageQuotasTest.php

155 lines
5.4 KiB
PHP

<?php
use App\Models\Host;
use App\Models\Instance;
use App\Models\Order;
use App\Models\ProvisioningRun;
use App\Provisioning\RunRunner;
use Illuminate\Support\Facades\Queue;
/**
* The estate that was sold a quota and never given one.
*
* ApplyStorageQuota runs in the `customer` and `plan-change` pipelines now, so
* anything built or changed from here on is enforced on the machine. Everything
* from before is not: `quota_gb` reached the instance row, Nextcloud was never
* told, and those customers still have the whole disk whatever they paid for.
* Nothing in the product could reach them, because both pipelines only run on a
* build or a change and these machines are having neither.
*/
/** A finished, running instance whose Nextcloud has never been told anything. */
function unquotedInstance(array $attributes = []): Instance
{
$host = Host::factory()->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);
});