create([ 'pipeline' => 'instance-mail', 'context' => ['instance_id' => $instance->id, 'node' => 'pve', 'vmid' => 201], ]); } /** * Eine Instanz, die tatsaechlich auf einem Host steht. * * Der Schritt ruft ProxmoxClient::forHost($instance->host) auf — genau wie * ApplyStorageQuota, dessen Testaufbau (ApplyStorageQuotasTest::unquotedInstance()) * aus demselben Grund einen Host anlegt. Eine Instanz ohne host_id gibt es in * diesem Bestand fuer eine Maschine, die tatsaechlich Gastbefehle bekommt, * nicht — FakeProxmoxClient::forHost() verlangt ein echtes Host-Objekt, weil * der reale Client genauso wenig ohne einen Host wuesste, wohin er soll. */ function instanzAufHost(): Instance { return Instance::factory()->create(['host_id' => Host::factory()]); } function versandbereitFuerSchritt(): void { Settings::set('mail.host', 'mail.clupilot.cloud'); Settings::set('mail.port', 587); Settings::set('mail.encryption', 'tls'); Mailbox::factory()->create([ 'key' => 'instance-relay', 'address' => 'noreply@clupilot.cloud', 'username' => 'noreply@clupilot.cloud', 'password' => 'geheim', 'active' => true, 'authenticates' => true, ]); } it('traegt jeden Wert einzeln in den Gast', function () { versandbereitFuerSchritt(); $pve = new FakeProxmoxClient; app()->instance(ProxmoxClient::class, $pve); $instance = instanzAufHost(); app(ConfigureInstanceMail::class)->execute(laufMitInstanz($instance)); $befehle = implode("\n", $pve->guestCommands); // Alle acht Werte aus GuestMailConfig::values(), nicht nur zwei — ein // vergessener Schluessel soll hier auffallen, nicht erst beim Kunden. expect($befehle)->toContain('config:system:set mail_smtpmode --value='.escapeshellarg('smtp')) ->and($befehle)->toContain('config:system:set mail_smtphost --value='.escapeshellarg('mail.clupilot.cloud')) ->and($befehle)->toContain('config:system:set mail_smtpport --value='.escapeshellarg('587')) ->and($befehle)->toContain('config:system:set mail_smtpsecure --value='.escapeshellarg('tls')) ->and($befehle)->toContain('config:system:set mail_smtpauth --value='.escapeshellarg('true')) ->and($befehle)->toContain('config:system:set mail_smtpname --value='.escapeshellarg('noreply@clupilot.cloud')) ->and($befehle)->toContain('config:system:set mail_from_address --value='.escapeshellarg('noreply')) ->and($befehle)->toContain('config:system:set mail_domain --value='.escapeshellarg('clupilot.cloud')); }); it('maskiert das Passwort, sodass es keinen zweiten Befehl starten kann', function () { // Verstecken laesst sich der Wert auf dieser Maschine nicht (siehe // Kopfkommentar des Schrittes). Was sehr wohl gilt und geprueft gehoert: // er darf aus seiner Klammerung nicht ausbrechen. Der Befehl laeuft als // root auf einer Kundenmaschine — ein Semikolon im Passwort waere dort // ein zweiter Befehl. 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' => "boes'; touch /tmp/PWNED; echo '", 'active' => true, 'authenticates' => true, ]); $pve = new FakeProxmoxClient; app()->instance(ProxmoxClient::class, $pve); app(ConfigureInstanceMail::class)->execute(laufMitInstanz(instanzAufHost())); $passwortbefehl = collect($pve->guestCommands) ->first(fn ($b) => str_contains($b, 'mail_smtppassword')); // Der ganze Wert steht in EINEM maskierten Argument: das Semikolon darf // nicht ausserhalb der Anfuehrungszeichen stehen. expect($passwortbefehl)->toContain(escapeshellarg("boes'; touch /tmp/PWNED; echo '")); }); it('schreibt GAR NICHTS, wenn der Mailserver fehlt', function () { // Nicht eingerichtet ist etwas anderes als kaputt: dieser Schritt sitzt in // der PFLICHT-Pipeline `customer`, und ein bezahlter Kunde bekommt seine // Cloud auch dann, wenn der Mailversand noch nicht steht — derselbe // Grundsatz wie bei RegisterMonitoring. Der Lauf geht deshalb WEITER // (advance), nicht in einen Fehlschlag; nur in den Gast wird nichts // geschrieben, und der Grund landet im Protokoll fuer den Betreiber. Log::spy(); Settings::set('mail.host', ''); $pve = new FakeProxmoxClient; app()->instance(ProxmoxClient::class, $pve); $instance = Instance::factory()->create(); $ergebnis = app(ConfigureInstanceMail::class)->execute(laufMitInstanz($instance)); expect($pve->guestCommands)->toBe([]) ->and($ergebnis->type)->toBe(\App\Provisioning\StepResult::ADVANCE); Log::shouldHaveReceived('warning')->once()->withArgs( fn ($message, $context) => $context['instance'] === $instance->uuid && $context['reason'] === 'no_server' ); }); it('haengt ein Lauf-Ereignis an, das der Betreiber in der Konsole sieht', function () { // Log::warning allein reicht hier nicht: bis der Mailserver steht, trifft // dieser Zweig auf JEDE Bestellung zu, und die einzige Rueckmeldung darf // nicht in einer Logdatei verschwinden, in die niemand schaut — // RegisterMonitoring macht sein Ueberspringen genauso am Lauf sichtbar. Settings::set('mail.host', ''); $pve = new FakeProxmoxClient; app()->instance(ProxmoxClient::class, $pve); $run = laufMitInstanz(Instance::factory()->create()); app(ConfigureInstanceMail::class)->execute($run); $ereignis = $run->events()->first(); expect($ereignis)->not->toBeNull() ->and($ereignis->step)->toBe('configure_instance_mail') ->and($ereignis->outcome)->toBe('info') ->and($ereignis->message)->toContain('no_server'); }); it('schreibt beim zweiten Lauf erneut, statt sich mit einem Merker zu sperren', function () { versandbereitFuerSchritt(); $pve = new FakeProxmoxClient; app()->instance(ProxmoxClient::class, $pve); $instance = instanzAufHost(); app(ConfigureInstanceMail::class)->execute(laufMitInstanz($instance)); $ersteRunde = count($pve->guestCommands); app(ConfigureInstanceMail::class)->execute(laufMitInstanz($instance)); // config:system:set ist von sich aus wiederholbar — derselbe Wert zweimal // geschrieben ist derselbe Wert. Der Schritt darf deshalb ohne Merker // erneut laufen; das ist bei einer Nachruestung ueber den Bestand der // Normalfall, nicht die Ausnahme. expect(count($pve->guestCommands))->toBe($ersteRunde * 2); });