'/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); });