Files
KantSites/tests/TranslationTest.php
T
kantarciandClaude Opus 5.5 e15b2517b1 Translate pages, areas, menus, forms and posts field by field instead of rebuilding them
A site can have further languages (Settings → Languages). A page is built once; each language
adds only its texts, keyed by the stable block IDs, plus its own address under /en/… (ADR 111,
document schema v27, migration 20261009000100 for page_translation).

- Editor: DE | EN switch, a list of all texts with original, state (missing, translated,
  original changed) and filter; structure stays with the main language. Undo, drafts,
  publishing and revisions cover all languages at once.
- Website: /en and translated slugs, lang, canonical, hreflang with x-default, og:locale,
  sitemap; links to translated pages follow the language; a language switch block.
- Header, footer and linked sections translate the same way; menu entries carry labels per
  language; fixed template texts come from a small dictionary.
- Forms: labels, help, options, button and success message per language; messages and the
  step script speak the language, the submission and the email stay in the main language.
- Posts, archives, category names, consent text and the 404 page follow the language.
- Single-language sites see none of it and never touch the new table.

The forms administration becomes an overview with one row per form and its own edit page;
fields are one line each with details collapsed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:56:52 +02:00

445 lines
27 KiB
PHP

<?php
namespace App\Tests;
use App\Content\DemoContent;
use App\Content\DocumentValidator;
use App\Content\Languages;
use App\Content\MediaLibrary;
use App\Content\PageRepository;
use App\Content\PartialRepository;
use App\Content\RedirectRepository;
use App\Content\SiteSettings;
use App\Content\Translations;
use App\Domain\InvalidDocument;
use App\Domain\InvalidPage;
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;
/**
* Mehrsprachigkeit als Übersetzungsebene (ADR 111).
*
* Umut: „Ich möchte nicht für die 2. Sprache die komplette Seite neu bauen.“ Eine Seite wird
* einmal gebaut; je Sprache kommen nur Texte und eine Adresse dazu. Diese Tests halten fest, was
* dabei garantiert ist: Übersetzungen durchlaufen dieselbe Prüfung wie die Hauptsprache, gehen
* mit der Fassung online, haben eigene Adressen unter /en/ und verlinken einander.
*/
final class TranslationTest extends TestCase
{
private string $path;
private Connection $db;
private PageRepository $pages;
private SiteSettings $settings;
private Languages $languages;
protected function setUp(): void
{
$this->path = tempnam(sys_get_temp_dir(), 'kant-lang-');
$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->settings = new SiteSettings($this->db, new MediaLibrary($this->db, $this->path.'-media'));
$this->pages = new PageRepository($this->db, new DocumentValidator(), $this->settings);
$this->languages = new Languages($this->db, $this->settings);
$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);
}
private function document(string $title = 'Leistungen', array $translations = []): array
{
$document = [
'schemaVersion' => 27,
'title' => $title,
'description' => 'Was wir für Sie tun',
'blocks' => [[
'id' => 's1', 'type' => 'section', 'children' => [
['id' => 'h1', 'type' => 'heading', 'props' => ['text' => $title, 'level' => 1]],
['id' => 't1', 'type' => 'text', 'props' => ['text' => '<p>Schreiben Sie uns über <a href="/kontakt">Kontakt</a>.</p>']],
['id' => 'b1', 'type' => 'button', 'props' => ['text' => 'Zum Kontakt', 'href' => '/kontakt']],
['id' => 'i1', 'type' => 'image', 'props' => ['src' => '/assets/image-placeholder.svg', 'alt' => '', 'decorative' => true, 'width' => 10, 'height' => 10]],
['id' => 'l1', 'type' => 'language', 'props' => ['display' => 'code']],
],
]],
];
if ([] !== $translations) {
$document['translations'] = $translations;
}
return $document;
}
/** Legt eine Seite an, speichert einen Entwurf und veröffentlicht ihn. */
private function publishedPage(string $slug, array $document, ?string $parent = null): string
{
$id = $this->pages->createPage($slug, $parent, 'page', $this->document());
$revision = $this->pages->saveDraft($id, $document, 1);
$this->pages->publish($id, $revision, 2);
return $id;
}
private function republish(string $id, array $document): void
{
$version = (int) $this->db->fetchOne('SELECT version FROM page WHERE id = ?', [$id]);
$revision = $this->pages->saveDraft($id, $document, $version);
$this->pages->publish($id, $revision, $version + 1);
}
public function testApplyLaysTranslatedTextsOverTheSamePageAndKeepsEverythingElse(): void
{
$document = $this->document('Leistungen', ['en' => [
'title' => 'Services',
'blocks' => ['h1' => ['text' => 'Services'], 'b1' => ['text' => ' '], 'i1' => ['alt' => 'A picture']],
]]);
$english = Translations::apply($document, 'en');
self::assertArrayNotHasKey('translations', $english);
self::assertSame('Services', $english['title']);
self::assertSame('Was wir für Sie tun', $english['description'], 'Ohne Übersetzung bleibt der Text der Hauptsprache.');
$children = $english['blocks'][0]['children'];
self::assertSame('Services', $children[0]['props']['text']);
self::assertSame(1, $children[0]['props']['level'], 'Aufbau und Einstellungen bleiben unverändert.');
self::assertSame('Zum Kontakt', $children[2]['props']['text'], 'Ein leerer Text gilt als nicht übersetzt.');
self::assertSame('/kontakt', $children[2]['props']['href']);
self::assertSame('', $children[3]['props']['alt'], 'Ein dekoratives Bild bekommt auch übersetzt keinen Text.');
self::assertSame(['total' => 5, 'done' => 2, 'stale' => 0], Translations::progress($document, 'en'));
}
public function testTranslationsPassTheSameChecksAsTheMainLanguage(): void
{
$id = $this->pages->createPage('leistungen', null, 'page', $this->document());
// Formatierter Text geht durch denselben Filter, Einträge verschwundener Blöcke fallen weg.
$this->pages->saveDraft($id, $this->document('Leistungen', ['en' => [
'blocks' => [
'h1' => ['text' => 'Services<script>alert(1)</script>', 'level' => 'kein Text-Feld'],
'weg' => ['text' => 'Block gibt es nicht'],
],
'source' => ['h1.text' => Translations::fingerprint('Leistungen'), 'weg.text' => '00000000'],
]]), 1);
$stored = $this->pages->findForEditor($id)['document']['translations']['en'];
self::assertSame(['h1' => ['text' => 'Services']], $stored['blocks']);
self::assertSame(['h1.text' => Translations::fingerprint('Leistungen')], $stored['source']);
$refused = [
'unbekannte Sprache' => ['xx' => ['title' => 'X']],
'zu langer Titel' => ['en' => ['title' => str_repeat('a', 161)]],
'falsche Adresse' => ['en' => ['title' => 'Services', 'slug' => 'Unsere Leistungen']],
'unbekanntes Feld' => ['en' => ['title' => 'Services', 'menu' => 'x']],
'zu langer Button' => ['en' => ['blocks' => ['b1' => ['text' => str_repeat('a', 301)]]]],
];
foreach ($refused as $case => $translations) {
try {
$this->pages->saveDraft($id, $this->document('Leistungen', $translations), 2);
self::fail('Abgelehnt werden muss: '.$case);
} catch (InvalidDocument) {
self::assertSame(2, (int) $this->db->fetchOne('SELECT version FROM page WHERE id = ?', [$id]), $case);
}
}
$older = $this->document('Leistungen', ['en' => ['title' => 'Services']]);
$older['schemaVersion'] = 26;
$older['blocks'][0]['children'] = array_slice($older['blocks'][0]['children'], 0, 4);
$this->expectException(InvalidDocument::class);
(new DocumentValidator())->validate($older);
}
public function testPublishingWritesTheTranslatedAddressesAndMovesFollow(): void
{
$this->languages->save(['en']);
$parent = $this->pages->createPage('leistungen', null, 'page', $this->document());
$revision = $this->pages->saveDraft($parent, $this->document('Leistungen', ['en' => ['title' => 'Services', 'slug' => 'services']]), 1);
self::assertSame([], $this->pages->translatedPaths($parent), 'Ein Entwurf ist noch nicht unter /en erreichbar.');
$this->pages->publish($parent, $revision, 2);
self::assertSame(['en' => 'services'], $this->pages->translatedPaths($parent));
$child = $this->publishedPage('webdesign', $this->document('Webdesign', ['en' => ['title' => 'Web design', 'slug' => 'web-design']]), $parent);
self::assertSame(['en' => 'services/web-design'], $this->pages->translatedPaths($child));
$english = $this->pages->findPublishedTranslation('services/web-design', 'en');
self::assertSame('Web design', $english['document']['title']);
self::assertSame('en', $english['locale']);
self::assertSame('leistungen/webdesign', $english['path'], 'Navigation und Brotkrumen hängen an der Adresse der Hauptsprache.');
self::assertArrayNotHasKey('translations', $this->pages->findPublished('leistungen/webdesign')['document']);
// Neue übersetzte Adresse der Elternseite: die Unterseite wandert mit, die alte Adresse leitet weiter.
$this->republish($parent, $this->document('Leistungen', ['en' => ['title' => 'Services', 'slug' => 'offers']]));
self::assertSame(['en' => 'offers/web-design'], $this->pages->translatedPaths($child));
self::assertSame(['to' => 'en/offers/web-design', 'status' => 301], (new RedirectRepository($this->db))->find('en/services/web-design'));
// Verschieben in der Hauptsprache trägt die Übersetzungen mit.
$other = $this->publishedPage('ueber-uns', $this->document('Über uns'));
$version = (int) $this->db->fetchOne('SELECT version FROM page WHERE id = ?', [$child]);
$this->pages->movePage($child, 'webdesign', $other, $version, new RedirectRepository($this->db));
self::assertSame(['en' => 'ueber-uns/web-design'], $this->pages->translatedPaths($child), 'Eine nicht übersetzte Elternseite steht mit ihrer Adresse der Hauptsprache davor.');
// Ohne Übersetzung veröffentlicht: die Sprache ist für diese Seite wieder weg.
$this->republish($child, $this->document('Webdesign'));
self::assertSame([], $this->pages->translatedPaths($child));
self::assertNull($this->pages->findPublishedTranslation('ueber-uns/web-design', 'en'));
}
public function testATranslatedAddressThatIsTakenIsRefusedWhenSaving(): void
{
$this->languages->save(['en']);
$this->publishedPage('leistungen', $this->document('Leistungen', ['en' => ['title' => 'Services', 'slug' => 'services']]));
$other = $this->pages->createPage('angebote', null, 'page', $this->document('Angebote'));
$this->expectException(InvalidPage::class);
$this->expectExceptionMessage('/en/services');
$this->pages->saveDraft($other, $this->document('Angebote', ['en' => ['title' => 'Offers', 'slug' => 'services']]), 1);
}
public function testALanguageCodeCannotCollideWithARootPage(): void
{
$this->pages->createPage('fr', null, 'page', $this->document('Frankreich'));
try {
$this->languages->save(['fr']);
self::fail('Die Seite /fr würde unerreichbar.');
} catch (InvalidPage $error) {
self::assertStringContainsString('/fr', $error->getMessage());
}
$this->languages->save(['en', 'de', 'en']);
self::assertSame(['en'], $this->languages->extra(), 'Die Hauptsprache und Doppelte fallen weg.');
$this->expectException(InvalidPage::class);
$this->pages->createPage('en', null, 'page', $this->document('Englisch'));
}
public function testTheWebsiteServesTheTranslationWithHreflangAndLinksInItsLanguage(): void
{
$this->languages->save(['en']);
$home = $this->publishedPage('home', $this->document('Start', ['en' => ['title' => 'Home', 'blocks' => ['h1' => ['text' => 'Welcome']]]]));
$this->publishedPage('kontakt', $this->document('Kontakt', ['en' => ['title' => 'Contact', 'slug' => 'contact']]));
$this->publishedPage('leistungen', $this->document('Leistungen', ['en' => [
'title' => 'Services', 'slug' => 'services', 'seoTitle' => 'Our services | KantSites',
'blocks' => ['h1' => ['text' => 'Services'], 't1' => ['text' => '<p>Write to us via <a href="/kontakt">contact</a>.</p>'], 'b1' => ['text' => 'Get in touch']],
]]));
$this->publishedPage('impressum', $this->document('Impressum'));
$english = $this->request('/en/services');
$html = (string) $english->getContent();
self::assertSame(200, $english->getStatusCode());
self::assertStringContainsString('<html lang="en"', $html);
self::assertStringContainsString('<title>Our services | KantSites</title>', $html);
self::assertStringContainsString('<link rel="canonical" href="https://cms.test/en/services">', $html);
self::assertStringContainsString('<link rel="alternate" hreflang="de" href="https://cms.test/leistungen">', $html);
self::assertStringContainsString('<link rel="alternate" hreflang="en" href="https://cms.test/en/services">', $html);
self::assertStringContainsString('hreflang="x-default" href="https://cms.test/leistungen"', $html);
self::assertStringContainsString('content="en_US"', $html);
self::assertStringContainsString('Get in touch', $html);
self::assertStringContainsString('href="/en/contact"', $html, 'Links auf übersetzte Seiten führen zur Übersetzung.');
self::assertStringNotContainsString('href="/kontakt"', $html);
self::assertMatchesRegularExpression('#<a href="/leistungen" hreflang="de" lang="de">DE#', $html, 'Die Sprachwahl zeigt bewusst auf die andere Sprache.');
self::assertMatchesRegularExpression('#<a href="/en/services" hreflang="en" lang="en" aria-current="page">EN#', $html);
$german = (string) $this->request('/leistungen')->getContent();
self::assertStringContainsString('<html lang="de"', $german);
self::assertStringContainsString('hreflang="en" href="https://cms.test/en/services"', $german);
self::assertStringContainsString('href="/kontakt"', $german);
self::assertStringContainsString('Zum Kontakt', $german);
self::assertStringNotContainsString('hreflang', (string) $this->request('/impressum')->getContent(), 'Eine Seite ohne Übersetzung hat keine Sprachalternativen.');
self::assertStringContainsString('Welcome', (string) $this->request('/en')->getContent(), '/en ist die Startseite der Sprache.');
self::assertSame(301, $this->request('/en/home')->getStatusCode());
self::assertSame(404, $this->request('/en/impressum')->getStatusCode());
self::assertSame(404, $this->request('/fr/services')->getStatusCode());
$sitemap = (string) $this->request('/sitemap.xml')->getContent();
self::assertStringContainsString('<loc>https://cms.test/en/services</loc>', $sitemap);
self::assertStringContainsString('<loc>https://cms.test/en</loc>', $sitemap);
unset($home);
// Sprache abgewählt: ihre Seiten sind weg, die Übersetzungen bleiben in den Seiten liegen.
$this->languages->save([]);
self::assertSame(404, $this->request('/en/services')->getStatusCode());
self::assertStringNotContainsString('hreflang', (string) $this->request('/leistungen')->getContent());
self::assertStringNotContainsString('/en/', (string) $this->request('/sitemap.xml')->getContent());
}
public function testHeaderMenuAndFixedTextsFollowTheLanguageOfThePage(): void
{
$this->languages->save(['en']);
$partials = new PartialRepository($this->db, new DocumentValidator());
$header = $partials->findByKind('header');
$partials->save($header['id'], [
'schemaVersion' => 27, 'title' => 'Kopf', 'description' => 'Kopfbereich',
'blocks' => [['id' => 'kopf', 'type' => 'section', 'children' => [
['id' => 'kopf-text', 'type' => 'text', 'props' => ['text' => '<p>Willkommen</p>']],
['id' => 'kopf-menu', 'type' => 'menu', 'props' => ['slot' => 'header', 'layout' => 'horizontal']],
]]],
'translations' => ['en' => ['blocks' => ['kopf-text' => ['text' => '<p>Welcome<script>x</script></p>'], 'weg' => ['text' => 'x']]]],
], $header['version']);
self::assertSame(['kopf-text' => ['text' => '<p>Welcome</p>']], $partials->find($header['id'])['document']['translations']['en']['blocks'], 'Ein Bereich wird beim Speichern bereinigt wie eine Seite.');
$this->publishedPage('home', $this->document('Start'));
$services = $this->publishedPage('leistungen', $this->document('Leistungen', ['en' => ['title' => 'Services', 'slug' => 'services']]));
$menus = new \App\Content\MenuRepository($this->db, $this->settings);
$menus->save('header', 'Hauptnavigation', [
['label' => 'Leistungen', 'type' => 'page', 'pageId' => $services, 'anchor' => '', 'labels' => ['en' => 'Our services'], 'children' => []],
], 0);
try {
$menus->save('header', 'Hauptnavigation', [['label' => 'X', 'type' => 'url', 'url' => '/x', 'labels' => ['xx' => 'Y'], 'children' => []]], 1);
self::fail('Eine unbekannte Sprache im Menü wird abgelehnt.');
} catch (\App\Domain\InvalidMenu) {
}
$english = (string) $this->request('/en/services')->getContent();
self::assertStringContainsString('<p>Welcome</p>', $english);
self::assertStringNotContainsString('Willkommen', $english);
self::assertMatchesRegularExpression('#<a href="/en/services"[^>]*>.*?Our services#s', $english, 'Menübeschriftung und Ziel in der Sprache der Seite.');
self::assertStringContainsString('>Skip to content</a>', $english);
self::assertStringContainsString('aria-label="Main navigation"', $english);
$german = (string) $this->request('/leistungen')->getContent();
self::assertStringContainsString('<p>Willkommen</p>', $german);
self::assertMatchesRegularExpression('#<a href="/leistungen"[^>]*>.*?Leistungen#s', $german);
self::assertStringNotContainsString('Our services', $german);
self::assertStringContainsString('>Zum Inhalt springen</a>', $german);
}
public function testFormsSpeakTheLanguageOfThePageButSendInTheMainLanguage(): void
{
$this->languages->save(['en']);
$forms = new \App\Content\FormRepository($this->db);
$translations = ['en' => ['buttonLabel' => 'Send request', 'success' => 'Thanks!', 'fields' => [
'art' => ['label' => 'Type of enquiry', 'options' => ['Beratung' => 'Advice', 'Angebot' => 'Quote', 'Gibt es nicht' => 'x']],
'email' => ['label' => 'Email', 'help' => ''],
'weg' => ['label' => 'Feld gibt es nicht'],
]]];
$fields = [
['name' => 'art', 'type' => 'radio', 'label' => 'Art der Anfrage', 'required' => true, 'options' => ['Beratung', 'Angebot']],
['name' => 'email', 'type' => 'email', 'label' => 'E-Mail', 'required' => true, 'help' => 'Für die Antwort'],
];
try {
$forms->save('anfrage', 'Anfrage', $fields, ['recipient' => 'info@example.org', 'translations' => ['xx' => []]], 0);
self::fail('Eine unbekannte Sprache wird abgelehnt.');
} catch (\App\Domain\InvalidForm) {
}
$forms->save('anfrage', 'Anfrage', $fields, ['recipient' => 'info@example.org', 'translations' => $translations], 0);
$stored = $forms->findBySlug('anfrage')['settings']['translations']['en'];
self::assertSame(['art' => ['label' => 'Type of enquiry', 'options' => ['Beratung' => 'Advice', 'Angebot' => 'Quote']], 'email' => ['label' => 'Email']], $stored['fields'], 'Nur Felder und Optionen, die es gibt; leere fallen weg.');
$checked = $forms->check($forms->findBySlug('anfrage'), ['art' => 'Advice', 'email' => 'kaputt'], 'en');
self::assertSame(['art' => 'Please choose one of the options offered.', 'email' => 'Please enter a valid email address.'], $checked['errors'], 'Abgeschickt wird der Wert der Hauptsprache, nicht die Anzeige.');
$document = $this->document('Kontakt', ['en' => ['title' => 'Contact', 'slug' => 'contact']]);
$document['blocks'][0]['children'][] = ['id' => 'f1', 'type' => 'form', 'props' => ['form' => 'anfrage']];
$this->publishedPage('kontakt', $document);
$english = (string) $this->request('/en/contact')->getContent();
self::assertMatchesRegularExpression('#<legend>Type of enquiry#', $english);
self::assertStringContainsString('type="radio" value="Beratung"', $english);
self::assertMatchesRegularExpression('#<label for="[^"]+-1">Advice</label>#', $english);
self::assertStringContainsString('<input type="hidden" name="language" value="en">', $english);
self::assertStringContainsString('>Send request</button>', $english);
self::assertStringContainsString('(required)', $english);
self::assertStringContainsString('Für die Antwort', $english, 'Ein nicht übersetzter Hilfetext bleibt in der Hauptsprache.');
$german = (string) $this->request('/kontakt')->getContent();
self::assertMatchesRegularExpression('#<label for="[^"]+-1">Beratung</label>#', $german);
self::assertStringNotContainsString('name="language"', $german);
$issued = time() - 10;
$body = ['page' => 'kontakt', 'block' => 'f1', 'language' => 'en', 'issued' => (string) $issued, 'issuedSignature' => hash_hmac('sha256', (string) $issued, 'test-only'), 'art' => 'Beratung', 'email' => ''];
$refused = $this->request('/formular/anfrage', 'POST', $body);
self::assertSame(422, $refused->getStatusCode());
self::assertStringContainsString('This field is required.', (string) $refused->getContent());
self::assertStringContainsString('<html lang="en"', (string) $refused->getContent());
$sent = $this->request('/formular/anfrage', 'POST', [...$body, 'email' => 'kunde@example.org']);
self::assertSame('/en/contact?gesendet=anfrage#f1', $sent->headers->get('Location'));
self::assertSame('Beratung', json_decode((string) $this->db->fetchOne('SELECT data FROM form_submission'), true)['art'], 'Gespeichert wird in der Hauptsprache.');
self::assertStringContainsString('Thanks!', (string) $this->request('/en/contact?gesendet=anfrage')->getContent());
}
public function testPostsArchivesConsentAndNotFoundFollowTheLanguage(): void
{
$this->languages->save(['en']);
$this->publishedPage('home', $this->document('Start', ['en' => ['title' => 'Home']]));
$posts = new \App\Content\PostRepository($this->db, new MediaLibrary($this->db, $this->path.'-media'), $this->settings);
$translated = $this->pages->createPage('neues-dach', null, 'post', $this->document('Neues Dach'));
$this->pages->publish($translated, $this->pages->saveDraft($translated, $this->document('Neues Dach', ['en' => ['title' => 'New roof', 'description' => 'A new roof for a farmhouse', 'slug' => 'new-roof']]), 1), 2);
$posts->saveMetadata($translated, [], ['Projekte'], [], 3);
$german = $this->pages->createPage('garten', null, 'post', $this->document('Garten'));
$this->pages->publish($german, $this->pages->saveDraft($german, $this->document('Garten'), 1), 2);
$this->settings->saveLanguageTexts(['en' => ['category:projekte' => 'Projects', 'tag:kaputt!' => 'x']], ['en' => ['title' => 'Your choice', 'text' => '']]);
self::assertSame(['en' => ['category:projekte' => 'Projects']], $this->settings->load()['termNames']);
$list = $posts->published(language: 'en')['posts'];
self::assertContains('New roof', array_column($list, 'title'));
self::assertContains('Garten', array_column($list, 'title'), 'Ein nicht übersetzter Beitrag steht in der Hauptsprache in der Liste.');
self::assertContains('A new roof for a farmhouse', array_column($list, 'excerpt'));
$archive = $this->request('/en/beitraege');
$html = (string) $archive->getContent();
self::assertSame(200, $archive->getStatusCode());
self::assertStringContainsString('<html lang="en"', $html);
self::assertStringContainsString('>Posts</h1>', $html);
self::assertStringContainsString('href="/en/new-roof"', $html);
self::assertStringContainsString('href="/garten"', $html);
self::assertStringContainsString('>Projects</a>', $html);
self::assertStringContainsString('href="/en/kategorie/projekte"', $html);
$category = (string) $this->request('/en/kategorie/projekte')->getContent();
self::assertStringContainsString('>Projects</h1>', $category);
self::assertStringContainsString('>Beiträge</h1>', (string) $this->request('/beitraege')->getContent());
$post = (string) $this->request('/en/new-roof')->getContent();
self::assertStringContainsString('<title>New roof</title>', $post);
self::assertStringContainsString('>Projects</a>', $post);
$missing = $this->request('/en/gibt-es-nicht');
self::assertSame(404, $missing->getStatusCode());
self::assertStringContainsString('<html lang="en"', (string) $missing->getContent());
self::assertStringContainsString('Page not found', (string) $missing->getContent());
self::assertStringContainsString('Seite nicht gefunden', (string) $this->request('/gibt-es-nicht')->getContent());
$consent = new \App\Content\Consent($this->settings);
$request = Request::create('/en');
$request->attributes->set('kant_locale', 'en');
self::assertSame('Your choice', $consent->state($request)['title']);
self::assertSame('', $consent->state(Request::create('/'))['title']);
}
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;
}
}
}
}
}