guestScript('status.php', 0, 'ok'); $s['pve']->guestScript('hostname -I', 0, '10.20.0.9'); $s['pve']->guestScript('occ status', 0, '{"installed":true,"maintenance":false}'); // deploy health + acceptance probe // Fake defaults: tasks stopped/OK, guest agent up, cert reachable. // ConfigureInstanceMail steht jetzt in dieser Pipeline. Mailversand ist // eingerichtet, damit DIESER Lauf den Schreib-Pfad beweist (siehe // Zusicherung weiter unten, dass der Gast die Werte tatsaechlich bekam). // Der Fall OHNE Einrichtung — ein bezahlter Kunde bekommt seine Cloud // trotzdem — hat einen eigenen Test weiter unten, weil er das genaue // Gegenteil dieser Fixture braucht. Settings::set('mail.host', 'mail.clupilot.cloud'); Settings::set('mail.port', 587); Mailbox::factory()->create([ 'key' => 'instance-relay', 'address' => 'noreply@clupilot.cloud', 'username' => 'noreply@clupilot.cloud', 'password' => 'geheim', 'active' => true, 'authenticates' => true, ]); Host::factory()->active()->create(['datacenter' => 'fsn', 'node' => 'pve', 'total_gb' => 1000]); $order = Order::factory()->withSubscription()->create(['status' => 'paid', 'plan' => 'start', 'datacenter' => 'fsn']); $run = ProvisioningRun::factory()->create([ 'subject_type' => Order::class, 'subject_id' => $order->id, 'pipeline' => 'customer', 'context' => [], ]); $runner = app(RunRunner::class); for ($i = 0; $i < 60; $i++) { $run->refresh(); if (in_array($run->status, ['completed', 'failed'], true)) { break; } if ($run->next_attempt_at?->isFuture()) { $run->update(['next_attempt_at' => now()]); } $runner->advance($run); } $run->refresh(); $instance = Instance::query()->where('order_id', $order->id)->firstOrFail(); expect($run->status)->toBe('completed') ->and($run->error)->toBeNull() ->and($order->fresh()->status)->toBe('active') ->and($instance->status)->toBe('active') ->and($instance->vmid)->not->toBeNull() ->and($instance->cert_ok)->toBeTrue() ->and($instance->onboardingTasks()->count())->toBeGreaterThan(0) // The storage allowance was DELIVERED, not merely walked past. This test // covers every step of the pipeline and asserted nothing about the quota one, so it // proved that inserting the step did not break the pipeline rather than // that the step does anything — and quota_applied_gb is written only after // the guest accepted the figure, which is the whole distinction between an // enforced allowance and a number in our database. ->and($instance->quota_applied_gb)->toBe($instance->quota_gb) ->and($instance->quota_applied_gb)->toBeGreaterThan(0) ->and($s['pve']->guestRan('config:app:set files default_quota'))->toBeTrue() // Mail war eingerichtet (siehe Fixture oben) — der Schreib-Pfad von // ConfigureInstanceMail lief also tatsaechlich, nicht nur der // Ueberspringen-Pfad, den der Test weiter unten prueft. ->and($s['pve']->guestRan('config:system:set mail_smtphost'))->toBeTrue(); // Every external resource created exactly once. foreach (['instance_id', 'vmid', 'dns_record_id', 'nc_admin', 'backup_job_id', 'monitoring_target_id'] as $kind) { expect(RunResource::where('run_id', $run->id)->where('kind', $kind)->count())->toBe(1); } // Secrets scrubbed from run state; the admin password moved onto the // instance instead — held there, not mailed, until the customer // acknowledges it on the dashboard. expect($run->context('admin_password'))->toBeNull() ->and($run->context('db_password'))->toBeNull() ->and($instance->fresh()->admin_password)->not->toBeNull() ->and($instance->fresh()->credentials_acknowledged_at)->toBeNull(); Notification::assertSentOnDemand(CloudReady::class); }); it('completes a paid order even when the mail server does not exist yet', function () { // Der Mailserver dieses Produkts wird gerade erst aufgesetzt — es gibt // ihn zum Zeitpunkt dieses Laufs noch nicht. Ein bezahlter Kunde bekommt // seine Cloud trotzdem: ConfigureInstanceMail ist eine Nebenfunktion in // einer PFLICHT-Pipeline und darf sie nicht aufhalten, genau wie // RegisterMonitoring die Bereitstellung nie an einer nicht erreichbaren // Ueberwachung scheitern laesst. Bewusst KEINE mail.host/mail.port- oder // Mailbox-Fixture hier — das ist der ganze Punkt dieses Tests. Notification::fake(); Queue::fake(); $s = fakeServices(); $s['pve']->guestScript('status.php', 0, 'ok'); $s['pve']->guestScript('hostname -I', 0, '10.20.0.9'); $s['pve']->guestScript('occ status', 0, '{"installed":true,"maintenance":false}'); Host::factory()->active()->create(['datacenter' => 'fsn', 'node' => 'pve', 'total_gb' => 1000]); $order = Order::factory()->withSubscription()->create(['status' => 'paid', 'plan' => 'start', 'datacenter' => 'fsn']); $run = ProvisioningRun::factory()->create([ 'subject_type' => Order::class, 'subject_id' => $order->id, 'pipeline' => 'customer', 'context' => [], ]); $runner = app(RunRunner::class); for ($i = 0; $i < 60; $i++) { $run->refresh(); if (in_array($run->status, ['completed', 'failed'], true)) { break; } if ($run->next_attempt_at?->isFuture()) { $run->update(['next_attempt_at' => now()]); } $runner->advance($run); } $run->refresh(); $instance = Instance::query()->where('order_id', $order->id)->firstOrFail(); expect($run->status)->toBe('completed') ->and($run->error)->toBeNull() ->and($order->fresh()->status)->toBe('active') ->and($instance->status)->toBe('active') // Das Gegenstueck zur Zusicherung im ersten Test: ohne Einrichtung // ging kein einziger Mailbefehl an den Gast, weder die Werte noch das // Passwort. ->and($s['pve']->guestRan('config:system:set mail_smtphost'))->toBeFalse() ->and($s['pve']->guestRan('mail_smtppassword'))->toBeFalse(); }); it('does not duplicate external resources when a step re-runs after a crash', function () { Notification::fake(); Queue::fake(); $s = fakeServices(); $s['pve']->guestScript('status.php', 0, 'ok'); $s['pve']->guestScript('hostname -I', 0, '10.20.0.9'); $s['pve']->guestScript('occ status', 0, '{"installed":true,"maintenance":false}'); Host::factory()->active()->create(['datacenter' => 'fsn', 'node' => 'pve', 'total_gb' => 1000]); $order = Order::factory()->withSubscription()->create(['status' => 'paid', 'plan' => 'start', 'datacenter' => 'fsn']); $run = ProvisioningRun::factory()->create([ 'subject_type' => Order::class, 'subject_id' => $order->id, 'pipeline' => 'customer', 'context' => [], ]); $runner = app(RunRunner::class); // Drive a few steps, then rewind to the clone step and replay it. for ($i = 0; $i < 5; $i++) { $run->refresh(); if ($run->next_attempt_at?->isFuture()) { $run->update(['next_attempt_at' => now()]); } $runner->advance($run); } $cloneIndex = array_search( CloneVirtualMachine::class, config('provisioning.pipelines.customer'), true, ); $run->update(['current_step' => $cloneIndex, 'status' => 'running']); $runner->advance($run->fresh()); expect(RunResource::where('run_id', $run->id)->where('kind', 'vmid')->count())->toBe(1); });