pytest-openapi 0.1.1.dev202601050151__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.
- pytest_openapi/__init__.py +1 -0
- pytest_openapi/case_generator.py +433 -0
- pytest_openapi/contract.py +834 -0
- pytest_openapi/openapi.py +259 -0
- pytest_openapi/plugin.py +92 -0
- pytest_openapi-0.1.1.dev202601050151.dist-info/METADATA +182 -0
- pytest_openapi-0.1.1.dev202601050151.dist-info/RECORD +11 -0
- pytest_openapi-0.1.1.dev202601050151.dist-info/WHEEL +5 -0
- pytest_openapi-0.1.1.dev202601050151.dist-info/entry_points.txt +2 -0
- pytest_openapi-0.1.1.dev202601050151.dist-info/licenses/LICENSE +21 -0
- pytest_openapi-0.1.1.dev202601050151.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.1.dev202601050151"
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Generate test cases from OpenAPI schemas."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def generate_string_test_cases(schema):
|
|
5
|
+
"""Generate string test cases from schema.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
schema: OpenAPI schema for a string field
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
list: List of test values
|
|
12
|
+
"""
|
|
13
|
+
test_cases = []
|
|
14
|
+
|
|
15
|
+
# Check for pattern (regex)
|
|
16
|
+
if "pattern" in schema:
|
|
17
|
+
try:
|
|
18
|
+
import exrex
|
|
19
|
+
|
|
20
|
+
# Generate a few test cases from the regex
|
|
21
|
+
test_cases.extend(list(exrex.generate(schema["pattern"]))[:3])
|
|
22
|
+
except ImportError:
|
|
23
|
+
print(
|
|
24
|
+
"⚠️ Warning: 'exrex' package not installed. Cannot generate test cases from regex patterns."
|
|
25
|
+
)
|
|
26
|
+
print(" Install with: pip install exrex")
|
|
27
|
+
test_cases.append("test-string")
|
|
28
|
+
except Exception as e:
|
|
29
|
+
print(
|
|
30
|
+
f"⚠️ Warning: Could not generate from pattern '{schema['pattern']}': {e}"
|
|
31
|
+
)
|
|
32
|
+
test_cases.append("test-string")
|
|
33
|
+
|
|
34
|
+
# Check for enum
|
|
35
|
+
elif "enum" in schema:
|
|
36
|
+
test_cases.extend(schema["enum"])
|
|
37
|
+
|
|
38
|
+
# Check for format
|
|
39
|
+
elif "format" in schema:
|
|
40
|
+
format_type = schema["format"]
|
|
41
|
+
if format_type == "email":
|
|
42
|
+
test_cases.extend(
|
|
43
|
+
[
|
|
44
|
+
"test@example.com",
|
|
45
|
+
"user+tag@subdomain.example.co.uk",
|
|
46
|
+
"test.user@example.com",
|
|
47
|
+
]
|
|
48
|
+
)
|
|
49
|
+
elif format_type in ["ipv4", "ip"]:
|
|
50
|
+
test_cases.extend(["192.168.1.1", "10.0.0.1", "127.0.0.1"])
|
|
51
|
+
elif format_type == "ipv6":
|
|
52
|
+
test_cases.extend(
|
|
53
|
+
["2001:0db8:85a3:0000:0000:8a2e:0370:7334", "::1", "fe80::1"]
|
|
54
|
+
)
|
|
55
|
+
elif format_type in ["hostname", "idn-hostname"]:
|
|
56
|
+
test_cases.extend(
|
|
57
|
+
["example.com", "subdomain.example.com", "test-server.local"]
|
|
58
|
+
)
|
|
59
|
+
elif format_type in ["uri", "url"]:
|
|
60
|
+
test_cases.extend(
|
|
61
|
+
[
|
|
62
|
+
"https://example.com/path",
|
|
63
|
+
"http://localhost:8080/api/v1/resource",
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
elif format_type == "date":
|
|
67
|
+
test_cases.extend(["2025-12-23", "2024-01-01"])
|
|
68
|
+
elif format_type == "date-time":
|
|
69
|
+
test_cases.extend(
|
|
70
|
+
["2025-12-23T10:30:00Z", "2024-01-01T00:00:00+00:00"]
|
|
71
|
+
)
|
|
72
|
+
elif format_type == "time":
|
|
73
|
+
test_cases.extend(["10:30:00", "23:59:59"])
|
|
74
|
+
elif format_type == "uuid":
|
|
75
|
+
test_cases.extend(
|
|
76
|
+
[
|
|
77
|
+
"550e8400-e29b-41d4-a716-446655440000",
|
|
78
|
+
"123e4567-e89b-12d3-a456-426614174000",
|
|
79
|
+
]
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
# Unknown format, use basic string
|
|
83
|
+
test_cases.append(f"test-{format_type}")
|
|
84
|
+
|
|
85
|
+
# No specific constraints, generate edge cases
|
|
86
|
+
else:
|
|
87
|
+
test_cases.extend(
|
|
88
|
+
[
|
|
89
|
+
"Lorem ipsum dolor sit amet", # Normal text
|
|
90
|
+
"Test with 'single' quotes", # Single quotes
|
|
91
|
+
'Test with "double" quotes', # Double quotes
|
|
92
|
+
"Test:with:colons", # Colons
|
|
93
|
+
"Test\\with\\backslashes", # Backslashes
|
|
94
|
+
"Test\nwith\nnewlines", # Newlines
|
|
95
|
+
"Test\r\nwith\r\nCRLF", # Carriage returns
|
|
96
|
+
"Test with UTF-8: café, naïve, 中文, 日本語", # UTF-8
|
|
97
|
+
"Test!@#$%^&*()_+-=[]{}|;:<>?,./`~", # Special characters
|
|
98
|
+
"", # Empty string (if allowed)
|
|
99
|
+
]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Check length constraints
|
|
103
|
+
min_length = schema.get("minLength", 0)
|
|
104
|
+
max_length = schema.get("maxLength")
|
|
105
|
+
|
|
106
|
+
# Filter test cases by length
|
|
107
|
+
filtered = []
|
|
108
|
+
for ex in test_cases:
|
|
109
|
+
if len(ex) >= min_length:
|
|
110
|
+
if max_length is None or len(ex) <= max_length:
|
|
111
|
+
filtered.append(ex)
|
|
112
|
+
|
|
113
|
+
# If we have length constraints but no valid test cases, generate one
|
|
114
|
+
if not filtered and (min_length > 0 or max_length is not None):
|
|
115
|
+
if max_length:
|
|
116
|
+
target_length = min(min_length + 5, max_length)
|
|
117
|
+
else:
|
|
118
|
+
target_length = min_length + 5
|
|
119
|
+
filtered.append("a" * target_length)
|
|
120
|
+
|
|
121
|
+
return filtered if filtered else ["test-string"]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def generate_integer_test_cases(schema, field_name="field"):
|
|
125
|
+
"""Generate integer test cases from schema.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
schema: OpenAPI schema for an integer field
|
|
129
|
+
field_name: Name of the field (for error messages)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
tuple: (list of test values, optional warning message)
|
|
133
|
+
"""
|
|
134
|
+
test_cases = []
|
|
135
|
+
warning = None
|
|
136
|
+
|
|
137
|
+
# Check for enum
|
|
138
|
+
if "enum" in schema:
|
|
139
|
+
return schema["enum"], None
|
|
140
|
+
|
|
141
|
+
# Get constraints
|
|
142
|
+
minimum = schema.get("minimum")
|
|
143
|
+
maximum = schema.get("maximum")
|
|
144
|
+
exclusive_min = schema.get("exclusiveMinimum")
|
|
145
|
+
exclusive_max = schema.get("exclusiveMaximum")
|
|
146
|
+
multiple_of = schema.get("multipleOf", 1)
|
|
147
|
+
format_type = schema.get("format")
|
|
148
|
+
|
|
149
|
+
# Determine actual bounds
|
|
150
|
+
if exclusive_min is not None:
|
|
151
|
+
min_val = exclusive_min + multiple_of
|
|
152
|
+
elif minimum is not None:
|
|
153
|
+
min_val = minimum
|
|
154
|
+
else:
|
|
155
|
+
# No minimum specified
|
|
156
|
+
if format_type == "int32":
|
|
157
|
+
min_val = -2147483648
|
|
158
|
+
elif format_type == "int64":
|
|
159
|
+
min_val = -9223372036854775808
|
|
160
|
+
else:
|
|
161
|
+
min_val = -1000000
|
|
162
|
+
warning = f"⚠️ Field '{field_name}': No minimum specified. Testing with very large negative numbers. Add 'minimum' to schema to restrict."
|
|
163
|
+
|
|
164
|
+
if exclusive_max is not None:
|
|
165
|
+
max_val = exclusive_max - multiple_of
|
|
166
|
+
elif maximum is not None:
|
|
167
|
+
max_val = maximum
|
|
168
|
+
else:
|
|
169
|
+
# No maximum specified
|
|
170
|
+
if format_type == "int32":
|
|
171
|
+
max_val = 2147483647
|
|
172
|
+
elif format_type == "int64":
|
|
173
|
+
max_val = 9223372036854775807
|
|
174
|
+
else:
|
|
175
|
+
max_val = 1000000
|
|
176
|
+
if not warning:
|
|
177
|
+
warning = f"⚠️ Field '{field_name}': No maximum specified. Testing with very large positive numbers. Add 'maximum' to schema to restrict."
|
|
178
|
+
|
|
179
|
+
# Generate examples respecting multipleOf
|
|
180
|
+
def round_to_multiple(val):
|
|
181
|
+
"""Round value to nearest multiple."""
|
|
182
|
+
return int(round(val / multiple_of) * multiple_of)
|
|
183
|
+
|
|
184
|
+
# Add boundary values
|
|
185
|
+
test_cases.append(round_to_multiple(min_val))
|
|
186
|
+
test_cases.append(round_to_multiple(max_val))
|
|
187
|
+
|
|
188
|
+
# Add middle value
|
|
189
|
+
mid_val = round_to_multiple((min_val + max_val) / 2)
|
|
190
|
+
if mid_val not in test_cases:
|
|
191
|
+
test_cases.append(mid_val)
|
|
192
|
+
|
|
193
|
+
# Add zero if in range and valid
|
|
194
|
+
if min_val <= 0 <= max_val:
|
|
195
|
+
zero_val = round_to_multiple(0)
|
|
196
|
+
if zero_val not in test_cases:
|
|
197
|
+
test_cases.append(zero_val)
|
|
198
|
+
|
|
199
|
+
# Add a negative value if allowed and not already present
|
|
200
|
+
if min_val < 0 and max_val > 0:
|
|
201
|
+
neg_val = (
|
|
202
|
+
round_to_multiple(min_val / 2)
|
|
203
|
+
if min_val > -1000
|
|
204
|
+
else round_to_multiple(-100)
|
|
205
|
+
)
|
|
206
|
+
if neg_val not in test_cases and min_val <= neg_val <= max_val:
|
|
207
|
+
test_cases.append(neg_val)
|
|
208
|
+
|
|
209
|
+
# Ensure all test cases respect multipleOf
|
|
210
|
+
test_cases = [int(ex) for ex in test_cases if ex % multiple_of == 0]
|
|
211
|
+
|
|
212
|
+
return sorted(set(test_cases)), warning
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def generate_number_test_cases(schema, field_name="field"):
|
|
216
|
+
"""Generate number (float) test cases from schema.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
schema: OpenAPI schema for a number field
|
|
220
|
+
field_name: Name of the field (for error messages)
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
tuple: (list of test values, optional warning message)
|
|
224
|
+
"""
|
|
225
|
+
test_cases = []
|
|
226
|
+
warning = None
|
|
227
|
+
|
|
228
|
+
# Check for enum
|
|
229
|
+
if "enum" in schema:
|
|
230
|
+
return schema["enum"], None
|
|
231
|
+
|
|
232
|
+
# Get constraints
|
|
233
|
+
minimum = schema.get("minimum")
|
|
234
|
+
maximum = schema.get("maximum")
|
|
235
|
+
exclusive_min = schema.get("exclusiveMinimum")
|
|
236
|
+
exclusive_max = schema.get("exclusiveMaximum")
|
|
237
|
+
multiple_of = schema.get("multipleOf")
|
|
238
|
+
|
|
239
|
+
# Determine actual bounds
|
|
240
|
+
if exclusive_min is not None:
|
|
241
|
+
min_val = exclusive_min + 0.01
|
|
242
|
+
elif minimum is not None:
|
|
243
|
+
min_val = minimum
|
|
244
|
+
else:
|
|
245
|
+
min_val = -1000000.0
|
|
246
|
+
warning = f"⚠️ Field '{field_name}': No minimum specified. Testing with very large negative numbers. Add 'minimum' to schema to restrict."
|
|
247
|
+
|
|
248
|
+
if exclusive_max is not None:
|
|
249
|
+
max_val = exclusive_max - 0.01
|
|
250
|
+
elif maximum is not None:
|
|
251
|
+
max_val = maximum
|
|
252
|
+
else:
|
|
253
|
+
max_val = 1000000.0
|
|
254
|
+
if not warning:
|
|
255
|
+
warning = f"⚠️ Field '{field_name}': No maximum specified. Testing with very large positive numbers. Add 'maximum' to schema to restrict."
|
|
256
|
+
|
|
257
|
+
# Add boundary values
|
|
258
|
+
test_cases.append(float(min_val))
|
|
259
|
+
test_cases.append(float(max_val))
|
|
260
|
+
|
|
261
|
+
# Add middle value
|
|
262
|
+
mid_val = (min_val + max_val) / 2
|
|
263
|
+
test_cases.append(mid_val)
|
|
264
|
+
|
|
265
|
+
# Add zero if in range
|
|
266
|
+
if min_val <= 0 <= max_val:
|
|
267
|
+
if multiple_of:
|
|
268
|
+
zero_val = 0.0 if 0.0 % multiple_of == 0 else multiple_of
|
|
269
|
+
test_cases.append(zero_val)
|
|
270
|
+
else:
|
|
271
|
+
test_cases.append(0.0)
|
|
272
|
+
|
|
273
|
+
# Add high-precision numbers
|
|
274
|
+
if min_val < 1 < max_val:
|
|
275
|
+
test_cases.extend([0.123456789, 0.999999999, 1.111111111])
|
|
276
|
+
|
|
277
|
+
# Add negative if allowed
|
|
278
|
+
if min_val < 0 < max_val:
|
|
279
|
+
test_cases.append(-0.123456789)
|
|
280
|
+
|
|
281
|
+
# Apply multipleOf constraint if specified
|
|
282
|
+
if multiple_of:
|
|
283
|
+
filtered = []
|
|
284
|
+
for ex in test_cases:
|
|
285
|
+
# Check if it's a valid multiple
|
|
286
|
+
if abs((ex / multiple_of) - round(ex / multiple_of)) < 0.0001:
|
|
287
|
+
filtered.append(ex)
|
|
288
|
+
if filtered:
|
|
289
|
+
test_cases = filtered
|
|
290
|
+
else:
|
|
291
|
+
# Generate some valid multiples
|
|
292
|
+
test_cases = [
|
|
293
|
+
multiple_of * i
|
|
294
|
+
for i in range(
|
|
295
|
+
int(min_val / multiple_of), int(max_val / multiple_of) + 1
|
|
296
|
+
)
|
|
297
|
+
if min_val <= multiple_of * i <= max_val
|
|
298
|
+
][:5]
|
|
299
|
+
|
|
300
|
+
# Filter to ensure within bounds
|
|
301
|
+
test_cases = [ex for ex in test_cases if min_val <= ex <= max_val]
|
|
302
|
+
|
|
303
|
+
return sorted(set(test_cases)), warning
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def generate_boolean_test_cases(schema):
|
|
307
|
+
"""Generate boolean test cases.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
schema: OpenAPI schema for a boolean field
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
list: [True, False]
|
|
314
|
+
"""
|
|
315
|
+
return [True, False]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def generate_array_test_cases(schema, field_name="field"):
|
|
319
|
+
"""Generate array test cases from schema.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
schema: OpenAPI schema for an array field
|
|
323
|
+
field_name: Name of the field (for error messages)
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
tuple: (list of test arrays, optional warning message)
|
|
327
|
+
"""
|
|
328
|
+
items_schema = schema.get("items", {})
|
|
329
|
+
min_items = schema.get("minItems", 0)
|
|
330
|
+
max_items = schema.get("maxItems", 3)
|
|
331
|
+
|
|
332
|
+
# Generate test cases for the item type
|
|
333
|
+
item_test_cases, warning = generate_test_cases_for_schema(
|
|
334
|
+
items_schema, f"{field_name}[]"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
arrays = []
|
|
338
|
+
|
|
339
|
+
# Empty array if allowed
|
|
340
|
+
if min_items == 0:
|
|
341
|
+
arrays.append([])
|
|
342
|
+
|
|
343
|
+
# Single item array if allowed
|
|
344
|
+
if min_items <= 1 <= max_items and item_test_cases:
|
|
345
|
+
arrays.append([item_test_cases[0]])
|
|
346
|
+
|
|
347
|
+
# Min items array
|
|
348
|
+
if min_items > 0 and item_test_cases:
|
|
349
|
+
arrays.append(item_test_cases[:min_items])
|
|
350
|
+
|
|
351
|
+
# Max items array
|
|
352
|
+
if item_test_cases:
|
|
353
|
+
arrays.append(item_test_cases[:max_items])
|
|
354
|
+
|
|
355
|
+
return arrays, warning
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def generate_object_test_cases(schema, field_name="field"):
|
|
359
|
+
"""Generate object test cases from schema.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
schema: OpenAPI schema for an object field
|
|
363
|
+
field_name: Name of the field (for error messages)
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
tuple: (list of test objects, list of warnings)
|
|
367
|
+
"""
|
|
368
|
+
properties = schema.get("properties", {})
|
|
369
|
+
|
|
370
|
+
warnings = []
|
|
371
|
+
|
|
372
|
+
# Collect test cases for each property
|
|
373
|
+
prop_values = {}
|
|
374
|
+
for prop_name, prop_schema in properties.items():
|
|
375
|
+
prop_test_cases, warning = generate_test_cases_for_schema(
|
|
376
|
+
prop_schema, f"{field_name}.{prop_name}"
|
|
377
|
+
)
|
|
378
|
+
if warning:
|
|
379
|
+
warnings.append(warning)
|
|
380
|
+
# Ensure we have at least one value
|
|
381
|
+
prop_values[prop_name] = prop_test_cases or [None]
|
|
382
|
+
|
|
383
|
+
# Create a bounded Cartesian product of property values to produce multiple objects
|
|
384
|
+
# Limit the total generated objects to avoid explosion
|
|
385
|
+
from itertools import product
|
|
386
|
+
|
|
387
|
+
MAX_COMBINATIONS = 10
|
|
388
|
+
keys = list(prop_values.keys())
|
|
389
|
+
# Build iterables in a deterministic order
|
|
390
|
+
iterables = [prop_values[k] for k in keys]
|
|
391
|
+
|
|
392
|
+
combos = []
|
|
393
|
+
for combo in product(*iterables):
|
|
394
|
+
obj = {}
|
|
395
|
+
for k, v in zip(keys, combo):
|
|
396
|
+
obj[k] = v
|
|
397
|
+
combos.append(obj)
|
|
398
|
+
if len(combos) >= MAX_COMBINATIONS:
|
|
399
|
+
break
|
|
400
|
+
|
|
401
|
+
# If no combos generated, fall back to one empty object
|
|
402
|
+
if not combos:
|
|
403
|
+
combos = [{}]
|
|
404
|
+
|
|
405
|
+
return combos, warnings
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def generate_test_cases_for_schema(schema, field_name="field"):
|
|
409
|
+
"""Generate test cases for any schema type.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
schema: OpenAPI schema
|
|
413
|
+
field_name: Name of the field (for error messages)
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
tuple: (list of test values, optional warning message or list of warnings)
|
|
417
|
+
"""
|
|
418
|
+
schema_type = schema.get("type", "string")
|
|
419
|
+
|
|
420
|
+
if schema_type == "string":
|
|
421
|
+
return generate_string_test_cases(schema), None
|
|
422
|
+
elif schema_type == "integer":
|
|
423
|
+
return generate_integer_test_cases(schema, field_name)
|
|
424
|
+
elif schema_type == "number":
|
|
425
|
+
return generate_number_test_cases(schema, field_name)
|
|
426
|
+
elif schema_type == "boolean":
|
|
427
|
+
return generate_boolean_test_cases(schema), None
|
|
428
|
+
elif schema_type == "array":
|
|
429
|
+
return generate_array_test_cases(schema, field_name)
|
|
430
|
+
elif schema_type == "object":
|
|
431
|
+
return generate_object_test_cases(schema, field_name)
|
|
432
|
+
else:
|
|
433
|
+
return ["test-value"], None
|