Files
KantSites/tests/HttpsRedirectTest.php
T
kantarciandClaude Opus 5.5 b982cb8944 Give mail delivery its own admin page and move the origin probe off /.well-known
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>
2026-10-12 21:07:27 +02:00

168 lines
7.3 KiB
PHP

<?php
namespace App\Tests;
use App\Ops\Download;
use App\Ops\HttpsRedirect;
use App\Ops\OriginCheck;
use PHPUnit\Framework\TestCase;
/**
* HTTPS erzwingen aus der Verwaltung (ADR 120): eingeschaltet wird nur, wenn HTTPS vorher und
* nachher hier ankommt, und eine Schleife wird zurückgenommen, bevor sie jemanden aussperrt.
*/
final class HttpsRedirectTest extends TestCase
{
private string $root;
private string $shipped;
/** @var list<?string> */
private array $answers = [];
/** @var array{status: int, location: string, error: string} */
private array $http = ['status' => 301, 'location' => 'https://www.example.de/kantsites-origin', 'error' => ''];
protected function setUp(): void
{
$this->root = sys_get_temp_dir().'/kant-https-'.bin2hex(random_bytes(4));
mkdir($this->root.'/public', 0o777, true);
$this->shipped = (string) file_get_contents(dirname(__DIR__).'/public/.htaccess');
file_put_contents($this->root.'/public/.htaccess', $this->shipped);
}
protected function tearDown(): void
{
@unlink($this->root.'/public/.htaccess');
@rmdir($this->root.'/public');
@rmdir($this->root);
}
public function testTheShippedFileHasTheRedirectBeforeTheFrontController(): void
{
// Hinter "RewriteRule ^index\.php$ - [L]" kam die Umleitung für Seiten nie an die Reihe:
// Nach dem Umschreiben auf index.php beginnt die Runde neu und endet an dieser Regel.
$redirect = strpos($this->shipped, '#'.HttpsRedirect::REDIRECT_LINES[0]);
$front = strpos($this->shipped, 'RewriteRule ^index\.php$ - [L]');
self::assertNotFalse($redirect);
self::assertNotFalse($front);
self::assertLessThan($front, $redirect);
self::assertSame(['redirect' => false, 'hsts' => false, 'found' => true, 'writable' => true], $this->redirect()->state());
}
public function testEnablingProbesBeforeAndAfterAndSwitchesOnlyTheThreeLines(): void
{
$this->answers = ['ok', 'ok'];
$result = $this->redirect()->enableRedirect('https://www.example.de/');
self::assertTrue($result['ok'], $result['message']);
self::assertStringContainsString('leitet mit 301', $result['message']);
$now = (string) file_get_contents($this->root.'/public/.htaccess');
foreach (HttpsRedirect::REDIRECT_LINES as $line) {
self::assertMatchesRegularExpression('/^'.preg_quote($line, '/').'$/m', $now);
}
self::assertStringContainsString('#'.HttpsRedirect::HSTS_LINE, $now, 'HSTS bleibt ein eigener Schritt.');
self::assertSame([], $this->answers, 'Vorher und nachher geprüft.');
$off = $this->redirect()->disableRedirect();
self::assertTrue($off['ok']);
self::assertSame($this->shipped, (string) file_get_contents($this->root.'/public/.htaccess'), 'Aus heißt: wieder wie ausgeliefert.');
}
public function testNothingChangesWithoutWorkingHttps(): void
{
$this->answers = [null];
$result = $this->redirect()->enableRedirect('https://www.example.de');
self::assertFalse($result['ok']);
self::assertStringContainsString('bleibt die Umleitung aus', $result['message']);
self::assertSame($this->shipped, (string) file_get_contents($this->root.'/public/.htaccess'));
$result = $this->redirect()->enableRedirect('http://www.example.de');
self::assertFalse($result['ok']);
self::assertStringContainsString('https://', $result['message']);
}
public function testALoopIsTakenBackAtOnce(): void
{
// Der Server meldet HTTPS nicht an die .htaccess: mit den Zeilen leitet auch https://
// auf sich selbst um, die Probe danach kommt nicht mehr an.
$this->answers = ['ok', null];
$result = $this->redirect()->enableRedirect('https://www.example.de');
self::assertFalse($result['ok']);
self::assertStringContainsString('zurückgesetzt', $result['message']);
self::assertSame($this->shipped, (string) file_get_contents($this->root.'/public/.htaccess'));
}
public function testAServerThatIgnoresTheRuleIsNamed(): void
{
$this->answers = ['ok', 'ok'];
$this->http = ['status' => 200, 'location' => '', 'error' => ''];
$result = $this->redirect()->enableRedirect('https://www.example.de');
self::assertTrue($result['ok']);
self::assertStringContainsString('antwortet mit 200', $result['message']);
}
public function testHstsOnlyAfterTheRedirect(): void
{
$this->answers = ['ok'];
$result = $this->redirect()->switchHsts(true, 'https://www.example.de');
self::assertFalse($result['ok']);
self::assertStringContainsString('erst, wenn die Umleitung', $result['message']);
$this->answers = ['ok', 'ok', 'ok'];
$this->redirect()->enableRedirect('https://www.example.de');
self::assertTrue($this->redirect()->switchHsts(true, 'https://www.example.de')['ok']);
self::assertSame(['redirect' => true, 'hsts' => true, 'found' => true, 'writable' => true], $this->redirect()->state());
// Die Umleitung aus nimmt HSTS mit.
$this->redirect()->disableRedirect();
self::assertSame($this->shipped, (string) file_get_contents($this->root.'/public/.htaccess'));
}
public function testAnUpdateKeepsWhatWasSwitchedOn(): void
{
$this->answers = ['ok', 'ok', 'ok'];
$this->redirect()->enableRedirect('https://www.example.de');
$this->redirect()->switchHsts(true, 'https://www.example.de');
$merged = HttpsRedirect::keepEnabled($this->shipped, (string) file_get_contents($this->root.'/public/.htaccess'));
self::assertTrue(HttpsRedirect::active($merged, HttpsRedirect::REDIRECT_LINES));
self::assertTrue(HttpsRedirect::active($merged, [HttpsRedirect::HSTS_LINE]));
self::assertSame($this->shipped, HttpsRedirect::keepEnabled($this->shipped, $this->shipped));
}
public function testTheGitUpdaterDoesNotCallTheSwitchedFileALocalChange(): void
{
$dirty = new \ReflectionMethod(\App\Ops\Updater::class, 'dirty');
self::assertSame([], $dirty->invoke(null, " M public/.htaccess\n"));
self::assertSame([], $dirty->invoke(null, 'M public/.htaccess'), 'Auch wenn die Ausgabe vorn gekürzt ist.');
self::assertSame([' M src/Kernel.php'], $dirty->invoke(null, " M public/.htaccess\n M src/Kernel.php\n"));
self::assertSame(['?? public/.htaccess.bak'], $dirty->invoke(null, '?? public/.htaccess.bak'));
}
private function redirect(): HttpsRedirect
{
$test = $this;
$download = new class($test) extends Download {
public function __construct(private HttpsRedirectTest $test)
{
parent::__construct();
}
public function get(string $url): ?string
{
return $this->test->nextAnswer();
}
};
$check = new OriginCheck('geheim', $download);
$this->token = $check->token();
return new HttpsRedirect($this->root, $check, fn (string $url): array => $this->http);
}
private string $token = '';
public function nextAnswer(): ?string
{
self::assertNotSame([], $this->answers, 'Mehr Proben als erwartet.');
$answer = array_shift($this->answers);
return null === $answer ? null : $this->token;
}
}