diff --git a/MILESTONES.md b/MILESTONES.md index e5a5a27..f6c1e2d 100644 --- a/MILESTONES.md +++ b/MILESTONES.md @@ -12,10 +12,10 @@ This document outlines the phased development roadmap for the Atlas Routing engi ## Milestone 2: Basic URI Matching & Methods *Goal: Implement the matching engine for standard HTTP methods and static URIs.* -- [ ] Implement fluent methods: `get()`, `post()`, `put()`, `patch()`, `delete()`. -- [ ] Build the URI Matcher for static paths. -- [ ] Support for PSR-7 `ServerRequestInterface` type-hinting in the matcher. -- [ ] Implement basic Error Handling (Global 404). +- [x] Implement fluent methods: `get()`, `post()`, `put()`, `patch()`, `delete()`. +- [x] Build the URI Matcher for static paths. +- [x] Support for PSR-7 `ServerRequestInterface` type-hinting in the matcher. +- [x] Implement basic Error Handling (Global 404). ## Milestone 3: Parameters & Validation *Goal: Support for dynamic URIs with the `{{var}}` syntax and parameter validation.* diff --git a/src/NotFoundRouteException.php b/src/NotFoundRouteException.php new file mode 100644 index 0000000..be97806 --- /dev/null +++ b/src/NotFoundRouteException.php @@ -0,0 +1,7 @@ +getMethod()); + $path = $this->normalizePath($request->getUri()->getPath()); + + foreach ($this->getRoutes() as $routeDefinition) { + if (strtoupper($routeDefinition->getMethod()) === $method && $routeDefinition->getPath() === $path) { + return $routeDefinition; + } + } + + throw new NotFoundRouteException('No route matched the request'); + } + + public function fallback(mixed $handler): self + { + $this->fallbackHandler = $handler; + return $this; + } + public function url(string $name, array $parameters = []): string { $routes = $this->getRoutes(); diff --git a/tests/Unit/ErrorHandlingTest.php b/tests/Unit/ErrorHandlingTest.php new file mode 100644 index 0000000..620b5a4 --- /dev/null +++ b/tests/Unit/ErrorHandlingTest.php @@ -0,0 +1,161 @@ + ['/path/to/modules'] + ]); + + $router = new Router($config); + + $uri = $this->createMock(UriInterface::class); + $uri->method('getPath')->willReturn('/nonexistent'); + $uri->method('getScheme')->willReturn('http'); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(80); + + $request = $this->createMock(ServerRequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + + $this->expectException(\Atlas\NotFoundRouteException::class); + + $router->matchOrFail($request); + } + + public function testMatchReturnsNullWhenNoRouteFound(): void + { + $config = new \Atlas\Config([ + 'modules_path' => ['/path/to/modules'] + ]); + + $router = new Router($config); + + $uri = $this->createMock(UriInterface::class); + $uri->method('getPath')->willReturn('/nonexistent'); + $uri->method('getScheme')->willReturn('http'); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(80); + + $request = $this->createMock(ServerRequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + + $result = $router->match($request); + + $this->assertNull($result); + } + + public function testRouteChainingWithDifferentHttpMethods(): void + { + $config = new \Atlas\Config([ + 'modules_path' => ['/path/to/modules'] + ]); + + $router = new Router($config); + + $result = $router->get('/test', 'GetHandler')->post('/test', 'PostHandler'); + + $this->assertTrue($result instanceof Router); + $this->assertCount(2, $router->getRoutes()); + } + + public function testMatchUsingRouteDefinition(): void + { + $config = new \Atlas\Config([ + 'modules_path' => ['/path/to/modules'] + ]); + + $router = new Router($config); + + $router->get('/test', 'TestMethod'); + + $routes = $router->getRoutes(); + $this->assertCount(1, $routes); + $this->assertInstanceOf(\Atlas\RouteDefinition::class, $routes[0]); + } + + public function testCaseInsensitiveHttpMethodMatching(): void + { + $config = new \Atlas\Config([ + 'modules_path' => ['/path/to/modules'] + ]); + + $router = new Router($config); + + $router->get('/test', 'TestHandler'); + + $uri = $this->createMock(UriInterface::class); + $uri->method('getPath')->willReturn('/test'); + $uri->method('getScheme')->willReturn('http'); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(80); + + $request = $this->createMock(ServerRequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + + $matchedRoute = $router->match($request); + + $this->assertNotNull($matchedRoute); + } + + public function testPathNormalizationLeadingSlashes(): void + { + $config = new \Atlas\Config([ + 'modules_path' => ['/path/to/modules'] + ]); + + $router = new Router($config); + + $router->get('/test', 'TestHandler'); + + $uri = $this->createMock(UriInterface::class); + $uri->method('getPath')->willReturn('/test'); + $uri->method('getScheme')->willReturn('http'); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(80); + + $request = $this->createMock(ServerRequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + + $matchedRoute = $router->match($request); + + $this->assertNotNull($matchedRoute); + } + + public function testMatchOrFailThrowsExceptionForMultipleRoutes(): void + { + $config = new \Atlas\Config([ + 'modules_path' => ['/path/to/modules'] + ]); + + $router = new Router($config); + + $router->get('/test', 'TestHandler'); + + $uri = $this->createMock(UriInterface::class); + $uri->method('getPath')->willReturn('/test'); + $uri->method('getScheme')->willReturn('http'); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(80); + + $request = $this->createMock(ServerRequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + + $matchedRoute = $router->matchOrFail($request); + + $this->assertInstanceOf(\Atlas\RouteDefinition::class, $matchedRoute); + } +} \ No newline at end of file