CluPilotCloud/tests/Feature/Readiness/MailPaceCheckTest.php

127 lines
5.3 KiB
PHP

<?php
use App\Services\Mail\MailLane;
use App\Support\Readiness;
use App\Support\Readiness\Check;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Str;
/**
* `deliveryCheck` ist in DeliveryChecksTest.php schon vergeben — beide Dateien
* laufen im selben Prozess, eine zweite Deklaration wäre ein Fataler Fehler.
*/
function mailPaceCheck(string $key): ?Check
{
return collect(Readiness::all())->firstWhere('key', $key);
}
/**
* Legt einen rohen Redis-Auftrag mit einem bestimmten Einreihungszeitpunkt in
* eine Spur — dieselbe `createdAt`-Nutzlast, die `RedisQueue::push()` selbst
* schreibt (Illuminate\Queue\Queue::createPayloadArray()) und die
* `creationTimeOfOldestPendingJob()` ausliest. Der Inhalt des restlichen
* Auftrags ist für die Prüfung irrelevant — sie liest nie mehr als dieses eine
* Feld —, deshalb bleibt er auf das Nötigste reduziert.
*/
function legeAufTraegInDieSpur(string $lane, DateTimeInterface $eingereihtAm): void
{
Queue::connection('redis')->pushRaw(json_encode([
'uuid' => (string) Str::uuid(),
'job' => 'unbenutzt',
'data' => [],
'id' => Str::random(32),
'attempts' => 0,
'createdAt' => $eingereihtAm->getTimestamp(),
]), $lane);
}
/**
* DB 0 ist die echte Schlange: derselbe Redis, den `docker-compose.yml`s
* `queue`-Dienst rund um die Uhr mit `queue:work redis --queue=mail-direkt,
* mail-wichtig,mail-ruhig,default` abhört (siehe die laufenden Container in
* dieser Umgebung). Ein hier abgelegter Testauftrag liefe Gefahr, vom echten
* Arbeiter weggeschnappt zu werden, bevor die Prüfung ihn liest — ein
* Wettlauf mit einem fremden Prozess, kein deterministischer Test. DB 15 ist
* in keiner Konfiguration dieses Projekts referenziert und für jenen
* Arbeiter unsichtbar; `creationTimeOfOldestPendingJob()` selbst bleibt der
* echte Weg über echtes Redis (Illuminate\Queue\RedisQueue), nur auf eine
* Datenbank verschoben, die niemand sonst anfasst.
*
* `config()` allein reicht nur, WEIL Laravels TestCase je Testmethode einen
* frischen Application-Container baut (`RedisManager` ist ein Singleton, der
* seine Konfiguration einmal beim ersten `Redis::connection()` dieses
* Containers einliest — in `artisan tinker`, wo EIN Container die ganze
* Sitzung lang lebt, kam genau deshalb beim Nachprüfen trotz gesetzter
* Config die 0 zurück, weil dort schon vorher etwas 'redis' aufgelöst hatte).
* Der Wächter unten prüft die WIRKLICH gewählte Datenbank am Client selbst,
* nicht nur die Einstellung, die sie hätte bewirken sollen — sollte diese
* Annahme durch eine künftige Änderung (z. B. Parallelisierung der Suite)
* doch einmal nicht mehr gelten, bricht der Test laut ab, statt still
* `flushdb()` gegen die echte Datenbank 0 abzufeuern.
*/
beforeEach(function () {
config(['database.redis.default.database' => 15]);
});
afterEach(function () {
$tatsaechlicheDb = Redis::connection('default')->client()->getDbNum();
if ($tatsaechlicheDb !== 15) {
throw new RuntimeException(
"Erwartete Redis-DB 15 fuer die Aufraeumung, aber die Verbindung steht auf {$tatsaechlicheDb} — flushdb() abgebrochen, um nicht versehentlich die echte Datenbank zu leeren."
);
}
Redis::connection('default')->flushdb();
});
/**
* Eine Spur, die steht, sieht von außen aus wie eine Spur, die leer ist. Die
* Grenze liegt bei einer Stunde: 20 je 10 Minuten (der langsamste Takt, Task
* 3) sind 120 in der Stunde, und ein Lauf dieser Größe steht bei elf Kunden
* nicht an. Schlägt die Prüfung trotzdem an, klemmt etwas.
*/
it('meldet eine Spur, in der etwas laenger als eine Stunde liegt', function () {
legeAufTraegInDieSpur(MailLane::CALM, now()->subHours(2));
expect(mailPaceCheck('delivery.mail_lanes_moving')->satisfied)->toBeFalse();
});
it('meldet eine Spur mit frischer Arbeit nicht', function () {
legeAufTraegInDieSpur(MailLane::CALM, now());
expect(mailPaceCheck('delivery.mail_lanes_moving')->satisfied)->toBeTrue();
});
/**
* Vollständigkeit: die drei Spuren sind gleichberechtigt vor der Prüfung.
* Ohne diesen Test bewiese der obige Fall nur, dass EINE bestimmte Spur
* beobachtet wird — mail-direkt oder mail-wichtig könnten unbeobachtet
* bleiben, und ein Kunde wartet dort ebenso vergeblich auf eine
* Kennwort-Zurücksetzung oder eine Wartungsankündigung.
*/
it('meldet jede der drei Spuren, wenn sie steht', function (string $lane) {
legeAufTraegInDieSpur($lane, now()->subHours(2));
expect(mailPaceCheck('delivery.mail_lanes_moving')->satisfied)->toBeFalse();
})->with([
'mail-direkt' => [MailLane::DIRECT],
'mail-wichtig' => [MailLane::URGENT],
'mail-ruhig' => [MailLane::CALM],
]);
/**
* Der Normalzustand: nichts wurde je eingereiht. `creationTimeOfOldestPendingJob()`
* liefert dann `null`, nicht 0 — ein Wert, der von einer erfundenen "leer"
* nicht zu unterscheiden wäre. Die Prüfung muss `null` als gesund lesen, ohne
* je selbst irgendetwas einzureihen.
*/
it('ist erfuellt, wenn noch nie etwas in einer Spur lag', function () {
expect(mailPaceCheck('delivery.mail_lanes_moving')->satisfied)->toBeTrue();
});
it('names what breaks when a mail lane is stuck', function () {
expect(mailPaceCheck('delivery.mail_lanes_moving')->breaks)->not->toBe('');
});