File size: 8,278 Bytes
bcc8074
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
"""
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")