110 lines
5.2 KiB
PHP
110 lines
5.2 KiB
PHP
<?php
|
|
|
|
use App\Models\Order;
|
|
use App\Models\ProvisioningRun;
|
|
use App\Provisioning\PipelineRegistry;
|
|
use App\Provisioning\Steps\Customer\ConfigureDnsAndTls;
|
|
use App\Provisioning\Steps\Customer\ConfigureNextcloud;
|
|
use App\Provisioning\WorkInFlight;
|
|
use Tests\Support\Steps\FakeAdvanceStep;
|
|
use Tests\Support\Steps\FakeRetryStep;
|
|
|
|
it('resolves step instances by index', function () {
|
|
$registry = new PipelineRegistry(['host' => [FakeAdvanceStep::class, FakeRetryStep::class]]);
|
|
|
|
expect($registry->count('host'))->toBe(2)
|
|
->and($registry->resolve('host', 0))->toBeInstanceOf(FakeAdvanceStep::class)
|
|
->and($registry->resolve('host', 1))->toBeInstanceOf(FakeRetryStep::class);
|
|
});
|
|
|
|
it('throws for an unknown pipeline', function () {
|
|
(new PipelineRegistry([]))->stepsFor('nope');
|
|
})->throws(InvalidArgumentException::class);
|
|
|
|
it('throws for an out-of-range step index', function () {
|
|
(new PipelineRegistry(['host' => [FakeAdvanceStep::class]]))->resolve('host', 5);
|
|
})->throws(InvalidArgumentException::class);
|
|
|
|
it('answers whether a run still to come carries out another pipeline whole', function () {
|
|
// The question an action about to start a run has to ask about the run already
|
|
// in flight. Asking merely "is something running?" is what made a domain
|
|
// proven during a restart never get routed, and a storage pack booked during
|
|
// an address run get charged monthly and never delivered.
|
|
$registry = new PipelineRegistry([
|
|
'both' => [FakeAdvanceStep::class, FakeRetryStep::class],
|
|
'first' => [FakeAdvanceStep::class],
|
|
'second' => [FakeRetryStep::class],
|
|
]);
|
|
|
|
// Everything still ahead of it.
|
|
expect($registry->stillCovers('both', 0, 'first'))->toBeTrue()
|
|
->and($registry->stillCovers('both', 0, 'second'))->toBeTrue()
|
|
// EVERY step, not any: half of an ordered piece of work is worse than
|
|
// none of it — a quota enforced over a filesystem that never grew.
|
|
->and($registry->stillCovers('first', 0, 'both'))->toBeFalse()
|
|
// Already past it: a run standing at its second step will not go back and
|
|
// do its first, which is the case a name comparison cannot see at all.
|
|
->and($registry->stillCovers('both', 1, 'first'))->toBeFalse()
|
|
->and($registry->stillCovers('both', 1, 'second'))->toBeTrue()
|
|
// The step it is standing on counts as still to come — it is about to run,
|
|
// or running, and every step reads the state when it gets there.
|
|
->and($registry->stillCovers('second', 0, 'second'))->toBeTrue();
|
|
});
|
|
|
|
it('asks that question of the runs actually in flight against an order', function () {
|
|
$order = Order::factory()->create();
|
|
$work = app(WorkInFlight::class);
|
|
|
|
$running = fn (string $pipeline) => ProvisioningRun::factory()->create([
|
|
'subject_type' => Order::class, 'subject_id' => $order->id,
|
|
'pipeline' => $pipeline, 'status' => ProvisioningRun::STATUS_RUNNING,
|
|
]);
|
|
|
|
expect($work->covers($order, 'address'))->toBeFalse()
|
|
->and($work->covers($order, 'storage'))->toBeFalse();
|
|
|
|
// A quota run writes the figure Nextcloud enforces and nothing else — it does
|
|
// not grow the disk that figure is a promise about, so it does not stand for a
|
|
// storage change. This is the pair that had a booked pack charged every month
|
|
// and never delivered.
|
|
$running('quota');
|
|
expect($work->covers($order, 'storage'))->toBeFalse()
|
|
->and($work->covers($order, 'address'))->toBeFalse();
|
|
|
|
// A plan change carries both pipelines whole; there is genuinely nothing to add.
|
|
ProvisioningRun::query()->delete();
|
|
$running('plan-change');
|
|
expect($work->covers($order, 'storage'))->toBeTrue()
|
|
->and($work->covers($order, 'address'))->toBeTrue();
|
|
|
|
// A run that has finished covers nothing, however much it contained.
|
|
ProvisioningRun::query()->update(['status' => ProvisioningRun::STATUS_COMPLETED]);
|
|
expect($work->covers($order, 'storage'))->toBeFalse()
|
|
->and($work->covers($order, 'address'))->toBeFalse();
|
|
|
|
// A pipeline that no longer exists — renamed by a deploy while a run was in
|
|
// flight — covers nothing either. Refusing to start the work over it would
|
|
// lose the work as well as the run.
|
|
ProvisioningRun::query()->delete();
|
|
$running('gone-in-a-deploy');
|
|
expect($work->covers($order, 'address'))->toBeFalse();
|
|
});
|
|
|
|
it('lays the maintenance pipelines out so a half-finished address is a safe one', function () {
|
|
// Nextcloud BEFORE the router, in the address pipeline and in the plan change
|
|
// that reuses it. Router first meant a run could get the certificate and then
|
|
// fail at Nextcloud — leaving the customer's own domain serving Nextcloud's
|
|
// untrusted-domain error under a valid certificate, with domain_cert_ok true
|
|
// so the portal called it live. The reasoning for both directions is written
|
|
// out above the `address` pipeline in config/provisioning.php.
|
|
$nextcloud = ConfigureNextcloud::class;
|
|
$dns = ConfigureDnsAndTls::class;
|
|
|
|
foreach (['address', 'plan-change', 'customer'] as $pipeline) {
|
|
$steps = (array) config('provisioning.pipelines.'.$pipeline);
|
|
|
|
expect(array_search($nextcloud, $steps, true))
|
|
->toBeLessThan(array_search($dns, $steps, true));
|
|
}
|
|
});
|