aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/Fixtures/ClassLevelController.php18
-rw-r--r--tests/Fixtures/FakeController.php33
-rw-r--r--tests/Fixtures/FakeMiddleware.php30
-rw-r--r--tests/Fixtures/FakeResolver.php28
-rw-r--r--tests/Fixtures/TestRequest.php40
-rw-r--r--tests/Fixtures/TestResponse.php43
-rw-r--r--tests/MiddlewarePipelineTest.php38
-rw-r--r--tests/RouteLoaderTest.php57
-rw-r--r--tests/RouteMatcherTest.php68
-rw-r--r--tests/RouterTest.php81
-rw-r--r--tests/bootstrap.php36
11 files changed, 472 insertions, 0 deletions
diff --git a/tests/Fixtures/ClassLevelController.php b/tests/Fixtures/ClassLevelController.php
new file mode 100644
index 0000000..88ef6e6
--- /dev/null
+++ b/tests/Fixtures/ClassLevelController.php
@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+namespace BrettParson\MiniRoute\Tests\Fixtures;
+
+use BrettParson\MiniRoute\Attribute\Get;
+use BrettParson\MiniRoute\Attribute\Middleware;
+
+#[Middleware(FakeMiddleware::class)]
+final class ClassLevelController
+{
+ #[Get('/admin')]
+ public function dashboard(): string
+ {
+ return 'dashboard';
+ }
+}
diff --git a/tests/Fixtures/FakeController.php b/tests/Fixtures/FakeController.php
new file mode 100644
index 0000000..3b476ae
--- /dev/null
+++ b/tests/Fixtures/FakeController.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace BrettParson\MiniRoute\Tests\Fixtures;
+
+use BrettParson\MiniRoute\Attribute\Get;
+use BrettParson\MiniRoute\Attribute\Middleware;
+use BrettParson\MiniRoute\Attribute\Post;
+use BrettParson\MiniRoute\Http\RequestInterface;
+use BrettParson\MiniRoute\Http\ResponseInterface;
+
+final class FakeController
+{
+ #[Get('/entries')]
+ public function index(RequestInterface $request): ResponseInterface
+ {
+ return new TestResponse(200, 'index');
+ }
+
+ #[Get('/entries/{slug}')]
+ public function show(RequestInterface $request): ResponseInterface
+ {
+ return new TestResponse(200, 'show:' . $request->param('slug'));
+ }
+
+ #[Post('/entries')]
+ #[Middleware(FakeMiddleware::class)]
+ public function store(RequestInterface $request): ResponseInterface
+ {
+ return new TestResponse(200, 'store');
+ }
+}
diff --git a/tests/Fixtures/FakeMiddleware.php b/tests/Fixtures/FakeMiddleware.php
new file mode 100644
index 0000000..d684e7c
--- /dev/null
+++ b/tests/Fixtures/FakeMiddleware.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace BrettParson\MiniRoute\Tests\Fixtures;
+
+use ArrayObject;
+use BrettParson\MiniRoute\Http\RequestInterface;
+use BrettParson\MiniRoute\Http\ResponseInterface;
+use BrettParson\MiniRoute\Middleware\MiddlewareInterface;
+
+final class FakeMiddleware implements MiddlewareInterface
+{
+ /** @var ArrayObject<int, string>|null */
+ private ?ArrayObject $log;
+
+ public function __construct(
+ private readonly string $name = 'fake',
+ ?ArrayObject $log = null,
+ ) {
+ $this->log = $log;
+ }
+
+ public function handle(RequestInterface $request, callable $next): ResponseInterface
+ {
+ $this->log?->append($this->name);
+
+ return $next($request);
+ }
+}
diff --git a/tests/Fixtures/FakeResolver.php b/tests/Fixtures/FakeResolver.php
new file mode 100644
index 0000000..af6ec1f
--- /dev/null
+++ b/tests/Fixtures/FakeResolver.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace BrettParson\MiniRoute\Tests\Fixtures;
+
+use BrettParson\MiniRoute\Controller\ControllerResolverInterface;
+use RuntimeException;
+
+final class FakeResolver implements ControllerResolverInterface
+{
+ /** @var array<string, object> */
+ private array $bindings = [];
+
+ public function bind(string $class, object $instance): void
+ {
+ $this->bindings[$class] = $instance;
+ }
+
+ public function resolve(string $class): object
+ {
+ if (!array_key_exists($class, $this->bindings)) {
+ throw new RuntimeException("No binding for {$class}");
+ }
+
+ return $this->bindings[$class];
+ }
+}
diff --git a/tests/Fixtures/TestRequest.php b/tests/Fixtures/TestRequest.php
new file mode 100644
index 0000000..c449610
--- /dev/null
+++ b/tests/Fixtures/TestRequest.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+namespace BrettParson\MiniRoute\Tests\Fixtures;
+
+use BrettParson\MiniRoute\Http\RequestInterface;
+
+final class TestRequest implements RequestInterface
+{
+ /**
+ * @param array<string, string> $params
+ */
+ public function __construct(
+ private readonly string $method,
+ private readonly string $path,
+ private readonly array $params = [],
+ ) {
+ }
+
+ public function method(): string
+ {
+ return $this->method;
+ }
+
+ public function path(): string
+ {
+ return $this->path;
+ }
+
+ public function param(string $name, string $default = ''): string
+ {
+ return $this->params[$name] ?? $default;
+ }
+
+ public function withParams(array $params): RequestInterface
+ {
+ return new self($this->method, $this->path, $params);
+ }
+}
diff --git a/tests/Fixtures/TestResponse.php b/tests/Fixtures/TestResponse.php
new file mode 100644
index 0000000..25a0307
--- /dev/null
+++ b/tests/Fixtures/TestResponse.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+namespace BrettParson\MiniRoute\Tests\Fixtures;
+
+use BrettParson\MiniRoute\Http\ResponseInterface;
+
+final class TestResponse implements ResponseInterface
+{
+ /**
+ * @param array<string, string> $headers
+ */
+ public function __construct(
+ private readonly int $status,
+ private readonly string $body,
+ private readonly array $headers = [],
+ ) {
+ }
+
+ public function status(): int
+ {
+ return $this->status;
+ }
+
+ public function body(): string
+ {
+ return $this->body;
+ }
+
+ public function withHeader(string $name, string $value): ResponseInterface
+ {
+ return new self($this->status, $this->body, array_merge($this->headers, [$name => $value]));
+ }
+
+ /**
+ * @return array<string, string>
+ */
+ public function headers(): array
+ {
+ return $this->headers;
+ }
+}
diff --git a/tests/MiddlewarePipelineTest.php b/tests/MiddlewarePipelineTest.php
new file mode 100644
index 0000000..107dce3
--- /dev/null
+++ b/tests/MiddlewarePipelineTest.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace BrettParson\MiniRoute\Tests;
+
+use ArrayObject;
+use BrettParson\MiniRoute\Http\RequestInterface;
+use BrettParson\MiniRoute\Http\ResponseInterface;
+use BrettParson\MiniRoute\Middleware\MiddlewarePipeline;
+use BrettParson\MiniRoute\Tests\Fixtures\FakeMiddleware;
+use BrettParson\MiniRoute\Tests\Fixtures\TestRequest;
+use BrettParson\MiniRoute\Tests\Fixtures\TestResponse;
+use PHPUnit\Framework\TestCase;
+
+final class MiddlewarePipelineTest extends TestCase
+{
+ public function testMiddlewareRunsOutermostFirstAndCoreLast(): void
+ {
+ $log = new ArrayObject();
+
+ $outer = new FakeMiddleware('outer', $log);
+ $inner = new FakeMiddleware('inner', $log);
+
+ $core = static function (RequestInterface $request) use ($log): ResponseInterface {
+ $log->append('core');
+
+ return new TestResponse(200, 'ok');
+ };
+
+ $pipeline = MiddlewarePipeline::compose([$outer, $inner], $core);
+
+ $response = $pipeline(new TestRequest('GET', '/'));
+
+ $this->assertSame(['outer', 'inner', 'core'], $log->getArrayCopy());
+ $this->assertSame(200, $response->status());
+ }
+}
diff --git a/tests/RouteLoaderTest.php b/tests/RouteLoaderTest.php
new file mode 100644
index 0000000..c20f58b
--- /dev/null
+++ b/tests/RouteLoaderTest.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+namespace BrettParson\MiniRoute\Tests;
+
+use BrettParson\MiniRoute\Routing\RouteLoader;
+use BrettParson\MiniRoute\Tests\Fixtures\ClassLevelController;
+use BrettParson\MiniRoute\Tests\Fixtures\FakeController;
+use BrettParson\MiniRoute\Tests\Fixtures\FakeMiddleware;
+use PHPUnit\Framework\TestCase;
+
+final class RouteLoaderTest extends TestCase
+{
+ public function testLoadsRouteAttributesFromControllerMethods(): void
+ {
+ $routes = (new RouteLoader())->load(FakeController::class);
+
+ $indexed = [];
+
+ foreach ($routes as $route) {
+ $indexed[$route->method . ' ' . $route->path] = $route;
+ }
+
+ $this->assertCount(3, $routes);
+
+ $this->assertSame('GET', $indexed['GET /entries']->method);
+ $this->assertSame('index', $indexed['GET /entries']->controllerMethod);
+ $this->assertSame([], $indexed['GET /entries']->middleware);
+
+ $this->assertSame('show', $indexed['GET /entries/{slug}']->controllerMethod);
+ }
+
+ public function testMethodMiddlewareIsAttached(): void
+ {
+ $routes = (new RouteLoader())->load(FakeController::class);
+
+ $post = null;
+
+ foreach ($routes as $route) {
+ if ($route->method === 'POST') {
+ $post = $route;
+ }
+ }
+
+ $this->assertNotNull($post);
+ $this->assertSame([FakeMiddleware::class], $post->middleware);
+ }
+
+ public function testClassLevelMiddlewareIsInheritedByMethods(): void
+ {
+ $routes = (new RouteLoader())->load(ClassLevelController::class);
+
+ $this->assertCount(1, $routes);
+ $this->assertSame([FakeMiddleware::class], $routes[0]->middleware);
+ }
+}
diff --git a/tests/RouteMatcherTest.php b/tests/RouteMatcherTest.php
new file mode 100644
index 0000000..5181c50
--- /dev/null
+++ b/tests/RouteMatcherTest.php
@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+
+namespace BrettParson\MiniRoute\Tests;
+
+use BrettParson\MiniRoute\Routing\RouteDef;
+use BrettParson\MiniRoute\Routing\RouteMatcher;
+use PHPUnit\Framework\TestCase;
+
+final class RouteMatcherTest extends TestCase
+{
+ private RouteMatcher $matcher;
+
+ protected function setUp(): void
+ {
+ $this->matcher = new RouteMatcher();
+ }
+
+ private function route(string $method, string $path): RouteDef
+ {
+ return new RouteDef($method, $path, 'C', 'm');
+ }
+
+ public function testMatchesExactPath(): void
+ {
+ $route = $this->route('GET', '/entries');
+
+ $this->assertTrue($this->matcher->matches($route, 'get', '/entries'));
+ $this->assertFalse($this->matcher->matches($route, 'POST', '/entries'));
+ $this->assertFalse($this->matcher->matches($route, 'GET', '/entries/1'));
+ }
+
+ public function testMatchesParameterizedPath(): void
+ {
+ $route = $this->route('GET', '/entries/{slug}');
+
+ $this->assertTrue($this->matcher->matches($route, 'GET', '/entries/hello-world'));
+ $this->assertFalse($this->matcher->matches($route, 'GET', '/entries/hello/world'));
+ }
+
+ public function testExtractsNamedParameters(): void
+ {
+ $route = $this->route('GET', '/entries/{slug}');
+
+ $this->assertSame(
+ ['slug' => 'hello-world'],
+ $this->matcher->extractParams($route, '/entries/hello-world'),
+ );
+ }
+
+ public function testLiteralBeatsParameter(): void
+ {
+ $literal = $this->route('GET', '/entries/archive');
+ $param = $this->route('GET', '/entries/{slug}');
+
+ $this->assertLessThan(0, $this->matcher->compare($literal, $param));
+ $this->assertGreaterThan(0, $this->matcher->compare($param, $literal));
+ }
+
+ public function testExactBeatsParameterAtSamePosition(): void
+ {
+ $exact = $this->route('POST', '/admin/entries/preview');
+ $param = $this->route('POST', '/admin/entries/{id}');
+
+ $this->assertLessThan(0, $this->matcher->compare($exact, $param));
+ }
+}
diff --git a/tests/RouterTest.php b/tests/RouterTest.php
new file mode 100644
index 0000000..b8d4d49
--- /dev/null
+++ b/tests/RouterTest.php
@@ -0,0 +1,81 @@
+<?php
+
+declare(strict_types=1);
+
+namespace BrettParson\MiniRoute\Tests;
+
+use ArrayObject;
+use BrettParson\MiniRoute\Routing\RouteNotFoundException;
+use BrettParson\MiniRoute\Routing\RouteRegistrationException;
+use BrettParson\MiniRoute\Routing\Router;
+use BrettParson\MiniRoute\Tests\Fixtures\FakeController;
+use BrettParson\MiniRoute\Tests\Fixtures\FakeMiddleware;
+use BrettParson\MiniRoute\Tests\Fixtures\FakeResolver;
+use BrettParson\MiniRoute\Tests\Fixtures\TestRequest;
+use PHPUnit\Framework\TestCase;
+
+final class RouterTest extends TestCase
+{
+ /**
+ * @return array{Router, FakeResolver, ArrayObject}
+ */
+ private function makeRouter(): array
+ {
+ $resolver = new FakeResolver();
+ $resolver->bind(FakeController::class, new FakeController());
+
+ $log = new ArrayObject();
+ $resolver->bind(FakeMiddleware::class, new FakeMiddleware('m', $log));
+
+ return [new Router($resolver), $resolver, $log];
+ }
+
+ public function testDispatchesToController(): void
+ {
+ [$router] = $this->makeRouter();
+ $router->registerController(FakeController::class);
+
+ $response = $router->dispatch(new TestRequest('GET', '/entries'));
+
+ $this->assertSame(200, $response->status());
+ $this->assertSame('index', $response->body());
+ }
+
+ public function testInjectsRouteParameters(): void
+ {
+ [$router] = $this->makeRouter();
+ $router->registerController(FakeController::class);
+
+ $response = $router->dispatch(new TestRequest('GET', '/entries/hello-world'));
+
+ $this->assertSame('show:hello-world', $response->body());
+ }
+
+ public function testAppliesRouteMiddleware(): void
+ {
+ [$router, , $log] = $this->makeRouter();
+ $router->registerController(FakeController::class);
+
+ $router->dispatch(new TestRequest('POST', '/entries'));
+
+ $this->assertSame(['m'], $log->getArrayCopy());
+ }
+
+ public function testDuplicateRouteThrows(): void
+ {
+ [$router] = $this->makeRouter();
+ $router->registerController(FakeController::class);
+
+ $this->expectException(RouteRegistrationException::class);
+ $router->registerController(FakeController::class);
+ }
+
+ public function testNotFoundThrowsWithoutHandler(): void
+ {
+ [$router] = $this->makeRouter();
+ $router->registerController(FakeController::class);
+
+ $this->expectException(RouteNotFoundException::class);
+ $router->dispatch(new TestRequest('GET', '/missing'));
+ }
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000..f49f712
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * Test bootstrap — registers a small PSR-4 autoloader so the library and its
+ * test fixtures can be loaded without requiring a Composer install. If
+ * vendor/autoload.php exists (Composer installed), prefer it.
+ */
+$vendorAutoload = dirname(__DIR__) . '/vendor/autoload.php';
+
+if (is_file($vendorAutoload)) {
+ require $vendorAutoload;
+} else {
+ spl_autoload_register(static function (string $class): void {
+ $prefixes = [
+ 'BrettParson\\MiniRoute\\Tests\\' => __DIR__ . '/',
+ 'BrettParson\\MiniRoute\\' => dirname(__DIR__) . '/src/',
+ ];
+
+ foreach ($prefixes as $prefix => $baseDir) {
+ if (!str_starts_with($class, $prefix)) {
+ continue;
+ }
+
+ $relative = substr($class, strlen($prefix));
+ $file = $baseDir . str_replace('\\', '/', $relative) . '.php';
+
+ if (is_file($file)) {
+ require $file;
+
+ return;
+ }
+ }
+ });
+}