- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
425 lines
14 KiB
PHP
425 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Attributes\Route;
|
|
use App\Framework\Core\RouteCompiler;
|
|
use App\Framework\Core\ValueObjects\ClassName;
|
|
use App\Framework\Core\ValueObjects\MethodName;
|
|
use App\Framework\Discovery\ValueObjects\AttributeTarget;
|
|
use App\Framework\Discovery\ValueObjects\DiscoveredAttribute;
|
|
use App\Framework\Http\Headers;
|
|
use App\Framework\Http\HttpRequest;
|
|
use App\Framework\Http\Method;
|
|
use App\Framework\Http\ServerEnvironment;
|
|
use App\Framework\Router\HttpRouter;
|
|
use App\Framework\Router\RouteMatchSuccess;
|
|
|
|
it('compiles routes with exact subdomain patterns', function () {
|
|
$compiler = new RouteCompiler();
|
|
|
|
// Create a discovered route with exact subdomain
|
|
$discoveredRoute = new DiscoveredAttribute(
|
|
className: ClassName::create('TestController'),
|
|
attributeClass: Route::class,
|
|
target: AttributeTarget::METHOD,
|
|
methodName: MethodName::create('apiEndpoint'),
|
|
arguments: [
|
|
'path' => '/api/test',
|
|
'method' => Method::GET,
|
|
'subdomain' => 'api',
|
|
],
|
|
additionalData: ['parameters' => []]
|
|
);
|
|
|
|
$compiled = $compiler->compile($discoveredRoute);
|
|
|
|
expect($compiled)->toHaveKey('GET')
|
|
->and($compiled['GET'])->toHaveKey('exact:api')
|
|
->and($compiled['GET']['exact:api']['static'])->toHaveKey('/api/test');
|
|
});
|
|
|
|
it('compiles routes with wildcard subdomain patterns', function () {
|
|
$compiler = new RouteCompiler();
|
|
|
|
$discoveredRoute = new DiscoveredAttribute(
|
|
className: ClassName::create('TestController'),
|
|
attributeClass: Route::class,
|
|
target: AttributeTarget::METHOD,
|
|
methodName: MethodName::create('wildcardEndpoint'),
|
|
arguments: [
|
|
'path' => '/wildcard',
|
|
'method' => Method::GET,
|
|
'subdomain' => '*.app',
|
|
],
|
|
additionalData: ['parameters' => []]
|
|
);
|
|
|
|
$compiled = $compiler->compile($discoveredRoute);
|
|
|
|
expect($compiled)->toHaveKey('GET')
|
|
->and($compiled['GET'])->toHaveKey('wildcard:*.app')
|
|
->and($compiled['GET']['wildcard:*.app']['static'])->toHaveKey('/wildcard');
|
|
});
|
|
|
|
it('compiles routes with multiple subdomain patterns', function () {
|
|
$compiler = new RouteCompiler();
|
|
|
|
$discoveredRoute = new DiscoveredAttribute(
|
|
className: ClassName::create('TestController'),
|
|
attributeClass: Route::class,
|
|
target: AttributeTarget::METHOD,
|
|
methodName: MethodName::create('multiSubdomain'),
|
|
arguments: [
|
|
'path' => '/multi',
|
|
'method' => Method::GET,
|
|
'subdomain' => ['api', 'admin'],
|
|
],
|
|
additionalData: ['parameters' => []]
|
|
);
|
|
|
|
$compiled = $compiler->compile($discoveredRoute);
|
|
|
|
expect($compiled['GET'])->toHaveKey('exact:api')
|
|
->and($compiled['GET'])->toHaveKey('exact:admin')
|
|
->and($compiled['GET']['exact:api']['static'])->toHaveKey('/multi')
|
|
->and($compiled['GET']['exact:admin']['static'])->toHaveKey('/multi');
|
|
});
|
|
|
|
it('falls back to default routes when no subdomain specified', function () {
|
|
$compiler = new RouteCompiler();
|
|
|
|
$discoveredRoute = new DiscoveredAttribute(
|
|
className: ClassName::create('TestController'),
|
|
attributeClass: Route::class,
|
|
target: AttributeTarget::METHOD,
|
|
methodName: MethodName::create('defaultRoute'),
|
|
arguments: [
|
|
'path' => '/default',
|
|
'method' => Method::GET,
|
|
'subdomain' => [],
|
|
],
|
|
additionalData: ['parameters' => []]
|
|
);
|
|
|
|
$compiled = $compiler->compile($discoveredRoute);
|
|
|
|
expect($compiled)->toHaveKey('GET')
|
|
->and($compiled['GET'])->toHaveKey('default')
|
|
->and($compiled['GET']['default']['static'])->toHaveKey('/default');
|
|
});
|
|
|
|
it('router matches exact subdomain routes correctly', function () {
|
|
$compiler = new RouteCompiler();
|
|
|
|
// Create routes for different subdomains
|
|
$apiRoute = new DiscoveredAttribute(
|
|
className: ClassName::create('ApiController'),
|
|
attributeClass: Route::class,
|
|
target: AttributeTarget::METHOD,
|
|
methodName: MethodName::create('index'),
|
|
arguments: [
|
|
'path' => '/test',
|
|
'method' => Method::GET,
|
|
'subdomain' => 'api',
|
|
],
|
|
additionalData: ['parameters' => []]
|
|
);
|
|
|
|
$defaultRoute = new DiscoveredAttribute(
|
|
className: ClassName::create('WebController'),
|
|
attributeClass: Route::class,
|
|
target: AttributeTarget::METHOD,
|
|
methodName: MethodName::create('index'),
|
|
arguments: [
|
|
'path' => '/test',
|
|
'method' => Method::GET,
|
|
'subdomain' => [],
|
|
],
|
|
additionalData: ['parameters' => []]
|
|
);
|
|
|
|
$compiledRoutes = $compiler->compileOptimized($apiRoute, $defaultRoute);
|
|
$router = new HttpRouter($compiledRoutes);
|
|
|
|
// Test API subdomain request
|
|
$apiRequest = new HttpRequest(
|
|
method: Method::GET,
|
|
path: '/test',
|
|
headers: new Headers(['Host' => 'api.example.com']),
|
|
server: new ServerEnvironment(['HTTP_HOST' => 'api.example.com'])
|
|
);
|
|
|
|
$apiContext = $router->match($apiRequest);
|
|
expect($apiContext->match)->toBeInstanceOf(RouteMatchSuccess::class);
|
|
|
|
if ($apiContext->match instanceof RouteMatchSuccess) {
|
|
expect($apiContext->match->route->controller)->toBe('ApiController');
|
|
}
|
|
|
|
// Test default domain request
|
|
$webRequest = new HttpRequest(
|
|
method: Method::GET,
|
|
path: '/test',
|
|
headers: new Headers(['Host' => 'example.com']),
|
|
server: new ServerEnvironment(['HTTP_HOST' => 'example.com'])
|
|
);
|
|
|
|
$webContext = $router->match($webRequest);
|
|
expect($webContext->match)->toBeInstanceOf(RouteMatchSuccess::class);
|
|
|
|
if ($webContext->match instanceof RouteMatchSuccess) {
|
|
expect($webContext->match->route->controller)->toBe('WebController');
|
|
}
|
|
});
|
|
|
|
it('router matches wildcard subdomain patterns', function () {
|
|
$compiler = new RouteCompiler();
|
|
|
|
$wildcardRoute = new DiscoveredAttribute(
|
|
className: ClassName::create('TenantController'),
|
|
attributeClass: Route::class,
|
|
target: AttributeTarget::METHOD,
|
|
methodName: MethodName::create('dashboard'),
|
|
arguments: [
|
|
'path' => '/dashboard',
|
|
'method' => Method::GET,
|
|
'subdomain' => '*.app',
|
|
],
|
|
additionalData: ['parameters' => []]
|
|
);
|
|
|
|
$compiledRoutes = $compiler->compileOptimized($wildcardRoute);
|
|
$router = new HttpRouter($compiledRoutes);
|
|
|
|
// Test wildcard match
|
|
$tenantRequest = new HttpRequest(
|
|
method: Method::GET,
|
|
path: '/dashboard',
|
|
headers: new Headers(['Host' => 'tenant1.app.example.com']),
|
|
server: new ServerEnvironment(['HTTP_HOST' => 'tenant1.app.example.com'])
|
|
);
|
|
|
|
$context = $router->match($tenantRequest);
|
|
expect($context->match)->toBeInstanceOf(RouteMatchSuccess::class);
|
|
|
|
if ($context->match instanceof RouteMatchSuccess) {
|
|
expect($context->match->route->controller)->toBe('TenantController');
|
|
}
|
|
});
|
|
|
|
it('router prioritizes exact subdomain over wildcard patterns', function () {
|
|
$compiler = new RouteCompiler();
|
|
|
|
$exactRoute = new DiscoveredAttribute(
|
|
className: ClassName::create('ExactController'),
|
|
attributeClass: Route::class,
|
|
target: AttributeTarget::METHOD,
|
|
methodName: MethodName::create('special'),
|
|
arguments: [
|
|
'path' => '/special',
|
|
'method' => Method::GET,
|
|
'subdomain' => 'admin',
|
|
],
|
|
additionalData: ['parameters' => []]
|
|
);
|
|
|
|
$wildcardRoute = new DiscoveredAttribute(
|
|
className: ClassName::create('WildcardController'),
|
|
attributeClass: Route::class,
|
|
target: AttributeTarget::METHOD,
|
|
methodName: MethodName::create('general'),
|
|
arguments: [
|
|
'path' => '/special',
|
|
'method' => Method::GET,
|
|
'subdomain' => '*',
|
|
],
|
|
additionalData: ['parameters' => []]
|
|
);
|
|
|
|
$compiledRoutes = $compiler->compileOptimized($exactRoute, $wildcardRoute);
|
|
$router = new HttpRouter($compiledRoutes);
|
|
|
|
// Test that exact match takes precedence
|
|
$request = new HttpRequest(
|
|
method: Method::GET,
|
|
path: '/special',
|
|
headers: new Headers(['Host' => 'admin.example.com']),
|
|
server: new ServerEnvironment(['HTTP_HOST' => 'admin.example.com'])
|
|
);
|
|
|
|
$context = $router->match($request);
|
|
expect($context->match)->toBeInstanceOf(RouteMatchSuccess::class);
|
|
|
|
if ($context->match instanceof RouteMatchSuccess) {
|
|
expect($context->match->route->controller)->toBe('ExactController');
|
|
}
|
|
});
|
|
|
|
it('compiles dynamic routes with subdomain patterns', function () {
|
|
$compiler = new RouteCompiler();
|
|
|
|
$dynamicRoute = new DiscoveredAttribute(
|
|
className: ClassName::create('ApiController'),
|
|
attributeClass: Route::class,
|
|
target: AttributeTarget::METHOD,
|
|
methodName: MethodName::create('getUser'),
|
|
arguments: [
|
|
'path' => '/api/users/{id}',
|
|
'method' => Method::GET,
|
|
'subdomain' => 'api',
|
|
],
|
|
additionalData: ['parameters' => []]
|
|
);
|
|
|
|
$compiled = $compiler->compile($dynamicRoute);
|
|
|
|
expect($compiled['GET']['exact:api']['dynamic'])->toHaveCount(1)
|
|
->and($compiled['GET']['exact:api']['dynamic'][0]->path)->toBe('/api/users/{id}')
|
|
->and($compiled['GET']['exact:api']['dynamic'][0]->paramNames)->toBe(['id']);
|
|
});
|
|
|
|
it('extracts subdomain correctly from various host patterns', function () {
|
|
$compiler = new RouteCompiler();
|
|
|
|
$testRoute = new DiscoveredAttribute(
|
|
className: ClassName::create('TestController'),
|
|
attributeClass: Route::class,
|
|
target: AttributeTarget::METHOD,
|
|
methodName: MethodName::create('test'),
|
|
arguments: [
|
|
'path' => '/test',
|
|
'method' => Method::GET,
|
|
'subdomain' => 'api',
|
|
],
|
|
additionalData: ['parameters' => []]
|
|
);
|
|
|
|
$compiledRoutes = $compiler->compileOptimized($testRoute);
|
|
$router = new HttpRouter($compiledRoutes);
|
|
|
|
// Test various host patterns
|
|
$testCases = [
|
|
'api.example.com' => 'api',
|
|
'www.example.com' => '', // www is ignored
|
|
'example.com' => '', // no subdomain
|
|
'sub.api.example.com' => 'sub.api', // full subdomain part
|
|
'test.localhost' => 'test', // development domain
|
|
'localhost' => '', // main localhost
|
|
];
|
|
|
|
foreach ($testCases as $host => $expectedSubdomain) {
|
|
$request = new HttpRequest(
|
|
method: Method::GET,
|
|
path: '/test',
|
|
headers: new Headers(['Host' => $host]),
|
|
server: new ServerEnvironment(['HTTP_HOST' => $host])
|
|
);
|
|
|
|
// Use reflection to test the private extractSubdomain method
|
|
$reflection = new ReflectionClass($router);
|
|
$method = $reflection->getMethod('extractSubdomain');
|
|
$method->setAccessible(true);
|
|
|
|
$result = $method->invoke($router, $host);
|
|
expect($result)->toBe($expectedSubdomain, "Failed for host: $host");
|
|
}
|
|
});
|
|
|
|
it('default routes are NOT accessible via subdomain', function () {
|
|
$compiler = new RouteCompiler();
|
|
|
|
// Create a default route (no subdomain specified)
|
|
$defaultRoute = new DiscoveredAttribute(
|
|
className: ClassName::create('HomeController'),
|
|
attributeClass: Route::class,
|
|
target: AttributeTarget::METHOD,
|
|
methodName: MethodName::create('index'),
|
|
arguments: [
|
|
'path' => '/home',
|
|
'method' => Method::GET,
|
|
'subdomain' => [], // No subdomain = default route
|
|
],
|
|
additionalData: ['parameters' => []]
|
|
);
|
|
|
|
$compiledRoutes = $compiler->compileOptimized($defaultRoute);
|
|
$router = new HttpRouter($compiledRoutes);
|
|
|
|
// Test 1: Should work on main domain (no subdomain)
|
|
$mainDomainRequest = new HttpRequest(
|
|
method: Method::GET,
|
|
path: '/home',
|
|
headers: new Headers(['Host' => 'example.com']),
|
|
server: new ServerEnvironment(['HTTP_HOST' => 'example.com'])
|
|
);
|
|
|
|
$mainContext = $router->match($mainDomainRequest);
|
|
expect($mainContext->match)->toBeInstanceOf(RouteMatchSuccess::class);
|
|
|
|
// Test 2: Should NOT work on subdomain
|
|
$subdomainRequest = new HttpRequest(
|
|
method: Method::GET,
|
|
path: '/home',
|
|
headers: new Headers(['Host' => 'api.example.com']),
|
|
server: new ServerEnvironment(['HTTP_HOST' => 'api.example.com'])
|
|
);
|
|
|
|
$subdomainContext = $router->match($subdomainRequest);
|
|
expect($subdomainContext->match)->toBeInstanceOf(\App\Framework\Router\NoRouteMatch::class);
|
|
});
|
|
|
|
it('subdomain-specific routes are NOT accessible via main domain', function () {
|
|
$compiler = new RouteCompiler();
|
|
|
|
// Create a subdomain-specific route
|
|
$apiRoute = new DiscoveredAttribute(
|
|
className: ClassName::create('ApiController'),
|
|
attributeClass: Route::class,
|
|
target: AttributeTarget::METHOD,
|
|
methodName: MethodName::create('endpoint'),
|
|
arguments: [
|
|
'path' => '/api/data',
|
|
'method' => Method::GET,
|
|
'subdomain' => 'api', // Only accessible via api.domain.com
|
|
],
|
|
additionalData: ['parameters' => []]
|
|
);
|
|
|
|
$compiledRoutes = $compiler->compileOptimized($apiRoute);
|
|
$router = new HttpRouter($compiledRoutes);
|
|
|
|
// Test 1: Should work on correct subdomain
|
|
$correctSubdomainRequest = new HttpRequest(
|
|
method: Method::GET,
|
|
path: '/api/data',
|
|
headers: new Headers(['Host' => 'api.example.com']),
|
|
server: new ServerEnvironment(['HTTP_HOST' => 'api.example.com'])
|
|
);
|
|
|
|
$correctContext = $router->match($correctSubdomainRequest);
|
|
expect($correctContext->match)->toBeInstanceOf(RouteMatchSuccess::class);
|
|
|
|
// Test 2: Should NOT work on main domain
|
|
$mainDomainRequest = new HttpRequest(
|
|
method: Method::GET,
|
|
path: '/api/data',
|
|
headers: new Headers(['Host' => 'example.com']),
|
|
server: new ServerEnvironment(['HTTP_HOST' => 'example.com'])
|
|
);
|
|
|
|
$mainContext = $router->match($mainDomainRequest);
|
|
expect($mainContext->match)->toBeInstanceOf(\App\Framework\Router\NoRouteMatch::class);
|
|
|
|
// Test 3: Should NOT work on wrong subdomain
|
|
$wrongSubdomainRequest = new HttpRequest(
|
|
method: Method::GET,
|
|
path: '/api/data',
|
|
headers: new Headers(['Host' => 'admin.example.com']),
|
|
server: new ServerEnvironment(['HTTP_HOST' => 'admin.example.com'])
|
|
);
|
|
|
|
$wrongContext = $router->match($wrongSubdomainRequest);
|
|
expect($wrongContext->match)->toBeInstanceOf(\App\Framework\Router\NoRouteMatch::class);
|
|
});
|