*/ 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; } }