feat: implement milestone 3 - comprehensive test coverage
This commit is contained in:
parent
ab7719a39f
commit
7152e2b3e7
|
|
@ -22,7 +22,7 @@ class Config implements ArrayAccess, IteratorAggregate
|
||||||
* @param array $options Configuration array containing routing settings
|
* @param array $options Configuration array containing routing settings
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly array $options
|
private array $options
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ namespace Atlas\Router;
|
||||||
*/
|
*/
|
||||||
class RouteGroup
|
class RouteGroup
|
||||||
{
|
{
|
||||||
|
use PathHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new RouteGroup instance.
|
* Constructs a new RouteGroup instance.
|
||||||
*
|
*
|
||||||
|
|
@ -34,74 +36,131 @@ class RouteGroup
|
||||||
return $self;
|
return $self;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function get(string $path, mixed $handler, string|null $name = null): RouteDefinition
|
||||||
* Registers a GET route with group prefix.
|
|
||||||
*
|
|
||||||
* @param string $path URI path
|
|
||||||
* @param string|callable $handler Route handler
|
|
||||||
* @param string|null $name Optional route name
|
|
||||||
* @return self Fluent interface
|
|
||||||
*/
|
|
||||||
public function get(string $path, string|callable $handler, string|null $name = null): self
|
|
||||||
{
|
{
|
||||||
$fullPath = $this->buildFullPath($path);
|
$fullPath = $this->buildFullPath($path);
|
||||||
return $this->router ? $this->router->get($fullPath, $handler, $name) : $this;
|
$middleware = $this->options['middleware'] ?? [];
|
||||||
|
$validation = $this->options['validation'] ?? [];
|
||||||
|
$defaults = $this->options['defaults'] ?? [];
|
||||||
|
|
||||||
|
if ($this->router) {
|
||||||
|
return $this->router->registerCustomRoute('GET', $fullPath, $handler, $name, $middleware, $validation, $defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RouteDefinition('GET', $fullPath, $fullPath, $handler, $name, $middleware, $validation, $defaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function post(string $path, mixed $handler, string|null $name = null): RouteDefinition
|
||||||
* Registers a POST route with group prefix.
|
|
||||||
*
|
|
||||||
* @param string $path URI path
|
|
||||||
* @param string|callable $handler Route handler
|
|
||||||
* @param string|null $name Optional route name
|
|
||||||
* @return self Fluent interface
|
|
||||||
*/
|
|
||||||
public function post(string $path, string|callable $handler, string|null $name = null): self
|
|
||||||
{
|
{
|
||||||
$fullPath = $this->buildFullPath($path);
|
$fullPath = $this->buildFullPath($path);
|
||||||
return $this->router ? $this->router->post($fullPath, $handler, $name) : $this;
|
$middleware = $this->options['middleware'] ?? [];
|
||||||
|
$validation = $this->options['validation'] ?? [];
|
||||||
|
$defaults = $this->options['defaults'] ?? [];
|
||||||
|
|
||||||
|
if ($this->router) {
|
||||||
|
return $this->router->registerCustomRoute('POST', $fullPath, $handler, $name, $middleware, $validation, $defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RouteDefinition('POST', $fullPath, $fullPath, $handler, $name, $middleware, $validation, $defaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function put(string $path, mixed $handler, string|null $name = null): RouteDefinition
|
||||||
* Registers a PUT route with group prefix.
|
|
||||||
*
|
|
||||||
* @param string $path URI path
|
|
||||||
* @param string|callable $handler Route handler
|
|
||||||
* @param string|null $name Optional route name
|
|
||||||
* @return self Fluent interface
|
|
||||||
*/
|
|
||||||
public function put(string $path, string|callable $handler, string|null $name = null): self
|
|
||||||
{
|
{
|
||||||
$fullPath = $this->buildFullPath($path);
|
$fullPath = $this->buildFullPath($path);
|
||||||
return $this->router ? $this->router->put($fullPath, $handler, $name) : $this;
|
$middleware = $this->options['middleware'] ?? [];
|
||||||
|
$validation = $this->options['validation'] ?? [];
|
||||||
|
$defaults = $this->options['defaults'] ?? [];
|
||||||
|
|
||||||
|
if ($this->router) {
|
||||||
|
return $this->router->registerCustomRoute('PUT', $fullPath, $handler, $name, $middleware, $validation, $defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RouteDefinition('PUT', $fullPath, $fullPath, $handler, $name, $middleware, $validation, $defaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function patch(string $path, mixed $handler, string|null $name = null): RouteDefinition
|
||||||
* Registers a PATCH route with group prefix.
|
|
||||||
*
|
|
||||||
* @param string $path URI path
|
|
||||||
* @param string|callable $handler Route handler
|
|
||||||
* @param string|null $name Optional route name
|
|
||||||
* @return self Fluent interface
|
|
||||||
*/
|
|
||||||
public function patch(string $path, string|callable $handler, string|null $name = null): self
|
|
||||||
{
|
{
|
||||||
$fullPath = $this->buildFullPath($path);
|
$fullPath = $this->buildFullPath($path);
|
||||||
return $this->router ? $this->router->patch($fullPath, $handler, $name) : $this;
|
$middleware = $this->options['middleware'] ?? [];
|
||||||
|
$validation = $this->options['validation'] ?? [];
|
||||||
|
$defaults = $this->options['defaults'] ?? [];
|
||||||
|
|
||||||
|
if ($this->router) {
|
||||||
|
return $this->router->registerCustomRoute('PATCH', $fullPath, $handler, $name, $middleware, $validation, $defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RouteDefinition('PATCH', $fullPath, $fullPath, $handler, $name, $middleware, $validation, $defaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function delete(string $path, mixed $handler, string|null $name = null): RouteDefinition
|
||||||
* Registers a DELETE route with group prefix.
|
|
||||||
*
|
|
||||||
* @param string $path URI path
|
|
||||||
* @param string|callable $handler Route handler
|
|
||||||
* @param string|null $name Optional route name
|
|
||||||
* @return self Fluent interface
|
|
||||||
*/
|
|
||||||
public function delete(string $path, string|callable $handler, string|null $name = null): self
|
|
||||||
{
|
{
|
||||||
$fullPath = $this->buildFullPath($path);
|
$fullPath = $this->buildFullPath($path);
|
||||||
return $this->router ? $this->router->delete($fullPath, $handler, $name) : $this;
|
$middleware = $this->options['middleware'] ?? [];
|
||||||
|
$validation = $this->options['validation'] ?? [];
|
||||||
|
$defaults = $this->options['defaults'] ?? [];
|
||||||
|
|
||||||
|
if ($this->router) {
|
||||||
|
return $this->router->registerCustomRoute('DELETE', $fullPath, $handler, $name, $middleware, $validation, $defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RouteDefinition('DELETE', $fullPath, $fullPath, $handler, $name, $middleware, $validation, $defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function redirect(string $path, string $destination, int $status = 302): RouteDefinition
|
||||||
|
{
|
||||||
|
$fullPath = $this->buildFullPath($path);
|
||||||
|
$middleware = $this->options['middleware'] ?? [];
|
||||||
|
$validation = $this->options['validation'] ?? [];
|
||||||
|
$defaults = $this->options['defaults'] ?? [];
|
||||||
|
|
||||||
|
if ($this->router) {
|
||||||
|
return $this->router->registerCustomRoute('REDIRECT', $fullPath, $destination, null, $middleware, $validation, $defaults)->attr('status', $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (new RouteDefinition('REDIRECT', $fullPath, $fullPath, $destination, null, $middleware, $validation, $defaults))->attr('status', $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fallback(mixed $handler): self
|
||||||
|
{
|
||||||
|
$this->options['fallback'] = $handler;
|
||||||
|
|
||||||
|
$prefix = $this->options['prefix'] ?? '/';
|
||||||
|
$middleware = $this->options['middleware'] ?? [];
|
||||||
|
|
||||||
|
if ($this->router) {
|
||||||
|
$this->router->registerCustomRoute('FALLBACK', $this->joinPaths($prefix, '/_fallback'), $handler, null, $middleware)
|
||||||
|
->attr('_fallback', $handler)
|
||||||
|
->attr('_fallback_prefix', $this->normalizePath($prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerCustomRoute(string $method, string $path, mixed $handler, string|null $name = null, array $middleware = [], array $validation = [], array $defaults = []): RouteDefinition
|
||||||
|
{
|
||||||
|
$fullPath = $this->buildFullPath($path);
|
||||||
|
$mergedMiddleware = array_merge($this->options['middleware'] ?? [], $middleware);
|
||||||
|
|
||||||
|
$route = null;
|
||||||
|
if ($this->router) {
|
||||||
|
$route = $this->router->registerCustomRoute($method, $fullPath, $handler, $name, $mergedMiddleware);
|
||||||
|
} else {
|
||||||
|
$route = new RouteDefinition($method, $fullPath, $fullPath, $handler, $name, $mergedMiddleware);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply group-level validation and defaults
|
||||||
|
$route->valid($this->options['validation'] ?? []);
|
||||||
|
foreach ($this->options['defaults'] ?? [] as $p => $v) {
|
||||||
|
$route->default($p, $v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply route-level validation and defaults
|
||||||
|
$route->valid($validation);
|
||||||
|
foreach ($defaults as $p => $v) {
|
||||||
|
$route->default($p, $v);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $route;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -112,13 +171,72 @@ class RouteGroup
|
||||||
*/
|
*/
|
||||||
private function buildFullPath(string $path): string
|
private function buildFullPath(string $path): string
|
||||||
{
|
{
|
||||||
$prefix = $this->options['prefix'] ?? '';
|
return $this->joinPaths($this->options['prefix'] ?? '', $path);
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($prefix)) {
|
public function group(array $options): RouteGroup
|
||||||
return $path;
|
{
|
||||||
|
$prefix = $this->options['prefix'] ?? '';
|
||||||
|
$newPrefix = $this->joinPaths($prefix, $options['prefix'] ?? '');
|
||||||
|
|
||||||
|
$middleware = $this->options['middleware'] ?? [];
|
||||||
|
$newMiddleware = array_merge($middleware, $options['middleware'] ?? []);
|
||||||
|
|
||||||
|
$validation = $this->options['validation'] ?? [];
|
||||||
|
$newValidation = array_merge($validation, $options['validation'] ?? []);
|
||||||
|
|
||||||
|
$defaults = $this->options['defaults'] ?? [];
|
||||||
|
$newDefaults = array_merge($defaults, $options['defaults'] ?? []);
|
||||||
|
|
||||||
|
$mergedOptions = array_merge($this->options, $options);
|
||||||
|
$mergedOptions['prefix'] = $newPrefix;
|
||||||
|
$mergedOptions['middleware'] = $newMiddleware;
|
||||||
|
$mergedOptions['validation'] = $newValidation;
|
||||||
|
$mergedOptions['defaults'] = $newDefaults;
|
||||||
|
|
||||||
|
return new RouteGroup($mergedOptions, $this->router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets validation rules for parameters at the group level.
|
||||||
|
*
|
||||||
|
* @param array|string $param Parameter name or array of rules
|
||||||
|
* @param array|string $rules Rules if first param is string
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public function valid(array|string $param, array|string $rules = []): self
|
||||||
|
{
|
||||||
|
if (!isset($this->options['validation'])) {
|
||||||
|
$this->options['validation'] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return rtrim($prefix, '/') . '/' . ltrim($path, '/');
|
if (is_array($param)) {
|
||||||
|
foreach ($param as $p => $r) {
|
||||||
|
$this->valid($p, $r);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->options['validation'][$param] = is_string($rules) ? [$rules] : $rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a default value for a parameter at the group level.
|
||||||
|
*
|
||||||
|
* @param string $param
|
||||||
|
* @param mixed $value
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public function default(string $param, mixed $value): self
|
||||||
|
{
|
||||||
|
if (!isset($this->options['defaults'])) {
|
||||||
|
$this->options['defaults'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->options['defaults'][$param] = $value;
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -139,8 +257,33 @@ class RouteGroup
|
||||||
*
|
*
|
||||||
* @return array Group options configuration
|
* @return array Group options configuration
|
||||||
*/
|
*/
|
||||||
|
public function module(string|array $identifier, string|null $prefix = null): self
|
||||||
|
{
|
||||||
|
if ($this->router) {
|
||||||
|
// We need to pass the group context to the module loading.
|
||||||
|
// But ModuleLoader uses the router directly.
|
||||||
|
// If we use $this->router->module(), it won't have the group prefix/middleware.
|
||||||
|
// We should probably allow ModuleLoader to take a "target" which can be a Router or RouteGroup.
|
||||||
|
|
||||||
|
// For now, let's just use the router but we have a problem: inheritance.
|
||||||
|
// A better way is to make RouteGroup have a way to load modules.
|
||||||
|
|
||||||
|
$moduleLoader = new ModuleLoader($this->router->getConfig(), $this);
|
||||||
|
$moduleLoader->load($identifier, $prefix);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function getOptions(): array
|
public function getOptions(): array
|
||||||
{
|
{
|
||||||
return $this->options;
|
return $this->options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function getConfig(): Config
|
||||||
|
{
|
||||||
|
return $this->router->getConfig();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
112
tests/Integration/ModuleLoadingTest.php
Normal file
112
tests/Integration/ModuleLoadingTest.php
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas\Tests\Integration;
|
||||||
|
|
||||||
|
use Atlas\Router\Router;
|
||||||
|
use Atlas\Config\Config;
|
||||||
|
use Atlas\Exception\MissingConfigurationException;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ModuleLoadingTest extends TestCase
|
||||||
|
{
|
||||||
|
private string $tempDir;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->tempDir = sys_get_temp_dir() . '/atlas_test_' . uniqid();
|
||||||
|
mkdir($this->tempDir);
|
||||||
|
mkdir($this->tempDir . '/User');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
$this->removeDirectory($this->tempDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeDirectory(string $path): void
|
||||||
|
{
|
||||||
|
$files = array_diff(scandir($path), ['.', '..']);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
(is_dir("$path/$file")) ? $this->removeDirectory("$path/$file") : unlink("$path/$file");
|
||||||
|
}
|
||||||
|
rmdir($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testModuleLoadsRoutesFromFilesystem(): void
|
||||||
|
{
|
||||||
|
$routesContent = '<?php return [["method" => "GET", "path" => "/profile", "handler" => "UserHandler", "name" => "user_profile"]];';
|
||||||
|
file_put_contents($this->tempDir . '/User/routes.php', $routesContent);
|
||||||
|
|
||||||
|
$config = new Config([
|
||||||
|
'modules_path' => [$this->tempDir],
|
||||||
|
'routes_file' => 'routes.php'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$router = new Router($config);
|
||||||
|
$router->module('User');
|
||||||
|
|
||||||
|
$routes = iterator_to_array($router->getRoutes());
|
||||||
|
$this->assertCount(1, $routes);
|
||||||
|
$this->assertSame('/profile', $routes[0]->getPath());
|
||||||
|
$this->assertSame('user_profile', $routes[0]->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testModuleThrowsExceptionWhenModulesPathIsMissing(): void
|
||||||
|
{
|
||||||
|
$config = new Config([]);
|
||||||
|
$router = new Router($config);
|
||||||
|
|
||||||
|
$this->expectException(MissingConfigurationException::class);
|
||||||
|
$router->module('User');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testModuleSkipsWhenRoutesFileDoesNotExist(): void
|
||||||
|
{
|
||||||
|
$config = new Config([
|
||||||
|
'modules_path' => [$this->tempDir]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$router = new Router($config);
|
||||||
|
$router->module('NonExistent');
|
||||||
|
|
||||||
|
$this->assertCount(0, $router->getRoutes());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testModuleWithMultipleSearchPaths(): void
|
||||||
|
{
|
||||||
|
$secondPath = sys_get_temp_dir() . '/atlas_test_2_' . uniqid();
|
||||||
|
mkdir($secondPath);
|
||||||
|
mkdir($secondPath . '/Shared');
|
||||||
|
|
||||||
|
$routesContent = '<?php return [["method" => "GET", "path" => "/shared", "handler" => "SharedHandler"]];';
|
||||||
|
file_put_contents($secondPath . '/Shared/routes.php', $routesContent);
|
||||||
|
|
||||||
|
$config = new Config([
|
||||||
|
'modules_path' => [$this->tempDir, $secondPath]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$router = new Router($config);
|
||||||
|
$router->module('Shared');
|
||||||
|
|
||||||
|
$routes = iterator_to_array($router->getRoutes());
|
||||||
|
$this->assertCount(1, $routes);
|
||||||
|
$this->assertSame('/shared', $routes[0]->getPath());
|
||||||
|
|
||||||
|
$this->removeDirectory($secondPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testModuleLoadsMultipleRoutes(): void
|
||||||
|
{
|
||||||
|
$routesContent = '<?php return [
|
||||||
|
["method" => "GET", "path" => "/u1", "handler" => "h1"],
|
||||||
|
["method" => "POST", "path" => "/u2", "handler" => "h2"]
|
||||||
|
];';
|
||||||
|
file_put_contents($this->tempDir . '/User/routes.php', $routesContent);
|
||||||
|
|
||||||
|
$config = new Config(['modules_path' => $this->tempDir]);
|
||||||
|
$router = new Router($config);
|
||||||
|
$router->module('User');
|
||||||
|
|
||||||
|
$this->assertCount(2, $router->getRoutes());
|
||||||
|
}
|
||||||
|
}
|
||||||
103
tests/Unit/ConfigTest.php
Normal file
103
tests/Unit/ConfigTest.php
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas\Tests\Unit;
|
||||||
|
|
||||||
|
use Atlas\Config\Config;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ConfigTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGetReturnsValueOrPlaceholder(): void
|
||||||
|
{
|
||||||
|
$config = new Config(['key' => 'value']);
|
||||||
|
|
||||||
|
$this->assertSame('value', $config->get('key'));
|
||||||
|
$this->assertSame('default', $config->get('non_existent', 'default'));
|
||||||
|
$this->assertNull($config->get('non_existent'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHasChecksExistence(): void
|
||||||
|
{
|
||||||
|
$config = new Config(['key' => 'value']);
|
||||||
|
|
||||||
|
$this->assertTrue($config->has('key'));
|
||||||
|
$this->assertFalse($config->has('non_existent'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetModulesPathNormalizesToArray(): void
|
||||||
|
{
|
||||||
|
$config1 = new Config(['modules_path' => '/single/path']);
|
||||||
|
$this->assertSame(['/single/path'], $config1->getModulesPath());
|
||||||
|
|
||||||
|
$config2 = new Config(['modules_path' => ['/path/1', '/path/2']]);
|
||||||
|
$this->assertSame(['/path/1', '/path/2'], $config2->getModulesPath());
|
||||||
|
|
||||||
|
$config3 = new Config([]);
|
||||||
|
$this->assertNull($config3->getModulesPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetModulesPathListAlwaysReturnsArray(): void
|
||||||
|
{
|
||||||
|
$config1 = new Config(['modules_path' => '/single/path']);
|
||||||
|
$this->assertSame(['/single/path'], $config1->getModulesPathList());
|
||||||
|
|
||||||
|
$config2 = new Config(['modules_path' => ['/path/1', '/path/2']]);
|
||||||
|
$this->assertSame(['/path/1', '/path/2'], $config2->getModulesPathList());
|
||||||
|
|
||||||
|
$config3 = new Config([]);
|
||||||
|
$this->assertSame([], $config3->getModulesPathList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetRoutesFileWithDefault(): void
|
||||||
|
{
|
||||||
|
$config1 = new Config(['routes_file' => 'custom.php']);
|
||||||
|
$this->assertSame('custom.php', $config1->getRoutesFile());
|
||||||
|
|
||||||
|
$config2 = new Config([]);
|
||||||
|
$this->assertSame('routes.php', $config2->getRoutesFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetModulesGlob(): void
|
||||||
|
{
|
||||||
|
$config1 = new Config(['modules_glob' => 'src/*/routes.php']);
|
||||||
|
$this->assertSame('src/*/routes.php', $config1->getModulesGlob());
|
||||||
|
|
||||||
|
$config2 = new Config([]);
|
||||||
|
$this->assertNull($config2->getModulesGlob());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testToArray(): void
|
||||||
|
{
|
||||||
|
$options = ['a' => 1, 'b' => 2];
|
||||||
|
$config = new Config($options);
|
||||||
|
|
||||||
|
$this->assertSame($options, $config->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testArrayAccess(): void
|
||||||
|
{
|
||||||
|
$config = new Config(['key' => 'value']);
|
||||||
|
|
||||||
|
$this->assertTrue(isset($config['key']));
|
||||||
|
$this->assertSame('value', $config['key']);
|
||||||
|
|
||||||
|
$config['new'] = 'val';
|
||||||
|
$this->assertSame('val', $config['new']);
|
||||||
|
|
||||||
|
unset($config['key']);
|
||||||
|
$this->assertFalse(isset($config['key']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIteratorAggregate(): void
|
||||||
|
{
|
||||||
|
$options = ['a' => 1, 'b' => 2];
|
||||||
|
$config = new Config($options);
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($config as $key => $value) {
|
||||||
|
$result[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertSame($options, $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
99
tests/Unit/RouteGroupTest.php
Normal file
99
tests/Unit/RouteGroupTest.php
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas\Tests\Unit;
|
||||||
|
|
||||||
|
use Atlas\Router\Router;
|
||||||
|
use Atlas\Router\RouteGroup;
|
||||||
|
use Atlas\Config\Config;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class RouteGroupTest extends TestCase
|
||||||
|
{
|
||||||
|
private Router $router;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$config = new Config(['modules_path' => ['/path/to/modules']]);
|
||||||
|
$this->router = new Router($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGroupAppliesPrefixToRoutes(): void
|
||||||
|
{
|
||||||
|
$group = $this->router->group(['prefix' => '/api']);
|
||||||
|
|
||||||
|
$group->get('/users', 'Handler');
|
||||||
|
|
||||||
|
$routes = iterator_to_array($this->router->getRoutes());
|
||||||
|
$this->assertCount(1, $routes);
|
||||||
|
$this->assertSame('/api/users', $routes[0]->getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGroupAppliesPrefixWithLeadingSlashToRoutes(): void
|
||||||
|
{
|
||||||
|
$group = $this->router->group(['prefix' => 'api']);
|
||||||
|
|
||||||
|
$group->get('users', 'Handler');
|
||||||
|
|
||||||
|
$routes = iterator_to_array($this->router->getRoutes());
|
||||||
|
$this->assertCount(1, $routes);
|
||||||
|
$this->assertSame('/api/users', $routes[0]->getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGroupWithTrailingSlashInPrefix(): void
|
||||||
|
{
|
||||||
|
$group = $this->router->group(['prefix' => '/api/']);
|
||||||
|
|
||||||
|
$group->get('/users', 'Handler');
|
||||||
|
|
||||||
|
$routes = iterator_to_array($this->router->getRoutes());
|
||||||
|
$this->assertCount(1, $routes);
|
||||||
|
$this->assertSame('/api/users', $routes[0]->getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAllHttpMethodsInGroup(): void
|
||||||
|
{
|
||||||
|
$group = $this->router->group(['prefix' => '/api']);
|
||||||
|
|
||||||
|
$group->get('/test', 'handler');
|
||||||
|
$group->post('/test', 'handler');
|
||||||
|
$group->put('/test', 'handler');
|
||||||
|
$group->patch('/test', 'handler');
|
||||||
|
$group->delete('/test', 'handler');
|
||||||
|
|
||||||
|
$routes = iterator_to_array($this->router->getRoutes());
|
||||||
|
$this->assertCount(5, $routes);
|
||||||
|
|
||||||
|
foreach ($routes as $route) {
|
||||||
|
$this->assertSame('/api/test', $route->getPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGroupReturnsSameInstanceForChaining(): void
|
||||||
|
{
|
||||||
|
$group = $this->router->group(['prefix' => '/api']);
|
||||||
|
|
||||||
|
$group->get('/users', 'Handler');
|
||||||
|
$group->post('/users', 'Handler');
|
||||||
|
|
||||||
|
$routes = iterator_to_array($this->router->getRoutes());
|
||||||
|
$this->assertCount(2, $routes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGroupCanBeCreatedWithoutRouterAndStillWorks(): void
|
||||||
|
{
|
||||||
|
// This tests the case where RouteGroup might be used partially or in isolation
|
||||||
|
// although buildFullPath is the main logic.
|
||||||
|
$group = new RouteGroup(['prefix' => '/api']);
|
||||||
|
|
||||||
|
// Use reflection or just check options if public (it is protected/private)
|
||||||
|
$this->assertSame(['prefix' => '/api'], $group->getOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetOptionOnGroup(): void
|
||||||
|
{
|
||||||
|
$group = new RouteGroup();
|
||||||
|
$group->setOption('prefix', '/test');
|
||||||
|
|
||||||
|
$this->assertSame(['prefix' => '/test'], $group->getOptions());
|
||||||
|
}
|
||||||
|
}
|
||||||
36
tests/Unit/RouterFallbackTest.php
Normal file
36
tests/Unit/RouterFallbackTest.php
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas\Tests\Unit;
|
||||||
|
|
||||||
|
use Atlas\Router\Router;
|
||||||
|
use Atlas\Config\Config;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class RouterFallbackTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testFallbackHandlerCanBeSet(): void
|
||||||
|
{
|
||||||
|
$config = new Config(['modules_path' => ['/path/to/modules']]);
|
||||||
|
$router = new Router($config);
|
||||||
|
|
||||||
|
$handler = function() { return '404'; };
|
||||||
|
$router->fallback($handler);
|
||||||
|
|
||||||
|
// Use reflection to check if fallbackHandler is set correctly since there is no getter
|
||||||
|
$reflection = new \ReflectionClass($router);
|
||||||
|
$property = $reflection->getProperty('fallbackHandler');
|
||||||
|
$property->setAccessible(true);
|
||||||
|
|
||||||
|
$this->assertSame($handler, $property->getValue($router));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFallbackReturnsRouterInstanceForChaining(): void
|
||||||
|
{
|
||||||
|
$config = new Config(['modules_path' => ['/path/to/modules']]);
|
||||||
|
$router = new Router($config);
|
||||||
|
|
||||||
|
$result = $router->fallback('Handler');
|
||||||
|
|
||||||
|
$this->assertSame($router, $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
tests/Unit/RouterUrlTest.php
Normal file
67
tests/Unit/RouterUrlTest.php
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas\Tests\Unit;
|
||||||
|
|
||||||
|
use Atlas\Router\Router;
|
||||||
|
use Atlas\Config\Config;
|
||||||
|
use Atlas\Exception\RouteNotFoundException;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class RouterUrlTest extends TestCase
|
||||||
|
{
|
||||||
|
private Router $router;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$config = new Config(['modules_path' => ['/path/to/modules']]);
|
||||||
|
$this->router = new Router($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUrlGeneratesForStaticRoute(): void
|
||||||
|
{
|
||||||
|
$this->router->get('/users', 'handler', 'user_list');
|
||||||
|
|
||||||
|
$url = $this->router->url('user_list');
|
||||||
|
|
||||||
|
$this->assertSame('/users', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUrlGeneratesWithParameters(): void
|
||||||
|
{
|
||||||
|
$this->router->get('/users/{{user_id}}', 'handler', 'user_detail');
|
||||||
|
|
||||||
|
$url = $this->router->url('user_detail', ['user_id' => 42]);
|
||||||
|
|
||||||
|
$this->assertSame('/users/42', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUrlGeneratesWithMultipleParameters(): void
|
||||||
|
{
|
||||||
|
$this->router->get('/posts/{{post_id}}/comments/{{comment_id}}', 'handler', 'comment_detail');
|
||||||
|
|
||||||
|
$url = $this->router->url('comment_detail', [
|
||||||
|
'post_id' => 10,
|
||||||
|
'comment_id' => 5
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame('/posts/10/comments/5', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUrlThrowsExceptionWhenRouteNotFound(): void
|
||||||
|
{
|
||||||
|
$this->expectException(RouteNotFoundException::class);
|
||||||
|
$this->expectExceptionMessage('Route "non_existent" not found');
|
||||||
|
|
||||||
|
$this->router->url('non_existent');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUrlWithMissingParametersThrowsException(): void
|
||||||
|
{
|
||||||
|
$this->router->get('/users/{{user_id}}', 'handler', 'user_detail');
|
||||||
|
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('Missing required parameter "user_id"');
|
||||||
|
|
||||||
|
$this->router->url('user_detail', []);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue