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,493 @@
|
|
|
1
|
+
"""Test suite for OtterAPI exceptions and configuration.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive tests for the custom exception hierarchy
|
|
4
|
+
and configuration loading functionality.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from unittest.mock import patch
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
from otterapi.config import (
|
|
17
|
+
CodegenConfig,
|
|
18
|
+
DocumentConfig,
|
|
19
|
+
_expand_env_vars,
|
|
20
|
+
_expand_env_vars_recursive,
|
|
21
|
+
create_default_config,
|
|
22
|
+
get_config,
|
|
23
|
+
load_json,
|
|
24
|
+
load_yaml,
|
|
25
|
+
)
|
|
26
|
+
from otterapi.exceptions import (
|
|
27
|
+
CodeGenerationError,
|
|
28
|
+
ConfigurationError,
|
|
29
|
+
EndpointGenerationError,
|
|
30
|
+
OtterAPIError,
|
|
31
|
+
OutputError,
|
|
32
|
+
SchemaError,
|
|
33
|
+
SchemaLoadError,
|
|
34
|
+
SchemaReferenceError,
|
|
35
|
+
SchemaValidationError,
|
|
36
|
+
TypeGenerationError,
|
|
37
|
+
UnsupportedFeatureError,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestOtterAPIError:
|
|
42
|
+
"""Tests for the base OtterAPIError exception."""
|
|
43
|
+
|
|
44
|
+
def test_basic_message(self):
|
|
45
|
+
"""Test that the error stores the message."""
|
|
46
|
+
error = OtterAPIError('Something went wrong')
|
|
47
|
+
assert error.message == 'Something went wrong'
|
|
48
|
+
assert str(error) == 'Something went wrong'
|
|
49
|
+
|
|
50
|
+
def test_inheritance(self):
|
|
51
|
+
"""Test that OtterAPIError inherits from Exception."""
|
|
52
|
+
error = OtterAPIError('Test')
|
|
53
|
+
assert isinstance(error, Exception)
|
|
54
|
+
|
|
55
|
+
def test_can_be_caught_as_exception(self):
|
|
56
|
+
"""Test that the error can be caught as a generic Exception."""
|
|
57
|
+
with pytest.raises(Exception):
|
|
58
|
+
raise OtterAPIError('Test error')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestSchemaErrors:
|
|
62
|
+
"""Tests for schema-related exceptions."""
|
|
63
|
+
|
|
64
|
+
def test_schema_error_inheritance(self):
|
|
65
|
+
"""Test that SchemaError inherits from OtterAPIError."""
|
|
66
|
+
error = SchemaError('Schema issue')
|
|
67
|
+
assert isinstance(error, OtterAPIError)
|
|
68
|
+
|
|
69
|
+
def test_schema_load_error_with_source(self):
|
|
70
|
+
"""Test SchemaLoadError with just a source."""
|
|
71
|
+
error = SchemaLoadError('https://api.example.com/openapi.json')
|
|
72
|
+
assert error.source == 'https://api.example.com/openapi.json'
|
|
73
|
+
assert error.cause is None
|
|
74
|
+
assert 'https://api.example.com/openapi.json' in str(error)
|
|
75
|
+
|
|
76
|
+
def test_schema_load_error_with_cause(self):
|
|
77
|
+
"""Test SchemaLoadError with a cause exception."""
|
|
78
|
+
cause = ConnectionError('Network unavailable')
|
|
79
|
+
error = SchemaLoadError('https://api.example.com/openapi.json', cause=cause)
|
|
80
|
+
assert error.source == 'https://api.example.com/openapi.json'
|
|
81
|
+
assert error.cause == cause
|
|
82
|
+
assert 'Network unavailable' in str(error)
|
|
83
|
+
|
|
84
|
+
def test_schema_validation_error_with_errors(self):
|
|
85
|
+
"""Test SchemaValidationError with validation errors."""
|
|
86
|
+
errors = ['Missing info.title', 'Invalid paths format']
|
|
87
|
+
error = SchemaValidationError('./api.yaml', errors=errors)
|
|
88
|
+
assert error.source == './api.yaml'
|
|
89
|
+
assert error.errors == errors
|
|
90
|
+
assert 'Missing info.title' in str(error)
|
|
91
|
+
|
|
92
|
+
def test_schema_validation_error_without_errors(self):
|
|
93
|
+
"""Test SchemaValidationError without detailed errors."""
|
|
94
|
+
error = SchemaValidationError('./api.yaml')
|
|
95
|
+
assert error.source == './api.yaml'
|
|
96
|
+
assert error.errors == []
|
|
97
|
+
|
|
98
|
+
def test_schema_reference_error(self):
|
|
99
|
+
"""Test SchemaReferenceError."""
|
|
100
|
+
error = SchemaReferenceError(
|
|
101
|
+
'#/components/schemas/Pet', reason='Schema not found'
|
|
102
|
+
)
|
|
103
|
+
assert error.reference == '#/components/schemas/Pet'
|
|
104
|
+
assert error.reason == 'Schema not found'
|
|
105
|
+
assert '#/components/schemas/Pet' in str(error)
|
|
106
|
+
assert 'Schema not found' in str(error)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TestCodeGenerationErrors:
|
|
110
|
+
"""Tests for code generation exceptions."""
|
|
111
|
+
|
|
112
|
+
def test_code_generation_error_basic(self):
|
|
113
|
+
"""Test basic CodeGenerationError."""
|
|
114
|
+
error = CodeGenerationError('Generation failed')
|
|
115
|
+
assert 'Generation failed' in str(error)
|
|
116
|
+
|
|
117
|
+
def test_code_generation_error_with_context(self):
|
|
118
|
+
"""Test CodeGenerationError with context."""
|
|
119
|
+
error = CodeGenerationError('Generation failed', context='Pet model')
|
|
120
|
+
assert error.context == 'Pet model'
|
|
121
|
+
assert 'Pet model' in str(error)
|
|
122
|
+
|
|
123
|
+
def test_code_generation_error_with_cause(self):
|
|
124
|
+
"""Test CodeGenerationError with a cause."""
|
|
125
|
+
cause = TypeError('Invalid type')
|
|
126
|
+
error = CodeGenerationError('Generation failed', cause=cause)
|
|
127
|
+
assert error.cause == cause
|
|
128
|
+
assert 'Invalid type' in str(error)
|
|
129
|
+
|
|
130
|
+
def test_type_generation_error(self):
|
|
131
|
+
"""Test TypeGenerationError."""
|
|
132
|
+
error = TypeGenerationError('Pet', schema_path='#/components/schemas/Pet')
|
|
133
|
+
assert error.type_name == 'Pet'
|
|
134
|
+
assert error.schema_path == '#/components/schemas/Pet'
|
|
135
|
+
assert 'Pet' in str(error)
|
|
136
|
+
|
|
137
|
+
def test_endpoint_generation_error(self):
|
|
138
|
+
"""Test EndpointGenerationError."""
|
|
139
|
+
error = EndpointGenerationError('listPets', method='GET', path='/pets')
|
|
140
|
+
assert error.operation_id == 'listPets'
|
|
141
|
+
assert error.method == 'GET'
|
|
142
|
+
assert error.path == '/pets'
|
|
143
|
+
assert 'listPets' in str(error)
|
|
144
|
+
assert 'GET /pets' in str(error)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TestConfigurationError:
|
|
148
|
+
"""Tests for ConfigurationError."""
|
|
149
|
+
|
|
150
|
+
def test_basic_configuration_error(self):
|
|
151
|
+
"""Test basic ConfigurationError."""
|
|
152
|
+
error = ConfigurationError('Invalid configuration')
|
|
153
|
+
assert 'Invalid configuration' in str(error)
|
|
154
|
+
|
|
155
|
+
def test_configuration_error_with_path(self):
|
|
156
|
+
"""Test ConfigurationError with config path."""
|
|
157
|
+
error = ConfigurationError('Invalid value', config_path='./otter.yml')
|
|
158
|
+
assert error.config_path == './otter.yml'
|
|
159
|
+
assert './otter.yml' in str(error)
|
|
160
|
+
|
|
161
|
+
def test_configuration_error_with_field(self):
|
|
162
|
+
"""Test ConfigurationError with field name."""
|
|
163
|
+
error = ConfigurationError('Invalid value', field='documents[0].source')
|
|
164
|
+
assert error.field == 'documents[0].source'
|
|
165
|
+
assert 'documents[0].source' in str(error)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class TestOutputError:
|
|
169
|
+
"""Tests for OutputError."""
|
|
170
|
+
|
|
171
|
+
def test_output_error_basic(self):
|
|
172
|
+
"""Test basic OutputError."""
|
|
173
|
+
error = OutputError('./output/models.py')
|
|
174
|
+
assert error.output_path == './output/models.py'
|
|
175
|
+
assert './output/models.py' in str(error)
|
|
176
|
+
|
|
177
|
+
def test_output_error_with_cause(self):
|
|
178
|
+
"""Test OutputError with cause."""
|
|
179
|
+
cause = PermissionError('Access denied')
|
|
180
|
+
error = OutputError('./output/models.py', cause=cause)
|
|
181
|
+
assert error.cause == cause
|
|
182
|
+
assert 'Access denied' in str(error)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class TestUnsupportedFeatureError:
|
|
186
|
+
"""Tests for UnsupportedFeatureError."""
|
|
187
|
+
|
|
188
|
+
def test_unsupported_feature_basic(self):
|
|
189
|
+
"""Test basic UnsupportedFeatureError."""
|
|
190
|
+
error = UnsupportedFeatureError('XML request bodies')
|
|
191
|
+
assert error.feature == 'XML request bodies'
|
|
192
|
+
assert 'XML request bodies' in str(error)
|
|
193
|
+
|
|
194
|
+
def test_unsupported_feature_with_suggestion(self):
|
|
195
|
+
"""Test UnsupportedFeatureError with suggestion."""
|
|
196
|
+
error = UnsupportedFeatureError(
|
|
197
|
+
'External $ref references',
|
|
198
|
+
suggestion='Use a bundler tool to inline external references',
|
|
199
|
+
)
|
|
200
|
+
assert error.suggestion is not None
|
|
201
|
+
assert 'bundler' in str(error)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class TestEnvironmentVariableExpansion:
|
|
205
|
+
"""Tests for environment variable expansion in configuration."""
|
|
206
|
+
|
|
207
|
+
def test_expand_simple_env_var(self):
|
|
208
|
+
"""Test expanding a simple environment variable."""
|
|
209
|
+
with patch.dict(os.environ, {'TEST_VAR': 'test_value'}):
|
|
210
|
+
result = _expand_env_vars('${TEST_VAR}')
|
|
211
|
+
assert result == 'test_value'
|
|
212
|
+
|
|
213
|
+
def test_expand_env_var_with_default(self):
|
|
214
|
+
"""Test expanding an env var with default when not set."""
|
|
215
|
+
# Ensure the variable is not set
|
|
216
|
+
os.environ.pop('UNSET_VAR', None)
|
|
217
|
+
result = _expand_env_vars('${UNSET_VAR:-default_value}')
|
|
218
|
+
assert result == 'default_value'
|
|
219
|
+
|
|
220
|
+
def test_expand_env_var_with_default_when_set(self):
|
|
221
|
+
"""Test that set env var overrides default."""
|
|
222
|
+
with patch.dict(os.environ, {'SET_VAR': 'actual_value'}):
|
|
223
|
+
result = _expand_env_vars('${SET_VAR:-default_value}')
|
|
224
|
+
assert result == 'actual_value'
|
|
225
|
+
|
|
226
|
+
def test_expand_multiple_env_vars(self):
|
|
227
|
+
"""Test expanding multiple env vars in one string."""
|
|
228
|
+
with patch.dict(os.environ, {'HOST': 'localhost', 'PORT': '8080'}):
|
|
229
|
+
result = _expand_env_vars('http://${HOST}:${PORT}/api')
|
|
230
|
+
assert result == 'http://localhost:8080/api'
|
|
231
|
+
|
|
232
|
+
def test_expand_env_vars_recursive(self):
|
|
233
|
+
"""Test recursive expansion in nested structures."""
|
|
234
|
+
with patch.dict(os.environ, {'API_URL': 'https://api.example.com'}):
|
|
235
|
+
data = {
|
|
236
|
+
'documents': [
|
|
237
|
+
{'source': '${API_URL}/openapi.json', 'output': './client'}
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
result = _expand_env_vars_recursive(data)
|
|
241
|
+
assert (
|
|
242
|
+
result['documents'][0]['source']
|
|
243
|
+
== 'https://api.example.com/openapi.json'
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class TestDocumentConfig:
|
|
248
|
+
"""Tests for DocumentConfig validation."""
|
|
249
|
+
|
|
250
|
+
def test_valid_document_config(self):
|
|
251
|
+
"""Test creating a valid DocumentConfig."""
|
|
252
|
+
config = DocumentConfig(source='./api.yaml', output='./client')
|
|
253
|
+
assert config.source == './api.yaml'
|
|
254
|
+
assert config.output == './client'
|
|
255
|
+
assert config.models_file == 'models.py'
|
|
256
|
+
assert config.endpoints_file == 'endpoints.py'
|
|
257
|
+
|
|
258
|
+
def test_source_cannot_be_empty(self):
|
|
259
|
+
"""Test that source cannot be empty."""
|
|
260
|
+
with pytest.raises(ValueError):
|
|
261
|
+
DocumentConfig(source='', output='./client')
|
|
262
|
+
|
|
263
|
+
def test_source_cannot_be_whitespace(self):
|
|
264
|
+
"""Test that source cannot be just whitespace."""
|
|
265
|
+
with pytest.raises(ValueError):
|
|
266
|
+
DocumentConfig(source=' ', output='./client')
|
|
267
|
+
|
|
268
|
+
def test_output_cannot_be_empty(self):
|
|
269
|
+
"""Test that output cannot be empty."""
|
|
270
|
+
with pytest.raises(ValueError):
|
|
271
|
+
DocumentConfig(source='./api.yaml', output='')
|
|
272
|
+
|
|
273
|
+
def test_models_file_must_end_with_py(self):
|
|
274
|
+
"""Test that models_file must end with .py."""
|
|
275
|
+
with pytest.raises(ValueError):
|
|
276
|
+
DocumentConfig(
|
|
277
|
+
source='./api.yaml', output='./client', models_file='models.txt'
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def test_endpoints_file_must_end_with_py(self):
|
|
281
|
+
"""Test that endpoints_file must end with .py."""
|
|
282
|
+
with pytest.raises(ValueError):
|
|
283
|
+
DocumentConfig(
|
|
284
|
+
source='./api.yaml', output='./client', endpoints_file='endpoints'
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def test_custom_filenames(self):
|
|
288
|
+
"""Test custom model and endpoint filenames."""
|
|
289
|
+
config = DocumentConfig(
|
|
290
|
+
source='./api.yaml',
|
|
291
|
+
output='./client',
|
|
292
|
+
models_file='api_models.py',
|
|
293
|
+
endpoints_file='api_endpoints.py',
|
|
294
|
+
)
|
|
295
|
+
assert config.models_file == 'api_models.py'
|
|
296
|
+
assert config.endpoints_file == 'api_endpoints.py'
|
|
297
|
+
|
|
298
|
+
def test_optional_fields_defaults(self):
|
|
299
|
+
"""Test that optional fields have correct defaults."""
|
|
300
|
+
config = DocumentConfig(source='./api.yaml', output='./client')
|
|
301
|
+
assert config.base_url is None
|
|
302
|
+
assert config.models_import_path is None
|
|
303
|
+
assert config.generate_async is True
|
|
304
|
+
assert config.generate_sync is True
|
|
305
|
+
assert config.client_class_name is None
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class TestCodegenConfig:
|
|
309
|
+
"""Tests for CodegenConfig validation."""
|
|
310
|
+
|
|
311
|
+
def test_valid_codegen_config(self):
|
|
312
|
+
"""Test creating a valid CodegenConfig."""
|
|
313
|
+
config = CodegenConfig(
|
|
314
|
+
documents=[DocumentConfig(source='./api.yaml', output='./client')]
|
|
315
|
+
)
|
|
316
|
+
assert len(config.documents) == 1
|
|
317
|
+
assert config.generate_endpoints is True
|
|
318
|
+
|
|
319
|
+
def test_documents_cannot_be_empty(self):
|
|
320
|
+
"""Test that documents list cannot be empty."""
|
|
321
|
+
with pytest.raises(ValueError):
|
|
322
|
+
CodegenConfig(documents=[])
|
|
323
|
+
|
|
324
|
+
def test_multiple_documents(self):
|
|
325
|
+
"""Test config with multiple documents."""
|
|
326
|
+
config = CodegenConfig(
|
|
327
|
+
documents=[
|
|
328
|
+
DocumentConfig(source='./api1.yaml', output='./client1'),
|
|
329
|
+
DocumentConfig(source='./api2.yaml', output='./client2'),
|
|
330
|
+
]
|
|
331
|
+
)
|
|
332
|
+
assert len(config.documents) == 2
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class TestConfigLoading:
|
|
336
|
+
"""Tests for configuration file loading."""
|
|
337
|
+
|
|
338
|
+
@pytest.fixture
|
|
339
|
+
def temp_dir(self):
|
|
340
|
+
"""Create a temporary directory."""
|
|
341
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
342
|
+
yield Path(tmpdir)
|
|
343
|
+
|
|
344
|
+
def test_load_yaml_file(self, temp_dir):
|
|
345
|
+
"""Test loading a YAML configuration file."""
|
|
346
|
+
config_file = temp_dir / 'otter.yaml'
|
|
347
|
+
config_data = {'documents': [{'source': './api.yaml', 'output': './client'}]}
|
|
348
|
+
config_file.write_text(yaml.dump(config_data))
|
|
349
|
+
|
|
350
|
+
result = load_yaml(config_file)
|
|
351
|
+
assert result['documents'][0]['source'] == './api.yaml'
|
|
352
|
+
|
|
353
|
+
def test_load_json_file(self, temp_dir):
|
|
354
|
+
"""Test loading a JSON configuration file."""
|
|
355
|
+
config_file = temp_dir / 'otter.json'
|
|
356
|
+
config_data = {'documents': [{'source': './api.json', 'output': './client'}]}
|
|
357
|
+
config_file.write_text(json.dumps(config_data))
|
|
358
|
+
|
|
359
|
+
result = load_json(config_file)
|
|
360
|
+
assert result['documents'][0]['source'] == './api.json'
|
|
361
|
+
|
|
362
|
+
def test_load_yaml_file_not_found(self, temp_dir):
|
|
363
|
+
"""Test that loading non-existent YAML file raises error."""
|
|
364
|
+
with pytest.raises(FileNotFoundError):
|
|
365
|
+
load_yaml(temp_dir / 'nonexistent.yaml')
|
|
366
|
+
|
|
367
|
+
def test_load_json_file_not_found(self, temp_dir):
|
|
368
|
+
"""Test that loading non-existent JSON file raises error."""
|
|
369
|
+
with pytest.raises(FileNotFoundError):
|
|
370
|
+
load_json(temp_dir / 'nonexistent.json')
|
|
371
|
+
|
|
372
|
+
def test_get_config_from_yaml(self, temp_dir):
|
|
373
|
+
"""Test get_config with a YAML file."""
|
|
374
|
+
config_file = temp_dir / 'otter.yaml'
|
|
375
|
+
config_data = {'documents': [{'source': './api.yaml', 'output': './client'}]}
|
|
376
|
+
config_file.write_text(yaml.dump(config_data))
|
|
377
|
+
|
|
378
|
+
config = get_config(str(config_file))
|
|
379
|
+
assert len(config.documents) == 1
|
|
380
|
+
assert config.documents[0].source == './api.yaml'
|
|
381
|
+
|
|
382
|
+
def test_get_config_from_json(self, temp_dir):
|
|
383
|
+
"""Test get_config with a JSON file."""
|
|
384
|
+
config_file = temp_dir / 'otter.json'
|
|
385
|
+
config_data = {'documents': [{'source': './api.json', 'output': './client'}]}
|
|
386
|
+
config_file.write_text(json.dumps(config_data))
|
|
387
|
+
|
|
388
|
+
config = get_config(str(config_file))
|
|
389
|
+
assert len(config.documents) == 1
|
|
390
|
+
|
|
391
|
+
def test_get_config_auto_discovery_yaml(self, temp_dir):
|
|
392
|
+
"""Test that get_config discovers otter.yaml in cwd."""
|
|
393
|
+
config_file = temp_dir / 'otter.yaml'
|
|
394
|
+
config_data = {'documents': [{'source': './api.yaml', 'output': './client'}]}
|
|
395
|
+
config_file.write_text(yaml.dump(config_data))
|
|
396
|
+
|
|
397
|
+
# Change to temp dir
|
|
398
|
+
original_cwd = os.getcwd()
|
|
399
|
+
try:
|
|
400
|
+
os.chdir(temp_dir)
|
|
401
|
+
config = get_config()
|
|
402
|
+
assert len(config.documents) == 1
|
|
403
|
+
finally:
|
|
404
|
+
os.chdir(original_cwd)
|
|
405
|
+
|
|
406
|
+
def test_get_config_auto_discovery_yml(self, temp_dir):
|
|
407
|
+
"""Test that get_config discovers otter.yml in cwd."""
|
|
408
|
+
config_file = temp_dir / 'otter.yml'
|
|
409
|
+
config_data = {'documents': [{'source': './api.yaml', 'output': './client'}]}
|
|
410
|
+
config_file.write_text(yaml.dump(config_data))
|
|
411
|
+
|
|
412
|
+
original_cwd = os.getcwd()
|
|
413
|
+
try:
|
|
414
|
+
os.chdir(temp_dir)
|
|
415
|
+
config = get_config()
|
|
416
|
+
assert len(config.documents) == 1
|
|
417
|
+
finally:
|
|
418
|
+
os.chdir(original_cwd)
|
|
419
|
+
|
|
420
|
+
def test_get_config_from_env_vars(self, temp_dir):
|
|
421
|
+
"""Test get_config from environment variables."""
|
|
422
|
+
original_cwd = os.getcwd()
|
|
423
|
+
try:
|
|
424
|
+
os.chdir(temp_dir)
|
|
425
|
+
with patch.dict(
|
|
426
|
+
os.environ,
|
|
427
|
+
{
|
|
428
|
+
'OTTER_SOURCE': 'https://api.example.com/openapi.json',
|
|
429
|
+
'OTTER_OUTPUT': './generated',
|
|
430
|
+
},
|
|
431
|
+
):
|
|
432
|
+
config = get_config()
|
|
433
|
+
assert len(config.documents) == 1
|
|
434
|
+
assert (
|
|
435
|
+
config.documents[0].source == 'https://api.example.com/openapi.json'
|
|
436
|
+
)
|
|
437
|
+
assert config.documents[0].output == './generated'
|
|
438
|
+
finally:
|
|
439
|
+
os.chdir(original_cwd)
|
|
440
|
+
|
|
441
|
+
def test_get_config_not_found(self, temp_dir):
|
|
442
|
+
"""Test that get_config raises error when no config found."""
|
|
443
|
+
original_cwd = os.getcwd()
|
|
444
|
+
try:
|
|
445
|
+
os.chdir(temp_dir)
|
|
446
|
+
# Clear any env vars that might provide config
|
|
447
|
+
env_to_clear = ['OTTER_SOURCE', 'OTTER_OUTPUT']
|
|
448
|
+
with patch.dict(os.environ, {k: '' for k in env_to_clear}, clear=False):
|
|
449
|
+
for k in env_to_clear:
|
|
450
|
+
os.environ.pop(k, None)
|
|
451
|
+
with pytest.raises(FileNotFoundError):
|
|
452
|
+
get_config()
|
|
453
|
+
finally:
|
|
454
|
+
os.chdir(original_cwd)
|
|
455
|
+
|
|
456
|
+
def test_env_var_expansion_in_yaml(self, temp_dir):
|
|
457
|
+
"""Test that env vars are expanded when loading YAML."""
|
|
458
|
+
config_file = temp_dir / 'otter.yaml'
|
|
459
|
+
config_content = """
|
|
460
|
+
documents:
|
|
461
|
+
- source: ${API_URL}/openapi.json
|
|
462
|
+
output: ./client
|
|
463
|
+
"""
|
|
464
|
+
config_file.write_text(config_content)
|
|
465
|
+
|
|
466
|
+
with patch.dict(os.environ, {'API_URL': 'https://api.example.com'}):
|
|
467
|
+
config = get_config(str(config_file))
|
|
468
|
+
assert config.documents[0].source == 'https://api.example.com/openapi.json'
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
class TestCreateDefaultConfig:
|
|
472
|
+
"""Tests for create_default_config function."""
|
|
473
|
+
|
|
474
|
+
def test_creates_valid_config(self):
|
|
475
|
+
"""Test that create_default_config returns valid config."""
|
|
476
|
+
config_dict = create_default_config()
|
|
477
|
+
|
|
478
|
+
# Should be a valid config structure
|
|
479
|
+
assert 'documents' in config_dict
|
|
480
|
+
assert len(config_dict['documents']) > 0
|
|
481
|
+
|
|
482
|
+
# Should have required fields
|
|
483
|
+
doc = config_dict['documents'][0]
|
|
484
|
+
assert 'source' in doc
|
|
485
|
+
assert 'output' in doc
|
|
486
|
+
|
|
487
|
+
def test_default_config_validates(self):
|
|
488
|
+
"""Test that default config passes validation."""
|
|
489
|
+
config_dict = create_default_config()
|
|
490
|
+
|
|
491
|
+
# Should not raise
|
|
492
|
+
config = CodegenConfig.model_validate(config_dict)
|
|
493
|
+
assert len(config.documents) == 1
|