mail: Bounce-Abruf liest RFC 6533, All-Inkl-Freitext und ganze INBOX
Befund aus 151 echten Rücklaeufern: 2 SmarterMail-DSNs (message/global-delivery-status) und 2 Freitext-Hinweise des eigenen Relays (X-KASMail-Error, formal auto-replied) fielen durch. Policy- und Quota-Codes zaehlen weich statt hart. Abruf ohne UNSEEN-Filter, weil das Verschieben die Zustandsmarke ist. Dazu Schutz in tests/TestCase.php: Suite bricht ab, wenn ein Config-Cache sie auf die Dev-Datenbank umlenkt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6e159ecf06
commit
4cefdf7f5c
9 changed files with 323 additions and 15 deletions
|
|
@ -12,13 +12,16 @@ use Webklex\PHPIMAP\Message;
|
|||
|
||||
/**
|
||||
* Holt Rückläufer aus dem Bounce-Postfach (Roadmap 1b.6, Konzept
|
||||
* `bounce-mails-2026-09-15.md`). Alle zehn Minuten auf UNSEEN — IDLE braucht
|
||||
* es bei zwei bis drei Mails am Tag nicht.
|
||||
* `bounce-mails-2026-09-15.md`). Alle zehn Minuten — IDLE braucht es bei
|
||||
* zwei bis drei Mails am Tag nicht.
|
||||
*
|
||||
* Jede Mail wird nach dem Lesen in einen Ordner verschoben: `Processed`,
|
||||
* `Ignored` oder `Unparsed`. Verschieben statt Flag: Bricht der Lauf
|
||||
* dazwischen ab, liest der nächste dieselbe Mail erneut — die Sperre bleibt
|
||||
* dank Message-ID-Unique trotzdem einmalig.
|
||||
* dank Message-ID-Unique trotzdem einmalig. Deshalb zählt auch alles in
|
||||
* der INBOX, nicht nur Ungelesenes: Was ein Mensch dort angesehen oder aus
|
||||
* `service@` hinübergeschoben hat, trägt das Gelesen-Flag schon und würde
|
||||
* sonst nie verarbeitet.
|
||||
*/
|
||||
class FetchMailBounces extends Command
|
||||
{
|
||||
|
|
@ -53,7 +56,7 @@ class FetchMailBounces extends Command
|
|||
throw new \RuntimeException('INBOX nicht gefunden.');
|
||||
}
|
||||
|
||||
$messages = $inbox->query()->whereUnseen()->setFetchBody(true)->limit(max(1, (int) $this->option('limit')))->get();
|
||||
$messages = $inbox->query()->all()->setFetchBody(true)->limit(max(1, (int) $this->option('limit')))->get();
|
||||
|
||||
/** @var Message $message */
|
||||
foreach ($messages as $message) {
|
||||
|
|
|
|||
|
|
@ -65,9 +65,7 @@ class BounceMailProcessor
|
|||
continue;
|
||||
}
|
||||
|
||||
// 4.x.x mit `failed` ist ein weicher Fehler, den der MTA
|
||||
// aufgegeben hat — zählt weich, nicht hart.
|
||||
if ($kind === EmailBounceKind::Hard && str_starts_with((string) $recipient['status'], '4.')) {
|
||||
if ($kind === EmailBounceKind::Hard && $this->isTransient($recipient['status'], $recipient['diagnostic'])) {
|
||||
$kind = EmailBounceKind::Soft;
|
||||
}
|
||||
|
||||
|
|
@ -95,6 +93,24 @@ class BounceMailProcessor
|
|||
return ['folder' => $bounces > 0 ? self::Processed : self::Ignored, 'bounces' => $bounces, 'subject' => $subject];
|
||||
}
|
||||
|
||||
/**
|
||||
* `failed` heißt nicht immer „Adresse existiert nicht". Weich zählen
|
||||
* (Sperre erst nach dem Schwellwert): 4.x.x, das der MTA aufgegeben hat;
|
||||
* volles Postfach (5.2.2, 5.2.5, SMTP 552); Policy-Ablehnungen 5.7.x
|
||||
* („Relay access denied", Spamfilter der Gegenseite) — die Adresse gibt
|
||||
* es, ihr Server nimmt gerade nur nichts an. Bestand vom 15.09.2026:
|
||||
* 16 von 147 DSNs fielen in diese Gruppe.
|
||||
*/
|
||||
private function isTransient(?string $status, ?string $diagnostic): bool
|
||||
{
|
||||
$status = (string) $status;
|
||||
|
||||
return str_starts_with($status, '4.')
|
||||
|| str_starts_with($status, '5.7.')
|
||||
|| in_array($status, ['5.2.2', '5.2.5'], true)
|
||||
|| preg_match('/^\s*(4\d\d|552)\b/', (string) $diagnostic) === 1;
|
||||
}
|
||||
|
||||
private function subject(string $raw): string
|
||||
{
|
||||
return preg_match('/^Subject:\s*(.*)$/mi', $raw, $m) === 1 ? mb_substr(trim($m[1]), 0, 200) : '(ohne Betreff)';
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ use Illuminate\Support\Carbon;
|
|||
* Ablehnungen, alte MTAs mit Freitext), liefert null. Das wandert in den
|
||||
* Ordner `Unparsed` und vor einen Menschen — zwei Mails pro Woche von Hand
|
||||
* sind akzeptabel, eine falsch gesperrte Adresse nicht.
|
||||
*
|
||||
* Zwei bewusste Ausnahmen: die UTF-8-Variante nach RFC 6533
|
||||
* (`message/global-delivery-status`, z. B. SmarterMail) und das
|
||||
* Freitextformat des eigenen All-Inkl-Relays („anti spam system at host …
|
||||
* list of non existent recipients"), das mit `X-KASMail-Error` eindeutig
|
||||
* erkennbar ist — 2 von 151 Rückläufern im Bestand vom 15.09.2026.
|
||||
*/
|
||||
class DsnParser
|
||||
{
|
||||
|
|
@ -36,18 +42,14 @@ class DsnParser
|
|||
foreach ($parts as $part) {
|
||||
$type = strtolower((string) ($part['headers']['content-type'] ?? ''));
|
||||
|
||||
if ($status === null && str_starts_with($type, 'message/delivery-status')) {
|
||||
if ($status === null && (str_starts_with($type, 'message/delivery-status') || str_starts_with($type, 'message/global-delivery-status'))) {
|
||||
$status = $part['body'];
|
||||
} elseif ($original === null && (str_starts_with($type, 'message/rfc822') || str_starts_with($type, 'text/rfc822-headers'))) {
|
||||
} elseif ($original === null && preg_match('#^(message/rfc822|message/global|text/rfc822-headers)#', $type) === 1) {
|
||||
$original = $part['body'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($status === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$recipients = $this->recipientsFrom($status);
|
||||
$recipients = $status !== null ? $this->recipientsFrom($status) : $this->recipientsFromKasNotice($headers, $topBody);
|
||||
|
||||
if ($recipients === []) {
|
||||
return null;
|
||||
|
|
@ -76,6 +78,14 @@ class DsnParser
|
|||
|
||||
$autoSubmitted = strtolower((string) ($headers['auto-submitted'] ?? ''));
|
||||
$subject = strtolower((string) ($headers['subject'] ?? ''));
|
||||
$from = strtolower((string) ($headers['from'] ?? ''));
|
||||
|
||||
// Ein Zustellsystem antwortet formal auch „auto-replied" — sein
|
||||
// Rückläufer ist aber nie eine Abwesenheitsnotiz.
|
||||
if (preg_match('/mailer-daemon|postmaster@/', $from) === 1
|
||||
|| preg_match('/undeliver|returned to sender|delivery (status|failure)|unzustellbar/', $subject) === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($autoSubmitted !== '' && $autoSubmitted !== 'no')
|
||||
|| isset($headers['x-autoreply'])
|
||||
|
|
@ -183,6 +193,39 @@ class DsnParser
|
|||
return $recipients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Freitext-Rückläufer des All-Inkl-Relays: kein DSN, aber ein festes
|
||||
* Format mit Adressliste. Nur bei gesetztem `X-KASMail-Error` und dem
|
||||
* wörtlichen Einleitungssatz — alles andere bleibt Sache eines Menschen.
|
||||
*
|
||||
* @param array<string, string> $headers
|
||||
* @return list<array{email: string, action: string, status: string|null, diagnostic: string|null}>
|
||||
*/
|
||||
private function recipientsFromKasNotice(array $headers, string $body): array
|
||||
{
|
||||
if (! isset($headers['x-kasmail-error'])
|
||||
|| preg_match('/list of non existent recipients:\s*\n(.+?)(?:\n\s*\n|\z)/is', $body, $m) !== 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$recipients = [];
|
||||
|
||||
foreach (preg_split('/\s+/', trim($m[1])) ?: [] as $candidate) {
|
||||
$email = EmailSuppressionService::normalise($candidate);
|
||||
|
||||
if ($email !== null) {
|
||||
$recipients[] = [
|
||||
'email' => $email,
|
||||
'action' => 'failed',
|
||||
'status' => '5.1.1',
|
||||
'diagnostic' => 'All-Inkl anti spam: recipient reported as non existent',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $recipients;
|
||||
}
|
||||
|
||||
private function date(?string $value): ?Carbon
|
||||
{
|
||||
if ($value === null) {
|
||||
|
|
|
|||
|
|
@ -1154,3 +1154,55 @@ läuft `public/vendor/livewire` bei jedem Livewire-Update wieder aus dem
|
|||
Takt), `optimize:clear`, `config:cache`, `view:cache`, `queue:restart`.
|
||||
Vorher auf dem Server `git checkout composer.lock`, damit der Pull nicht
|
||||
an der handgeänderten Datei hängen bleibt.
|
||||
|
||||
## 1b.6 — Erster Blick ins echte Postfach (15.09.2026)
|
||||
|
||||
Zugang zu `bounce@pressekonto.com` steht; 151 Rückläufer aus `service@`
|
||||
hinübergeschoben. Der Trockenlauf meldete zunächst **0**, weil alle das
|
||||
Gelesen-Flag mitgebracht hatten und der Abruf nur UNSEEN holte.
|
||||
|
||||
Befund über alle 151 (Dry-Run mit dem Parser):
|
||||
|
||||
- 147 klassische DSNs (Postfix/All-Inkl, Exchange, Gmail) — sauber gelesen.
|
||||
- 2 × `message/global-delivery-status` (RFC 6533, SmarterMail) — bisher
|
||||
„nicht lesbar".
|
||||
- 2 × Freitext des eigenen All-Inkl-Relays („anti spam system … list of
|
||||
non existent recipients", Header `X-KASMail-Error`) — mit
|
||||
`Auto-Submitted: auto-replied` und deshalb bisher als **Abwesenheit
|
||||
ignoriert**. Genau die Adressen wären nie gesperrt worden.
|
||||
- Statuscodes: 74 × 5.4.4 (Domain ohne MX, davon 66 × dieselbe Adresse),
|
||||
24 × 5.0.0, 13 × 5.1.1, 8 × 5.1.10, 8 × 5.7.1, Rest 5.2.x/4.x.
|
||||
- 37 eindeutige Adressen.
|
||||
|
||||
Vier Änderungen (Commit im Anschluss):
|
||||
|
||||
1. Abruf holt **alles in der INBOX**, nicht nur Ungelesenes — das
|
||||
Verschieben in `Processed/Ignored/Unparsed` ist ohnehin die Zustandsmarke.
|
||||
2. Parser versteht RFC 6533 (`message/global-delivery-status`,
|
||||
`message/global-headers`).
|
||||
3. Parser liest das All-Inkl-Freitextformat — nur bei `X-KASMail-Error`
|
||||
und dem wörtlichen Einleitungssatz. MAILER-DAEMON/„Undelivered" wird
|
||||
nie mehr als Abwesenheitsnotiz gewertet.
|
||||
4. Einstufung: `failed` mit 5.7.x (Policy, „Relay access denied"), 5.2.2/
|
||||
5.2.5 (Postfach voll) oder SMTP 552 im Diagnosetext zählt **weich**
|
||||
statt hart — die Adresse existiert, ihr Server nimmt nur nichts an.
|
||||
16 von 147 im Bestand.
|
||||
|
||||
Danach: 151/151 gelesen, 0 ignoriert, 0 nicht lesbar. Nicht scharf von
|
||||
hier gelaufen — das würde die Mails im Live-Postfach verschieben und die
|
||||
Sperren nur in die Dev-DB schreiben. Der scharfe Lauf gehört auf den
|
||||
Server nach dem Deploy (`php artisan mail:fetch-bounces`), der Scheduler
|
||||
holt danach alle zehn Minuten nach.
|
||||
|
||||
Fixtures: `global-delivery-status.eml`, `kas-antispam.eml`,
|
||||
`policy-5.7.1.eml`; Tests in `BounceMailProcessingTest` (+3, 9).
|
||||
|
||||
### Nebenbefund: Dev-DB von der Testsuite geleert
|
||||
|
||||
Um 15:16 lag ein `bootstrap/cache/config.php`; der überlagert die
|
||||
phpunit.xml-Umgebung (SQLite :memory:), und die Suite um 15:20 lief mit
|
||||
`RefreshDatabase` gegen die MySQL-Dev-DB `pressekonto` — die Bestandskopie
|
||||
ist weg (alle Tabellen neu angelegt 15:21:58–15:22:11). `composer run
|
||||
test` räumt den Cache vorher weg, `php artisan test` direkt nicht.
|
||||
`tests/TestCase.php` bricht jetzt hart ab, wenn die Verbindung nicht
|
||||
SQLite :memory: ist. Die Kopie muss neu eingespielt werden.
|
||||
|
|
|
|||
|
|
@ -77,6 +77,61 @@ test('free-text bounces are not guessed but parked for a human, auto-replies are
|
|||
->and(ActivityEvent::query()->where('type', ActivityType::MailSuppressed->value)->where('description', 'like', '%nicht lesbar%')->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
test('the utf-8 dsn variant of rfc 6533 is read like a classic report', function () {
|
||||
/** @var TestCase $this */
|
||||
// SmarterMail liefert `message/global-delivery-status` und die
|
||||
// Original-Header als `message/global-headers` (Bestand 15.09.2026).
|
||||
$report = app(DsnParser::class)->parse(bounceFixture('global-delivery-status'));
|
||||
|
||||
expect($report)->not->toBeNull()
|
||||
->and($report->recipients)->toHaveCount(1)
|
||||
->and($report->recipients[0]['email'])->toBe('weiterleitung@example.net')
|
||||
->and($report->recipients[0]['status'])->toBe('5.0.0')
|
||||
->and($report->reference)->toBe('user:4711');
|
||||
|
||||
$result = app(BounceMailProcessor::class)->process(bounceFixture('global-delivery-status'));
|
||||
|
||||
expect($result['folder'])->toBe(BounceMailProcessor::Processed)
|
||||
->and(EmailBounce::query()->first()->kind)->toBe(EmailBounceKind::Hard);
|
||||
});
|
||||
|
||||
test('the free-text notice of our own relay is read and never mistaken for an auto-reply', function () {
|
||||
/** @var TestCase $this */
|
||||
// All-Inkl schickt „Auto-Submitted: auto-replied" mit — als Abwesenheit
|
||||
// ignoriert, wären das genau die Adressen, die nie gesperrt würden.
|
||||
expect(app(DsnParser::class)->looksLikeAutoReply(bounceFixture('kas-antispam')))->toBeFalse();
|
||||
|
||||
$result = app(BounceMailProcessor::class)->process(bounceFixture('kas-antispam'));
|
||||
$suppressions = app(EmailSuppressionService::class);
|
||||
|
||||
expect($result['folder'])->toBe(BounceMailProcessor::Processed)
|
||||
->and($result['bounces'])->toBe(2)
|
||||
->and($suppressions->isSuppressed('presse@example.net'))->toBeTrue()
|
||||
->and($suppressions->isSuppressed('info@example.org'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('policy rejections and full mailboxes count soft, not hard', function () {
|
||||
/** @var TestCase $this */
|
||||
// 5.7.1 „Relay access denied": die Adresse existiert, ihr Server nimmt
|
||||
// nur nichts an — erst der Schwellwert sperrt.
|
||||
$result = app(BounceMailProcessor::class)->process(bounceFixture('policy-5.7.1'));
|
||||
|
||||
expect($result['folder'])->toBe(BounceMailProcessor::Processed)
|
||||
->and(EmailBounce::query()->first()->kind)->toBe(EmailBounceKind::Soft)
|
||||
->and(EmailBounce::query()->first()->reference)->toBe('pm:99:company:7')
|
||||
->and(app(EmailSuppressionService::class)->isSuppressed('pr@example.net'))->toBeFalse();
|
||||
|
||||
$quota = str_replace(
|
||||
['Status: 5.7.1', '554 5.7.1 <pr@example.net>: Relay access denied', '<20260915120000.ABC@'],
|
||||
['Status: 5.0.0', '552 Requested mail action aborted: exceeded storage allocation', '<20260915120001.ABC@'],
|
||||
bounceFixture('policy-5.7.1'),
|
||||
);
|
||||
app(BounceMailProcessor::class)->process($quota);
|
||||
|
||||
expect(EmailBounce::query()->count())->toBe(2)
|
||||
->and(EmailBounce::query()->where('kind', EmailBounceKind::Hard->value)->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('the fetch command does nothing without a configured mailbox', function () {
|
||||
/** @var TestCase $this */
|
||||
config()->set('imap.accounts.bounces.host', null);
|
||||
|
|
|
|||
53
tests/Fixtures/bounces/global-delivery-status.eml
Normal file
53
tests/Fixtures/bounces/global-delivery-status.eml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
Return-Path: <>
|
||||
From: System Administrator <noreply@example.org>
|
||||
To: empfang@example.org
|
||||
Subject: Failed: E-Mail-Adresse =?utf-8?b?YmVzdMOkdGlnZW4gZsO8cg==?= Ihr pressekonto
|
||||
Date: Tue, 15 Sep 2026 07:58:48 -0400
|
||||
Message-ID: <smartermail-4dafebf3@example.org>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/report;
|
||||
boundary=SmarterMail_NextPart_bea868a207e2438d93f1a587865b9442;
|
||||
report-type=delivery-status
|
||||
|
||||
--SmarterMail_NextPart_bea868a207e2438d93f1a587865b9442
|
||||
Content-Type: multipart/alternative;
|
||||
boundary=SmarterMail_NextPart_c2a1d6d5a7f547e881ec08ee079fab60
|
||||
|
||||
--SmarterMail_NextPart_c2a1d6d5a7f547e881ec08ee079fab60
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Delivery has failed to these recipients:
|
||||
|
||||
weiterleitung@example.net
|
||||
|
||||
Remote Server returned: '550 Sender has been throttled'=
|
||||
|
||||
--SmarterMail_NextPart_c2a1d6d5a7f547e881ec08ee079fab60--
|
||||
|
||||
--SmarterMail_NextPart_bea868a207e2438d93f1a587865b9442
|
||||
Content-Type: message/global-delivery-status; charset=utf-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Reporting-MTA: dns; mx.example.org
|
||||
|
||||
Original-Recipient: rfc822; weiterleitung@example.net
|
||||
Final-Recipient: rfc822; weiterleitung@example.net
|
||||
Action: failed
|
||||
Status: 5.0.0
|
||||
|
||||
--SmarterMail_NextPart_bea868a207e2438d93f1a587865b9442
|
||||
Content-Type: message/global-headers; charset=utf-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Return-Path: <bounce@pressekonto.com>
|
||||
From: "Service | pressekonto.com" <service@pressekonto.com>
|
||||
To: empfang@example.org
|
||||
Subject: E-Mail-Adresse bestätigen für Ihr pressekonto
|
||||
Message-Id: <e080bfba8cd885ac98a1979708debe2c@pressekonto.com>
|
||||
X-Pk-Ref: user:4711
|
||||
MIME-Version: 1.0
|
||||
Date: Tue, 15 Sep 2026 13:58:45 +0200
|
||||
Content-Type: text/html; charset=utf-8
|
||||
|
||||
--SmarterMail_NextPart_bea868a207e2438d93f1a587865b9442--
|
||||
28
tests/Fixtures/bounces/kas-antispam.eml
Normal file
28
tests/Fixtures/bounces/kas-antispam.eml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
Return-Path: <>
|
||||
X-Original-To: bounce@pressekonto.com
|
||||
From: MAILER-DAEMON@dd19238.kasserver.com (Mail Delivery System)
|
||||
To: bounce@pressekonto.com
|
||||
Auto-Submitted: auto-replied
|
||||
Subject: Undelivered Mail Returned to Sender
|
||||
Message-Id: <20260902102204.844FE5F41C65@dd19238.kasserver.com>
|
||||
Date: Wed, 2 Sep 2026 12:22:04 +0200 (CEST)
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
X-KASMail-Error: CFA07A22C8072A03
|
||||
X-KasLoop: m0805a0b
|
||||
|
||||
|
||||
|
||||
This is the anti spam system at host dd19238.kasserver.com.
|
||||
|
||||
I´m sorry to have to inform you that your message could not be delivered to one or more recipients.
|
||||
|
||||
The recipient mailprovider bounced Mails to one or more recipients you tried to contact as not existent in the recent past.
|
||||
|
||||
Please note the following list of non existent recipients:
|
||||
|
||||
presse@example.net
|
||||
info@example.org
|
||||
|
||||
Please check the addresses and try again.
|
||||
41
tests/Fixtures/bounces/policy-5.7.1.eml
Normal file
41
tests/Fixtures/bounces/policy-5.7.1.eml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
Return-Path: <>
|
||||
From: MAILER-DAEMON@dd19238.kasserver.com (Mail Delivery System)
|
||||
To: bounce@pressekonto.com
|
||||
Subject: Undelivered Mail Returned to Sender
|
||||
Message-Id: <20260915120000.ABC@dd19238.kasserver.com>
|
||||
Date: Tue, 15 Sep 2026 14:00:00 +0200 (CEST)
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/report; report-type=delivery-status;
|
||||
boundary="ABC123.1789473600/dd19238.kasserver.com"
|
||||
|
||||
--ABC123.1789473600/dd19238.kasserver.com
|
||||
Content-Description: Notification
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
This is the mail system at host dd19238.kasserver.com.
|
||||
|
||||
--ABC123.1789473600/dd19238.kasserver.com
|
||||
Content-Description: Delivery report
|
||||
Content-Type: message/delivery-status
|
||||
|
||||
Reporting-MTA: dns; dd19238.kasserver.com
|
||||
|
||||
Final-Recipient: rfc822; pr@example.net
|
||||
Original-Recipient: rfc822;pr@example.net
|
||||
Action: failed
|
||||
Status: 5.7.1
|
||||
Remote-MTA: dns; mx.example.net
|
||||
Diagnostic-Code: smtp; 554 5.7.1 <pr@example.net>: Relay access denied
|
||||
|
||||
--ABC123.1789473600/dd19238.kasserver.com
|
||||
Content-Description: Undelivered Message Headers
|
||||
Content-Type: text/rfc822-headers
|
||||
|
||||
Return-Path: <bounce@pressekonto.com>
|
||||
From: "Service | pressekonto.com" <service@pressekonto.com>
|
||||
To: pr@example.net
|
||||
Subject: Ihre Pressemitteilung ist online
|
||||
X-Pk-Ref: pm:99:company:7
|
||||
Message-Id: <abc@pressekonto.com>
|
||||
|
||||
--ABC123.1789473600/dd19238.kasserver.com--
|
||||
|
|
@ -3,8 +3,25 @@
|
|||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
use RuntimeException;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
/**
|
||||
* Schutz vor einem gecachten `bootstrap/cache/config.php`: Der überlagert
|
||||
* die phpunit.xml-Umgebung, die Suite läuft dann gegen die Dev-Datenbank
|
||||
* aus der .env — und `RefreshDatabase` leert sie (passiert am
|
||||
* 15.09.2026, 3 GB Bestandskopie weg). Lieber laut abbrechen.
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
if (config('database.default') !== 'sqlite' || config('database.connections.sqlite.database') !== ':memory:') {
|
||||
throw new RuntimeException(sprintf(
|
||||
'Tests laufen gegen „%s“ statt SQLite :memory: — vermutlich liegt ein Config-Cache. `php artisan config:clear` und erneut starten.',
|
||||
config('database.default'),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue