apigateway / tests /test_route_matcher.py
jebin2's picture
refactor
bcc8074
"""
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")