otterapi 0.0.5__py3-none-any.whl → 0.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- README.md +581 -8
- otterapi/__init__.py +73 -0
- otterapi/cli.py +327 -29
- otterapi/codegen/__init__.py +115 -0
- otterapi/codegen/ast_utils.py +134 -5
- otterapi/codegen/client.py +1271 -0
- otterapi/codegen/codegen.py +1736 -0
- otterapi/codegen/dataframes.py +392 -0
- otterapi/codegen/emitter.py +473 -0
- otterapi/codegen/endpoints.py +2597 -343
- otterapi/codegen/pagination.py +1026 -0
- otterapi/codegen/schema.py +593 -0
- otterapi/codegen/splitting.py +1397 -0
- otterapi/codegen/types.py +1345 -0
- otterapi/codegen/utils.py +180 -1
- otterapi/config.py +1017 -24
- otterapi/exceptions.py +231 -0
- otterapi/openapi/__init__.py +46 -0
- otterapi/openapi/v2/__init__.py +86 -0
- otterapi/openapi/v2/spec.json +1607 -0
- otterapi/openapi/v2/v2.py +1776 -0
- otterapi/openapi/v3/__init__.py +131 -0
- otterapi/openapi/v3/spec.json +1651 -0
- otterapi/openapi/v3/v3.py +1557 -0
- otterapi/openapi/v3_1/__init__.py +133 -0
- otterapi/openapi/v3_1/spec.json +1411 -0
- otterapi/openapi/v3_1/v3_1.py +798 -0
- otterapi/openapi/v3_2/__init__.py +133 -0
- otterapi/openapi/v3_2/spec.json +1666 -0
- otterapi/openapi/v3_2/v3_2.py +777 -0
- otterapi/tests/__init__.py +3 -0
- otterapi/tests/fixtures/__init__.py +455 -0
- otterapi/tests/test_ast_utils.py +680 -0
- otterapi/tests/test_codegen.py +610 -0
- otterapi/tests/test_dataframe.py +1038 -0
- otterapi/tests/test_exceptions.py +493 -0
- otterapi/tests/test_openapi_support.py +616 -0
- otterapi/tests/test_openapi_upgrade.py +215 -0
- otterapi/tests/test_pagination.py +1101 -0
- otterapi/tests/test_splitting_config.py +319 -0
- otterapi/tests/test_splitting_integration.py +427 -0
- otterapi/tests/test_splitting_resolver.py +512 -0
- otterapi/tests/test_splitting_tree.py +525 -0
- otterapi-0.0.6.dist-info/METADATA +627 -0
- otterapi-0.0.6.dist-info/RECORD +48 -0
- {otterapi-0.0.5.dist-info → otterapi-0.0.6.dist-info}/WHEEL +1 -1
- otterapi/codegen/generator.py +0 -358
- otterapi/codegen/openapi_processor.py +0 -27
- otterapi/codegen/type_generator.py +0 -559
- otterapi-0.0.5.dist-info/METADATA +0 -54
- otterapi-0.0.5.dist-info/RECORD +0 -16
- {otterapi-0.0.5.dist-info → otterapi-0.0.6.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
"""Tests for the module map resolver.
|
|
2
|
+
|
|
3
|
+
This module tests the ModuleMapResolver class and ResolvedModule dataclass
|
|
4
|
+
used for matching endpoint paths to target modules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from otterapi.codegen.splitting import ModuleMapResolver, ResolvedModule
|
|
8
|
+
from otterapi.config import ModuleDefinition, ModuleSplitConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestResolvedModule:
|
|
12
|
+
"""Tests for the ResolvedModule dataclass."""
|
|
13
|
+
|
|
14
|
+
def test_module_name_single(self):
|
|
15
|
+
"""Test module_name property with single component."""
|
|
16
|
+
resolved = ResolvedModule(module_path=['users'])
|
|
17
|
+
assert resolved.module_name == 'users'
|
|
18
|
+
|
|
19
|
+
def test_module_name_multiple(self):
|
|
20
|
+
"""Test module_name property with multiple components."""
|
|
21
|
+
resolved = ResolvedModule(module_path=['api', 'v1', 'users'])
|
|
22
|
+
assert resolved.module_name == 'api.v1.users'
|
|
23
|
+
|
|
24
|
+
def test_module_name_empty(self):
|
|
25
|
+
"""Test module_name property with empty path."""
|
|
26
|
+
resolved = ResolvedModule(module_path=[])
|
|
27
|
+
assert resolved.module_name == ''
|
|
28
|
+
|
|
29
|
+
def test_file_path_single(self):
|
|
30
|
+
"""Test file_path property with single component."""
|
|
31
|
+
resolved = ResolvedModule(module_path=['users'])
|
|
32
|
+
assert resolved.file_path == 'users.py'
|
|
33
|
+
|
|
34
|
+
def test_file_path_nested(self):
|
|
35
|
+
"""Test file_path property with nested path."""
|
|
36
|
+
resolved = ResolvedModule(module_path=['api', 'v1', 'users'])
|
|
37
|
+
assert resolved.file_path == 'api/v1/users.py'
|
|
38
|
+
|
|
39
|
+
def test_flat_file_path(self):
|
|
40
|
+
"""Test flat_file_path property."""
|
|
41
|
+
resolved = ResolvedModule(module_path=['api', 'v1', 'users'])
|
|
42
|
+
assert resolved.flat_file_path == 'api_v1_users.py'
|
|
43
|
+
|
|
44
|
+
def test_resolution_types(self):
|
|
45
|
+
"""Test different resolution types."""
|
|
46
|
+
custom = ResolvedModule(module_path=['users'], resolution='custom')
|
|
47
|
+
tag = ResolvedModule(module_path=['users'], resolution='tag')
|
|
48
|
+
path = ResolvedModule(module_path=['users'], resolution='path')
|
|
49
|
+
fallback = ResolvedModule(module_path=['common'], resolution='fallback')
|
|
50
|
+
|
|
51
|
+
assert custom.resolution == 'custom'
|
|
52
|
+
assert tag.resolution == 'tag'
|
|
53
|
+
assert path.resolution == 'path'
|
|
54
|
+
assert fallback.resolution == 'fallback'
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestModuleMapResolverBasic:
|
|
58
|
+
"""Basic tests for ModuleMapResolver."""
|
|
59
|
+
|
|
60
|
+
def test_resolver_initialization(self):
|
|
61
|
+
"""Test resolver initialization."""
|
|
62
|
+
config = ModuleSplitConfig(enabled=True)
|
|
63
|
+
resolver = ModuleMapResolver(config)
|
|
64
|
+
assert resolver.config == config
|
|
65
|
+
|
|
66
|
+
def test_fallback_resolution(self):
|
|
67
|
+
"""Test that unmatched paths go to fallback module."""
|
|
68
|
+
config = ModuleSplitConfig(
|
|
69
|
+
enabled=True,
|
|
70
|
+
strategy='custom',
|
|
71
|
+
fallback_module='misc',
|
|
72
|
+
module_map={},
|
|
73
|
+
)
|
|
74
|
+
resolver = ModuleMapResolver(config)
|
|
75
|
+
result = resolver.resolve('/unknown/path', 'GET')
|
|
76
|
+
|
|
77
|
+
assert result.module_path == ['misc']
|
|
78
|
+
assert result.resolution == 'fallback'
|
|
79
|
+
|
|
80
|
+
def test_custom_fallback_name(self):
|
|
81
|
+
"""Test custom fallback module name."""
|
|
82
|
+
config = ModuleSplitConfig(
|
|
83
|
+
enabled=True,
|
|
84
|
+
strategy='custom', # Use custom strategy with no module_map to force fallback
|
|
85
|
+
fallback_module='other',
|
|
86
|
+
module_map={},
|
|
87
|
+
)
|
|
88
|
+
resolver = ModuleMapResolver(config)
|
|
89
|
+
result = resolver.resolve('/unmatched', 'GET')
|
|
90
|
+
|
|
91
|
+
assert result.module_path == ['other']
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TestGlobalPrefixStripping:
|
|
95
|
+
"""Tests for global prefix stripping."""
|
|
96
|
+
|
|
97
|
+
def test_strip_api_prefix(self):
|
|
98
|
+
"""Test stripping /api prefix."""
|
|
99
|
+
config = ModuleSplitConfig(
|
|
100
|
+
enabled=True,
|
|
101
|
+
strategy='path',
|
|
102
|
+
global_strip_prefixes=['/api'],
|
|
103
|
+
)
|
|
104
|
+
resolver = ModuleMapResolver(config)
|
|
105
|
+
result = resolver.resolve('/api/users', 'GET')
|
|
106
|
+
|
|
107
|
+
assert result.module_path == ['users']
|
|
108
|
+
assert result.stripped_path == '/users'
|
|
109
|
+
|
|
110
|
+
def test_strip_versioned_prefix(self):
|
|
111
|
+
"""Test stripping versioned API prefix."""
|
|
112
|
+
config = ModuleSplitConfig(
|
|
113
|
+
enabled=True,
|
|
114
|
+
strategy='path',
|
|
115
|
+
global_strip_prefixes=['/api/v1', '/api/v2'],
|
|
116
|
+
)
|
|
117
|
+
resolver = ModuleMapResolver(config)
|
|
118
|
+
|
|
119
|
+
result_v1 = resolver.resolve('/api/v1/users', 'GET')
|
|
120
|
+
assert result_v1.stripped_path == '/users'
|
|
121
|
+
|
|
122
|
+
result_v2 = resolver.resolve('/api/v2/users', 'GET')
|
|
123
|
+
assert result_v2.stripped_path == '/users'
|
|
124
|
+
|
|
125
|
+
def test_no_matching_prefix(self):
|
|
126
|
+
"""Test path without matching prefix."""
|
|
127
|
+
config = ModuleSplitConfig(
|
|
128
|
+
enabled=True,
|
|
129
|
+
strategy='path',
|
|
130
|
+
global_strip_prefixes=['/api'],
|
|
131
|
+
)
|
|
132
|
+
resolver = ModuleMapResolver(config)
|
|
133
|
+
result = resolver.resolve('/users', 'GET')
|
|
134
|
+
|
|
135
|
+
assert result.stripped_path == '/users'
|
|
136
|
+
|
|
137
|
+
def test_prefix_order_matters(self):
|
|
138
|
+
"""Test that longer prefixes should be first for correct matching."""
|
|
139
|
+
config = ModuleSplitConfig(
|
|
140
|
+
enabled=True,
|
|
141
|
+
strategy='path',
|
|
142
|
+
global_strip_prefixes=['/api/v1', '/api'],
|
|
143
|
+
)
|
|
144
|
+
resolver = ModuleMapResolver(config)
|
|
145
|
+
result = resolver.resolve('/api/v1/users', 'GET')
|
|
146
|
+
|
|
147
|
+
# Should strip /api/v1, not just /api
|
|
148
|
+
assert result.stripped_path == '/users'
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TestCustomModuleMap:
|
|
152
|
+
"""Tests for custom module_map resolution."""
|
|
153
|
+
|
|
154
|
+
def test_simple_pattern_match(self):
|
|
155
|
+
"""Test simple pattern matching."""
|
|
156
|
+
config = ModuleSplitConfig(
|
|
157
|
+
enabled=True,
|
|
158
|
+
strategy='custom',
|
|
159
|
+
global_strip_prefixes=[],
|
|
160
|
+
module_map={'users': ['/users', '/users/*']},
|
|
161
|
+
)
|
|
162
|
+
resolver = ModuleMapResolver(config)
|
|
163
|
+
|
|
164
|
+
result = resolver.resolve('/users', 'GET')
|
|
165
|
+
assert result.module_path == ['users']
|
|
166
|
+
assert result.resolution == 'custom'
|
|
167
|
+
|
|
168
|
+
result = resolver.resolve('/users/123', 'GET')
|
|
169
|
+
assert result.module_path == ['users']
|
|
170
|
+
|
|
171
|
+
def test_wildcard_pattern(self):
|
|
172
|
+
"""Test wildcard * pattern matching."""
|
|
173
|
+
config = ModuleSplitConfig(
|
|
174
|
+
enabled=True,
|
|
175
|
+
strategy='custom',
|
|
176
|
+
global_strip_prefixes=[],
|
|
177
|
+
module_map={'users': ['/users/*']},
|
|
178
|
+
)
|
|
179
|
+
resolver = ModuleMapResolver(config)
|
|
180
|
+
|
|
181
|
+
# Should match
|
|
182
|
+
result = resolver.resolve('/users/123', 'GET')
|
|
183
|
+
assert result.module_path == ['users']
|
|
184
|
+
|
|
185
|
+
# Should not match (no wildcard for root)
|
|
186
|
+
result = resolver.resolve('/users', 'GET')
|
|
187
|
+
assert result.resolution == 'fallback'
|
|
188
|
+
|
|
189
|
+
def test_double_wildcard_pattern(self):
|
|
190
|
+
"""Test recursive ** pattern matching."""
|
|
191
|
+
config = ModuleSplitConfig(
|
|
192
|
+
enabled=True,
|
|
193
|
+
strategy='custom',
|
|
194
|
+
global_strip_prefixes=[],
|
|
195
|
+
module_map={'users': ['/users/**']},
|
|
196
|
+
)
|
|
197
|
+
resolver = ModuleMapResolver(config)
|
|
198
|
+
|
|
199
|
+
result = resolver.resolve('/users/123', 'GET')
|
|
200
|
+
assert result.module_path == ['users']
|
|
201
|
+
|
|
202
|
+
result = resolver.resolve('/users/123/profile', 'GET')
|
|
203
|
+
assert result.module_path == ['users']
|
|
204
|
+
|
|
205
|
+
result = resolver.resolve('/users/123/settings/privacy', 'GET')
|
|
206
|
+
assert result.module_path == ['users']
|
|
207
|
+
|
|
208
|
+
def test_multiple_modules(self):
|
|
209
|
+
"""Test multiple modules in module_map."""
|
|
210
|
+
config = ModuleSplitConfig(
|
|
211
|
+
enabled=True,
|
|
212
|
+
strategy='custom',
|
|
213
|
+
global_strip_prefixes=[],
|
|
214
|
+
module_map={
|
|
215
|
+
'users': ['/users/*'],
|
|
216
|
+
'orders': ['/orders/*'],
|
|
217
|
+
'health': ['/health', '/ready'],
|
|
218
|
+
},
|
|
219
|
+
)
|
|
220
|
+
resolver = ModuleMapResolver(config)
|
|
221
|
+
|
|
222
|
+
assert resolver.resolve('/users/123', 'GET').module_path == ['users']
|
|
223
|
+
assert resolver.resolve('/orders/456', 'GET').module_path == ['orders']
|
|
224
|
+
assert resolver.resolve('/health', 'GET').module_path == ['health']
|
|
225
|
+
assert resolver.resolve('/ready', 'GET').module_path == ['health']
|
|
226
|
+
|
|
227
|
+
def test_nested_modules(self):
|
|
228
|
+
"""Test nested module definitions."""
|
|
229
|
+
config = ModuleSplitConfig(
|
|
230
|
+
enabled=True,
|
|
231
|
+
strategy='custom',
|
|
232
|
+
global_strip_prefixes=[],
|
|
233
|
+
module_map={
|
|
234
|
+
'identity': ModuleDefinition(
|
|
235
|
+
paths=['/identity/**'],
|
|
236
|
+
modules={
|
|
237
|
+
'users': ModuleDefinition(paths=['/users/*']),
|
|
238
|
+
'auth': ModuleDefinition(paths=['/auth/*', '/login']),
|
|
239
|
+
},
|
|
240
|
+
),
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
resolver = ModuleMapResolver(config)
|
|
244
|
+
|
|
245
|
+
# Match the parent
|
|
246
|
+
result = resolver.resolve('/identity/settings', 'GET')
|
|
247
|
+
assert result.module_path == ['identity']
|
|
248
|
+
|
|
249
|
+
def test_module_strip_prefix(self):
|
|
250
|
+
"""Test per-module strip_prefix."""
|
|
251
|
+
config = ModuleSplitConfig(
|
|
252
|
+
enabled=True,
|
|
253
|
+
strategy='custom',
|
|
254
|
+
global_strip_prefixes=[],
|
|
255
|
+
module_map={
|
|
256
|
+
'v2': ModuleDefinition(
|
|
257
|
+
paths=['/v2/**'],
|
|
258
|
+
strip_prefix='/v2',
|
|
259
|
+
modules={
|
|
260
|
+
'users': ModuleDefinition(paths=['/users/*']),
|
|
261
|
+
},
|
|
262
|
+
),
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
resolver = ModuleMapResolver(config)
|
|
266
|
+
|
|
267
|
+
result = resolver.resolve('/v2/users/123', 'GET')
|
|
268
|
+
assert result.module_path == ['v2', 'users']
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class TestTagBasedResolution:
|
|
272
|
+
"""Tests for tag-based resolution."""
|
|
273
|
+
|
|
274
|
+
def test_tag_resolution(self):
|
|
275
|
+
"""Test resolution based on OpenAPI tags."""
|
|
276
|
+
config = ModuleSplitConfig(
|
|
277
|
+
enabled=True,
|
|
278
|
+
strategy='tag',
|
|
279
|
+
)
|
|
280
|
+
resolver = ModuleMapResolver(config)
|
|
281
|
+
|
|
282
|
+
result = resolver.resolve('/users/123', 'GET', tags=['Users'])
|
|
283
|
+
assert result.module_path == ['users']
|
|
284
|
+
assert result.resolution == 'tag'
|
|
285
|
+
|
|
286
|
+
def test_first_tag_used(self):
|
|
287
|
+
"""Test that only the first tag is used."""
|
|
288
|
+
config = ModuleSplitConfig(
|
|
289
|
+
enabled=True,
|
|
290
|
+
strategy='tag',
|
|
291
|
+
)
|
|
292
|
+
resolver = ModuleMapResolver(config)
|
|
293
|
+
|
|
294
|
+
result = resolver.resolve('/users/123', 'GET', tags=['Users', 'Admin'])
|
|
295
|
+
assert result.module_path == ['users']
|
|
296
|
+
|
|
297
|
+
def test_no_tags_fallback(self):
|
|
298
|
+
"""Test fallback when no tags provided."""
|
|
299
|
+
config = ModuleSplitConfig(
|
|
300
|
+
enabled=True,
|
|
301
|
+
strategy='tag',
|
|
302
|
+
fallback_module='common',
|
|
303
|
+
)
|
|
304
|
+
resolver = ModuleMapResolver(config)
|
|
305
|
+
|
|
306
|
+
result = resolver.resolve('/users/123', 'GET', tags=None)
|
|
307
|
+
assert result.module_path == ['common']
|
|
308
|
+
assert result.resolution == 'fallback'
|
|
309
|
+
|
|
310
|
+
def test_empty_tags_fallback(self):
|
|
311
|
+
"""Test fallback when tags list is empty."""
|
|
312
|
+
config = ModuleSplitConfig(
|
|
313
|
+
enabled=True,
|
|
314
|
+
strategy='tag',
|
|
315
|
+
)
|
|
316
|
+
resolver = ModuleMapResolver(config)
|
|
317
|
+
|
|
318
|
+
result = resolver.resolve('/users/123', 'GET', tags=[])
|
|
319
|
+
assert result.resolution == 'fallback'
|
|
320
|
+
|
|
321
|
+
def test_tag_sanitization(self):
|
|
322
|
+
"""Test that tags are sanitized to valid Python identifiers."""
|
|
323
|
+
config = ModuleSplitConfig(
|
|
324
|
+
enabled=True,
|
|
325
|
+
strategy='tag',
|
|
326
|
+
)
|
|
327
|
+
resolver = ModuleMapResolver(config)
|
|
328
|
+
|
|
329
|
+
result = resolver.resolve('/test', 'GET', tags=['User Management'])
|
|
330
|
+
assert result.module_path == ['user_management']
|
|
331
|
+
|
|
332
|
+
result = resolver.resolve('/test', 'GET', tags=['123-invalid'])
|
|
333
|
+
assert result.module_path == ['_123_invalid']
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class TestPathBasedResolution:
|
|
337
|
+
"""Tests for path-based resolution."""
|
|
338
|
+
|
|
339
|
+
def test_path_resolution_depth_1(self):
|
|
340
|
+
"""Test path resolution with depth 1."""
|
|
341
|
+
config = ModuleSplitConfig(
|
|
342
|
+
enabled=True,
|
|
343
|
+
strategy='path',
|
|
344
|
+
path_depth=1,
|
|
345
|
+
global_strip_prefixes=[],
|
|
346
|
+
)
|
|
347
|
+
resolver = ModuleMapResolver(config)
|
|
348
|
+
|
|
349
|
+
result = resolver.resolve('/users/123', 'GET')
|
|
350
|
+
assert result.module_path == ['users']
|
|
351
|
+
assert result.resolution == 'path'
|
|
352
|
+
|
|
353
|
+
def test_path_resolution_depth_2(self):
|
|
354
|
+
"""Test path resolution with depth 2."""
|
|
355
|
+
config = ModuleSplitConfig(
|
|
356
|
+
enabled=True,
|
|
357
|
+
strategy='path',
|
|
358
|
+
path_depth=2,
|
|
359
|
+
global_strip_prefixes=[],
|
|
360
|
+
)
|
|
361
|
+
resolver = ModuleMapResolver(config)
|
|
362
|
+
|
|
363
|
+
result = resolver.resolve('/api/users/123', 'GET')
|
|
364
|
+
assert result.module_path == ['api_users']
|
|
365
|
+
|
|
366
|
+
def test_path_ignores_parameters(self):
|
|
367
|
+
"""Test that path parameters are ignored."""
|
|
368
|
+
config = ModuleSplitConfig(
|
|
369
|
+
enabled=True,
|
|
370
|
+
strategy='path',
|
|
371
|
+
path_depth=2,
|
|
372
|
+
global_strip_prefixes=[],
|
|
373
|
+
)
|
|
374
|
+
resolver = ModuleMapResolver(config)
|
|
375
|
+
|
|
376
|
+
result = resolver.resolve('/users/{id}/profile', 'GET')
|
|
377
|
+
assert result.module_path == ['users_profile']
|
|
378
|
+
|
|
379
|
+
def test_path_sanitization(self):
|
|
380
|
+
"""Test that path segments are sanitized."""
|
|
381
|
+
config = ModuleSplitConfig(
|
|
382
|
+
enabled=True,
|
|
383
|
+
strategy='path',
|
|
384
|
+
global_strip_prefixes=[],
|
|
385
|
+
)
|
|
386
|
+
resolver = ModuleMapResolver(config)
|
|
387
|
+
|
|
388
|
+
result = resolver.resolve('/user-management/list', 'GET')
|
|
389
|
+
assert result.module_path == ['user_management']
|
|
390
|
+
|
|
391
|
+
def test_root_path_fallback(self):
|
|
392
|
+
"""Test fallback for root path."""
|
|
393
|
+
config = ModuleSplitConfig(
|
|
394
|
+
enabled=True,
|
|
395
|
+
strategy='path',
|
|
396
|
+
)
|
|
397
|
+
resolver = ModuleMapResolver(config)
|
|
398
|
+
|
|
399
|
+
result = resolver.resolve('/', 'GET')
|
|
400
|
+
assert result.resolution == 'fallback'
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class TestHybridStrategy:
|
|
404
|
+
"""Tests for hybrid strategy combining custom, tag, and path."""
|
|
405
|
+
|
|
406
|
+
def test_hybrid_custom_first(self):
|
|
407
|
+
"""Test that custom module_map takes priority in hybrid mode."""
|
|
408
|
+
config = ModuleSplitConfig(
|
|
409
|
+
enabled=True,
|
|
410
|
+
strategy='hybrid',
|
|
411
|
+
global_strip_prefixes=[],
|
|
412
|
+
module_map={'users': ['/users/*']},
|
|
413
|
+
)
|
|
414
|
+
resolver = ModuleMapResolver(config)
|
|
415
|
+
|
|
416
|
+
# Custom match should take priority over tags
|
|
417
|
+
result = resolver.resolve('/users/123', 'GET', tags=['Other'])
|
|
418
|
+
assert result.module_path == ['users']
|
|
419
|
+
assert result.resolution == 'custom'
|
|
420
|
+
|
|
421
|
+
def test_hybrid_tag_second(self):
|
|
422
|
+
"""Test that tags are used when no custom match."""
|
|
423
|
+
config = ModuleSplitConfig(
|
|
424
|
+
enabled=True,
|
|
425
|
+
strategy='hybrid',
|
|
426
|
+
global_strip_prefixes=[],
|
|
427
|
+
module_map={'users': ['/users/*']},
|
|
428
|
+
)
|
|
429
|
+
resolver = ModuleMapResolver(config)
|
|
430
|
+
|
|
431
|
+
# No custom match, should use tag
|
|
432
|
+
result = resolver.resolve('/orders/123', 'GET', tags=['Orders'])
|
|
433
|
+
assert result.module_path == ['orders']
|
|
434
|
+
assert result.resolution == 'tag'
|
|
435
|
+
|
|
436
|
+
def test_hybrid_path_third(self):
|
|
437
|
+
"""Test that path is used when no custom or tag match."""
|
|
438
|
+
config = ModuleSplitConfig(
|
|
439
|
+
enabled=True,
|
|
440
|
+
strategy='hybrid',
|
|
441
|
+
global_strip_prefixes=[],
|
|
442
|
+
module_map={'users': ['/users/*']},
|
|
443
|
+
)
|
|
444
|
+
resolver = ModuleMapResolver(config)
|
|
445
|
+
|
|
446
|
+
# No custom match, no tags, should use path
|
|
447
|
+
result = resolver.resolve('/orders/123', 'GET', tags=None)
|
|
448
|
+
assert result.module_path == ['orders']
|
|
449
|
+
assert result.resolution == 'path'
|
|
450
|
+
|
|
451
|
+
def test_hybrid_fallback_last(self):
|
|
452
|
+
"""Test fallback when nothing else matches."""
|
|
453
|
+
config = ModuleSplitConfig(
|
|
454
|
+
enabled=True,
|
|
455
|
+
strategy='hybrid',
|
|
456
|
+
global_strip_prefixes=[],
|
|
457
|
+
module_map={},
|
|
458
|
+
fallback_module='misc',
|
|
459
|
+
)
|
|
460
|
+
resolver = ModuleMapResolver(config)
|
|
461
|
+
|
|
462
|
+
result = resolver.resolve('/', 'GET', tags=None)
|
|
463
|
+
assert result.module_path == ['misc']
|
|
464
|
+
assert result.resolution == 'fallback'
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class TestPatternMatching:
|
|
468
|
+
"""Tests for glob pattern matching edge cases."""
|
|
469
|
+
|
|
470
|
+
def test_exact_match(self):
|
|
471
|
+
"""Test exact path matching."""
|
|
472
|
+
config = ModuleSplitConfig(
|
|
473
|
+
enabled=True,
|
|
474
|
+
strategy='custom',
|
|
475
|
+
global_strip_prefixes=[],
|
|
476
|
+
module_map={'health': ['/health']},
|
|
477
|
+
)
|
|
478
|
+
resolver = ModuleMapResolver(config)
|
|
479
|
+
|
|
480
|
+
assert resolver.resolve('/health', 'GET').module_path == ['health']
|
|
481
|
+
assert resolver.resolve('/health/', 'GET').module_path == ['health']
|
|
482
|
+
assert resolver.resolve('/healthy', 'GET').resolution == 'fallback'
|
|
483
|
+
|
|
484
|
+
def test_trailing_slash_handling(self):
|
|
485
|
+
"""Test that trailing slashes are handled correctly."""
|
|
486
|
+
config = ModuleSplitConfig(
|
|
487
|
+
enabled=True,
|
|
488
|
+
strategy='custom',
|
|
489
|
+
global_strip_prefixes=[],
|
|
490
|
+
module_map={'users': ['/users/*']},
|
|
491
|
+
)
|
|
492
|
+
resolver = ModuleMapResolver(config)
|
|
493
|
+
|
|
494
|
+
result1 = resolver.resolve('/users/123', 'GET')
|
|
495
|
+
result2 = resolver.resolve('/users/123/', 'GET')
|
|
496
|
+
|
|
497
|
+
assert result1.module_path == result2.module_path
|
|
498
|
+
|
|
499
|
+
def test_question_mark_pattern(self):
|
|
500
|
+
"""Test ? pattern for single character matching."""
|
|
501
|
+
config = ModuleSplitConfig(
|
|
502
|
+
enabled=True,
|
|
503
|
+
strategy='custom',
|
|
504
|
+
global_strip_prefixes=[],
|
|
505
|
+
module_map={'version': ['/v?/users']},
|
|
506
|
+
)
|
|
507
|
+
resolver = ModuleMapResolver(config)
|
|
508
|
+
|
|
509
|
+
assert resolver.resolve('/v1/users', 'GET').module_path == ['version']
|
|
510
|
+
assert resolver.resolve('/v2/users', 'GET').module_path == ['version']
|
|
511
|
+
# v10 has two characters, shouldn't match
|
|
512
|
+
assert resolver.resolve('/v10/users', 'GET').resolution == 'fallback'
|