- The installer only opens with a per-installation key that install.php creates and shows; the key is deleted with the first account. - Lock files keep .env, the database, backups and logs out of reach when a domain points at the project folder instead of public/. - Linked sections render by their own schema version, so an old section cannot carry unfiltered HTML into a newer page. - Editors no longer move, trash or change areas and post data of published pages; a scheduled publication goes live with the revision that was scheduled (migration 20261012000100). - Form stamps are bound to their form, IPv6 senders count per /64, and a flood per form or overall stops storing with a visible message. - The consent redirect rejects control characters, update targets must look like 1.2.3, and own CSS cannot reach outside its block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
352 lines
22 KiB
PHP
352 lines
22 KiB
PHP
<?php
|
||
|
||
namespace App\Tests;
|
||
|
||
use App\Content\DemoContent;
|
||
use App\Content\DocumentValidator;
|
||
use App\Content\FormRepository;
|
||
use App\Content\MediaLibrary;
|
||
use App\Content\PageRepository;
|
||
use App\Content\PartialRepository;
|
||
use App\Content\SiteSettings;
|
||
use App\Domain\InvalidForm;
|
||
use App\Infrastructure\ConnectionFactory;
|
||
use App\Kernel;
|
||
use Doctrine\DBAL\Connection;
|
||
use PHPUnit\Framework\TestCase;
|
||
use Symfony\Component\HttpFoundation\Request;
|
||
use Symfony\Component\HttpFoundation\Response;
|
||
|
||
/**
|
||
* Formulare mit Schritten, Bedingungen und den Feldtypen Zahl, Optionsfelder und Mehrfachauswahl.
|
||
*
|
||
* Umut wollte für die Immobilienbewertung „kein Kontaktformular weiteres, sondern so ein
|
||
* ausgebautes Form“ – den Preisfinder der laufenden Website: erst die Objektart, dann Felder je
|
||
* Objektart, dann Kontakt und Zusammenfassung. Der Kern wurde dafür erweitert statt ein Plugin
|
||
* zu bauen; diese Tests halten fest, was der Server dabei garantiert.
|
||
*/
|
||
final class FormStepsTest extends TestCase
|
||
{
|
||
private string $path;
|
||
private Connection $db;
|
||
private FormRepository $forms;
|
||
private PageRepository $pages;
|
||
|
||
protected function setUp(): void
|
||
{
|
||
$this->path = tempnam(sys_get_temp_dir(), 'kant-form-');
|
||
$this->db = ConnectionFactory::create('sqlite:///'.str_replace('\\', '/', $this->path));
|
||
TestSchema::build($this->db);
|
||
$this->db->insert('website', ['id' => 'default', 'name' => 'KantSites', 'default_locale' => 'de', 'theme' => '{}']);
|
||
$this->db->insert('page', ['id' => 'page-1', 'slug' => 'home', 'path' => 'home', 'type' => 'page', 'locale' => 'de', 'website_id' => 'default', 'version' => 0, 'created_at' => gmdate('Y-m-d H:i:s')]);
|
||
$settings = new SiteSettings($this->db, new MediaLibrary($this->db, $this->path.'-media'));
|
||
$this->pages = new PageRepository($this->db, new DocumentValidator(), $settings);
|
||
$this->forms = new FormRepository($this->db);
|
||
$partials = new PartialRepository($this->db, new DocumentValidator());
|
||
foreach (DemoContent::areas() as $kind => $area) {
|
||
$partials->create($kind, 'default', $area['name'], $area['document']);
|
||
}
|
||
}
|
||
|
||
protected function tearDown(): void
|
||
{
|
||
$this->db->close();
|
||
gc_collect_cycles();
|
||
@unlink($this->path);
|
||
}
|
||
|
||
/** Ein kleiner Preisfinder: Objektart, davon abhängige Eckdaten, ein bedingter Schritt, Kontakt. */
|
||
private function preisfinder(): array
|
||
{
|
||
return [
|
||
['name' => 'schritt_objekt', 'type' => 'step', 'label' => 'Ihre Immobilie', 'help' => 'Womit können wir helfen?'],
|
||
['name' => 'immobilienart', 'type' => 'radio', 'label' => 'Objektart', 'required' => true, 'options' => ['Wohnung', 'Haus', 'Grundstück']],
|
||
['name' => 'schritt_eckdaten', 'type' => 'step', 'label' => 'Eckdaten'],
|
||
['name' => 'wohnflaeche', 'type' => 'number', 'label' => 'Wohnfläche', 'required' => true, 'min' => 10, 'max' => 2000, 'unit' => 'm²', 'showIf' => ['field' => 'immobilienart', 'values' => ['Wohnung', 'Haus']]],
|
||
['name' => 'ausstattung', 'type' => 'checkboxes', 'label' => 'Ausstattung', 'required' => true, 'options' => ['Balkon', 'Garage', 'Keller'], 'showIfField' => 'immobilienart', 'showIfValues' => 'Wohnung, Haus'],
|
||
['name' => 'schritt_grund', 'type' => 'step', 'label' => 'Grundstück', 'showIf' => ['field' => 'immobilienart', 'values' => ['Grundstück']]],
|
||
['name' => 'flaeche', 'type' => 'number', 'label' => 'Grundstücksfläche', 'required' => true, 'min' => 10, 'unit' => 'm²'],
|
||
['name' => 'schritt_kontakt', 'type' => 'step', 'label' => 'Kontakt'],
|
||
['name' => 'email', 'type' => 'email', 'label' => 'E-Mail', 'required' => true],
|
||
['name' => 'datenschutz', 'type' => 'consent', 'label' => 'Einverstanden'],
|
||
];
|
||
}
|
||
|
||
private function settings(): array
|
||
{
|
||
return ['recipient' => 'deeg@example.org', 'subject' => 'Bewertung', 'replyToField' => 'email', 'success' => 'Danke.', 'summary' => true];
|
||
}
|
||
|
||
public function testStepsConditionsAndNumbersAreNormalisedWhenSaving(): void
|
||
{
|
||
$this->forms->save('preisfinder', 'Preisfinder', $this->preisfinder(), $this->settings(), 0);
|
||
$form = $this->forms->findBySlug('preisfinder');
|
||
$fields = array_column($form['fields'], null, 'name');
|
||
|
||
// Beide Schreibweisen der Bedingung landen in derselben gespeicherten Form.
|
||
self::assertSame(['field' => 'immobilienart', 'values' => ['Wohnung', 'Haus']], $fields['wohnflaeche']['showIf']);
|
||
self::assertSame(['field' => 'immobilienart', 'values' => ['Wohnung', 'Haus']], $fields['ausstattung']['showIf']);
|
||
self::assertSame(['field' => 'immobilienart', 'values' => ['Grundstück']], $fields['schritt_grund']['showIf']);
|
||
self::assertNull($fields['email']['showIf']);
|
||
// Grenzen sind Zahlen, die Einheit bleibt; ein Schritt fragt nichts und ist nie Pflicht.
|
||
self::assertEquals(10, $fields['wohnflaeche']['min']);
|
||
self::assertEquals(2000, $fields['wohnflaeche']['max']);
|
||
self::assertSame('m²', $fields['wohnflaeche']['unit']);
|
||
self::assertNull($fields['flaeche']['max']);
|
||
self::assertFalse($fields['schritt_objekt']['required']);
|
||
self::assertTrue($form['settings']['summary']);
|
||
self::assertTrue($this->forms->needsScript($form));
|
||
|
||
// Ein gewöhnliches Kontaktformular braucht das Skript nicht - es hat weder Schritte noch
|
||
// Bedingungen, und ohne Zusammenfassung bleibt die Seite ohne JavaScript.
|
||
$this->forms->save('kontakt', 'Kontakt', [['name' => 'email', 'type' => 'email', 'label' => 'E-Mail', 'required' => true]], ['recipient' => 'deeg@example.org'], 0);
|
||
$kontakt = $this->forms->findBySlug('kontakt');
|
||
self::assertFalse($this->forms->needsScript($kontakt));
|
||
self::assertFalse($kontakt['settings']['summary']);
|
||
self::assertNull($kontakt['fields'][0]['showIf'], 'Auch ohne Angabe kennt jedes Feld seine Schlüssel.');
|
||
}
|
||
|
||
public function testABrokenDefinitionIsRefusedWithAReason(): void
|
||
{
|
||
$faelle = [
|
||
'Bedingung auf ein unbekanntes Feld' => [
|
||
['name' => 'a', 'type' => 'text', 'label' => 'A', 'showIfField' => 'art', 'showIfValues' => 'Haus'],
|
||
],
|
||
'Bedingung auf ein Feld, das erst danach kommt' => [
|
||
['name' => 'a', 'type' => 'text', 'label' => 'A', 'showIfField' => 'art', 'showIfValues' => 'Haus'],
|
||
['name' => 'art', 'type' => 'select', 'label' => 'Art', 'options' => ['Haus', 'Wohnung']],
|
||
],
|
||
'Bedingung auf ein Textfeld' => [
|
||
['name' => 'art', 'type' => 'text', 'label' => 'Art'],
|
||
['name' => 'a', 'type' => 'text', 'label' => 'A', 'showIfField' => 'art', 'showIfValues' => 'Haus'],
|
||
],
|
||
'Bedingung mit einem Wert, der keine Option ist' => [
|
||
['name' => 'art', 'type' => 'select', 'label' => 'Art', 'options' => ['Haus', 'Wohnung']],
|
||
['name' => 'a', 'type' => 'text', 'label' => 'A', 'showIfField' => 'art', 'showIfValues' => 'Schloss'],
|
||
],
|
||
'Bedingung ohne Wert' => [
|
||
['name' => 'art', 'type' => 'select', 'label' => 'Art', 'options' => ['Haus', 'Wohnung']],
|
||
['name' => 'a', 'type' => 'text', 'label' => 'A', 'showIfField' => 'art', 'showIfValues' => ''],
|
||
],
|
||
'Untergrenze über der Obergrenze' => [
|
||
['name' => 'n', 'type' => 'number', 'label' => 'N', 'min' => '10', 'max' => '5'],
|
||
],
|
||
'Grenze, die keine Zahl ist' => [
|
||
['name' => 'n', 'type' => 'number', 'label' => 'N', 'min' => 'zehn'],
|
||
],
|
||
'Optionsfelder mit einer Option' => [
|
||
['name' => 'r', 'type' => 'radio', 'label' => 'R', 'options' => ['Ja']],
|
||
],
|
||
'Option mit Komma' => [
|
||
['name' => 'r', 'type' => 'radio', 'label' => 'R', 'options' => ['ja, neu', 'ja, alt']],
|
||
],
|
||
'nur Schritte' => [
|
||
['name' => 's', 'type' => 'step', 'label' => 'Schritt'],
|
||
],
|
||
'Bild für eine Option, die es nicht gibt' => [
|
||
['name' => 'r', 'type' => 'radio', 'label' => 'R', 'options' => ['Haus', 'Wohnung'], 'images' => ['Schloss' => str_repeat('a', 32)]],
|
||
],
|
||
'Bild, das keine Kennung ist' => [
|
||
['name' => 'r', 'type' => 'radio', 'label' => 'R', 'options' => ['Haus', 'Wohnung'], 'images' => 'Haus = bild.png'],
|
||
],
|
||
];
|
||
foreach ($faelle as $fall => $fields) {
|
||
try {
|
||
$this->forms->save('f', 'F', $fields, ['recipient' => 'a@example.org'], 0);
|
||
self::fail($fall.' wurde angenommen.');
|
||
} catch (InvalidForm $e) {
|
||
self::assertNotSame('', $e->getMessage(), $fall);
|
||
}
|
||
}
|
||
}
|
||
|
||
public function testOnlyFieldsWhoseConditionHoldsAreRequiredCheckedAndKept(): void
|
||
{
|
||
$this->forms->save('preisfinder', 'Preisfinder', $this->preisfinder(), $this->settings(), 0);
|
||
$form = $this->forms->findBySlug('preisfinder');
|
||
|
||
// Ein Grundstück: Wohnfläche und Ausstattung gelten nicht - nicht verlangt, nicht
|
||
// gespeichert, auch wenn der Browser etwas mitschickt. Der Grundstücksschritt gilt.
|
||
$grund = $this->forms->check($form, ['immobilienart' => 'Grundstück', 'wohnflaeche' => '80', 'ausstattung' => ['Balkon'], 'flaeche' => '1250.00', 'email' => 'x@example.org', 'datenschutz' => 'ja']);
|
||
self::assertSame([], $grund['errors']);
|
||
self::assertSame('', $grund['values']['wohnflaeche']);
|
||
self::assertSame('', $grund['values']['ausstattung']);
|
||
self::assertSame('1250', $grund['values']['flaeche'], 'Eine Zahl wird ohne unnötige Stellen gespeichert.');
|
||
self::assertArrayNotHasKey('schritt_objekt', $grund['values'], 'Ein Schritt ist keine Antwort.');
|
||
|
||
// Eine Wohnung: jetzt sind Wohnfläche und Ausstattung Pflicht, der Grundstücksschritt nicht.
|
||
$leer = $this->forms->check($form, ['immobilienart' => 'Wohnung', 'email' => 'x@example.org', 'datenschutz' => 'ja']);
|
||
self::assertSame('Dieses Feld wird benötigt.', $leer['errors']['wohnflaeche']);
|
||
self::assertSame('Bitte wähle mindestens eine Option.', $leer['errors']['ausstattung']);
|
||
self::assertArrayNotHasKey('flaeche', $leer['errors']);
|
||
|
||
// Zahlen mit Komma, außerhalb der Grenzen, keine Zahl; Mehrfachauswahl mit fremder Option.
|
||
$wohnung = $this->forms->check($form, ['immobilienart' => 'Wohnung', 'wohnflaeche' => '85,5', 'ausstattung' => ['Balkon', 'Keller'], 'email' => 'x@example.org', 'datenschutz' => 'ja']);
|
||
self::assertSame([], $wohnung['errors']);
|
||
self::assertSame('85.5', $wohnung['values']['wohnflaeche']);
|
||
self::assertSame('Balkon, Keller', $wohnung['values']['ausstattung']);
|
||
self::assertSame('', $wohnung['values']['flaeche']);
|
||
|
||
$zuViel = $this->forms->check($form, ['immobilienart' => 'Haus', 'wohnflaeche' => '5000', 'ausstattung' => ['Garage', 'Pool'], 'email' => 'x@example.org', 'datenschutz' => 'ja']);
|
||
self::assertSame('Bitte gib einen Wert zwischen 10 und 2000 an.', $zuViel['errors']['wohnflaeche']);
|
||
self::assertSame('Bitte wähle nur aus den angebotenen Optionen.', $zuViel['errors']['ausstattung']);
|
||
self::assertSame('Garage', $zuViel['values']['ausstattung'], 'Die fremde Option bleibt weg.');
|
||
|
||
$keineZahl = $this->forms->check($form, ['immobilienart' => 'Grundstück', 'flaeche' => 'groß', 'email' => 'x@example.org', 'datenschutz' => 'ja']);
|
||
self::assertSame('Bitte gib eine Zahl an.', $keineZahl['errors']['flaeche']);
|
||
$zuKlein = $this->forms->check($form, ['immobilienart' => 'Grundstück', 'flaeche' => '3', 'email' => 'x@example.org', 'datenschutz' => 'ja']);
|
||
self::assertSame('Bitte gib mindestens 10 an.', $zuKlein['errors']['flaeche']);
|
||
|
||
// Eine Objektart, die es nicht gibt: abgelehnt, und nichts hängt davon ab.
|
||
$fremd = $this->forms->check($form, ['immobilienart' => 'Schloss', 'email' => 'x@example.org', 'datenschutz' => 'ja']);
|
||
self::assertSame('Bitte wähle eine der angebotenen Optionen.', $fremd['errors']['immobilienart']);
|
||
self::assertArrayNotHasKey('wohnflaeche', $fremd['errors']);
|
||
}
|
||
|
||
public function testAStepFormIsRenderedAsFieldsetsAndBringsItsScript(): void
|
||
{
|
||
$this->forms->save('preisfinder', 'Preisfinder', $this->preisfinder(), $this->settings(), 0);
|
||
$document = DemoContent::starter('Bewertung');
|
||
$document['schemaVersion'] = 23;
|
||
$document['blocks'][] = ['id' => 'preis', 'type' => 'form', 'props' => ['form' => 'preisfinder']];
|
||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1);
|
||
|
||
$antwort = $this->request('/');
|
||
$html = $antwort->getContent();
|
||
self::assertSame(200, $antwort->getStatusCode());
|
||
// Schritte sind Abschnitte mit Titel; ohne Skript stehen sie untereinander.
|
||
self::assertSame(4, substr_count($html, '<fieldset class="form-step"'));
|
||
self::assertStringContainsString('<legend class="form-step-title" tabindex="-1">Ihre Immobilie</legend>', $html);
|
||
self::assertStringContainsString('<p class="form-step-intro">Womit können wir helfen?</p>', $html);
|
||
self::assertStringContainsString('data-steps data-summary>', $html);
|
||
// Bedingungen stehen am Feld und am Schritt, für das Skript - der Server prüft sie selbst.
|
||
self::assertStringContainsString('data-show-if="immobilienart" data-show-values="Wohnung|Haus"', $html);
|
||
self::assertStringContainsString('<fieldset class="form-step" data-step data-show-if="immobilienart" data-show-values="Grundstück">', $html);
|
||
// Optionsfelder, Mehrfachauswahl und Zahl mit Einheit.
|
||
self::assertStringContainsString('type="radio" value="Wohnung" required', $html);
|
||
self::assertStringContainsString('name="ausstattung[]" type="checkbox" value="Balkon"', $html);
|
||
self::assertStringContainsString('data-required', $html);
|
||
self::assertStringContainsString('type="number" step="any" inputmode="decimal"', $html);
|
||
self::assertStringContainsString('min="10"', $html);
|
||
self::assertStringContainsString('<span class="form-unit" aria-hidden="true">m²</span>', $html);
|
||
// Das Skript kommt nur mit diesem Formular, und die Richtlinie erlaubt es dann.
|
||
self::assertStringContainsString('<script src="/assets/form-steps.js?v=', $html);
|
||
self::assertStringContainsString("script-src 'self'", (string) $antwort->headers->get('Content-Security-Policy'));
|
||
|
||
// Ein gewöhnliches Formular bleibt ohne Skript und ohne Erlaubnis dafür.
|
||
$this->forms->save('kontakt', 'Kontakt', [['name' => 'email', 'type' => 'email', 'label' => 'E-Mail', 'required' => true]], ['recipient' => 'deeg@example.org'], 0);
|
||
$schlicht = DemoContent::starter('Kontakt');
|
||
$schlicht['schemaVersion'] = 23;
|
||
$schlicht['blocks'][] = ['id' => 'kontakt', 'type' => 'form', 'props' => ['form' => 'kontakt']];
|
||
$version = $this->pages->findForEditor('page-1')['version'];
|
||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $schlicht, $version), $version + 1);
|
||
$zweite = $this->request('/');
|
||
self::assertStringNotContainsString('form-steps.js', $zweite->getContent());
|
||
self::assertStringNotContainsString('<fieldset', $zweite->getContent());
|
||
self::assertStringContainsString("script-src 'none'", (string) $zweite->headers->get('Content-Security-Policy'));
|
||
}
|
||
|
||
public function testAChoiceWithPicturesBecomesClickableCards(): void
|
||
{
|
||
// Umut zeigte den Solar-Rechner von Elektro Bernhard: „Wieviele Personen leben im
|
||
// Haushalt“ als vier Bildkarten. Ein Bild je Option macht aus der Auswahl Karten;
|
||
// das Kästchen bleibt darin, für Tastatur und Vorlesesoftware.
|
||
$media = new MediaLibrary($this->db, $this->path.'-media');
|
||
$file = tempnam(sys_get_temp_dir(), 'kant-option-');
|
||
$image = imagecreatetruecolor(800, 600);
|
||
imagepng($image, $file);
|
||
imagedestroy($image);
|
||
$bild = $media->upload(new \Symfony\Component\HttpFoundation\File\UploadedFile($file, 'haus.png', test: true));
|
||
@unlink($file);
|
||
|
||
// Aus der Verwaltung kommt die Zuordnung als Zeilen, gespeichert wird sie als Paare.
|
||
$this->forms->save('personen', 'Personen', [
|
||
['name' => 'art', 'type' => 'radio', 'label' => 'Objektart', 'required' => true, 'options' => ['Haus', 'Wohnung'], 'images' => "Haus = {$bild['id']}\nWohnung = "],
|
||
], ['recipient' => 'deeg@example.org'], 0);
|
||
$form = $this->forms->findBySlug('personen');
|
||
self::assertSame(['Haus' => $bild['id']], $form['fields'][0]['images']);
|
||
|
||
$document = DemoContent::starter('Personen');
|
||
$document['schemaVersion'] = 23;
|
||
$document['blocks'][] = ['id' => 'p', 'type' => 'form', 'props' => ['form' => 'personen']];
|
||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1);
|
||
$html = $this->request('/')->getContent();
|
||
self::assertStringContainsString('form-choice form-choice-images', $html);
|
||
self::assertSame(2, substr_count($html, 'class="form-choice-card"'));
|
||
// Die Option mit Bild zeigt es in der kleinsten Fassung; die ohne Bild bleibt eine Karte mit Text.
|
||
self::assertStringContainsString('<img src="'.$bild['variants'][0]['src'].'" alt="" width="480"', $html);
|
||
self::assertStringContainsString('<span class="form-choice-text">Wohnung</span>', $html);
|
||
self::assertSame(1, substr_count($html, '<img src="/media/'), 'Nur die Option mit Bild bekommt eines.');
|
||
// Keine Schritte, keine Bedingungen: die Bildauswahl braucht kein Skript.
|
||
self::assertStringNotContainsString('form-steps.js', $html);
|
||
}
|
||
|
||
public function testASubmissionStoresOnlyWhatAppliedAndReRendersWithErrors(): void
|
||
{
|
||
$this->forms->save('preisfinder', 'Preisfinder', $this->preisfinder(), $this->settings(), 0);
|
||
$document = DemoContent::starter('Bewertung');
|
||
$document['schemaVersion'] = 23;
|
||
$document['blocks'][] = ['id' => 'preis', 'type' => 'form', 'props' => ['form' => 'preisfinder']];
|
||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1);
|
||
|
||
$issued = time() - 10;
|
||
$grund = [
|
||
'page' => 'home', 'block' => 'preis', 'issued' => (string) $issued, 'issuedSignature' => FormRepository::stamp('preisfinder', $issued, 'test-only'),
|
||
'immobilienart' => 'Grundstück', 'wohnflaeche' => '99', 'ausstattung' => ['Balkon'], 'flaeche' => '640', 'email' => 'x@example.org', 'datenschutz' => 'ja',
|
||
];
|
||
$antwort = $this->request('/formular/preisfinder', 'POST', $grund);
|
||
self::assertSame(303, $antwort->getStatusCode());
|
||
self::assertSame('/?gesendet=preisfinder#preis', $antwort->headers->get('Location'));
|
||
$gespeichert = json_decode((string) $this->db->fetchOne('SELECT data FROM form_submission'), true);
|
||
self::assertSame('Grundstück', $gespeichert['immobilienart']);
|
||
self::assertSame('640', $gespeichert['flaeche']);
|
||
self::assertSame('', $gespeichert['wohnflaeche'], 'Was nicht galt, wird nicht gespeichert.');
|
||
self::assertSame('', $gespeichert['ausstattung']);
|
||
|
||
// Eine Wohnung ohne Wohnfläche: die Seite kommt mit Meldungen zurück, die gewählte
|
||
// Objektart bleibt angekreuzt, die Mehrfachauswahl behält ihre Kreuze.
|
||
$wohnung = [...$grund, 'immobilienart' => 'Wohnung', 'wohnflaeche' => '', 'ausstattung' => ['Balkon', 'Keller'], 'flaeche' => ''];
|
||
$abgelehnt = $this->request('/formular/preisfinder', 'POST', $wohnung);
|
||
self::assertSame(422, $abgelehnt->getStatusCode());
|
||
$html = $abgelehnt->getContent();
|
||
self::assertStringContainsString('Bitte prüfe 1 Eingabe.', $html);
|
||
self::assertStringContainsString('type="radio" value="Wohnung" checked required', $html);
|
||
self::assertStringContainsString('value="Balkon" checked', $html);
|
||
self::assertStringContainsString('value="Keller" checked', $html);
|
||
self::assertStringContainsString('value="Garage">', $html);
|
||
self::assertStringContainsString('Dieses Feld wird benötigt.', $html);
|
||
self::assertSame(1, (int) $this->db->fetchOne('SELECT COUNT(*) FROM form_submission'), 'Eine abgelehnte Eingabe wird nicht gespeichert.');
|
||
}
|
||
|
||
private function request(string $url, string $method = 'GET', array $body = []): Response
|
||
{
|
||
$previous = [];
|
||
foreach (['APP_SECRET' => 'test-only', 'DATABASE_URL' => 'sqlite:///'.str_replace('\\', '/', $this->path), 'SITE_ORIGIN' => 'https://cms.test', 'MEDIA_DIRECTORY' => $this->path.'-media', 'FONT_DIRECTORY' => $this->path.'-fonts', 'ASSET_DIRECTORY' => $this->path.'-assets'] as $key => $value) {
|
||
$previous[$key] = [$_SERVER[$key] ?? null, $_ENV[$key] ?? null];
|
||
$_SERVER[$key] = $_ENV[$key] = $value;
|
||
}
|
||
$kernel = new Kernel('test', false);
|
||
try {
|
||
$request = Request::create($url, $method, $body);
|
||
$response = $kernel->handle($request);
|
||
$kernel->terminate($request, $response);
|
||
|
||
return $response;
|
||
} finally {
|
||
$kernel->shutdown();
|
||
foreach ($previous as $key => [$server, $env]) {
|
||
if (null === $server) {
|
||
unset($_SERVER[$key]);
|
||
} else {
|
||
$_SERVER[$key] = $server;
|
||
}
|
||
if (null === $env) {
|
||
unset($_ENV[$key]);
|
||
} else {
|
||
$_ENV[$key] = $env;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|