""" Unit tests for route matcher. Tests: - Exact path matching - Prefix pattern matching - Glob pattern matching - Regex pattern matching - RouteConfig precedence logic """ import pytest from services.base_service.route_matcher import RouteMatcher, RouteConfig class TestRouteMatcher: """Test RouteMatcher pattern matching.""" def test_exact_match(self): """Test exact path matching.""" matcher = RouteMatcher(["/api/users", "/api/posts"]) assert matcher.matches("/api/users") assert matcher.matches("/api/posts") assert not matcher.matches("/api/comments") assert not matcher.matches("/api/users/123") def test_prefix_match(self): """Test prefix wildcard matching.""" matcher = RouteMatcher(["/api/*", "/admin/*"]) assert matcher.matches("/api/users") assert matcher.matches("/api/posts") assert matcher.matches("/admin/dashboard") assert not matcher.matches("/public/page") def test_complex_glob_match(self): """Test complex glob patterns.""" matcher = RouteMatcher(["/api/users/*/posts", "/api/**/comments"]) assert matcher.matches("/api/users/123/posts") assert matcher.matches("/api/users/456/posts") assert matcher.matches("/api/v1/users/comments") assert matcher.matches("/api/deep/nested/path/comments") assert not matcher.matches("/api/users/posts") def test_regex_match(self): """Test regex pattern matching.""" matcher = RouteMatcher(["^/api/v[0-9]+/.*$", "^/users/[0-9]+$"]) assert matcher.matches("/api/v1/users") assert matcher.matches("/api/v2/posts") assert matcher.matches("/users/123") assert not matcher.matches("/api/v/users") assert not matcher.matches("/users/abc") def test_query_parameters_stripped(self): """Test that query parameters are ignored.""" matcher = RouteMatcher(["/api/users"]) assert matcher.matches("/api/users?page=1") assert matcher.matches("/api/users?page=1&limit=10") def test_fragments_stripped(self): """Test that URL fragments are ignored.""" matcher = RouteMatcher(["/api/users"]) assert matcher.matches("/api/users#section") def test_trailing_slash_normalized(self): """Test trailing slash normalization.""" matcher = RouteMatcher(["/api/users"]) assert matcher.matches("/api/users/") # Root path keeps trailing slash root_matcher = RouteMatcher(["/"]) assert root_matcher.matches("/") def test_empty_patterns(self): """Test with empty pattern list.""" matcher = RouteMatcher([]) assert not matcher.matches("/any/path") def test_get_matching_pattern(self): """Test getting the matched pattern.""" matcher = RouteMatcher([ "/api/users", "/api/*", "/admin/**" ]) assert matcher.get_matching_pattern("/api/users") == "/api/users" assert matcher.get_matching_pattern("/api/posts") == "/api/*" assert matcher.get_matching_pattern("/admin/deep/path") == "/admin/**" assert matcher.get_matching_pattern("/public") is None def test_mixed_patterns(self): """Test combination of all pattern types.""" matcher = RouteMatcher([ "/exact", "/prefix/*", "/glob/*/nested", "^/regex/[0-9]+$" ]) assert matcher.matches("/exact") assert matcher.matches("/prefix/anything") assert matcher.matches("/glob/123/nested") assert matcher.matches("/regex/456") assert not matcher.matches("/other") def test_invalid_regex_pattern(self): """Test that invalid regex is handled gracefully.""" # Should not raise, just log warning and skip pattern matcher = RouteMatcher(["^[invalid(regex$"]) assert not matcher.matches("/anything") class TestRouteConfig: """Test RouteConfig precedence logic.""" def test_required_routes(self): """Test required route checking.""" config = RouteConfig( required=["/api/users", "/api/posts"], ) assert config.is_required("/api/users") assert config.is_required("/api/posts") assert not config.is_required("/public") def test_optional_routes(self): """Test optional route checking.""" config = RouteConfig( optional=["/", "/home"], ) assert config.is_optional("/") assert config.is_optional("/home") assert not config.is_optional("/api/users") def test_public_routes(self): """Test public route checking.""" config = RouteConfig( public=["/health", "/docs"], ) assert config.is_public("/health") assert config.is_public("/docs") assert not config.is_public("/api/users") def test_public_overrides_required(self): """Test that public takes precedence over required.""" config = RouteConfig( required=["/api/*"], public=["/api/health"], ) # /api/health is public, so not required assert config.is_public("/api/health") assert not config.is_required("/api/health") # Other /api routes are required assert config.is_required("/api/users") assert not config.is_public("/api/users") def test_public_overrides_optional(self): """Test that public takes precedence over optional.""" config = RouteConfig( optional=["/api/*"], public=["/api/health"], ) # /api/health is public, so not optional assert config.is_public("/api/health") assert not config.is_optional("/api/health") # Other /api routes are optional assert config.is_optional("/api/users") def test_required_overrides_optional(self): """Test that required takes precedence over optional.""" config = RouteConfig( required=["/api/users"], optional=["/api/*"], ) # /api/users is required, so not optional assert config.is_required("/api/users") assert not config.is_optional("/api/users") # Other /api routes are optional assert config.is_optional("/api/posts") def test_requires_service(self): """Test requires_service helper.""" config = RouteConfig( required=["/api/users"], optional=["/api/posts"], public=["/health"], ) # Service required assert config.requires_service("/api/users") # Service optional (still requires service) assert config.requires_service("/api/posts") # Public (does not require service) assert not config.requires_service("/health") def test_empty_config(self): """Test with empty configuration.""" config = RouteConfig() assert not config.is_required("/any") assert not config.is_optional("/any") assert not config.is_public("/any") assert not config.requires_service("/any") def test_complex_precedence(self): """Test complex precedence scenarios.""" config = RouteConfig( required=["/api/users"], # Specific required path optional=["/api/*"], # Broader optional pattern public=["/api/health"], ) # Public overrides everything assert config.is_public("/api/health") assert not config.is_required("/api/health") assert not config.is_optional("/api/health") # Required path assert config.is_required("/api/users") assert not config.is_optional("/api/users") # Optional for other paths under /api assert config.is_optional("/api/posts") assert not config.is_required("/api/posts")