Text in admin cards had no size of its own, so 16px browser default sat next to 13px notes; cards now use 13px throughout while fields stay at 16px for iOS. The mail page gets option cards, a 2x2 SMTP grid, a styled save button in the card foot and a log with status pills and collapsed server messages, plus "Protokoll leeren". The package card uses the same sections and a styled file picker. The E-Mail-Versand subitem only shows inside System and sits beside it in the horizontal strip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
454 lines
26 KiB
PHP
454 lines
26 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());
|
||
|
||
// Umut: „Logs leeren Funktion brauch ich“ - nur mit Token, danach ist die Liste leer.
|
||
$client->request('POST', '/admin/system/e-mail/protokoll', ['_csrf_token' => 'erfunden']);
|
||
self::assertFileExists($this->path.'-mail.log', 'Ohne Token bleibt das Protokoll.');
|
||
$client->request('POST', '/admin/system/e-mail/protokoll', ['_csrf_token' => $token]);
|
||
self::assertFileDoesNotExist($this->path.'-mail.log');
|
||
$client->followRedirect();
|
||
$html = (string) $client->getResponse()->getContent();
|
||
self::assertStringContainsString('Das Protokoll ist leer.', $html);
|
||
self::assertStringContainsString('Noch nichts versendet', $html);
|
||
self::assertStringNotContainsString('Protokoll leeren', $html, 'Ohne Einträge gibt es nichts zu leeren.');
|
||
|
||
// Der Unterpunkt steht nur im Bereich System offen, nicht dauerhaft.
|
||
$client->request('GET', '/admin/settings');
|
||
self::assertStringNotContainsString('href="/admin/system/e-mail"', (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());
|
||
$redaktion->request('POST', '/admin/system/e-mail/protokoll', ['_csrf_token' => $token]);
|
||
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;
|
||
}
|
||
}
|