Das Postfach, auf dem alles steht, gibt es jetzt — leer und abgeschaltet

K4. GuestMailConfig sucht das gemeinsame Versandkonto der Kundeninstanzen unter
dem Schluessel `instance-relay`. Diesen Datensatz legte nirgends etwas an, und
die Konsole kann Postfaecher nur bearbeiten, nicht erstellen: der Betreiber
kaeme ohne Tinker gar nicht an den Start.

Daran haengt mehr als eine Einstellung. Ohne Postfach verschickt die
Kunden-Nextcloud keine Mail — aber `occ user:add --generate-password --email`
gelingt trotzdem: Nextcloud legt das Konto an, versucht die Willkommensmail,
protokolliert intern einen Fehler und beendet mit 0. Die Zeile sagte also
"Eingeladen" und die Meldung versprach einen Link, waehrend niemand eine Mail
bekam. Dieselbe Attrappe, die dieses Feature abschaffen sollte, eine Schicht
tiefer.

Die Wanderung legt die Zeile nach dem Muster der Postfach-Wanderung vom 28.07.
an: firstOrNew, mit der Adresse noreply@clupilot.cloud — inaktiv und ohne
Zugangsdaten. Ein Postfach, das ohne Zutun des Betreibers als einsatzbereit
dastuende, waere das naechste stille Versprechen; so erscheint es in der
Konsole als Zeile, die sichtbar noch etwas braucht, und isConfigured() bleibt
false. Ein bereits ausgefuelltes Postfach ruehrt ein zweiter Lauf nicht an —
sonst waere der stillste denkbare Ausfall genau ein `migrate` entfernt.

Die Zeile steht ab jetzt in jeder Testdatenbank. Die Fixtures, die sie bisher
selbst anlegten, fuellen sie aus, statt an der Eindeutigkeit des Schluessels
abzuprallen; die beiden Zusicherungen ueber die fuenf Betreiber-Postfaecher
nennen sie und halten fest, dass jede der beiden Wanderungen nur ihre eigenen
Zeilen zuruecknimmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat/versandtakt
nexxo 2026-08-03 23:57:54 +02:00
parent 1d0bacc650
commit 01eff483fa
7 changed files with 272 additions and 43 deletions

View File

@ -0,0 +1,96 @@
<?php
use App\Models\Mailbox;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
/**
* Das Postfach, auf dem die Mitarbeiterverwaltung steht als Zeile, die der
* Betreiber ausfüllen kann.
*
* `GuestMailConfig` sucht das gemeinsame Versandkonto der Kundeninstanzen
* unter dem Schlüssel `instance-relay`. Diesen Datensatz legte bisher
* NIRGENDS etwas an, und die Konsole kann Postfächer nur bearbeiten, nicht
* erstellen: der Betreiber käme ohne Tinker gar nicht an den Start.
*
* Und daran hängt mehr als eine Einstellung. Ohne Postfach verschickt die
* Kunden-Nextcloud keine Mail aber `occ user:add --generate-password
* --email` gelingt trotzdem: Nextcloud legt das Konto an, versucht die
* Willkommensmail, protokolliert intern einen Fehler und beendet mit 0. Das
* Portal schriebe also „Einladung verschickt. Der Mitarbeiter bekommt einen
* Link, an dem er sein Passwort selbst setzt", während niemand eine Mail
* bekommt. Dieselbe Attrappe, die dieses Feature abschaffen sollte, eine
* Schicht tiefer.
*
* INAKTIV und OHNE Zugangsdaten, mit Absicht: ein Postfach, das ohne Zutun des
* Betreibers als einsatzbereit dastünde, wäre das nächste stille
* Versprechen. So erscheint es in der Konsole als eine Zeile, die sichtbar
* noch etwas braucht `Mailbox::isConfigured()` bleibt `false`,
* `GuestMailConfig` meldet `no_mailbox`, und `ConfigureInstanceMail` geht mit
* einem sichtbaren Lauf-Ereignis weiter, statt halbe Werte in eine
* Kundeninstanz zu schreiben.
*
* `firstOrNew` statt `new Mailbox`, wie bei der Wanderung, die die fünf
* Betreiber-Postfächer anlegt: eine Wanderung, die auf halbem Weg scheitert,
* steht nicht in der migrations-Tabelle, und der zweite Lauf muss die Zeile
* aktualisieren statt an der Eindeutigkeit des Schlüssels abzuprallen. Er
* darf dabei nur die Felder anfassen, die noch NIE jemand ausgefüllt hat
* siehe unten.
*
* Der Schlüssel steht hier ausgeschrieben und nicht als
* `GuestMailConfig::RELAY_KEY`: eine Wanderung friert ein, was zum Zeitpunkt
* ihres Schreibens galt (siehe die Begründung in der seats-Wanderung vom
* selben Tag). Ändert die Konstante sich später, soll DIESE Zeile nicht
* mitwandern dann gehört eine neue Wanderung geschrieben, die den
* Datensatz umbenennt.
*/
return new class extends Migration
{
private const KEY = 'instance-relay';
public function up(): void
{
DB::transaction(function () {
$box = Mailbox::firstOrNew(['key' => self::KEY]);
// Ein bestehender Datensatz wird NICHT überschrieben. Läuft die
// Wanderung ein zweites Mal — ein Wiederholungslauf nach einem
// Abbruch, ein `migrate` auf einem Server, der schon eines hatte —
// hätte der Betreiber sonst Adresse, Anmeldung und Passwort neu
// einzutragen, und bis er es merkt, verschickt keine Kundencloud
// mehr eine Einladung. Der stillste denkbare Ausfall.
if ($box->exists) {
return;
}
$box->fill([
'address' => 'noreply@clupilot.cloud',
'display_name' => 'CluPilot',
// Null heisst „Anmeldename gleich Adresse" (siehe
// Mailbox::smtpUsername()). Die Adresse selbst steht schon
// oben; sie hier zu wiederholen wäre eine Kopie, die nur
// einmal auseinanderlaufen muss, um zum Fehler zu werden.
'username' => null,
'password' => null,
// Auf diese Adresse antwortet niemand. Ein „noreply", dem man
// schreiben kann, ist eine Lüge im Absender.
'no_reply' => true,
// Der Kern dieser Wanderung: die Zeile ist da, der Versand
// nicht. Erst wer Adresse und Passwort einträgt und dieses
// Häkchen setzt, hat den Mailversand der Kundeninstanzen
// wirklich in Betrieb genommen.
'active' => false,
// Ein Relay ohne Anmeldung ist erlaubt (siehe Mailbox::
// isConfigured()) — aber es ist die Ausnahme, und ein
// vorbelegtes „braucht kein Passwort" läse sich wie eine
// Aussage über einen Server, den hier noch niemand kennt.
'authenticates' => true,
])->save();
});
}
public function down(): void
{
DB::table('mailboxes')->where('key', self::KEY)->delete();
}
};

View File

@ -5,14 +5,31 @@ use App\Models\Mailbox;
use App\Services\Mail\GuestMailConfig;
use App\Support\Settings;
/**
* Das gemeinsame Versandkonto ausfüllen so, wie der Betreiber es in der
* Konsole täte.
*
* Die ZEILE legt eine eigene Wanderung an, inaktiv und ohne Zugangsdaten
* (siehe InstanceRelaySeedMigrationTest). Ein `Mailbox::factory()->create()`
* mit demselben Schlüssel liefe deshalb in die Eindeutigkeit der Spalte
* und das ist auch der ehrlichere Ablauf: ausgefüllt wird hier, angelegt
* wurde dort.
*/
function versandkonto(array $werte): Mailbox
{
$box = Mailbox::findByKey(GuestMailConfig::RELAY_KEY);
$box->update($werte);
return $box;
}
function versandbereit(): void
{
Settings::set('mail.host', 'mail.clupilot.cloud');
Settings::set('mail.port', 587);
Settings::set('mail.encryption', 'tls');
Mailbox::factory()->create([
'key' => 'instance-relay',
versandkonto([
'address' => 'noreply@clupilot.cloud',
'username' => 'noreply@clupilot.cloud',
'password' => 'geheim',
@ -43,17 +60,16 @@ it('baut die Werte aus Server UND Postfach zusammen', function () {
->and($config->password())->toBe('geheim');
});
it('schreibt mail_smtpauth false fuer ein Postfach ohne Anmeldung', function () {
// Ein Relay ohne Anmeldung ist bei Mailbox::isConfigured() ausdruecklich
// erlaubt und verlangt kein Passwort. Ein fest verdrahtetes 'true' wuerde
it('schreibt mail_smtpauth false für ein Postfach ohne Anmeldung', function () {
// Ein Relay ohne Anmeldung ist bei Mailbox::isConfigured() ausdrücklich
// erlaubt und verlangt kein Passwort. Ein fest verdrahtetes 'true' würde
// Nextcloud trotzdem zur Anmeldung mit leerem Passwort zwingen und jeden
// Versand still scheitern lassen — genau das darf hier nicht passieren.
Settings::set('mail.host', 'mail.clupilot.cloud');
Settings::set('mail.port', 587);
Settings::set('mail.encryption', 'tls');
Mailbox::factory()->create([
'key' => 'instance-relay',
versandkonto([
'address' => 'noreply@clupilot.cloud',
'password' => null,
'active' => true,
@ -66,22 +82,22 @@ it('schreibt mail_smtpauth false fuer ein Postfach ohne Anmeldung', function ()
->and($config->values())->toMatchArray(['mail_smtpauth' => 'false']);
});
it('traegt das Passwort NICHT unter values()', function () {
it('trägt das Passwort NICHT unter values()', function () {
// values() wandert in Befehle. Das Passwort geht einen eigenen Weg, damit
// niemand es versehentlich mit den uebrigen Werten mitschleift.
// niemand es versehentlich mit den übrigen Werten mitschleift.
versandbereit();
$values = GuestMailConfig::for(Instance::factory()->create())->values();
expect($values)->not->toHaveKey('mail_smtppassword')
// Der Schluessel allein beweist nichts: ein Leck unter einem anderen
// Namen faende der Test oben nicht. Deshalb zusaetzlich nach dem WERT
// suchen, egal unter welchem Schluessel er sich versteckt.
// Der Schlüssel allein beweist nichts: ein Leck unter einem anderen
// Namen fände der Test oben nicht. Deshalb zusätzlich nach dem WERT
// suchen, egal unter welchem Schlüssel er sich versteckt.
->and($values)->not->toContain('geheim');
});
it('sagt ohne Mailserver, dass nichts geschrieben werden darf', function () {
Mailbox::factory()->create(['key' => 'instance-relay', 'address' => 'noreply@clupilot.cloud', 'password' => 'geheim', 'active' => true]);
versandkonto(['address' => 'noreply@clupilot.cloud', 'password' => 'geheim', 'active' => true]);
Settings::set('mail.host', '');
$config = GuestMailConfig::for(Instance::factory()->create());
@ -91,6 +107,9 @@ it('sagt ohne Mailserver, dass nichts geschrieben werden darf', function () {
});
it('sagt ohne Absenderpostfach, dass nichts geschrieben werden darf', function () {
// Die Zeile GIBT es seit der eigenen Wanderung — inaktiv und ohne
// Zugangsdaten. Genau das muss hier immer noch „kein Postfach" heissen:
// ein Datensatz allein ist kein eingerichteter Versand.
Settings::set('mail.host', 'mail.clupilot.cloud');
Settings::set('mail.port', 587);
@ -103,7 +122,7 @@ it('sagt ohne Absenderpostfach, dass nichts geschrieben werden darf', function (
it('weist ein Postfach ohne Zugangsdaten ab', function () {
Settings::set('mail.host', 'mail.clupilot.cloud');
Settings::set('mail.port', 587);
Mailbox::factory()->create(['key' => 'instance-relay', 'address' => 'noreply@clupilot.cloud', 'password' => null, 'active' => true, 'authenticates' => true]);
versandkonto(['address' => 'noreply@clupilot.cloud', 'password' => null, 'active' => true, 'authenticates' => true]);
expect(GuestMailConfig::for(Instance::factory()->create())->problem())->toBe('no_mailbox');
});

View File

@ -0,0 +1,96 @@
<?php // tests/Feature/Mail/InstanceRelaySeedMigrationTest.php
use App\Models\Instance;
use App\Models\Mailbox;
use App\Services\Mail\GuestMailConfig;
use App\Support\Settings;
/**
* Lädt bei jedem Aufruf eine frische Instanz der anonymen Klasse `require`,
* nicht `require_once`, aus demselben Grund wie in MailboxSeedMigrationTest:
* PHP wertet den `return new class …`-Ausdruck jedes Mal neu aus, und nur so
* lässt sich up() gegen eine Datenbank fahren, die RefreshDatabase längst
* einmal migriert hat.
*/
function loadInstanceRelaySeedMigration(): object
{
return require database_path('migrations/2026_08_03_190000_ein_versandkonto_fuer_die_kundeninstanzen.php');
}
it('legt das gemeinsame Versandkonto der Kundeninstanzen an', function () {
// Ohne diesen Datensatz käme der Betreiber ohne Tinker gar nicht an den
// Start: GuestMailConfig sucht genau diesen Schlüssel, und die Konsole
// kann Postfächer nur bearbeiten, nicht erstellen.
$box = Mailbox::findByKey(GuestMailConfig::RELAY_KEY);
expect($box)->not->toBeNull()
->and($box->address)->toBe('noreply@clupilot.cloud');
});
it('legt es inaktiv und ohne Zugangsdaten an', function () {
// Ein Postfach, das ohne Zutun des Betreibers als einsatzbereit dastünde,
// wäre das nächste stille Versprechen: ConfigureInstanceMail schriebe
// dann Werte in die Kunden-Nextcloud, die niemand je eingetragen hat.
$box = Mailbox::findByKey(GuestMailConfig::RELAY_KEY);
expect($box->active)->toBeFalse()
->and($box->getRawOriginal('password'))->toBeNull()
->and($box->isConfigured())->toBeFalse();
});
it('hält GuestMailConfig davon ab, halb eingetragene Werte zu liefern', function () {
// Die Probe aufs Exempel: solange niemand das Postfach ausgefüllt hat,
// muss `no_mailbox` herauskommen — auch jetzt, wo es die Zeile GIBT.
Settings::set('mail.host', 'mail.clupilot.cloud');
Settings::set('mail.port', 587);
$config = GuestMailConfig::for(Instance::factory()->create());
expect($config->available())->toBeFalse()
->and($config->problem())->toBe('no_mailbox');
});
it('ist zweimal zu laufen unbedenklich', function () {
// `firstOrNew`, nicht `new Mailbox`: eine Wanderung, die auf halbem Weg
// scheitert, steht nicht in der migrations-Tabelle — der zweite Lauf muss
// die Zeile aktualisieren, nicht an der Eindeutigkeit des Schlüssels
// abprallen.
$threw = null;
try {
loadInstanceRelaySeedMigration()->up();
} catch (Throwable $e) {
$threw = $e;
}
expect($threw)->toBeNull()
->and(Mailbox::query()->where('key', GuestMailConfig::RELAY_KEY)->count())->toBe(1);
});
it('lässt ein bereits ausgefülltes Postfach beim zweiten Lauf in Ruhe', function () {
// Der Fall, der wirklich weh täte: der Betreiber hat Adresse und Passwort
// eingetragen und das Konto scharfgeschaltet, dann läuft die Wanderung
// aus irgendeinem Grund erneut. Sie darf ihm den Versand nicht wieder
// abschalten.
Mailbox::findByKey(GuestMailConfig::RELAY_KEY)->update([
'address' => 'versand@kunde.example',
'password' => 'geheim',
'active' => true,
]);
loadInstanceRelaySeedMigration()->up();
$box = Mailbox::findByKey(GuestMailConfig::RELAY_KEY);
expect($box->address)->toBe('versand@kunde.example')
->and($box->active)->toBeTrue()
->and($box->password)->toBe('geheim');
});
it('nimmt beim Zurücknehmen nur das eigene Postfach mit', function () {
loadInstanceRelaySeedMigration()->down();
expect(Mailbox::findByKey(GuestMailConfig::RELAY_KEY))->toBeNull()
// Die fünf Postfächer der früheren Wanderung gehören ihr, nicht
// dieser hier.
->and(Mailbox::findByKey('no-reply'))->not->toBeNull();
});

View File

@ -623,8 +623,12 @@ it('is safe to run twice — a migrations-table drift must not crash on the uniq
}
expect($threw)->toBeNull();
// 'instance-relay' comes from a migration of its own (the shared sending
// account the CUSTOMER instances use — see InstanceRelaySeedMigrationTest),
// and is listed here because nothing cleared the real baseline: this
// migration must leave it exactly where it found it.
expect(Mailbox::query()->pluck('key')->sort()->values()->all())
->toBe(['billing', 'info', 'no-reply', 'office', 'support']);
->toBe(['billing', 'info', 'instance-relay', 'no-reply', 'office', 'support']);
});
it('un-seeds on rollback, and does not leave the settings cache holding what it just deleted', function () {
@ -636,7 +640,11 @@ it('un-seeds on rollback, and does not leave the settings cache holding what it
loadMailboxSeedMigration()->down();
expect(Mailbox::query()->count())->toBe(0)
// Its own five rows, and only those: 'instance-relay' belongs to another
// migration and must survive this one's rollback — tearing down a
// neighbour's row would leave the customer instances' sending account
// gone with no migration recording it.
expect(Mailbox::query()->pluck('key')->all())->toBe(['instance-relay'])
->and(Settings::get('mail.purpose.system'))->toBeNull()
->and(Settings::get('mail.host'))->toBeNull();
});

View File

@ -6,8 +6,11 @@ use App\Services\Secrets\SecretVault;
use App\Support\Settings;
it('seeds the five mailboxes so the page shows what is expected', function () {
// `instance-relay` steht daneben, aus einer eigenen Wanderung: das
// gemeinsame Versandkonto der KUNDENINSTANZEN, nicht eines der fuenf
// Absender des Betreibers. Siehe InstanceRelaySeedMigrationTest.
expect(Mailbox::query()->pluck('key')->sort()->values()->all())
->toBe(['billing', 'info', 'no-reply', 'office', 'support']);
->toBe(['billing', 'info', 'instance-relay', 'no-reply', 'office', 'support']);
});
it('marks no-reply as unanswerable and the rest as answerable', function () {

View File

@ -19,14 +19,14 @@ function laufMitInstanz(Instance $instance): ProvisioningRun
}
/**
* Eine Instanz, die tatsaechlich auf einem Host steht.
* Eine Instanz, die tatsächlich 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,
* diesem Bestand für eine Maschine, die tatsächlich Gastbefehle bekommt,
* nicht FakeProxmoxClient::forHost() verlangt ein echtes Host-Objekt, weil
* der reale Client genauso wenig ohne einen Host wuesste, wohin er soll.
* der reale Client genauso wenig ohne einen Host wüsste, wohin er soll.
*/
function instanzAufHost(): Instance
{
@ -38,14 +38,18 @@ 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',
// Die Zeile legt eine eigene Wanderung an — inaktiv und ohne
// Zugangsdaten. Ausgefüllt wird sie hier, so wie der Betreiber es in der
// Konsole täte; ein factory()->create() mit demselben Schlüssel liefe in
// die Eindeutigkeit der Spalte.
Mailbox::findByKey('instance-relay')->update([
'address' => 'noreply@clupilot.cloud',
'username' => 'noreply@clupilot.cloud', 'password' => 'geheim',
'active' => true, 'authenticates' => true,
]);
}
it('traegt jeden Wert einzeln in den Gast', function () {
it('trägt jeden Wert einzeln in den Gast', function () {
versandbereitFuerSchritt();
$pve = new FakeProxmoxClient;
app()->instance(ProxmoxClient::class, $pve);
@ -56,7 +60,7 @@ it('traegt jeden Wert einzeln in den Gast', function () {
$befehle = implode("\n", $pve->guestCommands);
// Alle acht Werte aus GuestMailConfig::values(), nicht nur zwei — ein
// vergessener Schluessel soll hier auffallen, nicht erst beim Kunden.
// vergessener Schlüssel 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'))
@ -68,15 +72,15 @@ it('traegt jeden Wert einzeln in den Gast', function () {
});
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
// Verstecken lässt sich der Wert auf dieser Maschine nicht (siehe
// Kopfkommentar des Schrittes). Was sehr wohl gilt und geprüft gehört:
// er darf aus seiner Klammerung nicht ausbrechen. Der Befehl läuft als
// root auf einer Kundenmaschine — ein Semikolon im Passwort wäre 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',
Mailbox::findByKey('instance-relay')->update([
'address' => 'noreply@clupilot.cloud',
'username' => 'noreply@clupilot.cloud',
'password' => "boes'; touch /tmp/PWNED; echo '",
'active' => true, 'authenticates' => true,
@ -91,7 +95,7 @@ it('maskiert das Passwort, sodass es keinen zweiten Befehl starten kann', functi
->first(fn ($b) => str_contains($b, 'mail_smtppassword'));
// Der ganze Wert steht in EINEM maskierten Argument: das Semikolon darf
// nicht ausserhalb der Anfuehrungszeichen stehen.
// nicht ausserhalb der Anführungszeichen stehen.
expect($passwortbefehl)->toContain(escapeshellarg("boes'; touch /tmp/PWNED; echo '"));
});
@ -101,7 +105,7 @@ it('schreibt GAR NICHTS, wenn der Mailserver fehlt', function () {
// 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.
// geschrieben, und der Grund landet im Protokoll für den Betreiber.
Log::spy();
Settings::set('mail.host', '');
$pve = new FakeProxmoxClient;
@ -117,11 +121,11 @@ it('schreibt GAR NICHTS, wenn der Mailserver fehlt', function () {
);
});
it('haengt ein Lauf-Ereignis an, das der Betreiber in der Konsole sieht', function () {
it('hängt 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
// dieser Zweig auf JEDE Bestellung zu, und die einzige Rückmeldung darf
// nicht in einer Logdatei verschwinden, in die niemand schaut —
// RegisterMonitoring macht sein Ueberspringen genauso am Lauf sichtbar.
// RegisterMonitoring macht sein Überspringen genauso am Lauf sichtbar.
Settings::set('mail.host', '');
$pve = new FakeProxmoxClient;
app()->instance(ProxmoxClient::class, $pve);
@ -149,7 +153,7 @@ it('schreibt beim zweiten Lauf erneut, statt sich mit einem Merker zu sperren',
// 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
// erneut laufen; das ist bei einer Nachrüstung über den Bestand der
// Normalfall, nicht die Ausnahme.
expect(count($pve->guestCommands))->toBe($ersteRunde * 2);
});

View File

@ -24,14 +24,17 @@ it('provisions a paid order all the way to active (mocked)', function () {
// 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).
// Zusicherung weiter unten, dass der Gast die Werte tatsächlich 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',
// Ausgefüllt, nicht angelegt: die Zeile bringt eine eigene Wanderung mit
// — inaktiv und ohne Zugangsdaten, damit sie in der Konsole erscheint und
// dort ausgefüllt werden kann.
Mailbox::findByKey('instance-relay')->update([
'address' => 'noreply@clupilot.cloud',
'username' => 'noreply@clupilot.cloud', 'password' => 'geheim',
'active' => true, 'authenticates' => true,
]);
@ -74,8 +77,8 @@ it('provisions a paid order all the way to active (mocked)', function () {
->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.
// ConfigureInstanceMail lief also tatsächlich, nicht nur der
// Überspringen-Pfad, den der Test weiter unten prüft.
->and($s['pve']->guestRan('config:system:set mail_smtphost'))->toBeTrue();
// Every external resource created exactly once.
@ -99,7 +102,7 @@ it('completes a paid order even when the mail server does not exist yet', functi
// 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
// Überwachung scheitern lässt. Bewusst KEINE mail.host/mail.port- oder
// Mailbox-Fixture hier — das ist der ganze Punkt dieses Tests.
Notification::fake();
Queue::fake();
@ -133,7 +136,7 @@ it('completes a paid order even when the mail server does not exist yet', functi
->and($run->error)->toBeNull()
->and($order->fresh()->status)->toBe('active')
->and($instance->status)->toBe('active')
// Das Gegenstueck zur Zusicherung im ersten Test: ohne Einrichtung
// Das Gegenstück 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()