The mail page under System tries SMTP, PHP mail() and both sendmail modes one by one or all at once, and saves the chosen way to .env without ever showing the password. IONOS answers everything under /.well-known/ itself with 403, so the address and HTTPS checks never reached the installation; the probe now lives at /kantsites-origin (ADR 122). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
437 lines
25 KiB
PHP
437 lines
25 KiB
PHP
<?php
|
||
|
||
namespace App\Tests;
|
||
|
||
use App\Content\DemoContent;
|
||
use App\Content\DocumentValidator;
|
||
use App\Content\MediaLibrary;
|
||
use App\Content\PageRepository;
|
||
use App\Content\PartialRepository;
|
||
use App\Content\SiteSettings;
|
||
use App\Infrastructure\ConnectionFactory;
|
||
use App\Kernel;
|
||
use App\Ops\ArchiveUpdate;
|
||
use App\Ops\Backup;
|
||
use App\Ops\CommandRunner;
|
||
use App\Ops\Download;
|
||
use App\Ops\OriginCheck;
|
||
use App\Ops\Updater;
|
||
use Doctrine\DBAL\Connection;
|
||
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||
|
||
/**
|
||
* Der Bildschirm „System“ - die einzige Stelle, an der eine Installation ohne Konsole erfährt,
|
||
* dass etwas mit ihr nicht stimmt.
|
||
*
|
||
* Geprüft wird hier nicht das Aussehen, sondern zwei Eigenschaften, die schweigend verschwinden
|
||
* können: dass ausstehende Migrationen dort **auffallen**, und dass niemand sie auslösen kann,
|
||
* der nicht angemeldeter Administrator ist. Beides fiel beim Veröffentlichen von 1.4.3 auf -
|
||
* siehe ADR 070.
|
||
*/
|
||
final class SystemScreenTest extends WebTestCase
|
||
{
|
||
private const FIXTURE_PASSWORD = 'Fixture-only-password';
|
||
|
||
private Connection $db;
|
||
private string $path;
|
||
private array $previous;
|
||
|
||
protected static function getKernelClass(): string
|
||
{
|
||
return Kernel::class;
|
||
}
|
||
|
||
protected function setUp(): void
|
||
{
|
||
$this->path = tempnam(sys_get_temp_dir(), 'kant-system-');
|
||
$url = 'sqlite:///'.str_replace('\\', '/', $this->path);
|
||
$this->previous = [$_SERVER, $_ENV];
|
||
foreach (['APP_SECRET' => 'isolated-test-secret', 'DATABASE_URL' => $url, 'SITE_ORIGIN' => 'https://cms.test', 'MEDIA_DIRECTORY' => $this->path.'-media', 'MAIL_LOG' => $this->path.'-mail.log'] as $key => $value) {
|
||
$_SERVER[$key] = $_ENV[$key] = $value;
|
||
}
|
||
$this->db = ConnectionFactory::create($url);
|
||
// Die Tabellen entstehen hier über das Testschema und nicht über die Migrationen - genau
|
||
// die Lage einer Installation, deren Dateien von Hand hochgeladen wurden: Der Code ist
|
||
// neu, die Migrationsliste der Datenbank kennt ihn nicht.
|
||
TestSchema::build($this->db);
|
||
$this->db->insert('website', ['id' => 'default', 'name' => 'Test', 'default_locale' => 'de', 'theme' => '{}']);
|
||
$this->db->insert('page', ['id' => 'p1', 'slug' => 'home', 'path' => 'home', 'type' => 'page', 'locale' => 'de', 'website_id' => 'default', 'version' => 0, 'created_at' => gmdate('Y-m-d H:i:s')]);
|
||
$pages = new PageRepository($this->db, new DocumentValidator(), new SiteSettings($this->db, new MediaLibrary($this->db, $this->path.'-media')));
|
||
$pages->publish('p1', $pages->saveDraft('p1', DemoContent::pages()['home'], 0), 1);
|
||
$partials = new PartialRepository($this->db, new DocumentValidator());
|
||
foreach (DemoContent::areas() as $kind => $area) {
|
||
$partials->create($kind, 'default', $area['name'], $area['document']);
|
||
}
|
||
foreach (['admin' => 'ROLE_ADMIN', 'editor' => 'ROLE_EDITOR'] as $username => $role) {
|
||
$this->db->insert('cms_user', [
|
||
'id' => bin2hex(random_bytes(8)),
|
||
'username' => $username,
|
||
'password_hash' => password_hash(self::FIXTURE_PASSWORD, PASSWORD_BCRYPT, ['cost' => 4]),
|
||
'roles' => json_encode([$role], JSON_THROW_ON_ERROR),
|
||
'created_at' => gmdate('Y-m-d H:i:s'),
|
||
'password_changed_at' => gmdate('Y-m-d H:i:s'),
|
||
]);
|
||
}
|
||
}
|
||
|
||
protected function tearDown(): void
|
||
{
|
||
parent::tearDown();
|
||
$this->db->close();
|
||
gc_collect_cycles();
|
||
@unlink($this->path);
|
||
@unlink($this->path.'-mail.log');
|
||
foreach (glob($this->path.'-media/*') ?: [] as $file) {
|
||
unlink($file);
|
||
}
|
||
if (is_dir($this->path.'-media')) {
|
||
rmdir($this->path.'-media');
|
||
}
|
||
[$_SERVER, $_ENV] = $this->previous;
|
||
}
|
||
|
||
public function testOutstandingMigrationsAreImpossibleToOverlookAndCanBeRunFromThere(): void
|
||
{
|
||
$client = $this->login();
|
||
$client->request('GET', '/admin/system');
|
||
self::assertSame(200, $client->getResponse()->getStatusCode());
|
||
$html = (string) $client->getResponse()->getContent();
|
||
|
||
// Vorher gab es dafür nur die Konsole - also genau das, was auf geteiltem Hosting fehlt.
|
||
self::assertStringContainsString('Migrationen stehen aus', $html);
|
||
self::assertStringContainsString('Version20260912000100', $html, 'Die Namen gehören dazu: sie sagen, was passieren wird.');
|
||
self::assertStringContainsString('/admin/system/migrationen', $html);
|
||
// Eine Sicherung vorher ist keine Höflichkeit: Migrationen sind nicht umkehrbar.
|
||
self::assertStringContainsString('Sicherung', $html);
|
||
}
|
||
|
||
public function testTheScreenNamesTheDatabaseItActuallyUses(): void
|
||
{
|
||
// Umut hat `var/cms.db` gelöscht, weil in `.env` eine andere Datei stand - und die
|
||
// Website war weg. Der Grund: `.env.local` übersteuert `.env`, und der Installer schreibt
|
||
// nur `.env.local`. Wer nicht weiß, dass es diese Datei gibt, liest die falsche und
|
||
// handelt danach. Eine Verwaltung, die das verschweigt, lädt genau dazu ein.
|
||
$client = $this->login();
|
||
$client->request('GET', '/admin/system');
|
||
$html = (string) $client->getResponse()->getContent();
|
||
|
||
self::assertStringContainsString('SQLite', $html);
|
||
self::assertStringContainsString(
|
||
htmlspecialchars(str_replace('\\', '/', $this->path), ENT_QUOTES),
|
||
$html,
|
||
'Die Datei, die wirklich benutzt wird, muss dastehen - nicht die, die irgendwo konfiguriert ist.',
|
||
);
|
||
self::assertStringContainsString('Server-Umgebung', $html, 'Der Test übergibt die Verbindung ausdrücklich als Umgebungsvariable.');
|
||
self::assertStringNotContainsString('übersteuert <code>.env</code>', $html);
|
||
}
|
||
|
||
public function testTheScreenNamesThePublicAddressAndNoticesWhenItIsNotTheOneInUse(): void
|
||
{
|
||
// Die Verwaltung laeuft im Test unter localhost, eingetragen ist https://cms.test: der
|
||
// Bildschirm nennt beides, statt so zu tun, als passe es.
|
||
$client = $this->login();
|
||
$client->request('GET', '/admin/system');
|
||
$html = (string) $client->getResponse()->getContent();
|
||
self::assertStringContainsString('https://cms.test', $html);
|
||
self::assertStringContainsString('läuft gerade unter <code>localhost</code>', $html);
|
||
self::assertStringContainsString('Stimmt die nicht mehr, unten ändern.', $html);
|
||
|
||
// Unter der eingetragenen Adresse selbst: kein Hinweis, dafuer die Richtung der Umleitung.
|
||
$client->request('GET', 'https://cms.test/admin/login');
|
||
$client->submit($client->getCrawler()->selectButton('Anmelden')->form(['_username' => 'admin', '_password' => self::FIXTURE_PASSWORD]));
|
||
$client->request('GET', 'https://cms.test/admin/system');
|
||
$html = (string) $client->getResponse()->getContent();
|
||
self::assertStringContainsString('die Schreibweise mit www leitet hierher um', $html);
|
||
self::assertStringNotContainsString('läuft gerade unter', $html);
|
||
|
||
// Und die andere Schreibweise kommt gar nicht erst bis zur Verwaltung.
|
||
$client->request('GET', 'https://www.cms.test/admin/system');
|
||
self::assertSame(301, $client->getResponse()->getStatusCode());
|
||
self::assertSame('https://cms.test/admin/system', $client->getResponse()->headers->get('Location'));
|
||
}
|
||
|
||
public function testTheAddressIsSetInTheAdministrationButOnlyWhereItReallyArrives(): void
|
||
{
|
||
// Umut: „bau die Einstellung in der Verwaltung ein sodass es in die env eingetragen wird“ -
|
||
// wie die WordPress-Adresse. Im Test kommt SITE_ORIGIN aus der Server-Umgebung; dort hat
|
||
// sie Vorrang, also gibt es kein Feld, und auch ein POST ändert nichts.
|
||
$client = $this->login();
|
||
$client->request('GET', '/admin/system');
|
||
$html = (string) $client->getResponse()->getContent();
|
||
self::assertStringContainsString('lässt sich nur beim Hoster ändern', $html);
|
||
self::assertStringNotContainsString('name="origin"', $html);
|
||
$token = (string) $client->getCrawler()->filter('form[action$="/system/wartung"] input[name=_csrf_token]')->attr('value');
|
||
$client->request('POST', '/admin/system/adresse', ['_csrf_token' => $token, 'origin' => 'https://www.cms.test']);
|
||
$client->followRedirect();
|
||
self::assertStringContainsString('in der Server-Umgebung gesetzt', (string) $client->getResponse()->getContent());
|
||
|
||
// Steht sie in .env, gibt es das Feld samt Umschalter auf die andere Schreibweise.
|
||
$geladen = $_SERVER['SYMFONY_DOTENV_VARS'] ?? null;
|
||
$_SERVER['SYMFONY_DOTENV_VARS'] = 'SITE_ORIGIN';
|
||
try {
|
||
$client->request('GET', '/admin/system');
|
||
$html = (string) $client->getResponse()->getContent();
|
||
self::assertStringContainsString('name="origin" type="url"', $html);
|
||
self::assertStringContainsString('value="https://www.cms.test" formnovalidate>www.cms.test</button>', $html);
|
||
|
||
// Eine Adresse, unter der diese Website nicht antwortet, wird nicht gespeichert - sonst
|
||
// leitete die andere Schreibweise ins Leere. Das Feld behält sie, und erst jetzt
|
||
// erscheint „Trotzdem übernehmen“.
|
||
$client->request('POST', '/admin/system/adresse', ['_csrf_token' => $token, 'origin' => 'https://kein-server.invalid/']);
|
||
$client->followRedirect();
|
||
$html = (string) $client->getResponse()->getContent();
|
||
self::assertStringContainsString('Unter https://kein-server.invalid antwortet diese Website nicht', $html);
|
||
self::assertStringContainsString('Nichts geändert.', $html);
|
||
self::assertStringContainsString('value="https://kein-server.invalid"', $html);
|
||
self::assertStringContainsString('name="force"', $html);
|
||
foreach (['https://cms.test/start' => 'ohne Pfad', 'ftp://cms.test' => 'beginnt mit https://'] as $falsch => $meldung) {
|
||
$client->request('POST', '/admin/system/adresse', ['_csrf_token' => $token, 'origin' => $falsch]);
|
||
$client->followRedirect();
|
||
self::assertStringContainsString($meldung, (string) $client->getResponse()->getContent());
|
||
}
|
||
} finally {
|
||
if (null === $geladen) {
|
||
unset($_SERVER['SYMFONY_DOTENV_VARS']);
|
||
} else {
|
||
$_SERVER['SYMFONY_DOTENV_VARS'] = $geladen;
|
||
}
|
||
}
|
||
self::assertStringContainsString('SITE_ORIGIN', (string) file_get_contents(dirname(__DIR__).'/.env'));
|
||
self::assertStringNotContainsString('kein-server', (string) file_get_contents(dirname(__DIR__).'/.env'));
|
||
|
||
// Die Gegenstelle der Prüfung: öffentlich, ohne Anmeldung, und nur die Kennung.
|
||
$client->request('GET', '/kantsites-origin');
|
||
self::assertSame(200, $client->getResponse()->getStatusCode());
|
||
self::assertSame(hash_hmac('sha256', 'site-origin', 'isolated-test-secret'), $client->getResponse()->getContent());
|
||
}
|
||
|
||
public function testTheAddressCheckWantsThisInstallationsOwnAnswer(): void
|
||
{
|
||
$antwort = null;
|
||
$download = new class($antwort) extends Download {
|
||
public function __construct(private ?string &$antwort)
|
||
{
|
||
parent::__construct();
|
||
}
|
||
|
||
public function get(string $url): ?string
|
||
{
|
||
return $this->antwort;
|
||
}
|
||
};
|
||
$check = new OriginCheck('geheim', $download);
|
||
self::assertStringContainsString('antwortet diese Website nicht', (string) $check->problem('https://www.example.de'));
|
||
$antwort = hash_hmac('sha256', 'site-origin', 'anderes-geheimnis');
|
||
self::assertStringContainsString('antwortet eine andere Website', (string) $check->problem('https://www.example.de'));
|
||
$antwort = $check->token().'
|
||
';
|
||
self::assertNull($check->problem('https://www.example.de'));
|
||
self::assertStringContainsString('nur HTTPS', (string) $check->problem('http://www.example.de'));
|
||
}
|
||
|
||
public function testTheScreenShowsWhatTheArchiveUpdaterDidInsteadOfDyingOnIt(): void
|
||
{
|
||
// Der Fehler, den Umut auf seinem Hosting gesehen hat: Das Update lief, legte seine
|
||
// Schritte ab – und die Seite, die sie anzeigen soll, starb daran. Der Updater über Git
|
||
// nennt einen Schritt `step`, der über das Archiv nannte ihn `label`, und die Vorlage
|
||
// liest `step`. Ergebnis: 500 statt Ergebnis, und der zweite Knopf unerreichbar. Ein
|
||
// Test, der nur das Ergebnis-Array prüft, findet so etwas nie – dieser rendert die Seite.
|
||
$archiv = new ArchiveUpdate(
|
||
dirname(__DIR__),
|
||
new Updater(dirname(__DIR__), (string) $_ENV['DATABASE_URL'], new CommandRunner(), $this->db),
|
||
new Backup(dirname(__DIR__), (string) $_ENV['DATABASE_URL'], $this->path.'-media', $this->db, new CommandRunner()),
|
||
new Download(connectTimeout: 1, timeout: 2),
|
||
// Eine Adresse, die es nicht gibt: Der Lauf endet nach dem ersten Schritt, ohne
|
||
// irgendetwas anzufassen – und genau dieser eine Schritt muss auf der Seite stehen.
|
||
'https://kein-server.invalid/download',
|
||
'https://kein-server.invalid/api',
|
||
);
|
||
$ergebnis = $archiv->run();
|
||
self::assertFalse($ergebnis['ok']);
|
||
self::assertSame(
|
||
['step', 'ok', 'detail'],
|
||
array_keys($ergebnis['steps'][0]),
|
||
'Beide Updater beschreiben ihre Schritte gleich - die Vorlage kennt nur eine Form.',
|
||
);
|
||
|
||
$client = $this->login();
|
||
$client->request('GET', '/admin/system');
|
||
$sitzung = $client->getRequest()->getSession();
|
||
$sitzung->set('update_steps', $ergebnis['steps']);
|
||
$sitzung->save();
|
||
|
||
$client->request('GET', '/admin/system');
|
||
self::assertSame(200, $client->getResponse()->getStatusCode(), 'Die Ergebnisseite darf an den eigenen Schritten nicht sterben.');
|
||
self::assertStringContainsString('Fassung ermitteln', (string) $client->getResponse()->getContent());
|
||
}
|
||
|
||
public function testTheSecondUpdateStepRunsByItselfWithItsOneTimeKey(): void
|
||
{
|
||
// Umut am 09.10.2026: Update eingespielt, „direkt danach kam 500“, zurück, dann migriert.
|
||
// Die Anfrage, die die Dateien tauscht, stirbt am gemischten Code. Seit ADR 112 endet sie
|
||
// mit einer schlichten Seite, die den Abschluss über diesen Weg selbst aufruft.
|
||
$marke = dirname(__DIR__).'/'.ArchiveUpdate::MARKE;
|
||
@mkdir(dirname($marke), 0o775, true);
|
||
file_put_contents($marke, json_encode(['fassung' => '9.9.9', 'seit' => gmdate('c'), 'alt' => 'x', 'token' => str_repeat('a', 32)]));
|
||
// Die Testdatenbank entsteht aus dem Schema, nicht aus Migrationen. Eine echte
|
||
// Installation kennt ihre ausgeführten Migrationen - dasselbe Protokoll hier.
|
||
$this->db->executeStatement('CREATE TABLE IF NOT EXISTS migration_versions (version VARCHAR(191) NOT NULL PRIMARY KEY, executed_at DATETIME DEFAULT NULL, execution_time INTEGER DEFAULT NULL)');
|
||
foreach (glob(dirname(__DIR__).'/migrations/Version*.php') ?: [] as $datei) {
|
||
$this->db->executeStatement('INSERT OR IGNORE INTO migration_versions (version, executed_at, execution_time) VALUES (?, ?, 0)', ['DoctrineMigrations\\'.basename($datei, '.php'), gmdate('Y-m-d H:i:s')]);
|
||
}
|
||
try {
|
||
$redaktion = $this->login('editor');
|
||
$redaktion->request('GET', '/admin/system/abschliessen?token='.str_repeat('a', 32));
|
||
self::assertSame(403, $redaktion->getResponse()->getStatusCode(), 'Nur ein Administrator schließt ein Update ab.');
|
||
self::assertFileExists($marke);
|
||
|
||
$admin = $this->login();
|
||
$admin->request('GET', '/admin/system/abschliessen?token=falsch');
|
||
$admin->followRedirect();
|
||
self::assertStringContainsString('gehört zu keinem offenen Update', (string) $admin->getResponse()->getContent());
|
||
self::assertFileExists($marke, 'Ein falscher Schlüssel schließt nichts ab.');
|
||
|
||
$admin->request('GET', '/admin/system/abschliessen?token='.str_repeat('a', 32));
|
||
self::assertSame('/admin/system', parse_url((string) $admin->getResponse()->headers->get('Location'), PHP_URL_PATH));
|
||
$admin->followRedirect();
|
||
self::assertStringContainsString('Update auf 9.9.9 abgeschlossen', (string) $admin->getResponse()->getContent());
|
||
self::assertFileDoesNotExist($marke, 'Nach dem Abschluss ist nichts mehr offen.');
|
||
|
||
// Neu laden ist harmlos.
|
||
$admin->request('GET', '/admin/system/abschliessen?token='.str_repeat('a', 32));
|
||
$admin->followRedirect();
|
||
self::assertStringContainsString('bereits abgeschlossen', (string) $admin->getResponse()->getContent());
|
||
} finally {
|
||
@unlink($marke);
|
||
}
|
||
}
|
||
|
||
public function testTheOneClickUpdateEndpointsAreForAdministratorsOnly(): void
|
||
{
|
||
// Der Fortschritt für die Systemseite (ADR 113): Redakteure sehen ihn nicht, ein Start
|
||
// braucht das Token der Sitzung, ein Abschluss den Einmalschlüssel des laufenden Updates.
|
||
$redaktion = $this->login('editor');
|
||
$redaktion->request('GET', '/admin/system/update/status');
|
||
self::assertSame(403, $redaktion->getResponse()->getStatusCode());
|
||
|
||
$admin = $this->login();
|
||
$admin->request('GET', '/admin/system/update/status');
|
||
self::assertSame(200, $admin->getResponse()->getStatusCode());
|
||
self::assertArrayHasKey('installiert', json_decode((string) $admin->getResponse()->getContent(), true));
|
||
|
||
$admin->request('POST', '/admin/system/update/starten', [], [], ['HTTP_X_CSRF_TOKEN' => 'erfunden']);
|
||
self::assertSame(403, $admin->getResponse()->getStatusCode());
|
||
self::assertStringContainsString('Sitzung ist abgelaufen', (string) $admin->getResponse()->getContent());
|
||
|
||
$admin->request('POST', '/admin/system/update/abschliessen', ['token' => 'egal']);
|
||
self::assertStringContainsString('bereits abgeschlossen', (string) $admin->getResponse()->getContent(), 'Ohne offenes Update gibt es nichts abzuschließen.');
|
||
|
||
$marke = dirname(__DIR__).'/'.ArchiveUpdate::MARKE;
|
||
@mkdir(dirname($marke), 0o775, true);
|
||
file_put_contents($marke, json_encode(['fassung' => '9.9.9', 'seit' => gmdate('c'), 'alt' => 'x', 'token' => str_repeat('b', 32)]));
|
||
try {
|
||
$admin->request('POST', '/admin/system/update/abschliessen', ['token' => 'falsch']);
|
||
self::assertSame(403, $admin->getResponse()->getStatusCode());
|
||
self::assertFileExists($marke);
|
||
} finally {
|
||
@unlink($marke);
|
||
}
|
||
}
|
||
|
||
public function testNobodyButAnAdministratorCanStartAMigration(): void
|
||
{
|
||
// Ein Knopf, der die Datenbank umbaut, ist das lohnendste Ziel auf diesem Bildschirm.
|
||
self::ensureKernelShutdown();
|
||
$anonym = self::createClient();
|
||
$anonym->followRedirects(false);
|
||
$anonym->request('POST', '/admin/system/migrationen');
|
||
self::assertSame('http://localhost/admin/login', $anonym->getResponse()->headers->get('Location'));
|
||
|
||
$redaktion = $this->login('editor');
|
||
$redaktion->request('POST', '/admin/system/migrationen');
|
||
self::assertSame(403, $redaktion->getResponse()->getStatusCode(), 'Eine Redakteurin darf das nicht.');
|
||
|
||
// Und selbst als Administrator nicht ohne gültiges Formular aus dieser Sitzung.
|
||
$admin = $this->login();
|
||
$admin->request('POST', '/admin/system/migrationen', ['_csrf_token' => 'erfunden']);
|
||
$admin->followRedirect();
|
||
self::assertStringContainsString('Sitzung ist abgelaufen', (string) $admin->getResponse()->getContent());
|
||
self::assertStringContainsString('Migrationen stehen aus', (string) $admin->getResponse()->getContent(), 'Und es steht immer noch aus.');
|
||
}
|
||
|
||
public function testTheMailPageTestsAWayAndSaysWhatHappened(): void
|
||
{
|
||
// Umut: „Bau mal in den Backend einen Knopf ein, Testmail an Mailadresse, wo man einträgt,
|
||
// und wo Logs angezeigt werden, woran es liegt“ (ADR 121) - und dann: alle Wege probieren,
|
||
// SMTP dort setzen, auf einer eigenen Seite unter „System“ (ADR 122).
|
||
// MAILER_DSN gilt hier als Server-Umgebung: so schreibt kein Test in die echte .env.
|
||
$_SERVER['SYMFONY_DOTENV_VARS'] = '';
|
||
$client = $this->login();
|
||
$client->request('GET', '/admin/system');
|
||
$html = (string) $client->getResponse()->getContent();
|
||
self::assertStringContainsString('href="/admin/system/e-mail"', $html, 'Ein Unterpunkt von System im Menü.');
|
||
self::assertStringNotContainsString('Alle Wege testen', $html, 'Die Systemseite trägt den Versand nicht mehr selbst.');
|
||
|
||
$client->request('GET', '/admin/system/e-mail');
|
||
self::assertSame(200, $client->getResponse()->getStatusCode());
|
||
$html = (string) $client->getResponse()->getContent();
|
||
self::assertStringContainsString('Alle Wege testen', $html);
|
||
self::assertStringContainsString('value="cms.test"', $html, 'Der eingestellte SMTP-Server steht im Feld.');
|
||
self::assertStringContainsString('Noch nichts versendet', $html);
|
||
// Im Test steht MAILER_DSN nicht in .env: testen ja, speichern nein.
|
||
self::assertStringNotContainsString('value="speichern"', $html);
|
||
$token = (string) $client->getCrawler()->filter('form[action$="/system/e-mail"] input[name=_csrf_token]')->attr('value');
|
||
|
||
$client->request('POST', '/admin/system/e-mail', ['_csrf_token' => $token, 'aktion' => 'test', 'weg' => 'smtp', 'server' => 'cms.test', 'port' => '587', 'to' => 'kein-postfach']);
|
||
$client->followRedirect();
|
||
self::assertStringContainsString('keine gültige E-Mail-Adresse', (string) $client->getResponse()->getContent());
|
||
|
||
// Derselbe Weg wie eingestellt nimmt den Transport des Frameworks - im Test null://null.
|
||
$client->request('POST', '/admin/system/e-mail', ['_csrf_token' => $token, 'aktion' => 'test', 'weg' => 'smtp', 'server' => 'cms.test', 'port' => '587', 'to' => 'umut@cms.test', 'from' => 'formular@cms.test']);
|
||
self::assertStringEndsWith('/admin/system/e-mail', (string) $client->getResponse()->headers->get('Location'));
|
||
self::assertEmailCount(1);
|
||
$mail = self::getMailerMessage();
|
||
self::assertEmailAddressContains($mail, 'To', 'umut@cms.test');
|
||
self::assertEmailAddressContains($mail, 'From', 'formular@cms.test');
|
||
|
||
$client->request('GET', '/admin/system/e-mail');
|
||
$html = (string) $client->getResponse()->getContent();
|
||
self::assertStringContainsString('Testmail an umut@cms.test vom Mailserver angenommen', $html);
|
||
self::assertStringContainsString('Testmail an <code>umut@cms.test</code>', $html, 'Der Versuch steht danach unter „Letzte Versuche“.');
|
||
self::assertStringContainsString('value="umut@cms.test"', $html, 'Die Eingabe bleibt stehen.');
|
||
|
||
// SMTP mit Benutzer, aber ohne Passwort und ohne gespeichertes: abgelehnt, bevor etwas rausgeht.
|
||
$client->request('POST', '/admin/system/e-mail', ['_csrf_token' => $token, 'aktion' => 'test', 'weg' => 'smtp', 'server' => 'smtp.anderer.test', 'port' => '587', 'benutzer' => 'x@anderer.test', 'to' => 'umut@cms.test']);
|
||
self::assertEmailCount(0);
|
||
$client->followRedirect();
|
||
self::assertStringContainsString('das Passwort des Postfachs eintragen', (string) $client->getResponse()->getContent());
|
||
|
||
// Speichern geht nur, wenn die Einstellung in .env steht.
|
||
$client->request('POST', '/admin/system/e-mail', ['_csrf_token' => $token, 'aktion' => 'speichern', 'weg' => 'native']);
|
||
$client->followRedirect();
|
||
self::assertStringContainsString('in der Server-Umgebung gesetzt', (string) $client->getResponse()->getContent());
|
||
|
||
// Nur für Administratoren, und nur mit Token.
|
||
$client->request('POST', '/admin/system/e-mail', ['_csrf_token' => 'erfunden', 'aktion' => 'test', 'weg' => 'smtp', 'server' => 'cms.test', 'to' => 'umut@cms.test']);
|
||
self::assertEmailCount(0);
|
||
$redaktion = $this->login('editor');
|
||
$redaktion->request('GET', '/admin/system/e-mail');
|
||
self::assertSame(403, $redaktion->getResponse()->getStatusCode());
|
||
$redaktion->request('POST', '/admin/system/e-mail', ['_csrf_token' => $token, 'aktion' => 'test', 'weg' => 'smtp', 'server' => 'cms.test', 'to' => 'umut@cms.test']);
|
||
self::assertSame(403, $redaktion->getResponse()->getStatusCode());
|
||
}
|
||
|
||
private function login(string $username = 'admin'): KernelBrowser
|
||
{
|
||
self::ensureKernelShutdown();
|
||
$client = self::createClient();
|
||
$client->followRedirects(false);
|
||
$crawler = $client->request('GET', '/admin/login');
|
||
$client->submit($crawler->selectButton('Anmelden')->form(['_username' => $username, '_password' => self::FIXTURE_PASSWORD]));
|
||
$client->followRedirect();
|
||
|
||
return $client;
|
||
}
|
||
}
|