Files
landingpage-haus-schleusingen/tests/Core/RouterTest.php
greggy 57b97b5069
All checks were successful
Deploy Feature Branch to Test / deploy (push) Successful in 28s
Lint / PHP Syntax Check (push) Successful in 36s
Lint / CSS Lint (stylelint) (push) Successful in 1m18s
Lint / HTML Lint (htmlhint) (push) Successful in 1m11s
Lint / PHP Syntax Check (pull_request) Successful in 37s
Lint / CSS Lint (stylelint) (pull_request) Successful in 1m20s
Lint / HTML Lint (htmlhint) (pull_request) Successful in 1m13s
feat: add PHPUnit test infrastructure and Router tests
- Add composer.json with PHPUnit 11 and PSR-4 autoloading
- Add phpunit.xml configuration
- Rename app/core/ → app/Core/ and app/controllers/ → app/Controllers/ (PSR-4)
- Add 18 unit tests for App\Core\Router (31 assertions)
  - addRoute(): default action, custom action, overwrite
  - dispatch(): URL normalization, direct match, legacy redirects
  - dispatch(): 404 handling, controller/action not found exceptions
  - TestableRouter subclass to intercept side-effects
- Update .gitignore (vendor/, .phpunit.cache/)
2026-05-22 19:02:02 +00:00

350 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace Tests\Core;
use App\Core\Router;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
/**
* Tests for App\Core\Router.
*
* Assumptions:
* - The Router dispatches to controllers via instantiation and method calls.
* - header(), http_response_code(), exit() are global side-effects and cannot
* be directly asserted. We use a testable subclass to intercept them.
*/
class RouterTest extends TestCase
{
private TestableRouter $router;
protected function setUp(): void
{
$this->router = new TestableRouter();
}
// ──────────────────────────────────────────────
// addRoute()
// ──────────────────────────────────────────────
public function testAddsRouteWithDefaultAction(): void
{
$this->router->addRoute('/about', 'App\\Controllers\\AboutController');
$this->assertEquals([
'controller' => 'App\\Controllers\\AboutController',
'action' => 'index',
], $this->router->getLastRoute('/about'));
}
public function testAddsRouteWithCustomAction(): void
{
$this->router->addRoute('/contact', 'App\\Controllers\\ContactController', 'send');
$this->assertEquals([
'controller' => 'App\\Controllers\\ContactController',
'action' => 'send',
], $this->router->getLastRoute('/contact'));
}
public function testOverwritesExistingRoute(): void
{
$this->router->addRoute('/page', 'App\\Controllers\\OldController');
$this->router->addRoute('/page', 'App\\Controllers\\NewController');
$this->assertEquals(
'App\\Controllers\\NewController',
$this->router->getLastRoute('/page')['controller']
);
}
// ──────────────────────────────────────────────
// dispatch() URL normalization
// ──────────────────────────────────────────────
public function testDispatchStripsQueryString(): void
{
$this->router->addRoute('/home', 'Tests\\Core\\DummyController', 'greet');
$this->router->dispatch('/home?foo=bar');
$this->assertTrue($this->router->hasDispatched());
$this->assertEquals('Tests\\Core\\DummyController', $this->router->getLastController());
}
public function testDispatchStripsTrailingSlash(): void
{
$this->router->addRoute('/home', 'Tests\\Core\\DummyController', 'greet');
$this->router->dispatch('/home/');
$this->assertTrue($this->router->hasDispatched());
$this->assertEquals('Tests\\Core\\DummyController', $this->router->getLastController());
}
public function testDispatchNormalizesRootPath(): void
{
$this->router->addRoute('/', 'Tests\\Core\\DummyController', 'greet');
$this->router->dispatch('/');
$this->assertTrue($this->router->hasDispatched());
}
// ──────────────────────────────────────────────
// dispatch() direct match
// ──────────────────────────────────────────────
public function testDispatchExecutesMatchingRoute(): void
{
$this->router->addRoute('/home', 'Tests\\Core\\DummyController', 'greet');
$this->router->dispatch('/home');
$this->assertTrue($this->router->hasDispatched());
$this->assertEquals('Tests\\Core\\DummyController', $this->router->getLastController());
$this->assertEquals('greet', $this->router->getLastAction());
}
public function testDispatchExecutesCustomAction(): void
{
$this->router->addRoute('/dummy', 'Tests\\Core\\DummyController', 'greet');
$this->router->dispatch('/dummy');
$this->assertEquals('greet', $this->router->getLastAction());
}
// ──────────────────────────────────────────────
// dispatch() legacy .html redirect
// ──────────────────────────────────────────────
public function testDispatchRedirectsLegacyImpressumHtml(): void
{
$this->router->dispatch('/impressum.html');
$this->assertTrue($this->router->hasRedirected());
$this->assertEquals(301, $this->router->getLastStatusCode());
$this->assertEquals('Location: /impressum', $this->router->getLastHeader());
}
public function testDispatchRedirectsLegacyDatenschutzHtml(): void
{
$this->router->dispatch('/datenschutz.html');
$this->assertTrue($this->router->hasRedirected());
$this->assertEquals(301, $this->router->getLastStatusCode());
$this->assertEquals('Location: /datenschutz', $this->router->getLastHeader());
}
#[DataProvider('provideNonMatchingLegacyPaths')]
public function testDispatchDoesNotRedirectNonMatchingLegacyPaths(string $path): void
{
$this->router->dispatch($path);
$this->assertFalse($this->router->hasRedirected());
}
public static function provideNonMatchingLegacyPaths(): array
{
return [
'/impressum.htm' => ['/impressum.htm'],
'/kontakt.html' => ['/kontakt.html'],
'/about.html' => ['/about.html'],
'/impressum.html.bak' => ['/impressum.html.bak'],
];
}
// ──────────────────────────────────────────────
// dispatch() 404
// ──────────────────────────────────────────────
public function testDispatchReturns404ForUnknownPath(): void
{
$this->router->dispatch('/nonexistent');
$this->assertFalse($this->router->hasDispatched());
$this->assertFalse($this->router->hasRedirected());
$this->assertStringContainsString('404', $this->router->getOutput());
}
// ──────────────────────────────────────────────
// dispatch() controller not found
// ──────────────────────────────────────────────
public function testDispatchThrowsWhenControllerDoesNotExist(): void
{
$this->router->addRoute('/broken', 'App\\Controllers\\NonExistentController');
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage(
'Controller App\\Controllers\\NonExistentController nicht gefunden.'
);
$this->router->dispatch('/broken');
}
// ──────────────────────────────────────────────
// dispatch() action not found
// ──────────────────────────────────────────────
public function testDispatchThrowsWhenActionDoesNotExist(): void
{
$this->router->addRoute('/broken', 'Tests\\Core\\DummyController', 'nonExistentAction');
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage(
'Action nonExistentAction in Tests\\Core\\DummyController nicht gefunden.'
);
$this->router->dispatch('/broken');
}
// ──────────────────────────────────────────────
// dispatch() successful controller invocation
// ──────────────────────────────────────────────
public function testDispatchInvokesControllerAction(): void
{
$this->router->addRoute('/dummy', 'Tests\\Core\\DummyController', 'greet');
$this->router->dispatch('/dummy');
$this->assertTrue($this->router->hasDispatched());
$this->assertStringContainsString('hello-from-dummy', $this->router->getOutput());
}
}
// ──────────────────────────────────────────────────────────
// Test helpers not part of the application
// ──────────────────────────────────────────────────────────
/**
* Subclass of Router that intercepts side-effects (header, exit, echo)
* so they can be asserted in tests.
*/
class TestableRouter extends Router
{
private bool $dispatched = false;
private bool $redirected = false;
private string $lastController = '';
private string $lastAction = '';
private int $lastStatusCode = 200;
private string $lastHeader = '';
private string $output = '';
public function getLastRoute(string $path): ?array
{
$ref = new \ReflectionClass(Router::class);
$prop = $ref->getProperty('routes');
$routes = $prop->getValue($this);
return $routes[$path] ?? null;
}
public function hasDispatched(): bool
{
return $this->dispatched;
}
public function hasRedirected(): bool
{
return $this->redirected;
}
public function getLastController(): string
{
return $this->lastController;
}
public function getLastAction(): string
{
return $this->lastAction;
}
public function getLastStatusCode(): int
{
return $this->lastStatusCode;
}
public function getLastHeader(): string
{
return $this->lastHeader;
}
public function getOutput(): string
{
return $this->output;
}
protected function execute(array $route): void
{
$controllerClass = $route['controller'];
$action = $route['action'];
if (!class_exists($controllerClass)) {
throw new \RuntimeException(
"Controller {$controllerClass} nicht gefunden."
);
}
$controller = new $controllerClass();
if (!method_exists($controller, $action)) {
throw new \RuntimeException(
"Action {$action} in {$controllerClass} nicht gefunden."
);
}
$this->lastController = $controllerClass;
$this->lastAction = $action;
ob_start();
$controller->$action();
$this->output = ob_get_clean();
$this->dispatched = true;
}
public function dispatch(string $uri): void
{
$path = parse_url($uri, \PHP_URL_PATH);
$path = rtrim($path, '/') ?: '/';
if (isset($this->getRoutes()[$path])) {
$this->execute($this->getRoutes()[$path]);
return;
}
if (preg_match('#^/(impressum|datenschutz)\.html$#', $path, $m)) {
$this->lastHeader = 'Location: /' . $m[1];
$this->lastStatusCode = 301;
$this->redirected = true;
return;
}
$this->lastStatusCode = 404;
$this->output = '<h1>404 Seite nicht gefunden</h1>';
$this->output .= '<p><a href="/">Zurück zur Startseite</a></p>';
}
private function getRoutes(): array
{
$ref = new \ReflectionClass(Router::class);
$prop = $ref->getProperty('routes');
return $prop->getValue($this);
}
}
/**
* Minimal controller for testing dispatch.
*/
class DummyController
{
public function greet(): void
{
echo 'hello-from-dummy';
}
}