Spaces:
Sleeping
Sleeping
| """ | |
| 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") | |